Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
229143ffb2 | ||
|
|
3d4d1c8650 | ||
|
|
1479369db1 | ||
|
|
31ef283e7b | ||
|
|
6b70554e63 | ||
|
|
9a1c329434 | ||
|
|
4aaf6dee05 | ||
|
|
86f08ee891 | ||
|
|
0b85ba9f32 | ||
|
|
812ce8724d | ||
|
|
06f81ff759 | ||
|
|
2693075f9f | ||
|
|
c1713810b9 | ||
|
|
e08e5d14a2 | ||
|
|
a3d685e132 | ||
|
|
5471760278 | ||
|
|
969c784df4 | ||
|
|
85e49dea6d | ||
|
|
a497afe5cf | ||
|
|
2333171de7 | ||
|
|
04318d7580 | ||
|
|
5e6e5fce1e | ||
|
|
af2ad46958 | ||
|
|
432d7d4a56 | ||
|
|
f93685811a | ||
|
|
914d07df03 | ||
|
|
9c60f104c8 | ||
|
|
ee7e630177 | ||
|
|
ae168ae885 | ||
|
|
ad0fcaed46 | ||
|
|
304b81908d | ||
|
|
b10f8c5bed | ||
|
|
f6b9dbbbc4 | ||
|
|
59cc912378 | ||
|
|
33663bef3e | ||
|
|
e75cd45ec0 | ||
|
|
c98f3cd040 | ||
|
|
4c4caba002 | ||
|
|
45f6cc5247 | ||
|
|
69460db294 | ||
|
|
f8095fcdbf | ||
|
|
1a0a2b3658 | ||
|
|
f9141dd39c | ||
|
|
8c2d49f029 | ||
|
|
da1e120de9 | ||
|
|
457aca25ee | ||
|
|
ae73ae29c4 | ||
|
|
5abf421f11 | ||
|
|
e792532051 | ||
|
|
5c39665b6a | ||
|
|
d1c4579398 | ||
|
|
d80d8ef1f5 | ||
|
|
d49a8e772f | ||
|
|
1947a03e94 | ||
|
|
bc6ae4523f | ||
|
|
442ac9184f | ||
|
|
824f4ce3cf | ||
|
|
cc324024ba | ||
|
|
0921f89eb7 | ||
|
|
628c93bc1e | ||
|
|
25109a7ebb | ||
|
|
e6963d0307 | ||
|
|
9a2f16b29a | ||
|
|
6a17aa7c76 |
9
.github/ISSUE_TEMPLATE.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
**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?**
|
||||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -1,6 +1,52 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
## UNRELEASED v0.3.0 - 2016-04-06
|
## UNRELEASED
|
||||||
|
|
||||||
|
### Added
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Use Squirrel.Windows 1.3.0
|
||||||
|
- Fix installing when the app is already installed
|
||||||
|
- Don't kill unrelated processes on uninstall
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
## v0.3.3 - 2016-04-07
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- App icon was incorrect (OS X)
|
||||||
|
|
||||||
|
## v0.3.2 - 2016-04-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Register WebTorrent as default handler for magnet links (OS X)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Faster startup time (50ms)
|
||||||
|
- Update Electron to 0.37.5
|
||||||
|
- Remove the white flash when loading pages and resizing the window
|
||||||
|
- Fix crash when sending IPC messages
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix installation bugs with .deb file (Linux)
|
||||||
|
- Pause audio reliably when closing the window
|
||||||
|
- Enforce minimimum window size when resizing player (for audio-only .mov files, which are 0x0)
|
||||||
|
|
||||||
|
## v0.3.1 - 2016-04-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add crash reporter to torrent engine process
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix cast screen background: cover, don't tile
|
||||||
|
|
||||||
|
## v0.3.0 - 2016-04-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -22,7 +22,7 @@
|
|||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./static/screenshot.png" width="562" height="630" alt="screenshot" align="center">
|
<img src="https://webtorrent.io/img/screenshot-main.png" width="562" height="630" alt="screenshot" align="center">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## How to Contribute
|
## How to Contribute
|
||||||
@@ -50,12 +50,23 @@ $ npm run package
|
|||||||
To build for one platform:
|
To build for one platform:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm run package -- [platform] [package-type]
|
$ npm run package -- [platform]
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `[platform]` is `darwin`, `linux`, or `win32`
|
Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default).
|
||||||
|
|
||||||
and `[package-type]` is `all` (default), `deb` or `zip` (`linux` platform only)
|
The following optional arguments are available:
|
||||||
|
|
||||||
|
- `--sign` - Sign the application (OS X, Windows)
|
||||||
|
- `--package=[type]` - Package single output type.
|
||||||
|
- `deb` - Debian package
|
||||||
|
- `zip` - Linux zip file
|
||||||
|
- `dmg` - OS X disk image
|
||||||
|
- `exe` - Windows installer
|
||||||
|
- `portable` - Windows portable app
|
||||||
|
- `all` - All platforms (default)
|
||||||
|
|
||||||
|
Note: Even with the `--package` option, the auto-update files (.nupkg for Windows, *-darwin.zip for OS X) will always be produced.
|
||||||
|
|
||||||
#### Windows build notes
|
#### Windows build notes
|
||||||
|
|
||||||
|
|||||||
364
bin/package.js
364
bin/package.js
@@ -4,31 +4,60 @@
|
|||||||
* Builds app binaries for OS X, Linux, and Windows.
|
* Builds app binaries for OS X, Linux, and Windows.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var config = require('../config')
|
|
||||||
var cp = require('child_process')
|
var cp = require('child_process')
|
||||||
var electronPackager = require('electron-packager')
|
var electronPackager = require('electron-packager')
|
||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
|
var minimist = require('minimist')
|
||||||
|
var mkdirp = require('mkdirp')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var pkg = require('../package.json')
|
|
||||||
var rimraf = require('rimraf')
|
var rimraf = require('rimraf')
|
||||||
|
var series = require('run-series')
|
||||||
|
var zip = require('cross-zip')
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
var pkg = require('../package.json')
|
||||||
|
|
||||||
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path to folder with the following files:
|
||||||
|
* - Windows Authenticode private key and cert (authenticode.p12)
|
||||||
|
* - Windows Authenticode password file (authenticode.txt)
|
||||||
|
*/
|
||||||
|
var CERT_PATH = process.platform === 'win32'
|
||||||
|
? 'D:'
|
||||||
|
: '/Volumes/Certs'
|
||||||
|
|
||||||
|
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
||||||
|
|
||||||
|
var argv = minimist(process.argv.slice(2), {
|
||||||
|
boolean: [
|
||||||
|
'sign'
|
||||||
|
],
|
||||||
|
default: {
|
||||||
|
package: 'all',
|
||||||
|
sign: false
|
||||||
|
},
|
||||||
|
string: [
|
||||||
|
'package'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
function build () {
|
function build () {
|
||||||
var platform = process.argv[2]
|
rimraf.sync(DIST_PATH)
|
||||||
var packageType = process.argv.length > 3 ? process.argv[3] : 'all'
|
var platform = argv._[0]
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
buildDarwin(printDone)
|
buildDarwin(printDone)
|
||||||
} else if (platform === 'win32') {
|
} else if (platform === 'win32') {
|
||||||
buildWin32(printDone)
|
buildWin32(printDone)
|
||||||
} else if (platform === 'linux') {
|
} else if (platform === 'linux') {
|
||||||
buildLinux(packageType, printDone)
|
buildLinux(printDone)
|
||||||
} else {
|
} else {
|
||||||
buildDarwin(function (err, buildPath) {
|
buildDarwin(function (err) {
|
||||||
printDone(err, buildPath)
|
printDone(err)
|
||||||
buildWin32(function (err, buildPath) {
|
buildWin32(function (err) {
|
||||||
printDone(err, buildPath)
|
printDone(err)
|
||||||
buildLinux(packageType, printDone)
|
buildLinux(printDone)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -38,7 +67,8 @@ var all = {
|
|||||||
// Build 64 bit binaries only.
|
// Build 64 bit binaries only.
|
||||||
arch: 'x64',
|
arch: 'x64',
|
||||||
|
|
||||||
// The human-readable copyright line for the app.
|
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
|
||||||
|
// property on Windows, and `NSHumanReadableCopyright` on OS X.
|
||||||
'app-copyright': config.APP_COPYRIGHT,
|
'app-copyright': config.APP_COPYRIGHT,
|
||||||
|
|
||||||
// The release version of the application. Maps to the `ProductVersion` metadata
|
// The release version of the application. Maps to the `ProductVersion` metadata
|
||||||
@@ -64,13 +94,13 @@ var all = {
|
|||||||
|
|
||||||
// Pattern which specifies which files to ignore when copying files to create the
|
// Pattern which specifies which files to ignore when copying files to create the
|
||||||
// package(s).
|
// package(s).
|
||||||
ignore: /^\/dist|\/(appveyor.yml|.appveyor.yml|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
|
ignore: /^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
|
||||||
|
|
||||||
// The application name.
|
// The application name.
|
||||||
name: config.APP_NAME,
|
name: config.APP_NAME,
|
||||||
|
|
||||||
// The base directory where the finished package(s) are created.
|
// The base directory where the finished package(s) are created.
|
||||||
out: path.join(config.ROOT_PATH, 'dist'),
|
out: DIST_PATH,
|
||||||
|
|
||||||
// Replace an already existing output directory.
|
// Replace an already existing output directory.
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
@@ -131,7 +161,10 @@ var win32 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var linux = {
|
var linux = {
|
||||||
platform: 'linux'
|
platform: 'linux',
|
||||||
|
|
||||||
|
// Build 32/64 bit binaries.
|
||||||
|
arch: 'all'
|
||||||
|
|
||||||
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
||||||
}
|
}
|
||||||
@@ -141,8 +174,10 @@ build()
|
|||||||
function buildDarwin (cb) {
|
function buildDarwin (cb) {
|
||||||
var plist = require('plist')
|
var plist = require('plist')
|
||||||
|
|
||||||
|
console.log('OS X: Packaging electron...')
|
||||||
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
|
console.log('OS X: Packaged electron. ' + buildPath[0])
|
||||||
|
|
||||||
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||||
var contentsPath = path.join(appPath, 'Contents')
|
var contentsPath = path.join(appPath, 'Contents')
|
||||||
@@ -185,11 +220,24 @@ function buildDarwin (cb) {
|
|||||||
cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`)
|
cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`)
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
var appDmg = require('appdmg')
|
if (argv.sign) {
|
||||||
|
signApp(function (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
pack(cb)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
printWarning()
|
||||||
|
pack(cb)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printWarning()
|
||||||
|
}
|
||||||
|
|
||||||
|
function signApp (cb) {
|
||||||
var sign = require('electron-osx-sign')
|
var sign = require('electron-osx-sign')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sign the app with Apple Developer ID certificate. We sign the app for 2 reasons:
|
* Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
|
||||||
* - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by
|
* - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by
|
||||||
* the same author as the current version.
|
* the same author as the current version.
|
||||||
* - So users will not a see a warning about the app coming from an "Unidentified
|
* - So users will not a see a warning about the app coming from an "Unidentified
|
||||||
@@ -207,48 +255,71 @@ function buildDarwin (cb) {
|
|||||||
verbose: true
|
verbose: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('OS X: Signing app...')
|
||||||
sign(signOpts, function (err) {
|
sign(signOpts, function (err) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
|
console.log('OS X: Signed app.')
|
||||||
|
cb(null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Create .zip file (used by the auto-updater)
|
function pack (cb) {
|
||||||
var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '-darwin.zip')
|
packageZip() // always produce .zip file, used for automatic updates
|
||||||
cp.execSync(`cd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'}`)
|
|
||||||
console.log('Created OS X .zip file.')
|
|
||||||
|
|
||||||
var targetPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg')
|
if (argv.package === 'dmg' || argv.package === 'all') {
|
||||||
rimraf.sync(targetPath)
|
packageDmg(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create a .dmg (OS X disk image) file, for easy user installation.
|
function packageZip () {
|
||||||
var dmgOpts = {
|
// Create .zip file (used by the auto-updater)
|
||||||
basepath: config.ROOT_PATH,
|
console.log('OS X: Creating zip...')
|
||||||
target: targetPath,
|
|
||||||
specification: {
|
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||||
title: config.APP_NAME,
|
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
|
||||||
icon: config.APP_ICON + '.icns',
|
zip(inPath, outPath)
|
||||||
background: path.join(config.STATIC_PATH, 'appdmg.png'),
|
|
||||||
'icon-size': 128,
|
console.log('OS X: Created zip.')
|
||||||
contents: [
|
}
|
||||||
{ x: 122, y: 240, type: 'file', path: appPath },
|
|
||||||
{ x: 380, y: 240, type: 'link', path: '/Applications' },
|
function packageDmg (cb) {
|
||||||
// Hide hidden icons out of view, for users who have hidden files shown.
|
console.log('OS X: Creating dmg...')
|
||||||
// https://github.com/LinusU/node-appdmg/issues/45#issuecomment-153924954
|
|
||||||
{ x: 50, y: 500, type: 'position', path: '.background' },
|
var appDmg = require('appdmg')
|
||||||
{ x: 100, y: 500, type: 'position', path: '.DS_Store' },
|
|
||||||
{ x: 150, y: 500, type: 'position', path: '.Trashes' },
|
var targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
|
||||||
{ x: 200, y: 500, type: 'position', path: '.VolumeIcon.icns' }
|
rimraf.sync(targetPath)
|
||||||
]
|
|
||||||
}
|
// Create a .dmg (OS X disk image) file, for easy user installation.
|
||||||
|
var dmgOpts = {
|
||||||
|
basepath: config.ROOT_PATH,
|
||||||
|
target: targetPath,
|
||||||
|
specification: {
|
||||||
|
title: config.APP_NAME,
|
||||||
|
icon: config.APP_ICON + '.icns',
|
||||||
|
background: path.join(config.STATIC_PATH, 'appdmg.png'),
|
||||||
|
'icon-size': 128,
|
||||||
|
contents: [
|
||||||
|
{ x: 122, y: 240, type: 'file', path: appPath },
|
||||||
|
{ x: 380, y: 240, type: 'link', path: '/Applications' },
|
||||||
|
// Hide hidden icons out of view, for users who have hidden files shown.
|
||||||
|
// https://github.com/LinusU/node-appdmg/issues/45#issuecomment-153924954
|
||||||
|
{ x: 50, y: 500, type: 'position', path: '.background' },
|
||||||
|
{ x: 100, y: 500, type: 'position', path: '.DS_Store' },
|
||||||
|
{ x: 150, y: 500, type: 'position', path: '.Trashes' },
|
||||||
|
{ x: 200, y: 500, type: 'position', path: '.VolumeIcon.icns' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var dmg = appDmg(dmgOpts)
|
var dmg = appDmg(dmgOpts)
|
||||||
dmg.on('error', cb)
|
dmg.on('error', cb)
|
||||||
dmg.on('progress', function (info) {
|
dmg.on('progress', function (info) {
|
||||||
if (info.type === 'step-begin') console.log(info.title + '...')
|
if (info.type === 'step-begin') console.log(info.title + '...')
|
||||||
})
|
})
|
||||||
dmg.on('finish', function (info) {
|
dmg.on('finish', function (info) {
|
||||||
console.log('Created OS X disk image (.dmg) file.')
|
console.log('OS X: Created dmg.')
|
||||||
cb(null, buildPath)
|
cb(null)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -257,79 +328,148 @@ function buildDarwin (cb) {
|
|||||||
function buildWin32 (cb) {
|
function buildWin32 (cb) {
|
||||||
var installer = require('electron-winstaller')
|
var installer = require('electron-winstaller')
|
||||||
|
|
||||||
|
console.log('Windows: Packaging electron...')
|
||||||
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
|
console.log('Windows: Packaged electron. ' + buildPath[0])
|
||||||
|
|
||||||
console.log('Creating Windows installer...')
|
var signWithParams
|
||||||
installer.createWindowsInstaller({
|
if (process.platform === 'win32') {
|
||||||
appDirectory: buildPath[0],
|
if (argv.sign) {
|
||||||
authors: config.APP_TEAM,
|
var certificateFile = path.join(CERT_PATH, 'authenticode.p12')
|
||||||
// certificateFile: '', // TODO
|
var certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
|
||||||
description: config.APP_NAME,
|
var timestampServer = 'http://timestamp.comodoca.com'
|
||||||
exe: config.APP_NAME + '.exe',
|
signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256`
|
||||||
iconUrl: config.GITHUB_URL_RAW + '/static/' + config.APP_NAME + '.ico',
|
} else {
|
||||||
loadingGif: path.join(config.STATIC_PATH, 'loading.gif'),
|
printWarning()
|
||||||
remoteReleases: config.GITHUB_URL,
|
}
|
||||||
name: config.APP_NAME,
|
} else {
|
||||||
noMsi: true,
|
printWarning()
|
||||||
outputDirectory: path.join(config.ROOT_PATH, 'dist'),
|
}
|
||||||
productName: config.APP_NAME,
|
|
||||||
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
var tasks = []
|
||||||
setupIcon: config.APP_ICON + '.ico',
|
if (argv.package === 'exe' || argv.package === 'all') {
|
||||||
title: config.APP_NAME,
|
tasks.push((cb) => packageInstaller(cb))
|
||||||
usePackageJson: false,
|
}
|
||||||
version: pkg.version
|
if (argv.package === 'portable' || argv.package === 'all') {
|
||||||
}).then(function () {
|
tasks.push((cb) => packagePortable(cb))
|
||||||
console.log('Created Windows installer.')
|
}
|
||||||
cb(null, buildPath)
|
series(tasks, cb)
|
||||||
}).catch(cb)
|
|
||||||
|
function packageInstaller (cb) {
|
||||||
|
console.log('Windows: Creating installer...')
|
||||||
|
installer.createWindowsInstaller({
|
||||||
|
appDirectory: buildPath[0],
|
||||||
|
authors: config.APP_TEAM,
|
||||||
|
description: config.APP_NAME,
|
||||||
|
exe: config.APP_NAME + '.exe',
|
||||||
|
iconUrl: config.GITHUB_URL_RAW + '/static/' + config.APP_NAME + '.ico',
|
||||||
|
loadingGif: path.join(config.STATIC_PATH, 'loading.gif'),
|
||||||
|
name: config.APP_NAME,
|
||||||
|
noMsi: true,
|
||||||
|
outputDirectory: DIST_PATH,
|
||||||
|
productName: config.APP_NAME,
|
||||||
|
remoteReleases: config.GITHUB_URL,
|
||||||
|
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
||||||
|
setupIcon: config.APP_ICON + '.ico',
|
||||||
|
signWithParams: signWithParams,
|
||||||
|
title: config.APP_NAME,
|
||||||
|
usePackageJson: false,
|
||||||
|
version: pkg.version
|
||||||
|
}).then(function () {
|
||||||
|
console.log('Windows: Created installer.')
|
||||||
|
cb(null)
|
||||||
|
}).catch(cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
function packagePortable (cb) {
|
||||||
|
// Create Windows portable app
|
||||||
|
console.log('Windows: Creating portable app...')
|
||||||
|
|
||||||
|
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
||||||
|
mkdirp.sync(portablePath)
|
||||||
|
|
||||||
|
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
||||||
|
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
||||||
|
zip(inPath, outPath)
|
||||||
|
|
||||||
|
console.log('Windows: Created portable app.')
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLinux (packageType, cb) {
|
function buildLinux (cb) {
|
||||||
|
console.log('Linux: Packaging electron...')
|
||||||
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
|
console.log('Linux: Packaged electron. ' + buildPath[0])
|
||||||
|
|
||||||
var distPath = path.join(config.ROOT_PATH, 'dist')
|
var tasks = []
|
||||||
var filesPath = buildPath[0]
|
buildPath.forEach(function (filesPath) {
|
||||||
|
var destArch = filesPath.split('-').pop()
|
||||||
|
|
||||||
if (packageType === 'deb' || packageType === 'all') {
|
if (argv.package === 'deb' || argv.package === 'all') {
|
||||||
// Create .deb file for debian based platforms
|
tasks.push((cb) => packageDeb(filesPath, destArch, cb))
|
||||||
var deb = require('nobin-debian-installer')()
|
}
|
||||||
var destPath = path.join('/opt', pkg.name)
|
if (argv.package === 'zip' || argv.package === 'all') {
|
||||||
|
tasks.push((cb) => packageZip(filesPath, destArch, cb))
|
||||||
deb.pack({
|
}
|
||||||
package: pkg,
|
})
|
||||||
info: {
|
series(tasks, cb)
|
||||||
arch: 'amd64',
|
|
||||||
targetDir: distPath,
|
|
||||||
scripts: {
|
|
||||||
postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'),
|
|
||||||
postrm: path.join(config.STATIC_PATH, 'linux', 'postrm')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [{
|
|
||||||
src: ['./**'],
|
|
||||||
dest: destPath,
|
|
||||||
expand: true,
|
|
||||||
cwd: filesPath
|
|
||||||
}], function (err, done) {
|
|
||||||
if (err) return console.error(err.message || err)
|
|
||||||
console.log('Created Linux .deb file.')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packageType === 'zip' || packageType === 'all') {
|
|
||||||
// Create .zip file for Linux
|
|
||||||
var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '-linux.zip')
|
|
||||||
var appFolderName = path.basename(filesPath)
|
|
||||||
cp.execSync(`cd ${distPath} && zip -r -y ${zipPath} ${appFolderName}`)
|
|
||||||
console.log('Created Linux .zip file.')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function packageDeb (filesPath, destArch, cb) {
|
||||||
|
// Create .deb file for Debian-based platforms
|
||||||
|
console.log(`Linux: Creating ${destArch} deb...`)
|
||||||
|
|
||||||
|
var deb = require('nobin-debian-installer')()
|
||||||
|
var destPath = path.join('/opt', pkg.name)
|
||||||
|
|
||||||
|
deb.pack({
|
||||||
|
package: pkg,
|
||||||
|
info: {
|
||||||
|
arch: destArch === 'x64' ? 'amd64' : 'i386',
|
||||||
|
targetDir: DIST_PATH,
|
||||||
|
depends: 'libc6 (>= 2.4)',
|
||||||
|
scripts: {
|
||||||
|
postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'),
|
||||||
|
prerm: path.join(config.STATIC_PATH, 'linux', 'prerm')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [{
|
||||||
|
src: ['./**'],
|
||||||
|
dest: destPath,
|
||||||
|
expand: true,
|
||||||
|
cwd: filesPath
|
||||||
|
}], function (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
console.log(`Linux: Created ${destArch} deb.`)
|
||||||
|
cb(null)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageZip (filesPath, destArch, cb) {
|
||||||
|
// Create .zip file for Linux
|
||||||
|
console.log(`Linux: Creating ${destArch} zip...`)
|
||||||
|
|
||||||
|
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||||
|
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
|
||||||
|
zip(inPath, outPath)
|
||||||
|
|
||||||
|
console.log(`Linux: Created ${destArch} zip.`)
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function printDone (err, buildPath) {
|
function printDone (err) {
|
||||||
if (err) console.error(err.message || err)
|
if (err) console.error(err.message || err)
|
||||||
else console.log('Built ' + buildPath[0])
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Print a large warning when signing is disabled so we are less likely to accidentally
|
||||||
|
* ship unsigned binaries to users.
|
||||||
|
*/
|
||||||
|
function printWarning () {
|
||||||
|
console.log(fs.readFileSync(path.join(__dirname, 'warning.txt'), 'utf8'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
git diff --exit-code
|
git diff --exit-code
|
||||||
npm run package
|
npm run package -- --sign
|
||||||
git push
|
git push
|
||||||
git push --tags
|
git push --tags
|
||||||
npm publish
|
npm publish
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ npm run update-authors
|
|||||||
git diff --exit-code
|
git diff --exit-code
|
||||||
rm -rf node_modules/
|
rm -rf node_modules/
|
||||||
npm install
|
npm install
|
||||||
|
npm prune
|
||||||
|
npm dedupe
|
||||||
npm test
|
npm test
|
||||||
|
|||||||
12
bin/warning.txt
Normal file
12
bin/warning.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
*********************************************************
|
||||||
|
_ _ ___ ______ _ _ _____ _ _ _____
|
||||||
|
| | | |/ _ \ | ___ \ \ | |_ _| \ | | __ \
|
||||||
|
| | | / /_\ \| |_/ / \| | | | | \| | | \/
|
||||||
|
| |/\| | _ || /| . ` | | | | . ` | | __
|
||||||
|
\ /\ / | | || |\ \| |\ |_| |_| |\ | |_\ \
|
||||||
|
\/ \/\_| |_/\_| \_\_| \_/\___/\_| \_/\____/
|
||||||
|
|
||||||
|
Application is NOT signed. Do not ship this to users!
|
||||||
|
|
||||||
|
*********************************************************
|
||||||
65
config.js
65
config.js
@@ -1,10 +1,13 @@
|
|||||||
var applicationConfigPath = require('application-config-path')
|
var appConfig = require('application-config')('WebTorrent')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
var pathExists = require('path-exists')
|
||||||
|
|
||||||
var APP_NAME = 'WebTorrent'
|
var APP_NAME = 'WebTorrent'
|
||||||
var APP_TEAM = 'The WebTorrent Project'
|
var APP_TEAM = 'The WebTorrent Project'
|
||||||
var APP_VERSION = require('./package.json').version
|
var APP_VERSION = require('./package.json').version
|
||||||
|
|
||||||
|
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
||||||
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
||||||
@@ -14,59 +17,43 @@ module.exports = {
|
|||||||
APP_VERSION: APP_VERSION,
|
APP_VERSION: APP_VERSION,
|
||||||
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
||||||
|
|
||||||
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update?version=' + APP_VERSION,
|
|
||||||
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
|
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
|
||||||
|
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' +
|
||||||
|
'?version=' + APP_VERSION + '&platform=' + process.platform,
|
||||||
|
|
||||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||||
|
|
||||||
CONFIG_PATH: applicationConfigPath(APP_NAME),
|
CONFIG_PATH: getConfigPath(),
|
||||||
CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'),
|
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
CONFIG_TORRENT_PATH: path.join(applicationConfigPath(APP_NAME), 'Torrents'),
|
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
|
||||||
|
|
||||||
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
||||||
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
||||||
|
|
||||||
|
IS_PORTABLE: isPortable(),
|
||||||
IS_PRODUCTION: isProduction(),
|
IS_PRODUCTION: isProduction(),
|
||||||
|
|
||||||
ROOT_PATH: __dirname,
|
ROOT_PATH: __dirname,
|
||||||
STATIC_PATH: path.join(__dirname, 'static'),
|
STATIC_PATH: path.join(__dirname, 'static'),
|
||||||
|
|
||||||
SOUND_ADD: {
|
|
||||||
url: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'),
|
|
||||||
volume: 0.2
|
|
||||||
},
|
|
||||||
SOUND_DELETE: {
|
|
||||||
url: 'file://' + path.join(__dirname, 'static', 'sound', 'delete.wav'),
|
|
||||||
volume: 0.1
|
|
||||||
},
|
|
||||||
SOUND_DISABLE: {
|
|
||||||
url: 'file://' + path.join(__dirname, 'static', 'sound', 'disable.wav'),
|
|
||||||
volume: 0.2
|
|
||||||
},
|
|
||||||
SOUND_DONE: {
|
|
||||||
url: 'file://' + path.join(__dirname, 'static', 'sound', 'done.wav'),
|
|
||||||
volume: 0.2
|
|
||||||
},
|
|
||||||
SOUND_ENABLE: {
|
|
||||||
url: 'file://' + path.join(__dirname, 'static', 'sound', 'enable.wav'),
|
|
||||||
volume: 0.2
|
|
||||||
},
|
|
||||||
SOUND_ERROR: {
|
|
||||||
url: 'file://' + path.join(__dirname, 'static', 'sound', 'error.wav'),
|
|
||||||
volume: 0.2
|
|
||||||
},
|
|
||||||
SOUND_PLAY: {
|
|
||||||
url: 'file://' + path.join(__dirname, 'static', 'sound', 'play.wav'),
|
|
||||||
volume: 0.2
|
|
||||||
},
|
|
||||||
SOUND_STARTUP: {
|
|
||||||
url: 'file://' + path.join(__dirname, 'static', 'sound', 'startup.wav'),
|
|
||||||
volume: 0.4
|
|
||||||
},
|
|
||||||
|
|
||||||
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
|
||||||
|
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
|
||||||
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, 'renderer', 'webtorrent.html'),
|
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, 'renderer', 'webtorrent.html'),
|
||||||
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html')
|
|
||||||
|
WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents
|
||||||
|
WINDOW_MIN_WIDTH: 425
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfigPath () {
|
||||||
|
if (isPortable()) {
|
||||||
|
return PORTABLE_PATH
|
||||||
|
} else {
|
||||||
|
return path.dirname(appConfig.filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPortable () {
|
||||||
|
return process.platform === 'win32' && isProduction() && pathExists(PORTABLE_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProduction () {
|
function isProduction () {
|
||||||
|
|||||||
15
crash-reporter.js
Normal file
15
crash-reporter.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
init
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = require('./config')
|
||||||
|
var electron = require('electron')
|
||||||
|
|
||||||
|
function init () {
|
||||||
|
electron.crashReporter.start({
|
||||||
|
companyName: config.APP_NAME,
|
||||||
|
productName: config.APP_NAME,
|
||||||
|
submitURL: config.CRASH_REPORT_URL
|
||||||
|
})
|
||||||
|
console.log('crash reporter started')
|
||||||
|
}
|
||||||
@@ -30,16 +30,14 @@ function uninstall () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function installDarwin () {
|
function installDarwin () {
|
||||||
// TODO: Uncomment this once we upgrade past Electron 0.37.4.
|
var electron = require('electron')
|
||||||
|
var app = electron.app
|
||||||
|
|
||||||
// var electron = require('electron')
|
// On OS X, only protocols that are listed in Info.plist can be set as the default
|
||||||
// var app = electron.app
|
// handler at runtime.
|
||||||
|
app.setAsDefaultProtocolClient('magnet')
|
||||||
|
|
||||||
// // On OS X, only protocols that are listed in Info.plist can be set as the default
|
// File handlers are registered in the Info.plist.
|
||||||
// // handler at runtime.
|
|
||||||
// app.setAsDefaultProtocolClient('magnet')
|
|
||||||
|
|
||||||
// // File handlers are registered in the Info.plist.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstallDarwin () {}
|
function uninstallDarwin () {}
|
||||||
@@ -196,17 +194,17 @@ function uninstallWin32 () {
|
|||||||
})
|
})
|
||||||
commandKey.get('', function (err, item) {
|
commandKey.get('', function (err, item) {
|
||||||
if (!err && item.value.indexOf(command) >= 0) {
|
if (!err && item.value.indexOf(command) >= 0) {
|
||||||
eraseProtocol()
|
destroyProtocol()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function eraseProtocol () {
|
function destroyProtocol () {
|
||||||
var protocolKey = new Registry({
|
var protocolKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + protocol
|
key: '\\Software\\Classes\\' + protocol
|
||||||
})
|
})
|
||||||
protocolKey.erase(function () {})
|
protocolKey.destroy(function () {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,7 +216,7 @@ function uninstallWin32 () {
|
|||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + id
|
key: '\\Software\\Classes\\' + id
|
||||||
})
|
})
|
||||||
idKey.erase(getExt)
|
idKey.destroy(getExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExt () {
|
function getExt () {
|
||||||
@@ -228,24 +226,23 @@ function uninstallWin32 () {
|
|||||||
})
|
})
|
||||||
extKey.get('', function (err, item) {
|
extKey.get('', function (err, item) {
|
||||||
if (!err && item.value === id) {
|
if (!err && item.value === id) {
|
||||||
eraseExt()
|
destroyExt()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function eraseExt () {
|
function destroyExt () {
|
||||||
var extKey = new Registry({
|
var extKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + ext
|
key: '\\Software\\Classes\\' + ext
|
||||||
})
|
})
|
||||||
extKey.erase(function () {})
|
extKey.destroy(function () {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function installLinux () {
|
function installLinux () {
|
||||||
var fs = require('fs')
|
var fs = require('fs-extra')
|
||||||
var mkdirp = require('mkdirp')
|
|
||||||
var os = require('os')
|
var os = require('os')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
@@ -279,7 +276,7 @@ function installLinux () {
|
|||||||
'applications',
|
'applications',
|
||||||
'webtorrent-desktop.desktop'
|
'webtorrent-desktop.desktop'
|
||||||
)
|
)
|
||||||
mkdirp(path.dirname(desktopFilePath))
|
fs.mkdirp(path.dirname(desktopFilePath))
|
||||||
fs.writeFile(desktopFilePath, desktopFile, function (err) {
|
fs.writeFile(desktopFilePath, desktopFile, function (err) {
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
})
|
})
|
||||||
@@ -300,7 +297,7 @@ function installLinux () {
|
|||||||
'icons',
|
'icons',
|
||||||
'webtorrent-desktop.png'
|
'webtorrent-desktop.png'
|
||||||
)
|
)
|
||||||
mkdirp(path.dirname(iconFilePath))
|
fs.mkdirp(path.dirname(iconFilePath))
|
||||||
fs.writeFile(iconFilePath, iconFile, function (err) {
|
fs.writeFile(iconFilePath, iconFile, function (err) {
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
})
|
})
|
||||||
@@ -310,7 +307,7 @@ function installLinux () {
|
|||||||
function uninstallLinux () {
|
function uninstallLinux () {
|
||||||
var os = require('os')
|
var os = require('os')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var rimraf = require('rimraf')
|
var fs = require('fs-extra')
|
||||||
|
|
||||||
var desktopFilePath = path.join(
|
var desktopFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
@@ -319,7 +316,7 @@ function uninstallLinux () {
|
|||||||
'applications',
|
'applications',
|
||||||
'webtorrent-desktop.desktop'
|
'webtorrent-desktop.desktop'
|
||||||
)
|
)
|
||||||
rimraf.sync(desktopFilePath)
|
fs.removeSync(desktopFilePath)
|
||||||
|
|
||||||
var iconFilePath = path.join(
|
var iconFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
@@ -328,5 +325,5 @@ function uninstallLinux () {
|
|||||||
'icons',
|
'icons',
|
||||||
'webtorrent-desktop.png'
|
'webtorrent-desktop.png'
|
||||||
)
|
)
|
||||||
rimraf.sync(iconFilePath)
|
fs.removeSync(iconFilePath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
var crashReporter = electron.crashReporter
|
|
||||||
var ipcMain = electron.ipcMain
|
var ipcMain = electron.ipcMain
|
||||||
|
|
||||||
var autoUpdater = require('./auto-updater')
|
var autoUpdater = require('./auto-updater')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
|
var crashReporter = require('../crash-reporter')
|
||||||
var handlers = require('./handlers')
|
var handlers = require('./handlers')
|
||||||
var ipc = require('./ipc')
|
var ipc = require('./ipc')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
@@ -37,6 +37,10 @@ if (!shouldQuit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
|
if (config.IS_PORTABLE) {
|
||||||
|
app.setPath('userData', config.CONFIG_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
app.ipcReady = false // main window has finished loading and IPC is ready
|
app.ipcReady = false // main window has finished loading and IPC is ready
|
||||||
app.isQuitting = false
|
app.isQuitting = false
|
||||||
|
|
||||||
@@ -47,8 +51,8 @@ function init () {
|
|||||||
ipc.init()
|
ipc.init()
|
||||||
|
|
||||||
app.on('will-finish-launching', function () {
|
app.on('will-finish-launching', function () {
|
||||||
|
crashReporter.init()
|
||||||
autoUpdater.init()
|
autoUpdater.init()
|
||||||
setupCrashReporter()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('ready', function () {
|
app.on('ready', function () {
|
||||||
@@ -116,7 +120,7 @@ function sliceArgv (argv) {
|
|||||||
function processArgv (argv) {
|
function processArgv (argv) {
|
||||||
argv.forEach(function (arg) {
|
argv.forEach(function (arg) {
|
||||||
if (arg === '-n') {
|
if (arg === '-n') {
|
||||||
windows.main.send('dispatch', 'showCreateTorrent')
|
windows.main.send('dispatch', 'showOpenSeedFiles')
|
||||||
} else if (arg === '-o') {
|
} else if (arg === '-o') {
|
||||||
windows.main.send('dispatch', 'showOpenTorrentFile')
|
windows.main.send('dispatch', 'showOpenTorrentFile')
|
||||||
} else if (arg === '-u') {
|
} else if (arg === '-u') {
|
||||||
@@ -129,11 +133,3 @@ function processArgv (argv) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCrashReporter () {
|
|
||||||
crashReporter.start({
|
|
||||||
companyName: config.APP_NAME,
|
|
||||||
productName: config.APP_NAME,
|
|
||||||
submitURL: config.CRASH_REPORT_URL
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
18
main/ipc.js
18
main/ipc.js
@@ -20,10 +20,8 @@ function init () {
|
|||||||
ipcMain.on('ipcReady', function (e) {
|
ipcMain.on('ipcReady', function (e) {
|
||||||
app.ipcReady = true
|
app.ipcReady = true
|
||||||
app.emit('ipcReady')
|
app.emit('ipcReady')
|
||||||
setTimeout(function () {
|
windows.main.show()
|
||||||
windows.main.show()
|
console.timeEnd('init')
|
||||||
try { console.timeEnd('init') } catch (err) {}
|
|
||||||
}, 50)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var messageQueueMainToWebTorrent = []
|
var messageQueueMainToWebTorrent = []
|
||||||
@@ -38,7 +36,7 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
|
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
|
||||||
ipcMain.on('showCreateTorrent', menu.showCreateTorrent)
|
ipcMain.on('showOpenSeedFiles', menu.showOpenSeedFiles)
|
||||||
|
|
||||||
ipcMain.on('setBounds', function (e, bounds, maximize) {
|
ipcMain.on('setBounds', function (e, bounds, maximize) {
|
||||||
setBounds(bounds, maximize)
|
setBounds(bounds, maximize)
|
||||||
@@ -89,7 +87,7 @@ function init () {
|
|||||||
var oldEmit = ipcMain.emit
|
var oldEmit = ipcMain.emit
|
||||||
ipcMain.emit = function (name, e, ...args) {
|
ipcMain.emit = function (name, e, ...args) {
|
||||||
// Relay messages between the main window and the WebTorrent hidden window
|
// Relay messages between the main window and the WebTorrent hidden window
|
||||||
if (name.startsWith('wt-')) {
|
if (name.startsWith('wt-') && !app.isQuitting) {
|
||||||
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
|
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
|
||||||
// Send message to main window
|
// Send message to main window
|
||||||
windows.main.send(name, ...args)
|
windows.main.send(name, ...args)
|
||||||
@@ -142,6 +140,14 @@ function setBounds (bounds, maximize) {
|
|||||||
// Assuming we're not maximized or maximizing, set the window size
|
// Assuming we're not maximized or maximizing, set the window size
|
||||||
if (!willBeMaximized) {
|
if (!willBeMaximized) {
|
||||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
||||||
|
if (bounds.x === null && bounds.y === null) {
|
||||||
|
// X and Y not specified? By default, center on current screen
|
||||||
|
var screen = require('screen')
|
||||||
|
var scr = screen.getDisplayMatching(windows.main.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))
|
||||||
|
}
|
||||||
windows.main.setBounds(bounds, true)
|
windows.main.setBounds(bounds, true)
|
||||||
} else {
|
} else {
|
||||||
log('setBounds: not setting bounds because of window maximization')
|
log('setBounds: not setting bounds because of window maximization')
|
||||||
|
|||||||
30
main/menu.js
30
main/menu.js
@@ -5,7 +5,7 @@ module.exports = {
|
|||||||
onWindowShow,
|
onWindowShow,
|
||||||
onPlayerOpen,
|
onPlayerOpen,
|
||||||
onPlayerClose,
|
onPlayerClose,
|
||||||
showCreateTorrent,
|
showOpenSeedFiles,
|
||||||
showOpenTorrentFile,
|
showOpenTorrentFile,
|
||||||
toggleFullScreen
|
toggleFullScreen
|
||||||
}
|
}
|
||||||
@@ -70,11 +70,6 @@ function showWebTorrentWindow () {
|
|||||||
windows.webtorrent.webContents.openDevTools({ detach: true })
|
windows.webtorrent.webContents.openDevTools({ detach: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
function addFakeDevice (device) {
|
|
||||||
log('addFakeDevice %s', device)
|
|
||||||
windows.main.send('addFakeDevice', device)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWindowShow () {
|
function onWindowShow () {
|
||||||
log('onWindowShow')
|
log('onWindowShow')
|
||||||
getMenuItem('Full Screen').enabled = true
|
getMenuItem('Full Screen').enabled = true
|
||||||
@@ -114,7 +109,7 @@ function getMenuItem (label) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prompts the user for a file or folder, then makes a torrent out of the data
|
// Prompts the user for a file or folder, then makes a torrent out of the data
|
||||||
function showCreateTorrent () {
|
function showOpenSeedFiles () {
|
||||||
// Allow only a single selection
|
// Allow only a single selection
|
||||||
// To create a multi-file torrent, the user must select a folder
|
// To create a multi-file torrent, the user must select a folder
|
||||||
electron.dialog.showOpenDialog({
|
electron.dialog.showOpenDialog({
|
||||||
@@ -122,10 +117,8 @@ function showCreateTorrent () {
|
|||||||
properties: [ 'openFile', 'openDirectory' ]
|
properties: [ 'openFile', 'openDirectory' ]
|
||||||
}, function (filenames) {
|
}, function (filenames) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(filenames)) return
|
||||||
var options = {
|
var fileOrFolder = filenames[0]
|
||||||
files: filenames[0]
|
windows.main.send('dispatch', 'showCreateTorrent', fileOrFolder)
|
||||||
}
|
|
||||||
windows.main.send('dispatch', 'createTorrent', options)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +146,7 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
label: 'Create New Torrent...',
|
label: 'Create New Torrent...',
|
||||||
accelerator: 'CmdOrCtrl+N',
|
accelerator: 'CmdOrCtrl+N',
|
||||||
click: showCreateTorrent
|
click: showOpenSeedFiles
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent File...',
|
label: 'Open Torrent File...',
|
||||||
@@ -263,17 +256,6 @@ function getAppMenuTemplate () {
|
|||||||
? 'Alt+Command+P'
|
? 'Alt+Command+P'
|
||||||
: 'Ctrl+Shift+P',
|
: 'Ctrl+Shift+P',
|
||||||
click: showWebTorrentWindow
|
click: showWebTorrentWindow
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'separator'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Add Fake Airplay',
|
|
||||||
click: () => addFakeDevice('airplay')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Add Fake Chromecast',
|
|
||||||
click: () => addFakeDevice('chromecast')
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -389,7 +371,7 @@ function getDockMenuTemplate () {
|
|||||||
{
|
{
|
||||||
label: 'Create New Torrent...',
|
label: 'Create New Torrent...',
|
||||||
accelerator: 'CmdOrCtrl+N',
|
accelerator: 'CmdOrCtrl+N',
|
||||||
click: showCreateTorrent
|
click: showOpenSeedFiles
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent File...',
|
label: 'Open Torrent File...',
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ var electron = require('electron')
|
|||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var os = require('os')
|
var os = require('os')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var pathExists = require('path-exists')
|
|
||||||
|
|
||||||
var app = electron.app
|
var app = electron.app
|
||||||
|
|
||||||
@@ -118,7 +117,8 @@ function updateShortcuts (cb) {
|
|||||||
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
||||||
// Check if the desktop shortcut has been previously deleted and and keep it deleted
|
// Check if the desktop shortcut has been previously deleted and and keep it deleted
|
||||||
// if it was
|
// if it was
|
||||||
pathExists(desktopShortcutPath).then(function (desktopShortcutExists) {
|
fs.access(desktopShortcutPath, function (err) {
|
||||||
|
var desktopShortcutExists = !err
|
||||||
createShortcuts(function () {
|
createShortcuts(function () {
|
||||||
if (desktopShortcutExists) {
|
if (desktopShortcutExists) {
|
||||||
cb()
|
cb()
|
||||||
|
|||||||
33
main/tray.js
33
main/tray.js
@@ -1,7 +1,9 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init
|
init,
|
||||||
|
hasTray
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cp = require('child_process')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
|
|
||||||
@@ -17,6 +19,22 @@ function init () {
|
|||||||
// OS X has no tray icon
|
// OS X has no tray icon
|
||||||
if (process.platform === 'darwin') return
|
if (process.platform === 'darwin') return
|
||||||
|
|
||||||
|
// On Linux, asynchronously check for libappindicator1
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
checkLinuxTraySupport(function (supportsTray) {
|
||||||
|
if (supportsTray) createTrayIcon()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows always supports minimize-to-tray
|
||||||
|
if (process.platform === 'win32') createTrayIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTray () {
|
||||||
|
return !!trayIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTrayIcon () {
|
||||||
trayIcon = new Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
|
trayIcon = new Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
|
||||||
|
|
||||||
// On Windows, left click to open the app, right click for context menu
|
// On Windows, left click to open the app, right click for context menu
|
||||||
@@ -29,6 +47,18 @@ function init () {
|
|||||||
windows.main.on('hide', updateTrayMenu)
|
windows.main.on('hide', updateTrayMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkLinuxTraySupport (cb) {
|
||||||
|
// 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(false)
|
||||||
|
// Unfortunately there's no cleaner way, as far as I can tell, to check
|
||||||
|
// whether a debian package is installed:
|
||||||
|
cb(stdout.endsWith('\tinstall\n'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function updateTrayMenu () {
|
function updateTrayMenu () {
|
||||||
var showHideMenuItem
|
var showHideMenuItem
|
||||||
if (windows.main.isVisible()) {
|
if (windows.main.isVisible()) {
|
||||||
@@ -49,4 +79,5 @@ function showApp () {
|
|||||||
|
|
||||||
function hideApp () {
|
function hideApp () {
|
||||||
windows.main.hide()
|
windows.main.hide()
|
||||||
|
windows.main.send('dispatch', 'backToList')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ var electron = require('electron')
|
|||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
|
var tray = require('./tray')
|
||||||
|
|
||||||
function createAboutWindow () {
|
function createAboutWindow () {
|
||||||
if (windows.about) {
|
if (windows.about) {
|
||||||
@@ -49,7 +50,7 @@ function createAboutWindow () {
|
|||||||
|
|
||||||
function createWebTorrentHiddenWindow () {
|
function createWebTorrentHiddenWindow () {
|
||||||
var win = windows.webtorrent = new electron.BrowserWindow({
|
var win = windows.webtorrent = new electron.BrowserWindow({
|
||||||
backgroundColor: '#282828',
|
backgroundColor: '#1E1E1E',
|
||||||
show: false,
|
show: false,
|
||||||
center: true,
|
center: true,
|
||||||
title: 'webtorrent-hidden-window',
|
title: 'webtorrent-hidden-window',
|
||||||
@@ -72,6 +73,10 @@ function createWebTorrentHiddenWindow () {
|
|||||||
win.hide()
|
win.hide()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
win.once('closed', function () {
|
||||||
|
windows.webtorrent = null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMainWindow () {
|
function createMainWindow () {
|
||||||
@@ -79,11 +84,11 @@ function createMainWindow () {
|
|||||||
return focusWindow(windows.main)
|
return focusWindow(windows.main)
|
||||||
}
|
}
|
||||||
var win = windows.main = new electron.BrowserWindow({
|
var win = windows.main = new electron.BrowserWindow({
|
||||||
backgroundColor: '#282828',
|
backgroundColor: '#1E1E1E',
|
||||||
darkTheme: true, // Forces dark theme (GTK+3)
|
darkTheme: true, // Forces dark theme (GTK+3)
|
||||||
icon: config.APP_ICON + '.png',
|
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
|
||||||
minWidth: 425,
|
minWidth: config.WINDOW_MIN_WIDTH,
|
||||||
minHeight: 38 + (120 * 2), // header height + 2 torrents
|
minHeight: config.WINDOW_MIN_HEIGHT,
|
||||||
show: false, // Hide window until DOM finishes loading
|
show: false, // Hide window until DOM finishes loading
|
||||||
title: config.APP_WINDOW_TITLE,
|
title: config.APP_WINDOW_TITLE,
|
||||||
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
|
||||||
@@ -104,10 +109,12 @@ function createMainWindow () {
|
|||||||
win.on('leave-full-screen', () => menu.onToggleFullScreen(false))
|
win.on('leave-full-screen', () => menu.onToggleFullScreen(false))
|
||||||
|
|
||||||
win.on('close', function (e) {
|
win.on('close', function (e) {
|
||||||
if (!electron.app.isQuitting) {
|
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
||||||
|
electron.app.quit()
|
||||||
|
} else if (!electron.app.isQuitting) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
win.send('dispatch', 'pause')
|
|
||||||
win.hide()
|
win.hide()
|
||||||
|
win.send('dispatch', 'backToList')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
36
package.json
36
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
|
||||||
"version": "0.3.0",
|
"version": "0.3.3",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Feross Aboukhadijeh",
|
"name": "Feross Aboukhadijeh",
|
||||||
"email": "feross@feross.org",
|
"email": "feross@feross.org",
|
||||||
@@ -15,37 +15,42 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"airplay-js": "guerrerocarlos/node-airplay-js",
|
"airplay-js": "guerrerocarlos/node-airplay-js",
|
||||||
"application-config": "^0.2.0",
|
"application-config": "feross/node-application-config",
|
||||||
"application-config-path": "^0.1.0",
|
|
||||||
"bitfield": "^1.0.2",
|
"bitfield": "^1.0.2",
|
||||||
"chromecasts": "^1.8.0",
|
"chromecasts": "^1.8.0",
|
||||||
"create-torrent": "^3.22.1",
|
"concat-stream": "^1.5.1",
|
||||||
|
"create-torrent": "^3.24.5",
|
||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.0.3",
|
"dlnacasts": "^0.0.3",
|
||||||
"drag-drop": "^2.11.0",
|
"drag-drop": "^2.11.0",
|
||||||
"electron-localshortcut": "^0.6.0",
|
"electron-localshortcut": "^0.6.0",
|
||||||
"electron-prebuilt": "0.37.3",
|
"electron-prebuilt": "0.37.6",
|
||||||
|
"fs-extra": "^0.27.0",
|
||||||
"hyperx": "^2.0.2",
|
"hyperx": "^2.0.2",
|
||||||
|
"languagedetect": "^1.1.1",
|
||||||
"main-loop": "^3.2.0",
|
"main-loop": "^3.2.0",
|
||||||
"mkdirp": "^0.5.1",
|
|
||||||
"musicmetadata": "^2.0.2",
|
"musicmetadata": "^2.0.2",
|
||||||
"network-address": "^1.1.0",
|
"network-address": "^1.1.0",
|
||||||
"path-exists": "^2.1.0",
|
|
||||||
"prettier-bytes": "^1.0.1",
|
"prettier-bytes": "^1.0.1",
|
||||||
"rimraf": "^2.5.2",
|
|
||||||
"simple-get": "^2.0.0",
|
"simple-get": "^2.0.0",
|
||||||
|
"srt-to-vtt": "^1.1.1",
|
||||||
"upload-element": "^1.0.1",
|
"upload-element": "^1.0.1",
|
||||||
"virtual-dom": "^2.1.1",
|
"virtual-dom": "^2.1.1",
|
||||||
"webtorrent": "^0.90.0",
|
"wcjs-player": "^0.5.7",
|
||||||
"winreg": "feross/node-winreg"
|
"webchimera.js": "^0.2.3",
|
||||||
|
"webtorrent": "0.x",
|
||||||
|
"winreg": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"cross-zip": "^1.0.0",
|
||||||
"electron-osx-sign": "^0.3.0",
|
"electron-osx-sign": "^0.3.0",
|
||||||
"electron-packager": "^6.0.0",
|
"electron-packager": "^7.0.0",
|
||||||
"electron-winstaller": "feross/windows-installer#build",
|
"electron-winstaller": "feross/windows-installer#build",
|
||||||
"gh-release": "^2.0.3",
|
"gh-release": "^2.0.3",
|
||||||
"nobin-debian-installer": "^0.0.6",
|
"minimist": "^1.2.0",
|
||||||
|
"nobin-debian-installer": "^0.0.9",
|
||||||
"plist": "^1.2.0",
|
"plist": "^1.2.0",
|
||||||
|
"run-series": "^1.1.4",
|
||||||
"standard": "^6.0.5"
|
"standard": "^6.0.5"
|
||||||
},
|
},
|
||||||
"homepage": "https://webtorrent.io",
|
"homepage": "https://webtorrent.io",
|
||||||
@@ -67,10 +72,13 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "node ./bin/clean.js",
|
"clean": "node ./bin/clean.js",
|
||||||
"package": "npm install && npm prune && npm dedupe && node ./bin/package.js",
|
"package": "node ./bin/package.js",
|
||||||
"size": "npm run package -- darwin && du -ch dist/WebTorrent-darwin-x64 | grep total",
|
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"test": "standard",
|
"test": "standard",
|
||||||
"update-authors": "./bin/update-authors.sh"
|
"update-authors": "./bin/update-authors.sh"
|
||||||
|
},
|
||||||
|
"cmake-js": {
|
||||||
|
"runtime": "electron",
|
||||||
|
"runtimeVersion": "0.37.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,28 +113,32 @@ table {
|
|||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
.float-right {
|
||||||
* BUTTONS
|
float: right;
|
||||||
*/
|
|
||||||
|
|
||||||
a,
|
|
||||||
i {
|
|
||||||
cursor: default;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a:not(.disabled):hover,
|
.expand-collapse {
|
||||||
i:not(.disabled):hover {
|
cursor: pointer;
|
||||||
-webkit-filter: brightness(1.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.expand-collapse.expanded::before {
|
||||||
width: 40px;
|
content: '▲'
|
||||||
height: 40px;
|
}
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 22px;
|
.expand-collapse.collapsed::before {
|
||||||
transition: all 0.1s ease-out;
|
content: '▼'
|
||||||
text-align: center;
|
}
|
||||||
|
|
||||||
|
.expand-collapse::before {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-collapse.collapsed {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -266,64 +270,139 @@ i:not(.disabled):hover {
|
|||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.4);
|
box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.4);
|
||||||
background-color: white;
|
background-color: #eee;
|
||||||
color: #222;
|
color: #222;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-modal input,
|
.modal label {
|
||||||
.open-torrent-address-modal input {
|
font-size: 16px;
|
||||||
width: calc(100% - 100px)
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-modal .torrent-attribute {
|
.open-torrent-address-modal input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-torrent-page {
|
||||||
|
padding: 10px 25px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-torrent-page .torrent-attribute {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-modal .torrent-attribute>* {
|
.create-torrent-page .torrent-attribute>* {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-modal .torrent-attribute label {
|
.create-torrent-page .torrent-attribute label {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-torrent-modal .torrent-attribute div {
|
.create-torrent-page .torrent-attribute>div {
|
||||||
font-family: Consolas, monospace;
|
width: calc(100% - 90px);
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.create-torrent-page .torrent-attribute div {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-torrent-page .torrent-attribute textarea {
|
||||||
|
width: calc(100% - 80px);
|
||||||
|
height: 80px;
|
||||||
|
color: #eee;
|
||||||
|
background-color: transparent;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-torrent-page textarea.torrent-trackers {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-torrent-page input.torrent-is-private {
|
||||||
|
width: initial;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* BUTTONS
|
* BUTTONS
|
||||||
|
* See https://www.google.com/design/spec/components/buttons.html
|
||||||
*/
|
*/
|
||||||
|
|
||||||
button {
|
a,
|
||||||
|
i { /* Links and icons */
|
||||||
|
cursor: default;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not(.disabled):hover,
|
||||||
|
i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
|
||||||
|
-webkit-filter: brightness(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-round { /* Circular icon buttons, used on <i> tags */
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 22px;
|
||||||
|
transition: all 0.1s ease-out;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button { /* Rectangular text buttons */
|
||||||
background: transparent;
|
background: transparent;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.primary {
|
button.button-flat {
|
||||||
color: #0cf;
|
color: #222;
|
||||||
|
padding: 7px 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button.button-flat.light {
|
||||||
-webkit-filter: brightness(1.1);
|
color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active {
|
button.button-flat:hover,
|
||||||
-webkit-filter: brightness(1.1);
|
button.button-flat:focus { /* Material design: focused */
|
||||||
text-shadow: none;
|
background-color: rgba(153, 153, 153, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.button-flat:active { /* Material design: pressed */
|
||||||
|
background-color: rgba(153, 153, 153, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.button-raised {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: #eee;
|
||||||
|
padding: 7px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.button-raised:hover,
|
||||||
|
button.button-raised:focus {
|
||||||
|
background-color: #38a0f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.button-raised:active {
|
||||||
|
background-color: #51abf6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -336,7 +415,7 @@ input {
|
|||||||
padding: 6px;
|
padding: 6px;
|
||||||
border: 1px solid #bbb;
|
border: 1px solid #bbb;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 1px 1px 1px 0px rgba(0,0,0,0.1);
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -347,7 +426,7 @@ input {
|
|||||||
background: linear-gradient(to bottom right, #4B79A1, #283E51);
|
background: linear-gradient(to bottom right, #4B79A1, #283E51);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: 0 50%;
|
background-position: center;
|
||||||
transition: -webkit-filter 0.1s ease-out;
|
transition: -webkit-filter 0.1s ease-out;
|
||||||
position: relative;
|
position: relative;
|
||||||
animation: fadein .4s;
|
animation: fadein .4s;
|
||||||
@@ -591,11 +670,15 @@ body.drag .app::after {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player video {
|
.player .video-player {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -675,6 +758,8 @@ body.drag .app::after {
|
|||||||
|
|
||||||
.player-controls .device,
|
.player-controls .device,
|
||||||
.player-controls .fullscreen,
|
.player-controls .fullscreen,
|
||||||
|
.player-controls .closed-captions,
|
||||||
|
.player-controls .volume-icon,
|
||||||
.player-controls .back {
|
.player-controls .back {
|
||||||
display: block;
|
display: block;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
@@ -682,11 +767,13 @@ body.drag .app::after {
|
|||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-controls .volume,
|
||||||
.player-controls .back {
|
.player-controls .back {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .device,
|
.player-controls .device,
|
||||||
|
.player-controls .closed-captions,
|
||||||
.player-controls .fullscreen {
|
.player-controls .fullscreen {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
@@ -695,15 +782,50 @@ body.drag .app::after {
|
|||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-controls .volume-icon,
|
||||||
.player-controls .device {
|
.player-controls .device {
|
||||||
font-size: 18px; /* make the cast icons less huge */
|
font-size: 18px; /* make the cast icons less huge */
|
||||||
margin-top: 8px !important;
|
margin-top: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-controls .closed-captions.active,
|
||||||
.player-controls .device.active {
|
.player-controls .device.active {
|
||||||
color: #9af;
|
color: #9af;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-controls .volume {
|
||||||
|
display: block;
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-controls .volume-icon {
|
||||||
|
float: left;
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-controls .volume-slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 50px;
|
||||||
|
height: 3px;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-controls .volume-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background-color: #fff;
|
||||||
|
opacity: 1.0;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border: 1px solid #303233;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-controls .volume-slider:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.player .playback-bar:hover .loading-bar {
|
.player .playback-bar:hover .loading-bar {
|
||||||
height: 5px;
|
height: 5px;
|
||||||
}
|
}
|
||||||
@@ -714,6 +836,15 @@ body.drag .app::after {
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::cue {
|
||||||
|
background: none;
|
||||||
|
color: #FFF;
|
||||||
|
font: 24px;
|
||||||
|
line-height: 1.3em;
|
||||||
|
text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CHROMECAST / AIRPLAY CONTROLS
|
* CHROMECAST / AIRPLAY CONTROLS
|
||||||
*/
|
*/
|
||||||
@@ -740,6 +871,29 @@ body.drag .app::after {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Subtitles list
|
||||||
|
*/
|
||||||
|
|
||||||
|
.subtitles-list {
|
||||||
|
position: fixed;
|
||||||
|
background: rgba(40, 40, 40, 0.8);
|
||||||
|
min-width: 100px;
|
||||||
|
bottom: 45px;
|
||||||
|
right: 3px;
|
||||||
|
transition: opacity 0.15s ease-out;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 0;
|
||||||
|
list-style-type: none;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitles-list i {
|
||||||
|
font-size: 11px; /* make the cast icons less huge */
|
||||||
|
margin-right: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MEDIA OVERLAY / AUDIO DETAILS
|
* MEDIA OVERLAY / AUDIO DETAILS
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,44 +1,56 @@
|
|||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
var cfg = require('application-config')('WebTorrent')
|
var appConfig = require('application-config')('WebTorrent')
|
||||||
|
var concat = require('concat-stream')
|
||||||
var dragDrop = require('drag-drop')
|
var dragDrop = require('drag-drop')
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
var EventEmitter = require('events')
|
var fs = require('fs-extra')
|
||||||
var fs = require('fs')
|
|
||||||
var mainLoop = require('main-loop')
|
var mainLoop = require('main-loop')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var remote = require('remote')
|
var srtToVtt = require('srt-to-vtt')
|
||||||
|
var LanguageDetect = require('languagedetect')
|
||||||
|
|
||||||
var createElement = require('virtual-dom/create-element')
|
var createElement = require('virtual-dom/create-element')
|
||||||
var diff = require('virtual-dom/diff')
|
var diff = require('virtual-dom/diff')
|
||||||
var patch = require('virtual-dom/patch')
|
var patch = require('virtual-dom/patch')
|
||||||
|
|
||||||
var App = require('./views/app')
|
var App = require('./views/app')
|
||||||
var errors = require('./lib/errors')
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
|
var crashReporter = require('../crash-reporter')
|
||||||
|
var errors = require('./lib/errors')
|
||||||
|
var sound = require('./lib/sound')
|
||||||
|
var State = require('./state')
|
||||||
var TorrentPlayer = require('./lib/torrent-player')
|
var TorrentPlayer = require('./lib/torrent-player')
|
||||||
var util = require('./util')
|
var TorrentSummary = require('./lib/torrent-summary')
|
||||||
|
|
||||||
var {setDispatch} = require('./lib/dispatcher')
|
var {setDispatch} = require('./lib/dispatcher')
|
||||||
setDispatch(dispatch)
|
setDispatch(dispatch)
|
||||||
var State = require('./state')
|
|
||||||
|
|
||||||
// This dependency is the slowest-loading, so we lazy load it
|
appConfig.filePath = config.CONFIG_PATH + path.sep + 'config.json'
|
||||||
var Cast = null
|
|
||||||
|
|
||||||
// Electron apps have two processes: a main process (node) runs first and starts
|
// 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,
|
// 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
|
// and this IPC channel receives from and sends messages to the main process
|
||||||
var ipcRenderer = electron.ipcRenderer
|
var ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
var clipboard = electron.clipboard
|
var clipboard = electron.clipboard
|
||||||
var crashReporter = electron.crashReporter
|
|
||||||
var dialog = remote.require('dialog')
|
var dialog = electron.remote.dialog
|
||||||
|
var Menu = electron.remote.Menu
|
||||||
|
var MenuItem = electron.remote.MenuItem
|
||||||
|
var remote = electron.remote
|
||||||
|
|
||||||
|
// This dependency is the slowest-loading, so we lazy load it
|
||||||
|
var Cast = null
|
||||||
|
|
||||||
// For easy debugging in Developer Tools
|
// For easy debugging in Developer Tools
|
||||||
var state = global.state = State.getInitialState()
|
var state = global.state = State.getInitialState()
|
||||||
|
|
||||||
var vdomLoop
|
var vdomLoop
|
||||||
|
|
||||||
|
// Report crashes back to our server.
|
||||||
|
// Not global JS exceptions, not like Rollbar, handles segfaults/core dumps only
|
||||||
|
crashReporter.init()
|
||||||
|
|
||||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
// All state lives in state.js. `state.saved` is read from and written to a file.
|
||||||
// All other state is ephemeral. First we load state.saved then initialize the app.
|
// All other state is ephemeral. First we load state.saved then initialize the app.
|
||||||
loadState(init)
|
loadState(init)
|
||||||
@@ -49,15 +61,17 @@ loadState(init)
|
|||||||
* the dock icon and drag+drop.
|
* the dock icon and drag+drop.
|
||||||
*/
|
*/
|
||||||
function init () {
|
function init () {
|
||||||
setupCrashReporter()
|
// Clean up the freshly-loaded config file, which may be from an older version
|
||||||
|
cleanUpConfig()
|
||||||
|
|
||||||
// Push the first page into the location history
|
// Push the first page into the location history
|
||||||
state.location.go({ url: 'home' })
|
state.location.go({ url: 'home' })
|
||||||
|
|
||||||
initWebTorrent()
|
// Restart everything we were torrenting last time the app ran
|
||||||
|
resumeTorrents()
|
||||||
|
|
||||||
// Lazily load the Chromecast/Airplay/DLNA modules
|
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
||||||
window.setTimeout(lazyLoadCast, 5000)
|
window.setTimeout(delayedInit, 5000)
|
||||||
|
|
||||||
// The UI is built with virtual-dom, a minimalist library extracted from React
|
// The UI is built with virtual-dom, a minimalist library extracted from React
|
||||||
// The concepts--one way data flow, a pure function that renders state to a
|
// The concepts--one way data flow, a pure function that renders state to a
|
||||||
@@ -70,6 +84,11 @@ function init () {
|
|||||||
})
|
})
|
||||||
document.body.appendChild(vdomLoop.target)
|
document.body.appendChild(vdomLoop.target)
|
||||||
|
|
||||||
|
// Calling update() updates the UI given the current state
|
||||||
|
// Do this at least once a second to give every file in every torrentSummary
|
||||||
|
// a progress bar and to keep the cursor in sync when playing a video
|
||||||
|
setInterval(update, 1000)
|
||||||
|
|
||||||
// OS integrations:
|
// OS integrations:
|
||||||
// ...drag and drop a torrent or video file to play or seed
|
// ...drag and drop a torrent or video file to play or seed
|
||||||
dragDrop('body', (files) => dispatch('onOpen', files))
|
dragDrop('body', (files) => dispatch('onOpen', files))
|
||||||
@@ -78,41 +97,66 @@ function init () {
|
|||||||
document.addEventListener('paste', onPaste)
|
document.addEventListener('paste', onPaste)
|
||||||
|
|
||||||
// ...keyboard shortcuts
|
// ...keyboard shortcuts
|
||||||
document.addEventListener('keydown', function (e) {
|
document.addEventListener('keydown', onKeyDown)
|
||||||
if (e.which === 27) { /* ESC means either exit fullscreen or go back */
|
|
||||||
if (state.modal) {
|
|
||||||
dispatch('exitModal')
|
|
||||||
} else if (state.window.isFullScreen) {
|
|
||||||
dispatch('toggleFullScreen')
|
|
||||||
} else {
|
|
||||||
dispatch('back')
|
|
||||||
}
|
|
||||||
} else if (e.which === 32) { /* spacebar pauses or plays the video */
|
|
||||||
dispatch('playPause')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
|
// ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
|
||||||
window.addEventListener('focus', function () {
|
window.addEventListener('focus', onFocus)
|
||||||
state.window.isFocused = true
|
window.addEventListener('blur', onBlur)
|
||||||
state.dock.badge = 0
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener('blur', function () {
|
|
||||||
state.window.isFocused = false
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Listen for messages from the main process
|
// Listen for messages from the main process
|
||||||
setupIpc()
|
setupIpc()
|
||||||
|
|
||||||
// Done! Ideally we want to get here <100ms after the user clicks the app
|
// Done! Ideally we want to get here <100ms after the user clicks the app
|
||||||
playInterfaceSound('STARTUP')
|
sound.play('STARTUP')
|
||||||
|
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delayedInit () {
|
||||||
|
lazyLoadCast()
|
||||||
|
sound.preload()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
function cleanUpConfig () {
|
||||||
|
state.saved.torrents.forEach(function (ts) {
|
||||||
|
var infoHash = ts.infoHash
|
||||||
|
|
||||||
|
// Migration: replace torrentPath with torrentFileName
|
||||||
|
var src, dst
|
||||||
|
if (ts.torrentPath) {
|
||||||
|
console.log('migration: replacing torrentPath %s', ts.torrentPath)
|
||||||
|
src = path.isAbsolute(ts.torrentPath)
|
||||||
|
? ts.torrentPath
|
||||||
|
: path.join(config.STATIC_PATH, ts.torrentPath)
|
||||||
|
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
|
||||||
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
|
// that only runs once
|
||||||
|
if (src !== dst) fs.copySync(src, dst)
|
||||||
|
|
||||||
|
delete ts.torrentPath
|
||||||
|
ts.torrentFileName = infoHash + '.torrent'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration: replace posterURL with posterFileName
|
||||||
|
if (ts.posterURL) {
|
||||||
|
console.log('migration: replacing posterURL %s', ts.posterURL)
|
||||||
|
var extension = path.extname(ts.posterURL)
|
||||||
|
src = path.isAbsolute(ts.posterURL)
|
||||||
|
? ts.posterURL
|
||||||
|
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||||
|
dst = path.join(config.CONFIG_POSTER_PATH, infoHash + extension)
|
||||||
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
|
// that only runs once
|
||||||
|
if (src !== dst) fs.copySync(src, dst)
|
||||||
|
|
||||||
|
delete ts.posterURL
|
||||||
|
ts.posterFileName = infoHash + extension
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Lazily loads Chromecast and Airplay support
|
// Lazily loads Chromecast and Airplay support
|
||||||
function lazyLoadCast () {
|
function lazyLoadCast () {
|
||||||
if (!Cast) {
|
if (!Cast) {
|
||||||
@@ -122,17 +166,6 @@ function lazyLoadCast () {
|
|||||||
return Cast
|
return Cast
|
||||||
}
|
}
|
||||||
|
|
||||||
// Talk to WebTorrent process, resume torrents, start monitoring torrent progress
|
|
||||||
function initWebTorrent () {
|
|
||||||
// Restart everything we were torrenting last time the app ran
|
|
||||||
resumeTorrents()
|
|
||||||
|
|
||||||
// Calling update() updates the UI given the current state
|
|
||||||
// Do this at least once a second to give every file in every torrentSummary
|
|
||||||
// a progress bar and to keep the cursor in sync when playing a video
|
|
||||||
setInterval(update, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
|
// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
|
||||||
// tree. Any events, such as button clicks, will turn into calls to dispatch()
|
// tree. Any events, such as button clicks, will turn into calls to dispatch()
|
||||||
function render (state) {
|
function render (state) {
|
||||||
@@ -178,12 +211,15 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'addTorrent') {
|
if (action === 'addTorrent') {
|
||||||
addTorrent(args[0] /* torrent */)
|
addTorrent(args[0] /* torrent */)
|
||||||
}
|
}
|
||||||
if (action === 'showCreateTorrent') {
|
if (action === 'showOpenSeedFiles') {
|
||||||
ipcRenderer.send('showCreateTorrent') /* open file or folder to seed */
|
ipcRenderer.send('showOpenSeedFiles') /* open file or folder to seed */
|
||||||
}
|
}
|
||||||
if (action === 'showOpenTorrentFile') {
|
if (action === 'showOpenTorrentFile') {
|
||||||
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
|
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
|
||||||
}
|
}
|
||||||
|
if (action === 'showCreateTorrent') {
|
||||||
|
showCreateTorrent(args[0] /* fileOrFolder */)
|
||||||
|
}
|
||||||
if (action === 'createTorrent') {
|
if (action === 'createTorrent') {
|
||||||
createTorrent(args[0] /* options */)
|
createTorrent(args[0] /* options */)
|
||||||
}
|
}
|
||||||
@@ -214,6 +250,16 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'setDimensions') {
|
if (action === 'setDimensions') {
|
||||||
setDimensions(args[0] /* dimensions */)
|
setDimensions(args[0] /* dimensions */)
|
||||||
}
|
}
|
||||||
|
if (action === 'backToList') {
|
||||||
|
while (state.location.hasBack()) state.location.back()
|
||||||
|
|
||||||
|
// Work around virtual-dom issue: it doesn't expose its redraw function,
|
||||||
|
// and only redraws on requestAnimationFrame(). That means when the user
|
||||||
|
// closes the window (hide window / minimize to tray) and we want to pause
|
||||||
|
// the video, we update the vdom but it keeps playing until you reopen!
|
||||||
|
var mediaTag = document.querySelector('video,audio')
|
||||||
|
if (mediaTag) mediaTag.pause()
|
||||||
|
}
|
||||||
if (action === 'back') {
|
if (action === 'back') {
|
||||||
state.location.back()
|
state.location.back()
|
||||||
}
|
}
|
||||||
@@ -232,17 +278,7 @@ function dispatch (action, ...args) {
|
|||||||
},
|
},
|
||||||
onbeforeunload: closePlayer
|
onbeforeunload: closePlayer
|
||||||
})
|
})
|
||||||
playPause(false)
|
play()
|
||||||
}
|
|
||||||
if (action === 'pause') {
|
|
||||||
playPause(true)
|
|
||||||
|
|
||||||
// Work around virtual-dom issue: it doesn't expose its redraw function,
|
|
||||||
// and only redraws on requestAnimationFrame(). That means when the user
|
|
||||||
// closes the window (hide window / minimize to tray) and we want to pause
|
|
||||||
// the video, we update the vdom but it keeps playing until you reopen!
|
|
||||||
var videoTag = document.querySelector('video')
|
|
||||||
if (videoTag) videoTag.pause()
|
|
||||||
}
|
}
|
||||||
if (action === 'playbackJump') {
|
if (action === 'playbackJump') {
|
||||||
jumpToTime(args[0] /* seconds */)
|
jumpToTime(args[0] /* seconds */)
|
||||||
@@ -250,17 +286,26 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'changeVolume') {
|
if (action === 'changeVolume') {
|
||||||
changeVolume(args[0] /* increase */)
|
changeVolume(args[0] /* increase */)
|
||||||
}
|
}
|
||||||
if (action === 'mediaPlaying') {
|
if (action === 'setVolume') {
|
||||||
state.playing.isPaused = false
|
setVolume(args[0] /* increase */)
|
||||||
ipcRenderer.send('blockPowerSave')
|
|
||||||
}
|
}
|
||||||
if (action === 'mediaPaused') {
|
if (action === 'openSubtitles') {
|
||||||
state.playing.isPaused = true
|
openSubtitles()
|
||||||
ipcRenderer.send('unblockPowerSave')
|
}
|
||||||
|
if (action === 'selectSubtitle') {
|
||||||
|
selectSubtitle(args[0] /* label */)
|
||||||
|
}
|
||||||
|
if (action === 'showSubtitles') {
|
||||||
|
showSubtitles()
|
||||||
}
|
}
|
||||||
if (action === 'mediaStalled') {
|
if (action === 'mediaStalled') {
|
||||||
state.playing.isStalled = true
|
state.playing.isStalled = true
|
||||||
}
|
}
|
||||||
|
if (action === 'mediaError') {
|
||||||
|
state.location.back(function () {
|
||||||
|
onError(new Error('Unsupported file format'))
|
||||||
|
})
|
||||||
|
}
|
||||||
if (action === 'mediaTimeUpdate') {
|
if (action === 'mediaTimeUpdate') {
|
||||||
state.playing.lastTimeUpdate = new Date().getTime()
|
state.playing.lastTimeUpdate = new Date().getTime()
|
||||||
state.playing.isStalled = false
|
state.playing.isStalled = false
|
||||||
@@ -301,16 +346,30 @@ function updateAvailable (version) {
|
|||||||
state.modal = { id: 'update-available-modal', version: version }
|
state.modal = { id: 'update-available-modal', version: version }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plays or pauses the video. If isPaused is undefined, acts as a toggle
|
function play () {
|
||||||
function playPause (isPaused) {
|
if (!state.playing.isPaused) return
|
||||||
if (isPaused === state.playing.isPaused) {
|
state.playing.isPaused = false
|
||||||
return // Nothing to do
|
|
||||||
}
|
|
||||||
// Either isPaused is undefined, or it's the opposite of the current state. Toggle.
|
|
||||||
if (isCasting()) {
|
if (isCasting()) {
|
||||||
Cast.playPause()
|
Cast.play()
|
||||||
|
}
|
||||||
|
ipcRenderer.send('blockPowerSave')
|
||||||
|
}
|
||||||
|
|
||||||
|
function pause () {
|
||||||
|
if (state.playing.isPaused) return
|
||||||
|
state.playing.isPaused = true
|
||||||
|
if (isCasting()) {
|
||||||
|
Cast.pause()
|
||||||
|
}
|
||||||
|
ipcRenderer.send('unblockPowerSave')
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPause () {
|
||||||
|
if (state.playing.isPaused) {
|
||||||
|
play()
|
||||||
|
} else {
|
||||||
|
pause()
|
||||||
}
|
}
|
||||||
state.playing.isPaused = !state.playing.isPaused
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function jumpToTime (time) {
|
function jumpToTime (time) {
|
||||||
@@ -326,7 +385,6 @@ function changeVolume (delta) {
|
|||||||
setVolume(state.playing.volume + delta)
|
setVolume(state.playing.volume + delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: never called. Either remove or make a volume control that calls it
|
|
||||||
function setVolume (volume) {
|
function setVolume (volume) {
|
||||||
// check if its in [0.0 - 1.0] range
|
// check if its in [0.0 - 1.0] range
|
||||||
volume = Math.max(0, Math.min(1, volume))
|
volume = Math.max(0, Math.min(1, volume))
|
||||||
@@ -337,6 +395,17 @@ function setVolume (volume) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSubtitles () {
|
||||||
|
dialog.showOpenDialog({
|
||||||
|
title: 'Select a subtitles file.',
|
||||||
|
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
||||||
|
properties: [ 'openFile' ]
|
||||||
|
}, function (filenames) {
|
||||||
|
if (!Array.isArray(filenames)) return
|
||||||
|
addSubtitle({path: filenames[0]})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Checks whether we are connected and already casting
|
// Checks whether we are connected and already casting
|
||||||
// Returns false if we not casting (state.playing.location === 'local')
|
// Returns false if we not casting (state.playing.location === 'local')
|
||||||
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
|
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
|
||||||
@@ -364,13 +433,6 @@ function setupIpc () {
|
|||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcRenderer.on('addFakeDevice', function (e, device) {
|
|
||||||
var player = new EventEmitter()
|
|
||||||
player.play = (networkURL) => console.log(networkURL)
|
|
||||||
state.devices[device] = player
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcRenderer.on('wt-infohash', (e, ...args) => torrentInfoHash(...args))
|
ipcRenderer.on('wt-infohash', (e, ...args) => torrentInfoHash(...args))
|
||||||
ipcRenderer.on('wt-metadata', (e, ...args) => torrentMetadata(...args))
|
ipcRenderer.on('wt-metadata', (e, ...args) => torrentMetadata(...args))
|
||||||
ipcRenderer.on('wt-done', (e, ...args) => torrentDone(...args))
|
ipcRenderer.on('wt-done', (e, ...args) => torrentDone(...args))
|
||||||
@@ -387,9 +449,9 @@ function setupIpc () {
|
|||||||
|
|
||||||
// Load state.saved from the JSON state file
|
// Load state.saved from the JSON state file
|
||||||
function loadState (cb) {
|
function loadState (cb) {
|
||||||
cfg.read(function (err, data) {
|
appConfig.read(function (err, data) {
|
||||||
if (err) console.error(err)
|
if (err) console.error(err)
|
||||||
console.log('loaded state from ' + cfg.filePath)
|
console.log('loaded state from ' + appConfig.filePath)
|
||||||
|
|
||||||
// populate defaults if they're not there
|
// populate defaults if they're not there
|
||||||
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
|
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
|
||||||
@@ -419,7 +481,7 @@ function saveStateThrottled () {
|
|||||||
|
|
||||||
// Write state.saved to the JSON state file
|
// Write state.saved to the JSON state file
|
||||||
function saveState () {
|
function saveState () {
|
||||||
console.log('saving state to ' + cfg.filePath)
|
console.log('saving state to ' + appConfig.filePath)
|
||||||
|
|
||||||
// Clean up, so that we're not saving any pending state
|
// Clean up, so that we're not saving any pending state
|
||||||
var copy = Object.assign({}, state.saved)
|
var copy = Object.assign({}, state.saved)
|
||||||
@@ -441,7 +503,7 @@ function saveState () {
|
|||||||
return torrent
|
return torrent
|
||||||
})
|
})
|
||||||
|
|
||||||
cfg.write(copy, function (err) {
|
appConfig.write(copy, function (err) {
|
||||||
if (err) console.error(err)
|
if (err) console.error(err)
|
||||||
ipcRenderer.send('savedState')
|
ipcRenderer.send('savedState')
|
||||||
})
|
})
|
||||||
@@ -454,23 +516,14 @@ function onOpen (files) {
|
|||||||
if (!Array.isArray(files)) files = [ files ]
|
if (!Array.isArray(files)) files = [ files ]
|
||||||
|
|
||||||
// .torrent file = start downloading the torrent
|
// .torrent file = start downloading the torrent
|
||||||
files.filter(isTorrent).forEach(function (torrentFile) {
|
files.filter(isTorrent).forEach(addTorrent)
|
||||||
addTorrent(torrentFile)
|
|
||||||
})
|
// subtitle file
|
||||||
|
files.filter(isSubtitle).forEach(addSubtitle)
|
||||||
|
|
||||||
// everything else = seed these files
|
// everything else = seed these files
|
||||||
createTorrentFromFileObjects(files.filter(isNotTorrent))
|
var rest = files.filter(not(isTorrent)).filter(not(isSubtitle))
|
||||||
}
|
if (rest.length > 0) showCreateTorrent(rest)
|
||||||
|
|
||||||
function onPaste (e) {
|
|
||||||
if (e.target.tagName.toLowerCase() === 'input') return
|
|
||||||
|
|
||||||
var torrentIds = clipboard.readText().split('\n')
|
|
||||||
torrentIds.forEach(function (torrentId) {
|
|
||||||
torrentId = torrentId.trim()
|
|
||||||
if (torrentId.length === 0) return
|
|
||||||
dispatch('addTorrent', torrentId)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTorrent (file) {
|
function isTorrent (file) {
|
||||||
@@ -480,8 +533,16 @@ function isTorrent (file) {
|
|||||||
return isTorrentFile || isMagnet
|
return isTorrentFile || isMagnet
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNotTorrent (file) {
|
function isSubtitle (file) {
|
||||||
return !isTorrent(file)
|
var name = typeof file === 'string' ? file : file.name
|
||||||
|
var ext = path.extname(name).toLowerCase()
|
||||||
|
return ext === '.srt' || ext === '.vtt'
|
||||||
|
}
|
||||||
|
|
||||||
|
function not (test) {
|
||||||
|
return function (...args) {
|
||||||
|
return !test(...args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
|
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
|
||||||
@@ -504,6 +565,48 @@ function addTorrent (torrentId) {
|
|||||||
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addSubtitle (file) {
|
||||||
|
if (state.playing.type !== 'video') return
|
||||||
|
fs.createReadStream(file.path || file).pipe(srtToVtt()).pipe(concat(function (buf) {
|
||||||
|
// Set the cue text position so it appears above the player controls.
|
||||||
|
// The only way to change cue text position is by modifying the VTT. It is not
|
||||||
|
// possible via CSS.
|
||||||
|
var langDetected = (new LanguageDetect()).detect(buf.toString().replace(/(.*-->.*)/g, ''), 2)
|
||||||
|
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
||||||
|
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
||||||
|
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
|
||||||
|
var track = {
|
||||||
|
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
|
||||||
|
label: langDetected,
|
||||||
|
selected: true
|
||||||
|
}
|
||||||
|
state.playing.subtitles.tracks.forEach(function (trackItem) {
|
||||||
|
trackItem.selected = false
|
||||||
|
if (trackItem.label === track.label) {
|
||||||
|
track.label = Number.isNaN(track.label.slice(-1))
|
||||||
|
? track.label + ' 2'
|
||||||
|
: track.label.slice(0, -1) + (parseInt(track.label.slice(-1)) + 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
state.playing.subtitles.change = track.label
|
||||||
|
state.playing.subtitles.tracks.push(track)
|
||||||
|
state.playing.subtitles.enabled = true
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSubtitle (label) {
|
||||||
|
state.playing.subtitles.tracks.forEach(function (track) {
|
||||||
|
track.selected = (track.label === label)
|
||||||
|
})
|
||||||
|
state.playing.subtitles.enabled = !!label
|
||||||
|
state.playing.subtitles.change = label
|
||||||
|
state.playing.subtitles.show = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSubtitles () {
|
||||||
|
state.playing.subtitles.show = !state.playing.subtitles.show
|
||||||
|
}
|
||||||
|
|
||||||
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
|
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
|
||||||
function startTorrentingSummary (torrentSummary) {
|
function startTorrentingSummary (torrentSummary) {
|
||||||
var s = torrentSummary
|
var s = torrentSummary
|
||||||
@@ -515,8 +618,8 @@ function startTorrentingSummary (torrentSummary) {
|
|||||||
var path = s.path || state.saved.downloadPath
|
var path = s.path || state.saved.downloadPath
|
||||||
|
|
||||||
var torrentID
|
var torrentID
|
||||||
if (s.torrentPath) { // Load torrent file from disk
|
if (s.torrentFileName) { // Load torrent file from disk
|
||||||
torrentID = util.getAbsoluteStaticPath(s.torrentPath)
|
torrentID = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
} else { // Load torrent from DHT
|
} else { // Load torrent from DHT
|
||||||
torrentID = s.magnetURI || s.infoHash
|
torrentID = s.magnetURI || s.infoHash
|
||||||
}
|
}
|
||||||
@@ -524,62 +627,58 @@ function startTorrentingSummary (torrentSummary) {
|
|||||||
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes)
|
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: maybe have a "create torrent" modal in the future, with options like
|
|
||||||
// custom trackers, private flag, and so on?
|
|
||||||
//
|
|
||||||
// Right now create-torrent-modal is v basic, only user input is OK / Cancel
|
|
||||||
//
|
|
||||||
// Also, if you uncomment below below, creating a torrent thru
|
|
||||||
// File > Create New Torrent will still create a new torrent directly, while
|
|
||||||
// dragging files or folders onto the app opens the create-torrent-modal
|
|
||||||
//
|
|
||||||
// That's because the former gets a single string and the latter gets a list
|
|
||||||
// of W3C File objects. We should fix this inconsistency, ideally without
|
|
||||||
// duping this code in the drag-drop module:
|
|
||||||
// https://github.com/feross/drag-drop/blob/master/index.js
|
|
||||||
//
|
|
||||||
// function showCreateTorrentModal (files) {
|
|
||||||
// if (files.length === 0) return
|
|
||||||
// state.modal = {
|
|
||||||
// id: 'create-torrent-modal',
|
|
||||||
// files: files
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// TORRENT MANAGEMENT
|
// TORRENT MANAGEMENT
|
||||||
// Send commands to the WebTorrent process, handle events
|
// Send commands to the WebTorrent process, handle events
|
||||||
//
|
//
|
||||||
|
|
||||||
// Creates a new torrent from a drag-dropped file or folder
|
// Shows the Create Torrent page with options to seed a given file or folder
|
||||||
function createTorrentFromFileObjects (files) {
|
function showCreateTorrent (files) {
|
||||||
var filePaths = files.map((x) => x.path)
|
if (Array.isArray(files)) {
|
||||||
|
if (state.location.pending() || state.location.current().url !== 'home') return
|
||||||
// Single-file torrents are easy. Multi-file torrents require special handling
|
state.location.go({
|
||||||
// make sure WebTorrent seeds all files in place, without copying to /tmp
|
url: 'create-torrent',
|
||||||
if (filePaths.length === 1) {
|
files: files
|
||||||
return createTorrent({files: filePaths[0]})
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, extract the base folder that the files are all in
|
var fileOrFolder = files
|
||||||
var pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
findFilesRecursive(fileOrFolder, showCreateTorrent)
|
||||||
if (files.length > 0 && !pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
}
|
||||||
pathPrefix = path.dirname(pathPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
// Recursively finds {name, path, size} for all files in a folder
|
||||||
// as the default name. Show all files relative to the base folder.
|
// Calls `cb` on success, calls `onError` on failure
|
||||||
var defaultName = path.basename(pathPrefix)
|
function findFilesRecursive (fileOrFolder, cb) {
|
||||||
var basePath = path.dirname(pathPrefix)
|
fs.stat(fileOrFolder, function (err, stat) {
|
||||||
var options = {
|
if (err) return onError(err)
|
||||||
// TODO: we can't let the user choose their own name if we want WebTorrent
|
|
||||||
// to use the files in place rather than creating a new folder.
|
|
||||||
name: defaultName,
|
|
||||||
path: basePath,
|
|
||||||
files: filePaths
|
|
||||||
}
|
|
||||||
|
|
||||||
createTorrent(options)
|
// Files: return name, path, and size
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
var filePath = fileOrFolder
|
||||||
|
return cb([{
|
||||||
|
name: path.basename(filePath),
|
||||||
|
path: filePath,
|
||||||
|
size: stat.size
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folders: recurse, make a list of all the files
|
||||||
|
var folderPath = fileOrFolder
|
||||||
|
fs.readdir(folderPath, function (err, fileNames) {
|
||||||
|
if (err) return onError(err)
|
||||||
|
var numComplete = 0
|
||||||
|
var ret = []
|
||||||
|
fileNames.forEach(function (fileName) {
|
||||||
|
findFilesRecursive(path.join(folderPath, fileName), function (fileObjs) {
|
||||||
|
ret = ret.concat(fileObjs)
|
||||||
|
if (++numComplete === fileNames.length) {
|
||||||
|
cb(ret)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new torrent and start seeeding
|
// Creates a new torrent and start seeeding
|
||||||
@@ -599,7 +698,7 @@ function torrentInfoHash (torrentKey, infoHash) {
|
|||||||
status: 'new'
|
status: 'new'
|
||||||
}
|
}
|
||||||
state.saved.torrents.push(torrentSummary)
|
state.saved.torrents.push(torrentSummary)
|
||||||
playInterfaceSound('ADD')
|
sound.play('ADD')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentSummary.infoHash = infoHash
|
torrentSummary.infoHash = infoHash
|
||||||
@@ -615,7 +714,7 @@ function torrentError (torrentKey, message) {
|
|||||||
|
|
||||||
// TODO: WebTorrent should have semantic errors
|
// TODO: WebTorrent should have semantic errors
|
||||||
if (message.startsWith('There is already a swarm')) {
|
if (message.startsWith('There is already a swarm')) {
|
||||||
onError(new Error('Couldn\'t add duplicate torrent'))
|
onError(new Error('Can\'t add duplicate torrent'))
|
||||||
} else if (!torrentSummary) {
|
} else if (!torrentSummary) {
|
||||||
onError(message)
|
onError(message)
|
||||||
} else {
|
} else {
|
||||||
@@ -637,10 +736,10 @@ function torrentMetadata (torrentKey, torrentInfo) {
|
|||||||
update()
|
update()
|
||||||
|
|
||||||
// Save the .torrent file, if it hasn't been saved already
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
if (!torrentSummary.torrentPath) ipcRenderer.send('wt-save-torrent-file', torrentKey)
|
if (!torrentSummary.torrentFileName) ipcRenderer.send('wt-save-torrent-file', torrentKey)
|
||||||
|
|
||||||
// Auto-generate a poster image, if it hasn't been generated already
|
// Auto-generate a poster image, if it hasn't been generated already
|
||||||
if (!torrentSummary.posterURL) ipcRenderer.send('wt-generate-torrent-poster', torrentKey)
|
if (!torrentSummary.posterFileName) ipcRenderer.send('wt-generate-torrent-poster', torrentKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentDone (torrentKey, torrentInfo) {
|
function torrentDone (torrentKey, torrentInfo) {
|
||||||
@@ -693,16 +792,16 @@ function torrentFileModtimes (torrentKey, fileModtimes) {
|
|||||||
saveStateThrottled()
|
saveStateThrottled()
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentFileSaved (torrentKey, torrentPath) {
|
function torrentFileSaved (torrentKey, torrentFileName) {
|
||||||
console.log('torrent file saved %s: %s', torrentKey, torrentPath)
|
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
torrentSummary.torrentPath = torrentPath
|
torrentSummary.torrentFileName = torrentFileName
|
||||||
saveStateThrottled()
|
saveStateThrottled()
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentPosterSaved (torrentKey, posterPath) {
|
function torrentPosterSaved (torrentKey, posterFileName) {
|
||||||
var torrentSummary = getTorrentSummary(torrentKey)
|
var torrentSummary = getTorrentSummary(torrentKey)
|
||||||
torrentSummary.posterURL = posterPath
|
torrentSummary.posterFileName = posterFileName
|
||||||
saveStateThrottled()
|
saveStateThrottled()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,12 +838,6 @@ function pickFileToPlay (files) {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopServer () {
|
|
||||||
ipcRenderer.send('wt-stop-server')
|
|
||||||
state.playing = State.getDefaultPlayState()
|
|
||||||
state.server = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opens the video player
|
// Opens the video player
|
||||||
function openPlayer (infoHash, index, cb) {
|
function openPlayer (infoHash, index, cb) {
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
@@ -754,13 +847,13 @@ function openPlayer (infoHash, index, cb) {
|
|||||||
if (index === undefined) return cb(new errors.UnplayableError())
|
if (index === undefined) return cb(new errors.UnplayableError())
|
||||||
|
|
||||||
// update UI to show pending playback
|
// update UI to show pending playback
|
||||||
if (torrentSummary.progress !== 1) playInterfaceSound('PLAY')
|
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
||||||
torrentSummary.playStatus = 'requested'
|
torrentSummary.playStatus = 'requested'
|
||||||
update()
|
update()
|
||||||
|
|
||||||
var timeout = setTimeout(function () {
|
var timeout = setTimeout(function () {
|
||||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||||
playInterfaceSound('ERROR')
|
sound.play('ERROR')
|
||||||
cb(new Error('playback timed out'))
|
cb(new Error('playback timed out'))
|
||||||
update()
|
update()
|
||||||
}, 10000) /* give it a few seconds */
|
}, 10000) /* give it a few seconds */
|
||||||
@@ -812,23 +905,23 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closePlayer (cb) {
|
function closePlayer (cb) {
|
||||||
state.window.title = config.APP_WINDOW_TITLE
|
|
||||||
update() /* needed for OSX: toggleFullScreen animation w/ correct title */
|
|
||||||
|
|
||||||
if (isCasting()) {
|
if (isCasting()) {
|
||||||
Cast.close()
|
Cast.close()
|
||||||
}
|
}
|
||||||
|
state.window.title = config.APP_WINDOW_TITLE
|
||||||
|
state.playing = State.getDefaultPlayState()
|
||||||
|
state.server = null
|
||||||
|
|
||||||
if (state.window.isFullScreen) {
|
if (state.window.isFullScreen) {
|
||||||
dispatch('toggleFullScreen', false)
|
dispatch('toggleFullScreen', false)
|
||||||
}
|
}
|
||||||
restoreBounds()
|
restoreBounds()
|
||||||
stopServer()
|
|
||||||
update()
|
|
||||||
|
|
||||||
|
ipcRenderer.send('wt-stop-server')
|
||||||
ipcRenderer.send('unblockPowerSave')
|
ipcRenderer.send('unblockPowerSave')
|
||||||
ipcRenderer.send('onPlayerClose')
|
ipcRenderer.send('onPlayerClose')
|
||||||
|
|
||||||
|
update()
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,11 +950,11 @@ function toggleTorrent (infoHash) {
|
|||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
torrentSummary.status = 'new'
|
torrentSummary.status = 'new'
|
||||||
startTorrentingSummary(torrentSummary)
|
startTorrentingSummary(torrentSummary)
|
||||||
playInterfaceSound('ENABLE')
|
sound.play('ENABLE')
|
||||||
} else {
|
} else {
|
||||||
torrentSummary.status = 'paused'
|
torrentSummary.status = 'paused'
|
||||||
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
||||||
playInterfaceSound('DISABLE')
|
sound.play('DISABLE')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,7 +966,7 @@ function deleteTorrent (infoHash) {
|
|||||||
if (index > -1) state.saved.torrents.splice(index, 1)
|
if (index > -1) state.saved.torrents.splice(index, 1)
|
||||||
saveStateThrottled()
|
saveStateThrottled()
|
||||||
state.location.clearForward() // prevent user from going forward to a deleted torrent
|
state.location.clearForward() // prevent user from going forward to a deleted torrent
|
||||||
playInterfaceSound('DELETE')
|
sound.play('DELETE')
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelectTorrent (infoHash) {
|
function toggleSelectTorrent (infoHash) {
|
||||||
@@ -884,18 +977,18 @@ function toggleSelectTorrent (infoHash) {
|
|||||||
|
|
||||||
function openTorrentContextMenu (infoHash) {
|
function openTorrentContextMenu (infoHash) {
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
var menu = new remote.Menu()
|
var menu = new Menu()
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: 'Save Torrent File As...',
|
label: 'Save Torrent File As...',
|
||||||
click: () => saveTorrentFileAs(torrentSummary)
|
click: () => saveTorrentFileAs(torrentSummary)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: 'Copy Instant.io Link to Clipboard',
|
label: 'Copy Instant.io Link to Clipboard',
|
||||||
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
|
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.append(new remote.MenuItem({
|
menu.append(new MenuItem({
|
||||||
label: 'Copy Magnet Link to Clipboard',
|
label: 'Copy Magnet Link to Clipboard',
|
||||||
click: () => clipboard.writeText(torrentSummary.magnetURI)
|
click: () => clipboard.writeText(torrentSummary.magnetURI)
|
||||||
}))
|
}))
|
||||||
@@ -913,8 +1006,8 @@ function saveTorrentFileAs (torrentSummary) {
|
|||||||
{ name: 'All Files', extensions: ['*'] }
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
dialog.showSaveDialog(remote.getCurrentWindow(), opts, (savePath) => {
|
dialog.showSaveDialog(remote.getCurrentWindow(), opts, function (savePath) {
|
||||||
var torrentPath = util.getAbsoluteStaticPath(torrentSummary.torrentPath)
|
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
fs.writeFile(savePath, torrentFile, function (err) {
|
fs.writeFile(savePath, torrentFile, function (err) {
|
||||||
@@ -949,15 +1042,17 @@ function setDimensions (dimensions) {
|
|||||||
Math.min(screenWidth / dimensions.width, 1),
|
Math.min(screenWidth / dimensions.width, 1),
|
||||||
Math.min(screenHeight / dimensions.height, 1)
|
Math.min(screenHeight / dimensions.height, 1)
|
||||||
)
|
)
|
||||||
var width = Math.floor(dimensions.width * scaleFactor)
|
var width = Math.max(
|
||||||
var height = Math.floor(dimensions.height * scaleFactor)
|
Math.floor(dimensions.width * scaleFactor),
|
||||||
|
config.WINDOW_MIN_WIDTH
|
||||||
// Center window on screen
|
)
|
||||||
var x = Math.floor((screenWidth - width) / 2)
|
var height = Math.max(
|
||||||
var y = Math.floor((screenHeight - height) / 2)
|
Math.floor(dimensions.height * scaleFactor),
|
||||||
|
config.WINDOW_MIN_HEIGHT
|
||||||
|
)
|
||||||
|
|
||||||
ipcRenderer.send('setAspectRatio', aspectRatio)
|
ipcRenderer.send('setAspectRatio', aspectRatio)
|
||||||
ipcRenderer.send('setBounds', {x, y, width, height})
|
ipcRenderer.send('setBounds', {x: null, y: null, width, height})
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreBounds () {
|
function restoreBounds () {
|
||||||
@@ -967,20 +1062,6 @@ function restoreBounds () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError (err) {
|
|
||||||
console.error(err.stack || err)
|
|
||||||
playInterfaceSound('ERROR')
|
|
||||||
state.errors.push({
|
|
||||||
time: new Date().getTime(),
|
|
||||||
message: err.message || err
|
|
||||||
})
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
function onWarning (err) {
|
|
||||||
console.log('warning: %s', err.message || err)
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDoneNotification (torrent) {
|
function showDoneNotification (torrent) {
|
||||||
var notif = new window.Notification('Download Complete', {
|
var notif = new window.Notification('Download Complete', {
|
||||||
body: torrent.name,
|
body: torrent.name,
|
||||||
@@ -991,35 +1072,7 @@ function showDoneNotification (torrent) {
|
|||||||
ipcRenderer.send('focusWindow', 'main')
|
ipcRenderer.send('focusWindow', 'main')
|
||||||
}
|
}
|
||||||
|
|
||||||
playInterfaceSound('DONE')
|
sound.play('DONE')
|
||||||
}
|
|
||||||
|
|
||||||
function playInterfaceSound (name) {
|
|
||||||
var sound = config[`SOUND_${name}`]
|
|
||||||
if (!sound) throw new Error('Invalid sound name')
|
|
||||||
|
|
||||||
var audio = new window.Audio()
|
|
||||||
audio.volume = sound.volume
|
|
||||||
audio.src = sound.url
|
|
||||||
audio.play()
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupCrashReporter () {
|
|
||||||
crashReporter.start({
|
|
||||||
companyName: config.APP_NAME,
|
|
||||||
productName: config.APP_NAME,
|
|
||||||
submitURL: config.CRASH_REPORT_URL
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finds the longest common prefix
|
|
||||||
function findCommonPrefix (a, b) {
|
|
||||||
for (var i = 0; i < a.length && i < b.length; i++) {
|
|
||||||
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
|
||||||
}
|
|
||||||
if (i === a.length) return a
|
|
||||||
if (i === b.length) return b
|
|
||||||
return a.substring(0, i)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide player controls while playing video, if the mouse stays still for a while
|
// Hide player controls while playing video, if the mouse stays still for a while
|
||||||
@@ -1040,3 +1093,54 @@ function showOrHidePlayerControls () {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
function onError (err) {
|
||||||
|
console.error(err.stack || err)
|
||||||
|
sound.play('ERROR')
|
||||||
|
state.errors.push({
|
||||||
|
time: new Date().getTime(),
|
||||||
|
message: err.message || err
|
||||||
|
})
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWarning (err) {
|
||||||
|
console.log('warning: %s', err.message || err)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPaste (e) {
|
||||||
|
if (e.target.tagName.toLowerCase() === 'input') return
|
||||||
|
|
||||||
|
var torrentIds = clipboard.readText().split('\n')
|
||||||
|
torrentIds.forEach(function (torrentId) {
|
||||||
|
torrentId = torrentId.trim()
|
||||||
|
if (torrentId.length === 0) return
|
||||||
|
dispatch('addTorrent', torrentId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown (e) {
|
||||||
|
if (e.which === 27) { /* ESC means either exit fullscreen or go back */
|
||||||
|
if (state.modal) {
|
||||||
|
dispatch('exitModal')
|
||||||
|
} else if (state.window.isFullScreen) {
|
||||||
|
dispatch('toggleFullScreen')
|
||||||
|
} else {
|
||||||
|
dispatch('back')
|
||||||
|
}
|
||||||
|
} else if (e.which === 32) { /* spacebar pauses or plays the video */
|
||||||
|
dispatch('playPause')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFocus (e) {
|
||||||
|
state.window.isFocused = true
|
||||||
|
state.dock.badge = 0
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBlur () {
|
||||||
|
state.window.isFocused = false
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ module.exports = {
|
|||||||
init,
|
init,
|
||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
playPause,
|
play,
|
||||||
|
pause,
|
||||||
seek,
|
seek,
|
||||||
setVolume
|
setVolume
|
||||||
}
|
}
|
||||||
@@ -81,9 +82,12 @@ function chromecastPlayer (player) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function playPause (callback) {
|
function play (callback) {
|
||||||
if (!state.playing.isPaused) player.pause(callback)
|
player.play(null, null, callback)
|
||||||
else player.play(null, null, callback)
|
}
|
||||||
|
|
||||||
|
function pause (callback) {
|
||||||
|
player.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
@@ -113,7 +117,8 @@ function chromecastPlayer (player) {
|
|||||||
return {
|
return {
|
||||||
player: player,
|
player: player,
|
||||||
open: open,
|
open: open,
|
||||||
playPause: playPause,
|
play: play,
|
||||||
|
pause: pause,
|
||||||
stop: stop,
|
stop: stop,
|
||||||
status: status,
|
status: status,
|
||||||
seek: seek,
|
seek: seek,
|
||||||
@@ -138,9 +143,12 @@ function airplayPlayer (player) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function playPause (callback) {
|
function play (callback) {
|
||||||
if (!state.playing.isPaused) player.rate(0, callback)
|
player.rate(1, callback)
|
||||||
else player.rate(1, callback)
|
}
|
||||||
|
|
||||||
|
function pause (callback) {
|
||||||
|
player.rate(0, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
@@ -172,7 +180,8 @@ function airplayPlayer (player) {
|
|||||||
return {
|
return {
|
||||||
player: player,
|
player: player,
|
||||||
open: open,
|
open: open,
|
||||||
playPause: playPause,
|
play: play,
|
||||||
|
pause: pause,
|
||||||
stop: stop,
|
stop: stop,
|
||||||
status: status,
|
status: status,
|
||||||
seek: seek,
|
seek: seek,
|
||||||
@@ -217,9 +226,12 @@ function dlnaPlayer (player) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function playPause (callback) {
|
function play (callback) {
|
||||||
if (!state.playing.isPaused) player.pause(callback)
|
player.play(null, null, callback)
|
||||||
else player.play(null, null, callback)
|
}
|
||||||
|
|
||||||
|
function pause (callback) {
|
||||||
|
player.pause(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
@@ -253,7 +265,8 @@ function dlnaPlayer (player) {
|
|||||||
return {
|
return {
|
||||||
player: player,
|
player: player,
|
||||||
open: open,
|
open: open,
|
||||||
playPause: playPause,
|
play: play,
|
||||||
|
pause: pause,
|
||||||
stop: stop,
|
stop: stop,
|
||||||
status: status,
|
status: status,
|
||||||
seek: seek,
|
seek: seek,
|
||||||
@@ -317,10 +330,17 @@ function getDevice (location) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function playPause () {
|
function play () {
|
||||||
var device = getDevice()
|
var device = getDevice()
|
||||||
if (device) {
|
if (device) {
|
||||||
device.playPause(castCallback)
|
device.play(castCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pause () {
|
||||||
|
var device = getDevice()
|
||||||
|
if (device) {
|
||||||
|
device.pause(castCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ function dispatcher (...args) {
|
|||||||
var json = JSON.stringify(args)
|
var json = JSON.stringify(args)
|
||||||
var handler = _dispatchers[json]
|
var handler = _dispatchers[json]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
_dispatchers[json] = (e) => {
|
handler = _dispatchers[json] = (e) => {
|
||||||
// Don't click on whatever is below the button
|
if (e && e.stopPropagation && e.currentTarget) {
|
||||||
e.stopPropagation()
|
// Don't click on whatever is below the button
|
||||||
// Don't regisiter clicks on disabled buttons
|
e.stopPropagation()
|
||||||
if (e.currentTarget.classList.contains('disabled')) return
|
// Don't register clicks on disabled buttons
|
||||||
|
if (e.currentTarget.classList.contains('disabled')) return
|
||||||
|
}
|
||||||
_dispatch.apply(null, args)
|
_dispatch.apply(null, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,46 +7,56 @@ function LocationHistory () {
|
|||||||
this._pending = null
|
this._pending = null
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.go = function (page) {
|
LocationHistory.prototype.go = function (page, cb) {
|
||||||
console.log('go', page)
|
console.log('go', page)
|
||||||
this.clearForward()
|
this.clearForward()
|
||||||
this._go(page)
|
this._go(page, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype._go = function (page) {
|
LocationHistory.prototype._go = function (page, cb) {
|
||||||
if (this._pending) return
|
if (this._pending) return
|
||||||
if (page.onbeforeload) {
|
if (page.onbeforeload) {
|
||||||
this._pending = page
|
this._pending = page
|
||||||
page.onbeforeload((err) => {
|
page.onbeforeload((err) => {
|
||||||
if (this._pending !== page) return /* navigation was cancelled */
|
if (this._pending !== page) return /* navigation was cancelled */
|
||||||
this._pending = null
|
this._pending = null
|
||||||
if (err) return
|
if (err) {
|
||||||
|
if (cb) cb(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
this._history.push(page)
|
this._history.push(page)
|
||||||
|
if (cb) cb()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this._history.push(page)
|
this._history.push(page)
|
||||||
|
if (cb) cb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.back = function () {
|
LocationHistory.prototype.back = function (cb) {
|
||||||
if (this._history.length <= 1) return
|
if (this._history.length <= 1) return
|
||||||
|
|
||||||
var page = this._history.pop()
|
var page = this._history.pop()
|
||||||
|
|
||||||
if (page.onbeforeunload) {
|
if (page.onbeforeunload) {
|
||||||
|
// TODO: this is buggy. If the user clicks back twice, then those pages
|
||||||
|
// may end up in _forward in the wrong order depending on which onbeforeunload
|
||||||
|
// call finishes first.
|
||||||
page.onbeforeunload(() => {
|
page.onbeforeunload(() => {
|
||||||
this._forward.push(page)
|
this._forward.push(page)
|
||||||
|
if (cb) cb()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this._forward.push(page)
|
this._forward.push(page)
|
||||||
|
if (cb) cb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.forward = function () {
|
LocationHistory.prototype.forward = function (cb) {
|
||||||
if (this._forward.length === 0) return
|
if (this._forward.length === 0) return
|
||||||
|
|
||||||
var page = this._forward.pop()
|
var page = this._forward.pop()
|
||||||
this._go(page)
|
this._go(page, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.clearForward = function () {
|
LocationHistory.prototype.clearForward = function () {
|
||||||
|
|||||||
71
renderer/lib/sound.js
Normal file
71
renderer/lib/sound.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
module.exports = {
|
||||||
|
preload,
|
||||||
|
play
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = require('../../config')
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
|
/* Cache of Audio elements, for instant playback */
|
||||||
|
var cache = {}
|
||||||
|
|
||||||
|
var sounds = {
|
||||||
|
ADD: {
|
||||||
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
|
||||||
|
volume: 0.2
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
||||||
|
volume: 0.1
|
||||||
|
},
|
||||||
|
DISABLE: {
|
||||||
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
||||||
|
volume: 0.2
|
||||||
|
},
|
||||||
|
DONE: {
|
||||||
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
|
||||||
|
volume: 0.2
|
||||||
|
},
|
||||||
|
ENABLE: {
|
||||||
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
|
||||||
|
volume: 0.2
|
||||||
|
},
|
||||||
|
ERROR: {
|
||||||
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
|
||||||
|
volume: 0.2
|
||||||
|
},
|
||||||
|
PLAY: {
|
||||||
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
|
||||||
|
volume: 0.2
|
||||||
|
},
|
||||||
|
STARTUP: {
|
||||||
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
||||||
|
volume: 0.4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preload () {
|
||||||
|
for (var name in sounds) {
|
||||||
|
if (!cache[name]) {
|
||||||
|
var sound = sounds[name]
|
||||||
|
var audio = cache[name] = new window.Audio()
|
||||||
|
audio.volume = sound.volume
|
||||||
|
audio.src = sound.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function play (name) {
|
||||||
|
var audio = cache[name]
|
||||||
|
if (!audio) {
|
||||||
|
var sound = sounds[name]
|
||||||
|
if (!sound) {
|
||||||
|
throw new Error('Invalid sound name')
|
||||||
|
}
|
||||||
|
audio = cache[name] = new window.Audio()
|
||||||
|
audio.volume = sound.volume
|
||||||
|
audio.src = sound.url
|
||||||
|
}
|
||||||
|
audio.currentTime = 0
|
||||||
|
audio.play()
|
||||||
|
}
|
||||||
@@ -15,12 +15,12 @@ function isPlayable (file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isVideo (file) {
|
function isVideo (file) {
|
||||||
var ext = path.extname(file.name)
|
var ext = path.extname(file.name).toLowerCase()
|
||||||
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
|
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAudio (file) {
|
function isAudio (file) {
|
||||||
var ext = path.extname(file.name)
|
var ext = path.extname(file.name).toLowerCase()
|
||||||
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
|
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function torrentPoster (torrent, cb) {
|
|||||||
|
|
||||||
function getLargestFileByExtension (torrent, extensions) {
|
function getLargestFileByExtension (torrent, extensions) {
|
||||||
var files = torrent.files.filter(function (file) {
|
var files = torrent.files.filter(function (file) {
|
||||||
var extname = path.extname(file.name)
|
var extname = path.extname(file.name).toLowerCase()
|
||||||
return extensions.indexOf(extname) !== -1
|
return extensions.indexOf(extname) !== -1
|
||||||
})
|
})
|
||||||
if (files.length === 0) return undefined
|
if (files.length === 0) return undefined
|
||||||
@@ -64,6 +64,8 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
|||||||
|
|
||||||
server.destroy()
|
server.destroy()
|
||||||
|
|
||||||
|
if (buf.length === 0) return cb(new Error('Generated poster contains no data'))
|
||||||
|
|
||||||
cb(null, buf, '.jpg')
|
cb(null, buf, '.jpg')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
renderer/lib/torrent-summary.js
Normal file
24
renderer/lib/torrent-summary.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
module.exports = {
|
||||||
|
getPosterPath,
|
||||||
|
getTorrentPath
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = require('path')
|
||||||
|
var 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.CONFIG_TORRENT_PATH, torrentSummary.torrentFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expects a torrentSummary
|
||||||
|
// Returns an absolute path to the poster image, or null if unavailable
|
||||||
|
function getPosterPath (torrentSummary) {
|
||||||
|
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
||||||
|
var posterPath = path.join(config.CONFIG_POSTER_PATH, torrentSummary.posterFileName)
|
||||||
|
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
||||||
|
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||||
|
return posterPath.replace(/\\/g, '/')
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
var os = require('os')
|
var electron = require('electron')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
|
var remote = electron.remote
|
||||||
|
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var LocationHistory = require('./lib/location-history')
|
var LocationHistory = require('./lib/location-history')
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ function getInitialState () {
|
|||||||
/*
|
/*
|
||||||
* Saved state is read from and written to a file every time the app runs.
|
* Saved state is read from and written to a file every time the app runs.
|
||||||
* It should be simple and minimal and must be JSON.
|
* It should be simple and minimal and must be JSON.
|
||||||
|
* It must never contain absolute paths since we have a portable app.
|
||||||
*
|
*
|
||||||
* Config path:
|
* Config path:
|
||||||
*
|
*
|
||||||
@@ -70,7 +73,11 @@ function getDefaultPlayState () {
|
|||||||
isPaused: true,
|
isPaused: true,
|
||||||
isStalled: false,
|
isStalled: false,
|
||||||
lastTimeUpdate: 0, /* Unix time in ms */
|
lastTimeUpdate: 0, /* Unix time in ms */
|
||||||
mouseStationarySince: 0 /* Unix time in ms */
|
mouseStationarySince: 0, /* Unix time in ms */
|
||||||
|
subtitles: {
|
||||||
|
tracks: [], /* subtitles file (Buffer) */
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,10 +95,8 @@ function getDefaultSavedState () {
|
|||||||
torrentPath: 'bigBuckBunny.torrent',
|
torrentPath: 'bigBuckBunny.torrent',
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
'name': 'bbb_sunflower_1080p_30fps_normal.mp4',
|
length: 276134947,
|
||||||
'length': 276134947,
|
name: 'bbb_sunflower_1080p_30fps_normal.mp4'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 527
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -104,10 +109,8 @@ function getDefaultSavedState () {
|
|||||||
torrentPath: 'sintel.torrent',
|
torrentPath: 'sintel.torrent',
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
'name': 'sintel.mp4',
|
length: 129241752,
|
||||||
'length': 129241752,
|
name: 'sintel.mp4'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 987
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -120,10 +123,8 @@ function getDefaultSavedState () {
|
|||||||
torrentPath: 'tearsOfSteel.torrent',
|
torrentPath: 'tearsOfSteel.torrent',
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
'name': 'tears_of_steel_1080p.webm',
|
length: 571346576,
|
||||||
'length': 571346576,
|
name: 'tears_of_steel_1080p.webm'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 2180
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -136,62 +137,128 @@ function getDefaultSavedState () {
|
|||||||
torrentPath: 'cosmosLaundromat.torrent',
|
torrentPath: 'cosmosLaundromat.torrent',
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
'name': 'Cosmos Laundromat - First Cycle (1080p).gif',
|
length: 223580,
|
||||||
'length': 223580,
|
name: 'Cosmos Laundromat - First Cycle (1080p).gif'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Cosmos Laundromat - First Cycle (1080p).mp4',
|
length: 220087570,
|
||||||
'length': 220087570,
|
name: 'Cosmos Laundromat - First Cycle (1080p).mp4'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 421
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'Cosmos Laundromat - First Cycle (1080p).ogv',
|
length: 56832560,
|
||||||
'length': 56832560,
|
name: 'Cosmos Laundromat - First Cycle (1080p).ogv'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 109
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'CosmosLaundromat-FirstCycle1080p.en.srt',
|
length: 3949,
|
||||||
'length': 3949,
|
name: 'CosmosLaundromat-FirstCycle1080p.en.srt'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'CosmosLaundromat-FirstCycle1080p.es.srt',
|
length: 3907,
|
||||||
'length': 3907,
|
name: 'CosmosLaundromat-FirstCycle1080p.es.srt'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'CosmosLaundromat-FirstCycle1080p.fr.srt',
|
length: 4119,
|
||||||
'length': 4119,
|
name: 'CosmosLaundromat-FirstCycle1080p.fr.srt'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'CosmosLaundromat-FirstCycle1080p.it.srt',
|
length: 3941,
|
||||||
'length': 3941,
|
name: 'CosmosLaundromat-FirstCycle1080p.it.srt'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'CosmosLaundromatFirstCycle_meta.sqlite',
|
length: 11264,
|
||||||
'length': 11264,
|
name: 'CosmosLaundromatFirstCycle_meta.sqlite'
|
||||||
'numPiecesPresent': 0,
|
|
||||||
'numPieces': 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'CosmosLaundromatFirstCycle_meta.xml',
|
length: 1204,
|
||||||
'length': 1204,
|
name: 'CosmosLaundromatFirstCycle_meta.xml'
|
||||||
'numPiecesPresent': 0,
|
}
|
||||||
'numPieces': 1
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 'paused',
|
||||||
|
infoHash: '3ba219a8634bf7bae3d848192b2da75ae995589d',
|
||||||
|
magnetURI: 'magnet:?xt=urn:btih:3ba219a8634bf7bae3d848192b2da75ae995589d&dn=The+WIRED+CD+-+Rip.+Sample.+Mash.+Share.&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F',
|
||||||
|
displayName: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
||||||
|
posterURL: 'wired-cd.jpg',
|
||||||
|
torrentPath: 'wired-cd.torrent',
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
length: 1964275,
|
||||||
|
name: '01 - Beastie Boys - Now Get Busy.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 3610523,
|
||||||
|
name: '02 - David Byrne - My Fair Lady.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 2759377,
|
||||||
|
name: '03 - Zap Mama - Wadidyusay.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 5816537,
|
||||||
|
name: '04 - My Morning Jacket - One Big Holiday.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 2106421,
|
||||||
|
name: '05 - Spoon - Revenge!.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 3347550,
|
||||||
|
name: '06 - Gilberto Gil - Oslodum.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 2107577,
|
||||||
|
name: '07 - Dan The Automator - Relaxation Spa Treatment.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 3108130,
|
||||||
|
name: '08 - Thievery Corporation - Dc 3000.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 3051528,
|
||||||
|
name: '09 - Le Tigre - Fake French.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 3270259,
|
||||||
|
name: '10 - Paul Westerberg - Looking Up In Heaven.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 3263528,
|
||||||
|
name: '11 - Chuck D - No Meaning No (feat. Fine Arts Militia).mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 6380952,
|
||||||
|
name: '12 - The Rapture - Sister Saviour (Blackstrobe Remix).mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 6550396,
|
||||||
|
name: '13 - Cornelius - Wataridori 2.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 3034692,
|
||||||
|
name: '14 - DJ Danger Mouse - What U Sittin\' On (feat. Jemini, Cee Lo And Tha Alkaholiks).mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 3854611,
|
||||||
|
name: '15 - DJ Dolores - Oslodum 2004.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 1762120,
|
||||||
|
name: '16 - Matmos - Action At A Distance.mp3'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 4071,
|
||||||
|
name: 'README.md'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
length: 78163,
|
||||||
|
name: 'poster.jpg'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
downloadPath: path.join(os.homedir(), 'Downloads')
|
downloadPath: config.IS_PORTABLE
|
||||||
|
? path.join(config.CONFIG_PATH, 'Downloads')
|
||||||
|
: remote.app.getPath('downloads')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
var path = require('path')
|
|
||||||
|
|
||||||
var config = require('../config')
|
|
||||||
|
|
||||||
exports.getAbsoluteStaticPath = function (filePath) {
|
|
||||||
return path.isAbsolute(filePath)
|
|
||||||
? filePath
|
|
||||||
: path.join(config.STATIC_PATH, filePath)
|
|
||||||
}
|
|
||||||
@@ -5,15 +5,17 @@ var hyperx = require('hyperx')
|
|||||||
var hx = hyperx(h)
|
var hx = hyperx(h)
|
||||||
|
|
||||||
var Header = require('./header')
|
var Header = require('./header')
|
||||||
var Player = require('./player')
|
var Views = {
|
||||||
var TorrentList = require('./torrent-list')
|
'home': require('./torrent-list'),
|
||||||
|
'player': require('./player'),
|
||||||
|
'create-torrent': require('./create-torrent-page')
|
||||||
|
}
|
||||||
var Modals = {
|
var Modals = {
|
||||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||||
'update-available-modal': require('./update-available-modal'),
|
'update-available-modal': require('./update-available-modal')
|
||||||
'create-torrent-modal': require('./create-torrent-modal')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function App (state, dispatch) {
|
function App (state) {
|
||||||
// Hide player controls while playing video, if the mouse stays still for a while
|
// Hide player controls while playing video, if the mouse stays still for a while
|
||||||
// Never hide the controls when:
|
// Never hide the controls when:
|
||||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||||
@@ -40,47 +42,43 @@ function App (state, dispatch) {
|
|||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='app ${cls.join(' ')}'>
|
<div class='app ${cls.join(' ')}'>
|
||||||
${Header(state, dispatch)}
|
${Header(state)}
|
||||||
${getErrorPopover()}
|
${getErrorPopover(state)}
|
||||||
<div class='content'>${getView()}</div>
|
<div class='content'>${getView(state)}</div>
|
||||||
${getModal()}
|
${getModal(state)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
}
|
||||||
function getErrorPopover () {
|
|
||||||
var now = new Date().getTime()
|
function getErrorPopover (state) {
|
||||||
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
var now = new Date().getTime()
|
||||||
|
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
||||||
var errorElems = recentErrors.map(function (error) {
|
|
||||||
return hx`<div class='error'>${error.message}</div>`
|
var errorElems = recentErrors.map(function (error) {
|
||||||
})
|
return hx`<div class='error'>${error.message}</div>`
|
||||||
return hx`
|
})
|
||||||
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
|
return hx`
|
||||||
<div class='title'>Error</div>
|
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
|
||||||
${errorElems}
|
<div class='title'>Error</div>
|
||||||
</div>
|
${errorElems}
|
||||||
`
|
</div>
|
||||||
}
|
`
|
||||||
|
}
|
||||||
function getModal () {
|
|
||||||
if (state.modal) {
|
function getModal (state) {
|
||||||
var contents = Modals[state.modal.id](state, dispatch)
|
if (!state.modal) return
|
||||||
return hx`
|
var contents = Modals[state.modal.id](state)
|
||||||
<div class='modal'>
|
return hx`
|
||||||
<div class='modal-background'></div>
|
<div class='modal'>
|
||||||
<div class='modal-content'>
|
<div class='modal-background'></div>
|
||||||
${contents}
|
<div class='modal-content'>
|
||||||
</div>
|
${contents}
|
||||||
</div>
|
</div>
|
||||||
`
|
</div>
|
||||||
}
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getView () {
|
function getView (state) {
|
||||||
if (state.location.current().url === 'home') {
|
var url = state.location.current().url
|
||||||
return TorrentList(state, dispatch)
|
return Views[url](state)
|
||||||
} else if (state.location.current().url === 'player') {
|
|
||||||
return Player(state, dispatch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
module.exports = UpdateAvailableModal
|
|
||||||
|
|
||||||
var h = require('virtual-dom/h')
|
|
||||||
var hyperx = require('hyperx')
|
|
||||||
var hx = hyperx(h)
|
|
||||||
|
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
var {dispatch} = require('../lib/dispatcher')
|
|
||||||
|
|
||||||
function UpdateAvailableModal (state) {
|
|
||||||
// First, extract the base folder that the files are all in
|
|
||||||
var files = state.modal.files
|
|
||||||
var pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
|
||||||
if (files.length > 0 && !pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
|
||||||
pathPrefix = path.dirname(pathPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
|
||||||
// as the default name. Show all files relative to the base folder.
|
|
||||||
var defaultName = path.basename(pathPrefix)
|
|
||||||
var basePath = path.dirname(pathPrefix)
|
|
||||||
var fileElems = files.map(function (file) {
|
|
||||||
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
|
|
||||||
return hx`<div>${relativePath}</div>`
|
|
||||||
})
|
|
||||||
|
|
||||||
return hx`
|
|
||||||
<div class='create-torrent-modal'>
|
|
||||||
<p><strong>Create New Torrent</strong></p>
|
|
||||||
<p class='torrent-attribute'>
|
|
||||||
<label>Name:</label>
|
|
||||||
<div class='torrent-attribute'>${defaultName}</div>
|
|
||||||
</p>
|
|
||||||
<p class='torrent-attribute'>
|
|
||||||
<label>Path:</label>
|
|
||||||
<div class='torrent-attribute'>${pathPrefix}</div>
|
|
||||||
</p>
|
|
||||||
<p class='torrent-attribute'>
|
|
||||||
<label>Files:</label>
|
|
||||||
<div>${fileElems}</div>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<button class='primary' onclick=${handleOK}>Create Torrent</button>
|
|
||||||
<button class='cancel' onclick=${handleCancel}>Cancel</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
|
|
||||||
function handleOK () {
|
|
||||||
var options = {
|
|
||||||
// TODO: we can't let the user choose their own name if we want WebTorrent
|
|
||||||
// to use the files in place rather than creating a new folder.
|
|
||||||
// name: document.querySelector('.torrent-name').value
|
|
||||||
name: defaultName,
|
|
||||||
path: basePath,
|
|
||||||
files: files
|
|
||||||
}
|
|
||||||
dispatch('createTorrent', options)
|
|
||||||
dispatch('exitModal')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCancel () {
|
|
||||||
dispatch('exitModal')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finds the longest common prefix
|
|
||||||
function findCommonPrefix (a, b) {
|
|
||||||
for (var i = 0; i < a.length && i < b.length; i++) {
|
|
||||||
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
|
||||||
}
|
|
||||||
if (i === a.length) return a
|
|
||||||
if (i === b.length) return b
|
|
||||||
return a.substring(0, i)
|
|
||||||
}
|
|
||||||
138
renderer/views/create-torrent-page.js
Normal file
138
renderer/views/create-torrent-page.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
module.exports = CreateTorrentPage
|
||||||
|
|
||||||
|
var h = require('virtual-dom/h')
|
||||||
|
var hyperx = require('hyperx')
|
||||||
|
var hx = hyperx(h)
|
||||||
|
|
||||||
|
var createTorrent = require('create-torrent')
|
||||||
|
var path = require('path')
|
||||||
|
var prettyBytes = require('prettier-bytes')
|
||||||
|
|
||||||
|
var {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
function CreateTorrentPage (state) {
|
||||||
|
var info = state.location.current()
|
||||||
|
|
||||||
|
// Preprocess: exclude .DS_Store and other dotfiles
|
||||||
|
var files = info.files
|
||||||
|
.filter((f) => !f.name.startsWith('.'))
|
||||||
|
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
||||||
|
|
||||||
|
// First, extract the base folder that the files are all in
|
||||||
|
var pathPrefix = info.folderPath
|
||||||
|
if (!pathPrefix) {
|
||||||
|
if (files.length > 0) {
|
||||||
|
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
||||||
|
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||||
|
pathPrefix = path.dirname(pathPrefix)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pathPrefix = files[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check: show the number of files and total size
|
||||||
|
var numFiles = files.length
|
||||||
|
console.log('FILES', files)
|
||||||
|
var totalBytes = files
|
||||||
|
.map((f) => f.size)
|
||||||
|
.reduce((a, b) => a + b, 0)
|
||||||
|
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
|
||||||
|
|
||||||
|
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
||||||
|
// as the default name. Show all files relative to the base folder.
|
||||||
|
var defaultName = path.basename(pathPrefix)
|
||||||
|
var basePath = path.dirname(pathPrefix)
|
||||||
|
var maxFileElems = 100
|
||||||
|
var fileElems = files.slice(0, maxFileElems).map(function (file) {
|
||||||
|
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
|
||||||
|
return hx`<div>${relativePath}</div>`
|
||||||
|
})
|
||||||
|
if (files.length > maxFileElems) {
|
||||||
|
fileElems.push(hx`<div>+ ${maxFileElems - files.length} more</div>`)
|
||||||
|
}
|
||||||
|
var trackers = createTorrent.announceList.join('\n')
|
||||||
|
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
|
||||||
|
|
||||||
|
return hx`
|
||||||
|
<div class='create-torrent-page'>
|
||||||
|
<h2>Create torrent ${defaultName}</h2>
|
||||||
|
<p class="torrent-info">
|
||||||
|
${torrentInfo}
|
||||||
|
</p>
|
||||||
|
<p class='torrent-attribute'>
|
||||||
|
<label>Path:</label>
|
||||||
|
<div class='torrent-attribute'>${pathPrefix}</div>
|
||||||
|
</p>
|
||||||
|
<div class='expand-collapse ${collapsedClass}' onclick=${handleToggleShowAdvanced}>
|
||||||
|
${info.showAdvanced ? 'Basic' : 'Advanced'}
|
||||||
|
</div>
|
||||||
|
<div class="create-torrent-advanced ${collapsedClass}">
|
||||||
|
<p class='torrent-attribute'>
|
||||||
|
<label>Comment:</label>
|
||||||
|
<textarea class='torrent-attribute torrent-comment'></textarea>
|
||||||
|
</p>
|
||||||
|
<p class='torrent-attribute'>
|
||||||
|
<label>Trackers:</label>
|
||||||
|
<textarea class='torrent-attribute torrent-trackers'>${trackers}</textarea>
|
||||||
|
</p>
|
||||||
|
<p class='torrent-attribute'>
|
||||||
|
<label>Private:</label>
|
||||||
|
<input type='checkbox' class='torrent-is-private' value='torrent-is-private'>
|
||||||
|
</p>
|
||||||
|
<p class='torrent-attribute'>
|
||||||
|
<label>Files:</label>
|
||||||
|
<div>${fileElems}</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p class="float-right">
|
||||||
|
<button class='button-flat light' onclick=${handleCancel}>Cancel</button>
|
||||||
|
<button class='button-raised' onclick=${handleOK}>Create Torrent</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
function handleOK () {
|
||||||
|
var announceList = document.querySelector('.torrent-trackers').value
|
||||||
|
.split('\n')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s !== '')
|
||||||
|
var isPrivate = document.querySelector('.torrent-is-private').checked
|
||||||
|
var comment = document.querySelector('.torrent-comment').value.trim()
|
||||||
|
var options = {
|
||||||
|
// We can't let the user choose their own name if we want WebTorrent
|
||||||
|
// to use the files in place rather than creating a new folder.
|
||||||
|
// If we ever want to add support for that:
|
||||||
|
// name: document.querySelector('.torrent-name').value
|
||||||
|
name: defaultName,
|
||||||
|
path: basePath,
|
||||||
|
files: files,
|
||||||
|
announce: announceList,
|
||||||
|
private: isPrivate,
|
||||||
|
comment: comment
|
||||||
|
}
|
||||||
|
dispatch('createTorrent', options)
|
||||||
|
dispatch('backToList')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCancel () {
|
||||||
|
dispatch('backToList')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleShowAdvanced () {
|
||||||
|
// TODO: what's the clean way to handle this?
|
||||||
|
// Should every button on every screen have its own dispatch()?
|
||||||
|
info.showAdvanced = !info.showAdvanced
|
||||||
|
dispatch('update')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the longest common prefix
|
||||||
|
function findCommonPrefix (a, b) {
|
||||||
|
for (var i = 0; i < a.length && i < b.length; i++) {
|
||||||
|
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
||||||
|
}
|
||||||
|
if (i === a.length) return a
|
||||||
|
if (i === b.length) return b
|
||||||
|
return a.substring(0, i)
|
||||||
|
}
|
||||||
@@ -9,12 +9,15 @@ var {dispatch} = require('../lib/dispatcher')
|
|||||||
function OpenTorrentAddressModal (state) {
|
function OpenTorrentAddressModal (state) {
|
||||||
return hx`
|
return hx`
|
||||||
<div class='open-torrent-address-modal'>
|
<div class='open-torrent-address-modal'>
|
||||||
<p><strong>Enter torrent address or magnet link</strong></p>
|
<p><label>Enter torrent address or magnet link</label></p>
|
||||||
<p>
|
<p>
|
||||||
<input id='add-torrent-url' type='text' autofocus onkeypress=${handleKeyPress} />
|
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
|
||||||
<button class='primary' onclick=${handleOK}>OK</button>
|
|
||||||
<button class='cancel' onclick=${handleCancel}>Cancel</button>
|
|
||||||
</p>
|
</p>
|
||||||
|
<p class='float-right'>
|
||||||
|
<button class='button button-flat' onclick=${handleCancel}>CANCEL</button>
|
||||||
|
<button class='button button-raised' onclick=${handleOK}>OK</button>
|
||||||
|
</p>
|
||||||
|
<script>document.querySelector('#add-torrent-url').focus()</script>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ var h = require('virtual-dom/h')
|
|||||||
var hyperx = require('hyperx')
|
var hyperx = require('hyperx')
|
||||||
var hx = hyperx(h)
|
var hx = hyperx(h)
|
||||||
|
|
||||||
|
var WebChimeraPlayer = require('wcjs-player')
|
||||||
var prettyBytes = require('prettier-bytes')
|
var prettyBytes = require('prettier-bytes')
|
||||||
var Bitfield = require('bitfield')
|
var Bitfield = require('bitfield')
|
||||||
|
|
||||||
var util = require('../util')
|
var TorrentSummary = require('../lib/torrent-summary')
|
||||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||||
@@ -27,11 +28,17 @@ function Player (state) {
|
|||||||
|
|
||||||
function renderMedia (state) {
|
function renderMedia (state) {
|
||||||
if (!state.server) return
|
if (!state.server) return
|
||||||
|
if (false) return renderMediaTag(state)
|
||||||
|
else return renderMediaVLC(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders using a <video> or <audio> tag
|
||||||
|
// Handles only a subset of codecs, but it's cleaner and more efficient
|
||||||
|
// See renderMediaVLC()
|
||||||
|
function renderMediaTag (state) {
|
||||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||||
// Instead, grab the DOM node and play/pause it if necessary
|
// Instead, grab the DOM node and play/pause it if necessary
|
||||||
var mediaType = state.playing.type /* 'audio' or 'video' */
|
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
|
||||||
var mediaElement = document.querySelector(mediaType) /* get the <video> or <audio> tag */
|
|
||||||
if (mediaElement !== null) {
|
if (mediaElement !== null) {
|
||||||
if (state.playing.isPaused && !mediaElement.paused) {
|
if (state.playing.isPaused && !mediaElement.paused) {
|
||||||
mediaElement.pause()
|
mediaElement.pause()
|
||||||
@@ -49,11 +56,36 @@ function renderMedia (state) {
|
|||||||
state.playing.setVolume = null
|
state.playing.setVolume = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fix textTrack cues not been removed <track> rerender
|
||||||
|
if (state.playing.subtitles.change) {
|
||||||
|
var tracks = mediaElement.textTracks
|
||||||
|
for (var j = 0; j < tracks.length; j++) {
|
||||||
|
// mode is not an <track> attribute, only available on DOM
|
||||||
|
tracks[j].mode = (tracks[j].label === state.playing.subtitles.change) ? 'showing' : 'hidden'
|
||||||
|
}
|
||||||
|
state.playing.subtitles.change = null
|
||||||
|
}
|
||||||
|
|
||||||
state.playing.currentTime = mediaElement.currentTime
|
state.playing.currentTime = mediaElement.currentTime
|
||||||
state.playing.duration = mediaElement.duration
|
state.playing.duration = mediaElement.duration
|
||||||
state.playing.volume = mediaElement.volume
|
state.playing.volume = mediaElement.volume
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add subtitles to the <video> tag
|
||||||
|
var trackTags = []
|
||||||
|
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
|
||||||
|
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||||
|
var track = state.playing.subtitles.tracks[i]
|
||||||
|
trackTags.push(hx`
|
||||||
|
<track
|
||||||
|
${track.selected ? 'default' : ''}
|
||||||
|
label=${track.label}
|
||||||
|
type='subtitles'
|
||||||
|
src=${track.buffer}>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the <audio> or <video> tag
|
// Create the <audio> or <video> tag
|
||||||
var mediaTag = hx`
|
var mediaTag = hx`
|
||||||
<div
|
<div
|
||||||
@@ -61,14 +93,111 @@ function renderMedia (state) {
|
|||||||
ondblclick=${dispatcher('toggleFullScreen')}
|
ondblclick=${dispatcher('toggleFullScreen')}
|
||||||
onloadedmetadata=${onLoadedMetadata}
|
onloadedmetadata=${onLoadedMetadata}
|
||||||
onended=${onEnded}
|
onended=${onEnded}
|
||||||
onplay=${dispatcher('mediaPlaying')}
|
|
||||||
onpause=${dispatcher('mediaPaused')}
|
|
||||||
onstalling=${dispatcher('mediaStalled')}
|
onstalling=${dispatcher('mediaStalled')}
|
||||||
|
onerror=${dispatcher('mediaError')}
|
||||||
ontimeupdate=${dispatcher('mediaTimeUpdate')}
|
ontimeupdate=${dispatcher('mediaTimeUpdate')}
|
||||||
autoplay>
|
autoplay>
|
||||||
|
${trackTags}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
mediaTag.tagName = state.playing.type // conditional tag name
|
||||||
|
|
||||||
|
// Show the media.
|
||||||
|
return hx`
|
||||||
|
<div
|
||||||
|
class='letterbox'
|
||||||
|
onmousemove=${dispatcher('mediaMouseMoved')}>
|
||||||
|
${mediaTag}
|
||||||
|
${renderOverlay(state)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
// As soon as the video loads enough to know the video dimensions, resize the window
|
||||||
|
function onLoadedMetadata (e) {
|
||||||
|
if (state.playing.type !== 'video') return
|
||||||
|
var video = e.target
|
||||||
|
var dimensions = {
|
||||||
|
width: video.videoWidth,
|
||||||
|
height: video.videoHeight
|
||||||
|
}
|
||||||
|
dispatch('setDimensions', dimensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When the video completes, pause the video instead of looping
|
||||||
|
function onEnded (e) {
|
||||||
|
state.playing.isPaused = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders using WebChimera.js to render using VLC
|
||||||
|
// That lets us play media that the <video> tag can't play
|
||||||
|
function renderMediaVLC (state) {
|
||||||
|
// Unfortunately, WebChimera can't be done just by modifying HTML.
|
||||||
|
// Instead, grab the DOM node
|
||||||
|
if (document.querySelector('#media-player')) {
|
||||||
|
if (!state.playing.chimera) {
|
||||||
|
state.playing.chimera = new WebChimeraPlayer('#media-player')
|
||||||
|
.addPlayer({
|
||||||
|
autoplay: true,
|
||||||
|
vlcArgs: ['-vvv'],
|
||||||
|
wcjsRendererOptions: {'fallbackRenderer': true}
|
||||||
|
})
|
||||||
|
.onPlaying(dispatcher('mediaPlaying'))
|
||||||
|
.onPaused(dispatcher('mediaPaused'))
|
||||||
|
.onBuffering(dispatcher('mediaStalled'))
|
||||||
|
.onTime(dispatcher('mediaTimeUpdate'))
|
||||||
|
.onEnded(onEnded)
|
||||||
|
.onFrameSetup(onLoadedMetadata)
|
||||||
|
.addPlaylist(state.server.localURL)
|
||||||
|
state.playing.chimera.ui(false)
|
||||||
|
} else {
|
||||||
|
var player = state.playing.chimera
|
||||||
|
if (state.playing.isPaused && player.playing()) {
|
||||||
|
player.pause()
|
||||||
|
} else if (!state.playing.isPaused && !player.playing()) {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
// When the user clicks or drags on the progress bar, jump to that position
|
||||||
|
if (state.playing.jumpToTime) {
|
||||||
|
player.time(state.playing.jumpToTime * 1000) // WebChimera expects milliseconds
|
||||||
|
state.playing.jumpToTime = null
|
||||||
|
}
|
||||||
|
// Set volume
|
||||||
|
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
||||||
|
player.volume(Math.round(state.playing.setVolume * 100)) // WebChimera expects integer percent
|
||||||
|
state.playing.setVolume = null
|
||||||
|
}
|
||||||
|
|
||||||
|
state.playing.currentTime = player.time() / 1000
|
||||||
|
state.playing.duration = player.length() / 1000
|
||||||
|
state.playing.volume = player.volume() / 100
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.playing.chimera = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subtitles to the <video> tag
|
||||||
|
var trackTags = []
|
||||||
|
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
|
||||||
|
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||||
|
var track = state.playing.subtitles.tracks[i]
|
||||||
|
trackTags.push(hx`
|
||||||
|
<track
|
||||||
|
default=${track.selected ? 'default' : ''}
|
||||||
|
label=${track.language}
|
||||||
|
type='subtitles'
|
||||||
|
src=${track.buffer}>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the <audio> or <video> tag
|
||||||
|
var mediaType = state.playing.type /* 'video' or 'audio' */
|
||||||
|
var mediaTag = hx`
|
||||||
|
<div id='media-player' class='${mediaType}-player'>
|
||||||
|
${trackTags}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
mediaTag.tagName = mediaType
|
|
||||||
|
|
||||||
// Show the media.
|
// Show the media.
|
||||||
return hx`
|
return hx`
|
||||||
@@ -83,10 +212,9 @@ function renderMedia (state) {
|
|||||||
// As soon as the video loads enough to know the video dimensions, resize the window
|
// As soon as the video loads enough to know the video dimensions, resize the window
|
||||||
function onLoadedMetadata (e) {
|
function onLoadedMetadata (e) {
|
||||||
if (mediaType !== 'video') return
|
if (mediaType !== 'video') return
|
||||||
var video = e.target
|
|
||||||
var dimensions = {
|
var dimensions = {
|
||||||
width: video.videoWidth,
|
width: player.width(),
|
||||||
height: video.videoHeight
|
height: player.height()
|
||||||
}
|
}
|
||||||
dispatch('setDimensions', dimensions)
|
dispatch('setDimensions', dimensions)
|
||||||
}
|
}
|
||||||
@@ -213,9 +341,27 @@ function renderCastScreen (state) {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSubtitlesOptions (state) {
|
||||||
|
var subtitles = state.playing.subtitles
|
||||||
|
if (subtitles.tracks.length && subtitles.show) {
|
||||||
|
return hx`<ul.subtitles-list>
|
||||||
|
${subtitles.tracks.map(function (w, i) {
|
||||||
|
return hx`<li onclick=${dispatcher('selectSubtitle', w.label)}><i.icon>${w.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>${w.label}</li>`
|
||||||
|
})}
|
||||||
|
<li onclick=${dispatcher('selectSubtitle', '')}><i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>None</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderPlayerControls (state) {
|
function renderPlayerControls (state) {
|
||||||
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
||||||
|
var captionsClass = state.playing.subtitles.tracks.length === 0
|
||||||
|
? 'disabled'
|
||||||
|
: state.playing.subtitles.enabled
|
||||||
|
? 'active'
|
||||||
|
: ''
|
||||||
|
|
||||||
var elements = [
|
var elements = [
|
||||||
hx`
|
hx`
|
||||||
@@ -236,6 +382,17 @@ function renderPlayerControls (state) {
|
|||||||
`
|
`
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (state.playing.type === 'video') {
|
||||||
|
// show closed captions icon
|
||||||
|
elements.push(hx`
|
||||||
|
<i.icon.closed-captions
|
||||||
|
class=${captionsClass}
|
||||||
|
onclick=${handleSubtitles}>
|
||||||
|
closed_captions
|
||||||
|
</i>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||||
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
||||||
var isOnAirplay = state.playing.location.startsWith('airplay')
|
var isOnAirplay = state.playing.location.startsWith('airplay')
|
||||||
@@ -310,6 +467,31 @@ function renderPlayerControls (state) {
|
|||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// render volume
|
||||||
|
var volume = state.playing.volume
|
||||||
|
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
|
||||||
|
var volumeStyle = { background: '-webkit-gradient(linear, left top, right top, ' +
|
||||||
|
'color-stop(' + (volume * 100) + '%, #eee), ' +
|
||||||
|
'color-stop(' + (volume * 100) + '%, #727272))'
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.push(hx`
|
||||||
|
<div.volume
|
||||||
|
onwheel=${handleVolumeWheel}>
|
||||||
|
<i.icon.volume-icon onmousedown=${handleVolumeMute}>
|
||||||
|
${volumeIcon}
|
||||||
|
</i>
|
||||||
|
<input.volume-slider
|
||||||
|
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
|
||||||
|
onmousedown=${handleVolumeScrub}
|
||||||
|
onmouseup=${handleVolumeScrub}
|
||||||
|
onmousemove=${handleVolumeScrub}
|
||||||
|
onwheel=${handleVolumeWheel}
|
||||||
|
style=${volumeStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
|
||||||
// Finally, the big button in the center plays or pauses the video
|
// Finally, the big button in the center plays or pauses the video
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i class='icon play-pause' onclick=${dispatcher('playPause')}>
|
<i class='icon play-pause' onclick=${dispatcher('playPause')}>
|
||||||
@@ -317,7 +499,12 @@ function renderPlayerControls (state) {
|
|||||||
</i>
|
</i>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
return hx`<div class='player-controls'>${elements}</div>`
|
return hx`
|
||||||
|
<div class='player-controls'>
|
||||||
|
${elements}
|
||||||
|
${renderSubtitlesOptions(state)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
// Handles a click or drag to scrub (jump to another position in the video)
|
// Handles a click or drag to scrub (jump to another position in the video)
|
||||||
function handleScrub (e) {
|
function handleScrub (e) {
|
||||||
@@ -327,8 +514,53 @@ function renderPlayerControls (state) {
|
|||||||
var position = fraction * state.playing.duration /* seconds */
|
var position = fraction * state.playing.duration /* seconds */
|
||||||
dispatch('playbackJump', position)
|
dispatch('playbackJump', position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles volume change by wheel
|
||||||
|
function handleVolumeWheel (e) {
|
||||||
|
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles volume muting and Unmuting
|
||||||
|
function handleVolumeMute (e) {
|
||||||
|
if (state.playing.volume === 0.0) {
|
||||||
|
dispatch('setVolume', 1.0)
|
||||||
|
} else {
|
||||||
|
dispatch('setVolume', 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles volume slider scrub
|
||||||
|
function handleVolumeScrub (e) {
|
||||||
|
switch (e.type) {
|
||||||
|
case 'mouseup':
|
||||||
|
volumeChanging = false
|
||||||
|
dispatch('setVolume', e.offsetX / 50)
|
||||||
|
break
|
||||||
|
case 'mousedown':
|
||||||
|
volumeChanging = this.value
|
||||||
|
break
|
||||||
|
case 'mousemove':
|
||||||
|
// only change if move was started by click
|
||||||
|
if (volumeChanging !== false) {
|
||||||
|
volumeChanging = this.value
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubtitles (e) {
|
||||||
|
if (!state.playing.subtitles.tracks.length || e.ctrlKey || e.metaKey) {
|
||||||
|
// if no subtitles available select it
|
||||||
|
dispatch('openSubtitles')
|
||||||
|
} else {
|
||||||
|
dispatch('showSubtitles')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lets scrub without sending to volume backend
|
||||||
|
var volumeChanging = false
|
||||||
|
|
||||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||||
// can be "spongey" / non-contiguous
|
// can be "spongey" / non-contiguous
|
||||||
function renderLoadingBar (state) {
|
function renderLoadingBar (state) {
|
||||||
@@ -370,10 +602,9 @@ function renderLoadingBar (state) {
|
|||||||
// Returns the CSS background-image string for a poster image + dark vignette
|
// Returns the CSS background-image string for a poster image + dark vignette
|
||||||
function cssBackgroundImagePoster (state) {
|
function cssBackgroundImagePoster (state) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var torrentSummary = getPlayingTorrentSummary(state)
|
||||||
if (!torrentSummary || !torrentSummary.posterURL) return ''
|
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
|
if (!posterPath) return ''
|
||||||
var cleanURL = posterURL.replace(/\\/g, '/')
|
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
||||||
return cssBackgroundImageDarkGradient() + `, url(${cleanURL})`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cssBackgroundImageDarkGradient () {
|
function cssBackgroundImageDarkGradient () {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ var hyperx = require('hyperx')
|
|||||||
var hx = hyperx(h)
|
var hx = hyperx(h)
|
||||||
var prettyBytes = require('prettier-bytes')
|
var prettyBytes = require('prettier-bytes')
|
||||||
|
|
||||||
var util = require('../util')
|
var TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
|
||||||
var TorrentPlayer = require('../lib/torrent-player')
|
var TorrentPlayer = require('../lib/torrent-player')
|
||||||
var {dispatcher} = require('../lib/dispatcher')
|
var {dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
@@ -31,15 +30,12 @@ function TorrentList (state) {
|
|||||||
|
|
||||||
// Background image: show some nice visuals, like a frame from the movie, if possible
|
// Background image: show some nice visuals, like a frame from the movie, if possible
|
||||||
var style = {}
|
var style = {}
|
||||||
if (torrentSummary.posterURL) {
|
if (torrentSummary.posterFileName) {
|
||||||
var gradient = isSelected
|
var gradient = isSelected
|
||||||
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
||||||
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
|
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
|
||||||
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
|
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
style.backgroundImage = gradient + `, url('${posterPath}')`
|
||||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
|
||||||
var cleanURL = posterURL.replace(/\\/g, '/')
|
|
||||||
style.backgroundImage = gradient + `, url('${cleanURL}')`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Foreground: name of the torrent, basic info like size, play button,
|
// Foreground: name of the torrent, basic info like size, play button,
|
||||||
@@ -141,7 +137,7 @@ function TorrentList (state) {
|
|||||||
var playButton
|
var playButton
|
||||||
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
||||||
playButton = hx`
|
playButton = hx`
|
||||||
<i.btn.icon.play
|
<i.button-round.icon.play
|
||||||
title=${playTooltip}
|
title=${playTooltip}
|
||||||
class=${playClass}
|
class=${playClass}
|
||||||
onclick=${dispatcher('play', infoHash)}>
|
onclick=${dispatcher('play', infoHash)}>
|
||||||
@@ -153,7 +149,7 @@ function TorrentList (state) {
|
|||||||
return hx`
|
return hx`
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
${playButton}
|
${playButton}
|
||||||
<i.btn.icon.download
|
<i.button-round.icon.download
|
||||||
class=${torrentSummary.status}
|
class=${torrentSummary.status}
|
||||||
title=${downloadTooltip}
|
title=${downloadTooltip}
|
||||||
onclick=${dispatcher('toggleTorrent', infoHash)}>
|
onclick=${dispatcher('toggleTorrent', infoHash)}>
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ var WebTorrent = require('webtorrent')
|
|||||||
var defaultAnnounceList = require('create-torrent').announceList
|
var defaultAnnounceList = require('create-torrent').announceList
|
||||||
var deepEqual = require('deep-equal')
|
var deepEqual = require('deep-equal')
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
var fs = require('fs')
|
var fs = require('fs-extra')
|
||||||
var mkdirp = require('mkdirp')
|
|
||||||
var musicmetadata = require('musicmetadata')
|
var musicmetadata = require('musicmetadata')
|
||||||
var networkAddress = require('network-address')
|
var networkAddress = require('network-address')
|
||||||
|
var path = require('path')
|
||||||
|
|
||||||
|
var crashReporter = require('../crash-reporter')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var torrentPoster = require('./lib/torrent-poster')
|
var torrentPoster = require('./lib/torrent-poster')
|
||||||
var path = require('path')
|
|
||||||
|
// Report when the process crashes
|
||||||
|
crashReporter.init()
|
||||||
|
|
||||||
// Send & receive messages from the main window
|
// Send & receive messages from the main window
|
||||||
var ipc = electron.ipcRenderer
|
var ipc = electron.ipcRenderer
|
||||||
@@ -64,10 +68,21 @@ function init () {
|
|||||||
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||||
function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
|
function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
|
||||||
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
||||||
var torrent = client.add(torrentID, {
|
var torrent
|
||||||
path: path,
|
try {
|
||||||
fileModtimes: fileModtimes
|
torrent = client.add(torrentID, {
|
||||||
})
|
path: path,
|
||||||
|
fileModtimes: fileModtimes
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
return ipc.send('wt-error', torrentKey, err.message)
|
||||||
|
}
|
||||||
|
// If we add a duplicate magnet URI or infohash, WebTorrent returns the
|
||||||
|
// existing torrent object! (If we add a duplicate torrent file, it creates a
|
||||||
|
// new torrent object and raises an error later.) Workaround:
|
||||||
|
if (torrent.key) {
|
||||||
|
return ipc.send('wt-error', torrentKey, 'Can\'t add duplicate torrent')
|
||||||
|
}
|
||||||
torrent.key = torrentKey
|
torrent.key = torrentKey
|
||||||
addTorrentEvents(torrent)
|
addTorrentEvents(torrent)
|
||||||
return torrent
|
return torrent
|
||||||
@@ -80,8 +95,9 @@ function stopTorrenting (infoHash) {
|
|||||||
|
|
||||||
// Create a new torrent, start seeding
|
// Create a new torrent, start seeding
|
||||||
function createTorrent (torrentKey, options) {
|
function createTorrent (torrentKey, options) {
|
||||||
console.log('creating torrent %s', torrentKey, options)
|
console.log('creating torrent', torrentKey, options)
|
||||||
var torrent = client.seed(options.files, options)
|
var paths = options.files.map((f) => f.path)
|
||||||
|
var torrent = client.seed(paths, options)
|
||||||
torrent.key = torrentKey
|
torrent.key = torrentKey
|
||||||
addTorrentEvents(torrent)
|
addTorrentEvents(torrent)
|
||||||
ipc.send('wt-new-torrent')
|
ipc.send('wt-new-torrent')
|
||||||
@@ -154,9 +170,10 @@ function getTorrentFileInfo (file) {
|
|||||||
function saveTorrentFile (torrentKey) {
|
function saveTorrentFile (torrentKey) {
|
||||||
var torrent = getTorrent(torrentKey)
|
var torrent = getTorrent(torrentKey)
|
||||||
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
|
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
|
||||||
|
var fileName = torrent.infoHash + '.torrent'
|
||||||
if (exists) {
|
if (exists) {
|
||||||
// We've already saved the file
|
// We've already saved the file
|
||||||
return ipc.send('wt-file-saved', torrentKey, torrentPath)
|
return ipc.send('wt-file-saved', torrentKey, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, save the .torrent file, under the app config folder
|
// Otherwise, save the .torrent file, under the app config folder
|
||||||
@@ -164,7 +181,7 @@ function saveTorrentFile (torrentKey) {
|
|||||||
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||||
console.log('saved torrent file %s', torrentPath)
|
console.log('saved torrent file %s', torrentPath)
|
||||||
return ipc.send('wt-file-saved', torrentKey, torrentPath)
|
return ipc.send('wt-file-saved', torrentKey, fileName)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -186,13 +203,14 @@ function generateTorrentPoster (torrentKey) {
|
|||||||
torrentPoster(torrent, function (err, buf, extension) {
|
torrentPoster(torrent, function (err, buf, extension) {
|
||||||
if (err) return console.log('error generating poster: %o', err)
|
if (err) return console.log('error generating poster: %o', err)
|
||||||
// save it for next time
|
// save it for next time
|
||||||
mkdirp(config.CONFIG_POSTER_PATH, function (err) {
|
fs.mkdirp(config.CONFIG_POSTER_PATH, function (err) {
|
||||||
if (err) return console.log('error creating poster dir: %o', err)
|
if (err) return console.log('error creating poster dir: %o', err)
|
||||||
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, torrent.infoHash + extension)
|
var posterFileName = torrent.infoHash + extension
|
||||||
|
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, posterFileName)
|
||||||
fs.writeFile(posterFilePath, buf, function (err) {
|
fs.writeFile(posterFilePath, buf, function (err) {
|
||||||
if (err) return console.log('error saving poster: %o', err)
|
if (err) return console.log('error saving poster: %o', err)
|
||||||
// show the poster
|
// show the poster
|
||||||
ipc.send('wt-poster', torrentKey, posterFilePath)
|
ipc.send('wt-poster', torrentKey, posterFileName)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
BIN
static/WebTorrentSmaller.png
Normal file
BIN
static/WebTorrentSmaller.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
chmod +x /opt/webtorrent-desktop/WebTorrent
|
chmod +x /opt/webtorrent-desktop/WebTorrent
|
||||||
ln -s /opt/webtorrent-desktop/WebTorrent /usr/bin/webtorrent-desktop
|
ln -s -f /opt/webtorrent-desktop/WebTorrent /usr/bin/webtorrent-desktop
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 726 KiB |
BIN
static/wired-cd.jpg
Normal file
BIN
static/wired-cd.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
BIN
static/wired-cd.torrent
Normal file
BIN
static/wired-cd.torrent
Normal file
Binary file not shown.
Reference in New Issue
Block a user