Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d61968f64 | ||
|
|
8637de27b9 | ||
|
|
447413e4b9 | ||
|
|
82c8ad7562 | ||
|
|
f2bbd97eeb | ||
|
|
c788b3358a | ||
|
|
b0672cce9e | ||
|
|
0681169653 | ||
|
|
ae6b86d233 | ||
|
|
8bba565609 | ||
|
|
78f08487c4 | ||
|
|
bdf7110135 | ||
|
|
9d35ece954 | ||
|
|
00e4cc1864 | ||
|
|
ad09012587 | ||
|
|
8b5de572f1 | ||
|
|
20c6b81047 | ||
|
|
aecead4a2d | ||
|
|
7b02edca0f | ||
|
|
109094d0e1 | ||
|
|
e0856a5274 | ||
|
|
c8886fb606 | ||
|
|
fd5f4dd139 | ||
|
|
0a51da13a4 | ||
|
|
5540ed9ce1 | ||
|
|
b6516dc40f | ||
|
|
8b57e13735 | ||
|
|
cb3dd716dd | ||
|
|
4895fb930c | ||
|
|
1f2985bbc3 | ||
|
|
32ad0f0926 | ||
|
|
3e448da0ba | ||
|
|
219e717021 | ||
|
|
1885b6a89e | ||
|
|
d8a5b8a701 | ||
|
|
d41e08b209 | ||
|
|
9518670c7b | ||
|
|
eff0b6eb23 | ||
|
|
f56af6402c | ||
|
|
ebcc814ca7 | ||
|
|
f7029c811c | ||
|
|
a578d7555f | ||
|
|
dff969f955 | ||
|
|
b964240a20 | ||
|
|
7f7a395d67 | ||
|
|
475ef8c6d0 | ||
|
|
e269489639 | ||
|
|
20d5d5d60d | ||
|
|
590dc99c51 | ||
|
|
10e9b36aea | ||
|
|
5250f55bf7 |
48
CHANGELOG.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# WebTorrent.app Version History
|
||||||
|
|
||||||
|
## v0.1.0
|
||||||
|
|
||||||
|
- **Windows support!**
|
||||||
|
- See `WebTorrentSetup.exe` in the downloads below!
|
||||||
|
- Auto-updater included, just like the OS X version.
|
||||||
|
- Automatically installs desktop/start menu shortcuts
|
||||||
|
- Windows top menu is no longer automatically hidden.
|
||||||
|
- **Audio file support!**
|
||||||
|
- Supports playback of .mp3, .aac, .ogg, .wav
|
||||||
|
- Audio file metadata gets shown in the UI
|
||||||
|
- Focus the WebTorrent window after opening magnet link in third-party app
|
||||||
|
- Subtler app sounds
|
||||||
|
- Fix for some magnet links failing to open
|
||||||
|
|
||||||
|
Thanks to @dcposch, @ngoldman, and @feross for contributing to this release.
|
||||||
|
|
||||||
|
## v0.0.1
|
||||||
|
|
||||||
|
- Wait 10 seconds (instead of 60 seconds) after app launch before checking for updates.
|
||||||
|
|
||||||
|
## v0.0.0
|
||||||
|
|
||||||
|
The first official release of WebTorrent.app, the streaming torrent client for OS X,
|
||||||
|
Windows, and Linux. For now, we're only releasing binaries for OS X.
|
||||||
|
|
||||||
|
WebTorrent.app is in ALPHA and under very active development – expect lots more polish in
|
||||||
|
the coming weeks! If you know JavaScript and want to help us out, there's
|
||||||
|
[lots to do](https://github.com/feross/webtorrent-app/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+contribution%22)!
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Lightweight, fast torrent client**
|
||||||
|
- **Beautiful user experience**
|
||||||
|
- **Instantly stream video and audio** from torrents!
|
||||||
|
- WebTorrent fetches file pieces from the network **on-demand**, for instant playback.
|
||||||
|
- Even when the file is not fully downloaded, **seeking still works!** (Seeking just reprioritizes what pieces are fetched from the network.)
|
||||||
|
- Stream videos to **AirPlay** and **Chromecast**
|
||||||
|
- **Pure Javascript**, so it's very easy to contribute code!
|
||||||
|
- Based on the most popular and comprehensive torrent package in Node.js, [`webtorrent`](https://www.npmjs.com/package/webtorrent).
|
||||||
|
- Lots of **features**, without the bloat:
|
||||||
|
- Opens magnet links and .torrent files
|
||||||
|
- Drag-and-drop makes adding torrents easy!
|
||||||
|
- Seed files/folders by dragging them onto the app
|
||||||
|
- Discovers peers via tracker servers, DHT (Distributed Hash Table), and peer exchange
|
||||||
|
- Make the video window "float on top" for watching video while you work!
|
||||||
|
- Supports WebTorrent protocol – for connecting to WebRTC peers (i.e. web browsers)
|
||||||
18
README.md
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
**WebTorrent.app** is still under very active development. Expect a release very soon!
|
**WebTorrent.app** is still under very active development. An [alpha release](https://github.com/feross/webtorrent-app/releases) is currently available for OS X.
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||
@@ -58,10 +58,20 @@ To build for one platform:
|
|||||||
$ npm run package -- [platform]
|
$ npm run package -- [platform]
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `[platform]` is `--darwin`, `--linux`, or `--win32`.
|
Where `[platform]` is `darwin`, `linux`, or `win32`.
|
||||||
|
|
||||||
To package a Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs
|
#### Windows build notes
|
||||||
to be installed. On OS X, it is installable via [Homebrew](http://brew.sh/).
|
|
||||||
|
To package the Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs
|
||||||
|
to be installed.
|
||||||
|
|
||||||
|
On OS X, first install [XQuartz](http://www.xquartz.org/), then run:
|
||||||
|
|
||||||
|
```
|
||||||
|
brew install wine
|
||||||
|
```
|
||||||
|
|
||||||
|
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
||||||
|
|
||||||
### Code Style
|
### Code Style
|
||||||
|
|
||||||
|
|||||||
120
bin/package.js
@@ -15,11 +15,11 @@ var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
|||||||
|
|
||||||
function build () {
|
function build () {
|
||||||
var platform = process.argv[2]
|
var platform = process.argv[2]
|
||||||
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(printDone)
|
buildLinux(printDone)
|
||||||
} else {
|
} else {
|
||||||
buildDarwin(function (err, buildPath) {
|
buildDarwin(function (err, buildPath) {
|
||||||
@@ -59,7 +59,7 @@ 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|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|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,
|
||||||
@@ -82,14 +82,14 @@ var darwin = {
|
|||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
|
|
||||||
// The bundle identifier to use in the application's plist (OS X only).
|
// The bundle identifier to use in the application's plist (OS X only).
|
||||||
'app-bundle-id': 'io.webtorrent.app',
|
'app-bundle-id': 'io.webtorrent.webtorrent',
|
||||||
|
|
||||||
// The application category type, as shown in the Finder via "View" -> "Arrange by
|
// The application category type, as shown in the Finder via "View" -> "Arrange by
|
||||||
// Application Category" when viewing the Applications directory (OS X only).
|
// Application Category" when viewing the Applications directory (OS X only).
|
||||||
'app-category-type': 'public.app-category.utilities',
|
'app-category-type': 'public.app-category.utilities',
|
||||||
|
|
||||||
// The bundle identifier to use in the application helper's plist (OS X only).
|
// The bundle identifier to use in the application helper's plist (OS X only).
|
||||||
'helper-bundle-id': 'io.webtorrent.app.helper',
|
'helper-bundle-id': 'io.webtorrent.webtorrent-helper',
|
||||||
|
|
||||||
// Application icon.
|
// Application icon.
|
||||||
icon: config.APP_ICON + '.icns'
|
icon: config.APP_ICON + '.icns'
|
||||||
@@ -137,9 +137,7 @@ var linux = {
|
|||||||
build()
|
build()
|
||||||
|
|
||||||
function buildDarwin (cb) {
|
function buildDarwin (cb) {
|
||||||
var appDmg = require('appdmg')
|
|
||||||
var plist = require('plist')
|
var plist = require('plist')
|
||||||
var sign = require('electron-osx-sign')
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -180,56 +178,70 @@ function buildDarwin (cb) {
|
|||||||
infoPlist.NSHumanReadableCopyright = config.APP_COPYRIGHT
|
infoPlist.NSHumanReadableCopyright = config.APP_COPYRIGHT
|
||||||
|
|
||||||
fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
|
fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
|
||||||
|
|
||||||
|
// Copy torrent file icon into app bundle
|
||||||
cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`)
|
cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`)
|
||||||
|
|
||||||
var zipPath = path.join(buildPath[0], BUILD_NAME + '.zip')
|
|
||||||
cp.execSync(`zip -r -y ${zipPath} ${appPath}`)
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Signing OS X apps for distribution outside the App Store requires:
|
|
||||||
*
|
|
||||||
* - Xcode
|
|
||||||
* - Xcode Command Line Tools (xcode-select --install)
|
|
||||||
* - Membership in the Apple Developer Program
|
|
||||||
*/
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
|
var appDmg = require('appdmg')
|
||||||
|
var sign = require('electron-osx-sign')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sign the app with Apple Developer ID certificate. We sign the app for 2 reasons:
|
||||||
|
* - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by
|
||||||
|
* the same author as the current version.
|
||||||
|
* - So users will not a see a warning about the app coming from an "Unidentified
|
||||||
|
* Developer" when they open it for the first time (OS X Gatekeeper).
|
||||||
|
*
|
||||||
|
* To sign an OS X app for distribution outside the App Store, the following are
|
||||||
|
* required:
|
||||||
|
* - Xcode
|
||||||
|
* - Xcode Command Line Tools (xcode-select --install)
|
||||||
|
* - Membership in the Apple Developer Program
|
||||||
|
*/
|
||||||
var signOpts = {
|
var signOpts = {
|
||||||
app: appPath,
|
app: appPath,
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
verbose: true
|
verbose: true
|
||||||
}
|
}
|
||||||
|
|
||||||
var dmgPath = path.join(buildPath[0], BUILD_NAME + '.dmg')
|
|
||||||
var dmgOpts = {
|
|
||||||
basepath: config.ROOT_PATH,
|
|
||||||
target: dmgPath,
|
|
||||||
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' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sign(signOpts, function (err) {
|
sign(signOpts, function (err) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
|
|
||||||
|
// Create .zip file (used by the auto-updater)
|
||||||
|
var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.zip')
|
||||||
|
cp.execSync(`pushd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'} && popd`)
|
||||||
|
console.log('Created OS X .zip file.')
|
||||||
|
|
||||||
|
// Create a .dmg (OS X disk image) file, for easy user installation.
|
||||||
|
var dmgOpts = {
|
||||||
|
basepath: config.ROOT_PATH,
|
||||||
|
target: path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg'),
|
||||||
|
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.')
|
||||||
cb(null, buildPath)
|
cb(null, buildPath)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -238,7 +250,33 @@ function buildDarwin (cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildWin32 (cb) {
|
function buildWin32 (cb) {
|
||||||
electronPackager(Object.assign({}, all, win32), cb)
|
var installer = require('electron-winstaller')
|
||||||
|
|
||||||
|
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
|
||||||
|
console.log('Creating Windows installer...')
|
||||||
|
installer.createWindowsInstaller({
|
||||||
|
name: config.APP_NAME,
|
||||||
|
productName: config.APP_NAME,
|
||||||
|
title: config.APP_NAME,
|
||||||
|
exe: config.APP_NAME + '.exe',
|
||||||
|
|
||||||
|
appDirectory: buildPath[0],
|
||||||
|
outputDirectory: path.join(config.ROOT_PATH, 'dist'),
|
||||||
|
version: pkg.version,
|
||||||
|
description: config.APP_NAME,
|
||||||
|
authors: config.APP_TEAM,
|
||||||
|
iconUrl: config.APP_ICON + '.ico',
|
||||||
|
setupIcon: config.APP_ICON + '.ico',
|
||||||
|
// certificateFile: '', // TODO
|
||||||
|
usePackageJson: false,
|
||||||
|
loadingGif: path.join(config.STATIC_PATH, 'loading.gif')
|
||||||
|
}).then(function () {
|
||||||
|
console.log('Created Windows installer.')
|
||||||
|
cb(null, buildPath)
|
||||||
|
}).catch(cb)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLinux (cb) {
|
function buildLinux (cb) {
|
||||||
|
|||||||
8
bin/release-_post.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
git diff --exit-code
|
||||||
|
npm run package
|
||||||
|
git push
|
||||||
|
git push --tags
|
||||||
|
gh-release
|
||||||
9
bin/release-_pre.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
git pull
|
||||||
|
npm run update-authors
|
||||||
|
git diff --exit-code
|
||||||
|
rm -rf node_modules/
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
7
bin/release-major.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
BIN=`dirname $0`
|
||||||
|
|
||||||
|
$BIN/release-_pre.sh
|
||||||
|
npm version major
|
||||||
|
$BIN/release-_post.sh
|
||||||
7
bin/release-minor.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
BIN=`dirname $0`
|
||||||
|
|
||||||
|
$BIN/release-_pre.sh
|
||||||
|
npm version minor
|
||||||
|
$BIN/release-_post.sh
|
||||||
7
bin/release-patch.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
BIN=`dirname $0`
|
||||||
|
|
||||||
|
$BIN/release-_pre.sh
|
||||||
|
npm version patch
|
||||||
|
$BIN/release-_post.sh
|
||||||
@@ -10,6 +10,7 @@ while (<>) {
|
|||||||
next if $seen{$_};
|
next if $seen{$_};
|
||||||
next if /<support\@greenkeeper.io>/;
|
next if /<support\@greenkeeper.io>/;
|
||||||
next if /<ungoldman\@gmail.com>/;
|
next if /<ungoldman\@gmail.com>/;
|
||||||
|
next if /<grunjol\@users.noreply.github.com>/;
|
||||||
$seen{$_} = push @authors, "- ", $_;
|
$seen{$_} = push @authors, "- ", $_;
|
||||||
}
|
}
|
||||||
END {
|
END {
|
||||||
|
|||||||
46
config.js
@@ -2,17 +2,19 @@ var applicationConfigPath = require('application-config-path')
|
|||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
var APP_NAME = 'WebTorrent'
|
var APP_NAME = 'WebTorrent'
|
||||||
|
var APP_TEAM = 'The WebTorrent Project'
|
||||||
var APP_VERSION = require('./package.json').version
|
var APP_VERSION = require('./package.json').version
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
APP_COPYRIGHT: 'Copyright © 2014-2016 The WebTorrent Project',
|
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
||||||
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
|
||||||
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
|
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
|
||||||
APP_NAME: APP_NAME,
|
APP_NAME: APP_NAME,
|
||||||
|
APP_TEAM: APP_TEAM,
|
||||||
APP_VERSION: APP_VERSION,
|
APP_VERSION: APP_VERSION,
|
||||||
|
|
||||||
AUTO_UPDATE_URL: 'https://webtorrent.io/app/update?version=' + APP_VERSION,
|
AUTO_UPDATE_URL: 'https://webtorrent.io/app/update?version=' + APP_VERSION,
|
||||||
AUTO_UPDATE_CHECK_STARTUP_DELAY: 60 * 1000 /* 1 minute */,
|
AUTO_UPDATE_CHECK_STARTUP_DELAY: 10 * 1000 /* 10 seconds */,
|
||||||
|
|
||||||
CONFIG_PATH: applicationConfigPath(APP_NAME),
|
CONFIG_PATH: applicationConfigPath(APP_NAME),
|
||||||
CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'),
|
CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'),
|
||||||
@@ -25,14 +27,38 @@ module.exports = {
|
|||||||
ROOT_PATH: __dirname,
|
ROOT_PATH: __dirname,
|
||||||
STATIC_PATH: path.join(__dirname, 'static'),
|
STATIC_PATH: path.join(__dirname, 'static'),
|
||||||
|
|
||||||
SOUND_ADD: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'),
|
SOUND_ADD: {
|
||||||
SOUND_DELETE: 'file://' + path.join(__dirname, 'static', 'sound', 'delete.wav'),
|
url: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'),
|
||||||
SOUND_DISABLE: 'file://' + path.join(__dirname, 'static', 'sound', 'disable.wav'),
|
volume: 0.2
|
||||||
SOUND_DONE: 'file://' + path.join(__dirname, 'static', 'sound', 'done.wav'),
|
},
|
||||||
SOUND_ENABLE: 'file://' + path.join(__dirname, 'static', 'sound', 'enable.wav'),
|
SOUND_DELETE: {
|
||||||
SOUND_ERROR: 'file://' + path.join(__dirname, 'static', 'sound', 'error.wav'),
|
url: 'file://' + path.join(__dirname, 'static', 'sound', 'delete.wav'),
|
||||||
SOUND_PLAY: 'file://' + path.join(__dirname, 'static', 'sound', 'play.wav'),
|
volume: 0.1
|
||||||
SOUND_STARTUP: 'file://' + path.join(__dirname, 'static', 'sound', 'startup.wav')
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProduction () {
|
function isProduction () {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
init
|
||||||
|
}
|
||||||
|
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
|
|
||||||
module.exports = function () {
|
function init () {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
|
||||||
162
main/index.js
@@ -4,94 +4,112 @@ var app = electron.app
|
|||||||
|
|
||||||
var autoUpdater = require('./auto-updater')
|
var autoUpdater = require('./auto-updater')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
|
var handlers = require('./handlers')
|
||||||
var ipc = require('./ipc')
|
var ipc = require('./ipc')
|
||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var menu = require('./menu')
|
var menu = require('./menu')
|
||||||
var registerProtocolHandler = require('./register-handlers')
|
|
||||||
var shortcuts = require('./shortcuts')
|
var shortcuts = require('./shortcuts')
|
||||||
|
var squirrelWin32 = require('./squirrel-win32')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
// Prevent multiple instances of the app from running at the same time. New instances
|
var shouldQuit = false
|
||||||
// signal this instance and exit.
|
var argv = sliceArgv(process.argv)
|
||||||
var shouldQuit = app.makeSingleInstance(function (newArgv) {
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
shouldQuit = squirrelWin32.handleEvent(argv[0])
|
||||||
|
argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldQuit) {
|
||||||
|
// Prevent multiple instances of app from running at same time. New instances signal
|
||||||
|
// this instance and quit.
|
||||||
|
shouldQuit = app.makeSingleInstance(onAppOpen)
|
||||||
|
if (shouldQuit) {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldQuit) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
function init () {
|
||||||
|
app.ipcReady = false // main window has finished loading and IPC is ready
|
||||||
|
app.isQuitting = false
|
||||||
|
|
||||||
|
// Open handlers must be added as early as possible
|
||||||
|
app.on('open-file', onOpen)
|
||||||
|
app.on('open-url', onOpen)
|
||||||
|
|
||||||
|
ipc.init()
|
||||||
|
|
||||||
|
app.on('will-finish-launching', function () {
|
||||||
|
autoUpdater.init()
|
||||||
|
setupCrashReporter()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('ready', function () {
|
||||||
|
menu.init()
|
||||||
|
windows.createMainWindow()
|
||||||
|
shortcuts.init()
|
||||||
|
if (process.platform !== 'win32') handlers.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('ipcReady', function () {
|
||||||
|
log('Command line args:', argv)
|
||||||
|
argv.forEach(function (torrentId) {
|
||||||
|
windows.main.send('dispatch', 'onOpen', torrentId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('before-quit', function () {
|
||||||
|
app.isQuitting = true
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('activate', function () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.show()
|
||||||
|
} else {
|
||||||
|
windows.createMainWindow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('window-all-closed', function () {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpen (e, torrentId) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (app.ipcReady) {
|
||||||
|
windows.main.send('dispatch', 'onOpen', torrentId)
|
||||||
|
// Magnet links opened from Chrome won't focus the app without a setTimeout. The
|
||||||
|
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
|
||||||
|
// Electron issue: https://github.com/atom/electron/issues/4338
|
||||||
|
setTimeout(function () {
|
||||||
|
windows.focusMainWindow()
|
||||||
|
}, 100)
|
||||||
|
} else {
|
||||||
|
argv.push(torrentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAppOpen (newArgv) {
|
||||||
newArgv = sliceArgv(newArgv)
|
newArgv = sliceArgv(newArgv)
|
||||||
|
|
||||||
if (app.ipcReady) {
|
if (app.ipcReady) {
|
||||||
log('Second app instance attempted to open but was prevented')
|
log('Second app instance opened, but was prevented:', newArgv)
|
||||||
|
windows.focusMainWindow()
|
||||||
|
|
||||||
newArgv.forEach(function (torrentId) {
|
newArgv.forEach(function (torrentId) {
|
||||||
windows.main.send('dispatch', 'onOpen', torrentId)
|
windows.main.send('dispatch', 'onOpen', torrentId)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (windows.main.isMinimized()) {
|
|
||||||
windows.main.restore()
|
|
||||||
}
|
|
||||||
windows.main.focus()
|
|
||||||
} else {
|
} else {
|
||||||
argv.push(...newArgv)
|
argv.push(...newArgv)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
if (shouldQuit) {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
var argv = sliceArgv(process.argv)
|
|
||||||
|
|
||||||
app.on('open-file', onOpen)
|
|
||||||
app.on('open-url', onOpen)
|
|
||||||
app.on('will-finish-launching', function () {
|
|
||||||
autoUpdater.init()
|
|
||||||
setupCrashReporter()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.ipcReady = false // main window has finished loading and IPC is ready
|
|
||||||
app.isQuitting = false
|
|
||||||
|
|
||||||
app.on('ready', function () {
|
|
||||||
menu.init()
|
|
||||||
windows.createMainWindow()
|
|
||||||
shortcuts.init()
|
|
||||||
registerProtocolHandler()
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('ipcReady', function () {
|
|
||||||
log('IS_PRODUCTION:', config.IS_PRODUCTION)
|
|
||||||
if (argv.length) {
|
|
||||||
log('command line args:', process.argv)
|
|
||||||
}
|
|
||||||
argv.forEach(function (torrentId) {
|
|
||||||
windows.main.send('dispatch', 'onOpen', torrentId)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('before-quit', function () {
|
|
||||||
app.isQuitting = true
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('activate', function () {
|
|
||||||
if (windows.main) {
|
|
||||||
windows.main.show()
|
|
||||||
} else {
|
|
||||||
windows.createMainWindow(menu)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('window-all-closed', function () {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipc.init()
|
|
||||||
|
|
||||||
function onOpen (e, torrentId) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (app.ipcReady) {
|
|
||||||
windows.main.send('dispatch', 'onOpen', torrentId)
|
|
||||||
} else {
|
|
||||||
argv.push(torrentId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sliceArgv (argv) {
|
function sliceArgv (argv) {
|
||||||
|
|||||||
134
main/squirrel-win32.js
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
module.exports = {
|
||||||
|
handleEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
var cp = require('child_process')
|
||||||
|
var electron = require('electron')
|
||||||
|
var fs = require('fs')
|
||||||
|
var os = require('os')
|
||||||
|
var path = require('path')
|
||||||
|
var pathExists = require('path-exists')
|
||||||
|
|
||||||
|
var app = electron.app
|
||||||
|
|
||||||
|
var handlers = require('./handlers')
|
||||||
|
|
||||||
|
var exeName = path.basename(process.execPath)
|
||||||
|
var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||||
|
|
||||||
|
function handleEvent (cmd) {
|
||||||
|
if (cmd === '--squirrel-install') {
|
||||||
|
// App was installed.
|
||||||
|
|
||||||
|
// Install protocol/file handlers, desktop/start menu shortcuts.
|
||||||
|
handlers.init()
|
||||||
|
|
||||||
|
createShortcuts(function () {
|
||||||
|
// Ensure user sees install splash screen so they realize that Setup.exe actually
|
||||||
|
// installed an application and isn't the application itself.
|
||||||
|
setTimeout(function () {
|
||||||
|
app.quit()
|
||||||
|
}, 5000)
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === '--squirrel-updated') {
|
||||||
|
// App was updated. (Called on new version of app)
|
||||||
|
updateShortcuts(function () {
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === '--squirrel-uninstall') {
|
||||||
|
// App was just uninstalled. Undo anything we did in the --squirrel-install and
|
||||||
|
// --squirrel-updated handlers
|
||||||
|
removeShortcuts(function () {
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === '--squirrel-obsolete') {
|
||||||
|
// App will be updated. (Called on outgoing version of app)
|
||||||
|
app.quit()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === '--squirrel-firstrun') {
|
||||||
|
// This is called on the app's first run. Do not quit, allow startup to continue.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn a command and invoke the callback when it completes with an error and the output
|
||||||
|
// from standard out.
|
||||||
|
function spawn (command, args, cb) {
|
||||||
|
var stdout = ''
|
||||||
|
|
||||||
|
var child
|
||||||
|
try {
|
||||||
|
child = cp.spawn(command, args)
|
||||||
|
} catch (err) {
|
||||||
|
// Spawn can throw an error
|
||||||
|
process.nextTick(function () {
|
||||||
|
cb(error, stdout)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
child.stdout.on('data', function (data) {
|
||||||
|
stdout += data
|
||||||
|
})
|
||||||
|
|
||||||
|
var error = null
|
||||||
|
child.on('error', function (processError) {
|
||||||
|
error = processError
|
||||||
|
})
|
||||||
|
child.on('close', function (code, signal) {
|
||||||
|
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
|
||||||
|
if (error) error.stdout = stdout
|
||||||
|
cb(error, stdout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn Squirrel's Update.exe with the given arguments and invoke the callback when the
|
||||||
|
// command completes.
|
||||||
|
function spawnUpdate (args, cb) {
|
||||||
|
spawn(updateDotExe, args, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create desktop/start menu shortcuts using the Squirrel Update.exe command line API
|
||||||
|
function createShortcuts (cb) {
|
||||||
|
spawnUpdate(['--createShortcut', exeName], cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update desktop/start menu shortcuts using the Squirrel Update.exe command line API
|
||||||
|
function updateShortcuts (cb) {
|
||||||
|
var homeDir = os.homedir()
|
||||||
|
if (homeDir) {
|
||||||
|
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
||||||
|
// Check if the desktop shortcut has been previously deleted and and keep it deleted
|
||||||
|
// if it was
|
||||||
|
pathExists(desktopShortcutPath).then(function (desktopShortcutExists) {
|
||||||
|
createShortcuts(function () {
|
||||||
|
if (desktopShortcutExists) {
|
||||||
|
cb()
|
||||||
|
} else {
|
||||||
|
// Remove the unwanted desktop shortcut that was recreated
|
||||||
|
fs.unlink(desktopShortcutPath, cb)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
createShortcuts(cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API
|
||||||
|
function removeShortcuts (cb) {
|
||||||
|
spawnUpdate(['--removeShortcut', exeName], cb)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
var windows = module.exports = {
|
var windows = module.exports = {
|
||||||
main: null,
|
main: null,
|
||||||
createMainWindow: createMainWindow
|
createMainWindow: createMainWindow,
|
||||||
|
focusMainWindow: focusMainWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -12,7 +13,6 @@ var menu = require('./menu')
|
|||||||
|
|
||||||
function createMainWindow () {
|
function createMainWindow () {
|
||||||
var win = windows.main = new electron.BrowserWindow({
|
var win = windows.main = new electron.BrowserWindow({
|
||||||
autoHideMenuBar: true, // Hide top menu bar unless Alt key is pressed (Windows, Linux)
|
|
||||||
backgroundColor: '#282828',
|
backgroundColor: '#282828',
|
||||||
darkTheme: true, // Forces dark theme (GTK+3)
|
darkTheme: true, // Forces dark theme (GTK+3)
|
||||||
icon: config.APP_ICON + '.png',
|
icon: config.APP_ICON + '.png',
|
||||||
@@ -52,3 +52,10 @@ function createMainWindow () {
|
|||||||
windows.main = null
|
windows.main = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusMainWindow () {
|
||||||
|
if (windows.main.isMinimized()) {
|
||||||
|
windows.main.restore()
|
||||||
|
}
|
||||||
|
windows.main.show() // shows and gives focus
|
||||||
|
}
|
||||||
|
|||||||
16
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-app",
|
"name": "WebTorrent",
|
||||||
"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.0.0",
|
"version": "0.1.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Feross Aboukhadijeh",
|
"name": "Feross Aboukhadijeh",
|
||||||
"email": "feross@feross.org",
|
"email": "feross@feross.org",
|
||||||
@@ -22,23 +22,29 @@
|
|||||||
"hyperx": "^2.0.2",
|
"hyperx": "^2.0.2",
|
||||||
"main-loop": "^3.2.0",
|
"main-loop": "^3.2.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
|
"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",
|
||||||
"upload-element": "^1.0.1",
|
"upload-element": "^1.0.1",
|
||||||
"virtual-dom": "^2.1.1",
|
"virtual-dom": "^2.1.1",
|
||||||
"webtorrent": "^0.86.0",
|
"webtorrent": "^0.87.1",
|
||||||
"winreg": "^1.0.1"
|
"winreg": "^1.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"appdmg": "^0.3.6",
|
|
||||||
"electron-osx-sign": "^0.3.0",
|
"electron-osx-sign": "^0.3.0",
|
||||||
"electron-packager": "^5.0.0",
|
"electron-packager": "^5.0.0",
|
||||||
"electron-prebuilt": "0.37.2",
|
"electron-prebuilt": "0.37.2",
|
||||||
|
"electron-winstaller": "^2.0.5",
|
||||||
|
"gh-release": "^2.0.2",
|
||||||
"path-exists": "^2.1.0",
|
"path-exists": "^2.1.0",
|
||||||
"plist": "^1.2.0",
|
"plist": "^1.2.0",
|
||||||
"rimraf": "^2.5.2",
|
"rimraf": "^2.5.2",
|
||||||
"standard": "^6.0.5"
|
"standard": "^6.0.5"
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"appdmg": "^0.3.6"
|
||||||
|
},
|
||||||
"homepage": "https://webtorrent.io",
|
"homepage": "https://webtorrent.io",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"electron",
|
"electron",
|
||||||
@@ -54,7 +60,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "node ./bin/clean.js",
|
"clean": "node ./bin/clean.js",
|
||||||
"debug": "DEBUG=* electron .",
|
"debug": "DEBUG=* electron .",
|
||||||
"package": "npm prune && npm dedupe && node ./bin/package.js",
|
"package": "npm install && npm prune && npm dedupe && node ./bin/package.js",
|
||||||
"size": "npm run package -- --darwin && du -ch dist/WebTorrent-darwin-x64 | grep total",
|
"size": "npm run package -- --darwin && du -ch dist/WebTorrent-darwin-x64 | grep total",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"test": "standard",
|
"test": "standard",
|
||||||
|
|||||||
@@ -181,10 +181,10 @@ i:not(.disabled):hover {
|
|||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1000;
|
|
||||||
transition: opacity 0.15s ease-out;
|
transition: opacity 0.15s ease-out;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app:not(.is-focused) .header {
|
.app:not(.is-focused) .header {
|
||||||
@@ -456,8 +456,7 @@ input {
|
|||||||
background-color: #F44336;
|
background-color: #F44336;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent.timeout .play,
|
.torrent.timeout .play {
|
||||||
.torrent.unplayable .play {
|
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,13 +731,40 @@ body.drag .torrent-placeholder span {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* AUDIO DETAILS
|
||||||
|
*/
|
||||||
|
|
||||||
|
.audio-metadata {
|
||||||
|
width: 500px;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
align-self: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-metadata .audio-title {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-metadata label {
|
||||||
|
display:inline-block;
|
||||||
|
width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-right: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ERRORS
|
* ERRORS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.error-popover {
|
.error-popover {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1001;
|
|
||||||
top: 36px;
|
top: 36px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ var EventEmitter = require('events')
|
|||||||
var fs = require('fs')
|
var fs = require('fs')
|
||||||
var mainLoop = require('main-loop')
|
var mainLoop = require('main-loop')
|
||||||
var mkdirp = require('mkdirp')
|
var mkdirp = require('mkdirp')
|
||||||
|
var musicmetadata = require('musicmetadata')
|
||||||
var networkAddress = require('network-address')
|
var networkAddress = require('network-address')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
var remote = require('remote')
|
var remote = require('remote')
|
||||||
@@ -130,7 +131,7 @@ function init () {
|
|||||||
|
|
||||||
// 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
|
||||||
document.querySelector('.loading').remove() /* TODO: no spinner once fast enough */
|
document.querySelector('.loading').remove() /* TODO: no spinner once fast enough */
|
||||||
playInterfaceSound(config.SOUND_STARTUP)
|
playInterfaceSound('STARTUP')
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ function updateElectron () {
|
|||||||
|
|
||||||
// Events from the UI never modify state directly. Instead they call dispatch()
|
// Events from the UI never modify state directly. Instead they call dispatch()
|
||||||
function dispatch (action, ...args) {
|
function dispatch (action, ...args) {
|
||||||
if (['videoMouseMoved', 'playbackJump'].indexOf(action) === -1) {
|
if (['mediaMouseMoved', 'playbackJump'].indexOf(action) === -1) {
|
||||||
console.log('dispatch: %s %o', action, args) /* log user interactions, but don't spam */
|
console.log('dispatch: %s %o', action, args) /* log user interactions, but don't spam */
|
||||||
}
|
}
|
||||||
if (action === 'onOpen') {
|
if (action === 'onOpen') {
|
||||||
@@ -235,20 +236,20 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'playbackJump') {
|
if (action === 'playbackJump') {
|
||||||
jumpToTime(args[0] /* seconds */)
|
jumpToTime(args[0] /* seconds */)
|
||||||
}
|
}
|
||||||
if (action === 'videoPlaying') {
|
if (action === 'mediaPlaying') {
|
||||||
state.video.isPaused = false
|
state.playing.isPaused = false
|
||||||
ipcRenderer.send('blockPowerSave')
|
ipcRenderer.send('blockPowerSave')
|
||||||
}
|
}
|
||||||
if (action === 'videoPaused') {
|
if (action === 'mediaPaused') {
|
||||||
state.video.isPaused = true
|
state.playing.isPaused = true
|
||||||
ipcRenderer.send('unblockPowerSave')
|
ipcRenderer.send('unblockPowerSave')
|
||||||
}
|
}
|
||||||
if (action === 'toggleFullScreen') {
|
if (action === 'toggleFullScreen') {
|
||||||
ipcRenderer.send('toggleFullScreen', args[0])
|
ipcRenderer.send('toggleFullScreen', args[0])
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
if (action === 'videoMouseMoved') {
|
if (action === 'mediaMouseMoved') {
|
||||||
state.video.mouseStationarySince = new Date().getTime()
|
state.playing.mouseStationarySince = new Date().getTime()
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
if (action === 'exitModal') {
|
if (action === 'exitModal') {
|
||||||
@@ -259,14 +260,14 @@ function dispatch (action, ...args) {
|
|||||||
|
|
||||||
// Plays or pauses the video. If isPaused is undefined, acts as a toggle
|
// Plays or pauses the video. If isPaused is undefined, acts as a toggle
|
||||||
function playPause (isPaused) {
|
function playPause (isPaused) {
|
||||||
if (isPaused === state.video.isPaused) {
|
if (isPaused === state.playing.isPaused) {
|
||||||
return // Nothing to do
|
return // Nothing to do
|
||||||
}
|
}
|
||||||
// Either isPaused is undefined, or it's the opposite of the current state. Toggle.
|
// Either isPaused is undefined, or it's the opposite of the current state. Toggle.
|
||||||
if (Cast.isCasting()) {
|
if (Cast.isCasting()) {
|
||||||
Cast.playPause()
|
Cast.playPause()
|
||||||
}
|
}
|
||||||
state.video.isPaused = !state.video.isPaused
|
state.playing.isPaused = !state.playing.isPaused
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +275,7 @@ function jumpToTime (time) {
|
|||||||
if (Cast.isCasting()) {
|
if (Cast.isCasting()) {
|
||||||
Cast.seek(time)
|
Cast.seek(time)
|
||||||
} else {
|
} else {
|
||||||
state.video.jumpToTime = time
|
state.playing.jumpToTime = time
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,7 +423,7 @@ function addTorrentToList (torrent) {
|
|||||||
infoHash: torrent.infoHash
|
infoHash: torrent.infoHash
|
||||||
})
|
})
|
||||||
saveState()
|
saveState()
|
||||||
playInterfaceSound(config.SOUND_ADD)
|
playInterfaceSound('ADD')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +512,7 @@ function updateTorrentProgress () {
|
|||||||
var changed = false
|
var changed = false
|
||||||
state.client.torrents.forEach(function (torrent) {
|
state.client.torrents.forEach(function (torrent) {
|
||||||
var torrentSummary = getTorrentSummary(torrent.infoHash)
|
var torrentSummary = getTorrentSummary(torrent.infoHash)
|
||||||
if (!torrentSummary) return
|
if (!torrentSummary || !torrent.ready) return
|
||||||
torrent.files.forEach(function (file, index) {
|
torrent.files.forEach(function (file, index) {
|
||||||
var numPieces = file._endPiece - file._startPiece + 1
|
var numPieces = file._endPiece - file._startPiece + 1
|
||||||
var numPiecesPresent = 0
|
var numPiecesPresent = 0
|
||||||
@@ -601,21 +602,25 @@ function startServer (torrentSummary, index, cb) {
|
|||||||
|
|
||||||
function startServerFromReadyTorrent (torrent, index, cb) {
|
function startServerFromReadyTorrent (torrent, index, cb) {
|
||||||
// automatically choose which file in the torrent to play, if necessary
|
// automatically choose which file in the torrent to play, if necessary
|
||||||
if (!index) {
|
if (index === undefined) index = pickFileToPlay(torrent.files)
|
||||||
// filter out file formats that the <video> tag definitely can't play
|
if (index === undefined) return cb(new errors.UnplayableError())
|
||||||
var files = torrent.files.filter(TorrentPlayer.isPlayable)
|
var file = torrent.files[index]
|
||||||
if (files.length === 0) return cb(new errors.UnplayableError())
|
|
||||||
// use largest file
|
|
||||||
var largestFile = files.reduce(function (a, b) {
|
|
||||||
return a.length > b.length ? a : b
|
|
||||||
})
|
|
||||||
index = torrent.files.indexOf(largestFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
state.playing.infoHash = torrent.infoHash
|
state.playing.infoHash = torrent.infoHash
|
||||||
state.playing.fileIndex = index
|
state.playing.fileIndex = index
|
||||||
|
state.playing.type = TorrentPlayer.isVideo(file) ? 'video' : 'audio'
|
||||||
|
state.playing.audioInfo = null
|
||||||
|
|
||||||
|
// if it's audio, parse out the metadata (artist, title, etc)
|
||||||
|
musicmetadata(file.createReadStream(), function (err, info) {
|
||||||
|
if (err) return
|
||||||
|
console.log('Got audio metadata for %s: %v', file.name, info)
|
||||||
|
state.playing.audioInfo = info
|
||||||
|
update()
|
||||||
|
})
|
||||||
|
|
||||||
|
// either way, start a streaming torrent-to-http server
|
||||||
var server = torrent.createServer()
|
var server = torrent.createServer()
|
||||||
server.listen(0, function () {
|
server.listen(0, function () {
|
||||||
var port = server.address().port
|
var port = server.address().port
|
||||||
@@ -629,6 +634,28 @@ function startServerFromReadyTorrent (torrent, index, cb) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Picks the default file to play from a list of torrent or torrentSummary files
|
||||||
|
// Returns an index or undefined, if no files are playable
|
||||||
|
function pickFileToPlay (files) {
|
||||||
|
// first, try to find the biggest video file
|
||||||
|
var videoFiles = files.filter(TorrentPlayer.isVideo)
|
||||||
|
if (videoFiles.length > 0) {
|
||||||
|
var largestVideoFile = videoFiles.reduce(function (a, b) {
|
||||||
|
return a.length > b.length ? a : b
|
||||||
|
})
|
||||||
|
return files.indexOf(largestVideoFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are no videos, play the first audio file
|
||||||
|
var audioFiles = files.filter(TorrentPlayer.isAudio)
|
||||||
|
if (audioFiles.length > 0) {
|
||||||
|
return files.indexOf(audioFiles[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// no video or audio means nothing is playable
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
function stopServer () {
|
function stopServer () {
|
||||||
if (!state.server) return
|
if (!state.server) return
|
||||||
state.server.server.destroy()
|
state.server.server.destroy()
|
||||||
@@ -640,13 +667,13 @@ function stopServer () {
|
|||||||
// Opens the video player
|
// Opens the video player
|
||||||
function openPlayer (torrentSummary, index, cb) {
|
function openPlayer (torrentSummary, index, cb) {
|
||||||
var torrent = state.client.get(torrentSummary.infoHash)
|
var torrent = state.client.get(torrentSummary.infoHash)
|
||||||
if (!torrent || !torrent.done) playInterfaceSound(config.SOUND_PLAY)
|
if (!torrent || !torrent.done) playInterfaceSound('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(config.SOUND_ERROR)
|
playInterfaceSound('ERROR')
|
||||||
update()
|
update()
|
||||||
}, 10000) /* give it a few seconds */
|
}, 10000) /* give it a few seconds */
|
||||||
|
|
||||||
@@ -654,7 +681,7 @@ function openPlayer (torrentSummary, index, cb) {
|
|||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
if (err) {
|
if (err) {
|
||||||
torrentSummary.playStatus = 'unplayable'
|
torrentSummary.playStatus = 'unplayable'
|
||||||
playInterfaceSound(config.SOUND_ERROR)
|
playInterfaceSound('ERROR')
|
||||||
update()
|
update()
|
||||||
return onError(err)
|
return onError(err)
|
||||||
}
|
}
|
||||||
@@ -714,11 +741,11 @@ function toggleTorrent (torrentSummary) {
|
|||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
torrentSummary.status = 'new'
|
torrentSummary.status = 'new'
|
||||||
startTorrentingSummary(torrentSummary)
|
startTorrentingSummary(torrentSummary)
|
||||||
playInterfaceSound(config.SOUND_ENABLE)
|
playInterfaceSound('ENABLE')
|
||||||
} else {
|
} else {
|
||||||
torrentSummary.status = 'paused'
|
torrentSummary.status = 'paused'
|
||||||
stopTorrenting(torrentSummary.infoHash)
|
stopTorrenting(torrentSummary.infoHash)
|
||||||
playInterfaceSound(config.SOUND_DISABLE)
|
playInterfaceSound('DISABLE')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,7 +758,7 @@ function deleteTorrent (torrentSummary) {
|
|||||||
if (index > -1) state.saved.torrents.splice(index, 1)
|
if (index > -1) state.saved.torrents.splice(index, 1)
|
||||||
saveState()
|
saveState()
|
||||||
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(config.SOUND_DELETE)
|
playInterfaceSound('DELETE')
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelectTorrent (infoHash) {
|
function toggleSelectTorrent (infoHash) {
|
||||||
@@ -785,7 +812,7 @@ function restoreBounds () {
|
|||||||
|
|
||||||
function onError (err) {
|
function onError (err) {
|
||||||
console.error(err.stack || err)
|
console.error(err.stack || err)
|
||||||
playInterfaceSound(config.SOUND_ERROR)
|
playInterfaceSound('ERROR')
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
time: new Date().getTime(),
|
time: new Date().getTime(),
|
||||||
message: err.message || err
|
message: err.message || err
|
||||||
@@ -809,12 +836,15 @@ function showDoneNotification (torrent) {
|
|||||||
window.focus()
|
window.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
playInterfaceSound(config.SOUND_DONE)
|
playInterfaceSound('DONE')
|
||||||
}
|
}
|
||||||
|
|
||||||
function playInterfaceSound (url) {
|
function playInterfaceSound (name) {
|
||||||
|
var sound = config[`SOUND_${name}`]
|
||||||
|
if (!sound) throw new Error('Invalid sound name')
|
||||||
|
|
||||||
var audio = new window.Audio()
|
var audio = new window.Audio()
|
||||||
audio.volume = 0.3
|
audio.volume = sound.volume
|
||||||
audio.src = url
|
audio.src = sound.url
|
||||||
audio.play()
|
audio.play()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,14 +57,14 @@ function pollCastStatus (state) {
|
|||||||
if (state.playing.location === 'chromecast') {
|
if (state.playing.location === 'chromecast') {
|
||||||
state.devices.chromecast.status(function (err, status) {
|
state.devices.chromecast.status(function (err, status) {
|
||||||
if (err) return console.log('Error getting %s status: %o', state.playing.location, err)
|
if (err) return console.log('Error getting %s status: %o', state.playing.location, err)
|
||||||
state.video.isPaused = status.playerState === 'PAUSED'
|
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||||
state.video.currentTime = status.currentTime
|
state.playing.currentTime = status.currentTime
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
} else if (state.playing.location === 'airplay') {
|
} else if (state.playing.location === 'airplay') {
|
||||||
state.devices.airplay.status(function (status) {
|
state.devices.airplay.status(function (status) {
|
||||||
state.video.isPaused = status.rate === 0
|
state.playing.isPaused = status.rate === 0
|
||||||
state.video.currentTime = status.position
|
state.playing.currentTime = status.position
|
||||||
update()
|
update()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ function stopCasting () {
|
|||||||
|
|
||||||
function stoppedCasting () {
|
function stoppedCasting () {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.video.jumpToTime = state.video.currentTime
|
state.playing.jumpToTime = state.playing.currentTime
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,11 +137,11 @@ function playPause () {
|
|||||||
var device
|
var device
|
||||||
if (state.playing.location === 'chromecast') {
|
if (state.playing.location === 'chromecast') {
|
||||||
device = state.devices.chromecast
|
device = state.devices.chromecast
|
||||||
if (!state.video.isPaused) device.pause(castCallback)
|
if (!state.playing.isPaused) device.pause(castCallback)
|
||||||
else device.play(null, null, castCallback)
|
else device.play(null, null, castCallback)
|
||||||
} else if (state.playing.location === 'airplay') {
|
} else if (state.playing.location === 'airplay') {
|
||||||
device = state.devices.airplay
|
device = state.devices.airplay
|
||||||
if (!state.video.isPaused) device.rate(0, castCallback)
|
if (!state.playing.isPaused) device.rate(0, castCallback)
|
||||||
else device.rate(1, castCallback)
|
else device.rate(1, castCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
isPlayable: isPlayable
|
isPlayable,
|
||||||
|
isVideo,
|
||||||
|
isAudio
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
@@ -8,6 +10,15 @@ var path = require('path')
|
|||||||
* Determines whether a file in a torrent is audio/video we can play
|
* Determines whether a file in a torrent is audio/video we can play
|
||||||
*/
|
*/
|
||||||
function isPlayable (file) {
|
function isPlayable (file) {
|
||||||
var extname = path.extname(file.name)
|
return isVideo(file) || isAudio(file)
|
||||||
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(extname) !== -1
|
}
|
||||||
|
|
||||||
|
function isVideo (file) {
|
||||||
|
var ext = path.extname(file.name)
|
||||||
|
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAudio (file) {
|
||||||
|
var ext = path.extname(file.name)
|
||||||
|
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,21 +20,20 @@ module.exports = {
|
|||||||
title: config.APP_NAME /* current window title */
|
title: config.APP_NAME /* current window title */
|
||||||
},
|
},
|
||||||
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
||||||
playing: { /* the torrent and file we're currently streaming */
|
playing: { /* the media (audio or video) that we're currently playing */
|
||||||
infoHash: null, /* the info hash of the torrent we're playing */
|
infoHash: null, /* the info hash of the torrent we're playing */
|
||||||
fileIndex: null, /* the zero-based index within the torrent */
|
fileIndex: null, /* the zero-based index within the torrent */
|
||||||
location: 'local' /* 'local', 'chromecast', 'airplay' */
|
location: 'local', /* 'local', 'chromecast', 'airplay' */
|
||||||
},
|
type: null, /* 'audio' or 'video' */
|
||||||
devices: { /* playback devices like Chromecast and AppleTV */
|
|
||||||
airplay: null, /* airplay client. finds and manages AppleTVs */
|
|
||||||
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
|
||||||
},
|
|
||||||
video: { /* state of the video player screen */
|
|
||||||
currentTime: 0, /* seconds */
|
currentTime: 0, /* seconds */
|
||||||
duration: 1, /* seconds */
|
duration: 1, /* seconds */
|
||||||
isPaused: true,
|
isPaused: true,
|
||||||
mouseStationarySince: 0 /* Unix time in ms */
|
mouseStationarySince: 0 /* Unix time in ms */
|
||||||
},
|
},
|
||||||
|
devices: { /* playback devices like Chromecast and AppleTV */
|
||||||
|
airplay: null, /* airplay client. finds and manages AppleTVs */
|
||||||
|
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
||||||
|
},
|
||||||
dock: {
|
dock: {
|
||||||
badge: 0,
|
badge: 0,
|
||||||
progress: 0
|
progress: 0
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ function App (state, dispatch) {
|
|||||||
// * The video is paused
|
// * The video is paused
|
||||||
// * The video is playing remotely on Chromecast or Airplay
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
var hideControls = state.location.current().url === 'player' &&
|
var hideControls = state.location.current().url === 'player' &&
|
||||||
state.video.mouseStationarySince !== 0 &&
|
state.playing.mouseStationarySince !== 0 &&
|
||||||
new Date().getTime() - state.video.mouseStationarySince > 2000 &&
|
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||||
!state.video.isPaused &&
|
!state.playing.isPaused &&
|
||||||
state.playing.location === 'local'
|
state.playing.location === 'local'
|
||||||
|
|
||||||
// Hide the header on Windows/Linux when in the player
|
// Hide the header on Windows/Linux when in the player
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ var h = require('virtual-dom/h')
|
|||||||
var hyperx = require('hyperx')
|
var hyperx = require('hyperx')
|
||||||
var hx = hyperx(h)
|
var hx = hyperx(h)
|
||||||
|
|
||||||
|
|
||||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||||
function Player (state, dispatch) {
|
function Player (state, dispatch) {
|
||||||
// Show the video as large as will fit in the window, play immediately
|
// Show the video as large as will fit in the window, play immediately
|
||||||
@@ -12,50 +13,69 @@ function Player (state, dispatch) {
|
|||||||
return hx`
|
return hx`
|
||||||
<div
|
<div
|
||||||
class='player'
|
class='player'
|
||||||
onmousemove=${() => dispatch('videoMouseMoved')}>
|
onmousemove=${() => dispatch('mediaMouseMoved')}>
|
||||||
${showVideo ? renderVideo(state, dispatch) : renderCastScreen(state, dispatch)}
|
${showVideo ? renderMedia(state, dispatch) : renderCastScreen(state, dispatch)}
|
||||||
${renderPlayerControls(state, dispatch)}
|
${renderPlayerControls(state, dispatch)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVideo (state, dispatch) {
|
function renderMedia (state, dispatch) {
|
||||||
|
if (!state.server) return
|
||||||
|
|
||||||
// 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 videoElement = document.querySelector('video')
|
var mediaType = state.playing.type /* 'audio' or 'video' */
|
||||||
if (videoElement !== null) {
|
var mediaElement = document.querySelector(mediaType) /* get the <video> or <audio> tag */
|
||||||
if (state.video.isPaused && !videoElement.paused) {
|
if (mediaElement !== null) {
|
||||||
videoElement.pause()
|
if (state.playing.isPaused && !mediaElement.paused) {
|
||||||
} else if (!state.video.isPaused && videoElement.paused) {
|
mediaElement.pause()
|
||||||
videoElement.play()
|
} else if (!state.playing.isPaused && mediaElement.paused) {
|
||||||
|
mediaElement.play()
|
||||||
}
|
}
|
||||||
// When the user clicks or drags on the progress bar, jump to that position
|
// When the user clicks or drags on the progress bar, jump to that position
|
||||||
if (state.video.jumpToTime) {
|
if (state.playing.jumpToTime) {
|
||||||
videoElement.currentTime = state.video.jumpToTime
|
mediaElement.currentTime = state.playing.jumpToTime
|
||||||
state.video.jumpToTime = null
|
state.playing.jumpToTime = null
|
||||||
}
|
}
|
||||||
state.video.currentTime = videoElement.currentTime
|
state.playing.currentTime = mediaElement.currentTime
|
||||||
state.video.duration = videoElement.duration
|
state.playing.duration = mediaElement.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the <audio> or <video> tag
|
||||||
|
var mediaTag = hx`
|
||||||
|
<div
|
||||||
|
src='${state.server.localURL}'
|
||||||
|
ondblclick=${() => dispatch('toggleFullScreen')}
|
||||||
|
onloadedmetadata=${onLoadedMetadata}
|
||||||
|
onended=${onEnded}
|
||||||
|
onplay=${() => dispatch('mediaPlaying')}
|
||||||
|
onpause=${() => dispatch('mediaPaused')}
|
||||||
|
autoplay>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
mediaTag.tagName = mediaType
|
||||||
|
|
||||||
|
// Show the media.
|
||||||
|
// Video fills the window, centered with black bars if necessary
|
||||||
|
// Audio gets a static poster image and a summary of the file metadata.
|
||||||
|
var isAudio = mediaType === 'audio'
|
||||||
|
var style = {
|
||||||
|
backgroundImage: isAudio ? cssBackgroundImagePoster(state) : ''
|
||||||
|
}
|
||||||
return hx`
|
return hx`
|
||||||
<div
|
<div
|
||||||
class='letterbox'
|
class='letterbox'
|
||||||
onmousemove=${() => dispatch('videoMouseMoved')}>
|
style=${style}
|
||||||
<video
|
onmousemove=${() => dispatch('mediaMouseMoved')}>
|
||||||
src='${state.server.localURL}'
|
${mediaTag}
|
||||||
ondblclick=${() => dispatch('toggleFullScreen')}
|
${renderAudioMetadata(state)}
|
||||||
onloadedmetadata=${onLoadedMetadata}
|
|
||||||
onended=${onEnded}
|
|
||||||
onplay=${() => dispatch('videoPlaying')}
|
|
||||||
onpause=${() => dispatch('videoPaused')}
|
|
||||||
autoplay>
|
|
||||||
</video>
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
// 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
|
||||||
var video = e.target
|
var video = e.target
|
||||||
var dimensions = {
|
var dimensions = {
|
||||||
width: video.videoWidth,
|
width: video.videoWidth,
|
||||||
@@ -66,10 +86,38 @@ function renderVideo (state, dispatch) {
|
|||||||
|
|
||||||
// When the video completes, pause the video instead of looping
|
// When the video completes, pause the video instead of looping
|
||||||
function onEnded (e) {
|
function onEnded (e) {
|
||||||
state.video.isPaused = true
|
state.playing.isPaused = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderAudioMetadata (state) {
|
||||||
|
if (!state.playing.audioInfo) return
|
||||||
|
var info = state.playing.audioInfo
|
||||||
|
|
||||||
|
// Get audio track info
|
||||||
|
var title = info.title
|
||||||
|
if (!title) {
|
||||||
|
var torrentSummary = getPlayingTorrentSummary(state)
|
||||||
|
title = torrentSummary.files[state.playing.fileIndex].name
|
||||||
|
}
|
||||||
|
var artist = info.artist && info.artist[0]
|
||||||
|
var album = info.album
|
||||||
|
if (album && info.year && !album.includes(info.year)) {
|
||||||
|
album += ' (' + info.year + ')'
|
||||||
|
}
|
||||||
|
var track
|
||||||
|
if (info.track && info.track.no && info.track.of) {
|
||||||
|
track = info.track.no + ' of ' + info.track.of
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show a small info box in the middle of the screen
|
||||||
|
var elems = [hx`<div class='audio-title'><label></label>${title}</div>`]
|
||||||
|
if (artist) elems.push(hx`<div class='audio-artist'><label>Artist</label>${artist}</div>`)
|
||||||
|
if (album) elems.push(hx`<div class='audio-album'><label>Album</label>${album}</div>`)
|
||||||
|
if (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`)
|
||||||
|
return hx`<div class='audio-metadata'>${elems}</div>`
|
||||||
|
}
|
||||||
|
|
||||||
function renderCastScreen (state, dispatch) {
|
function renderCastScreen (state, dispatch) {
|
||||||
var isChromecast = state.playing.location.startsWith('chromecast')
|
var isChromecast = state.playing.location.startsWith('chromecast')
|
||||||
var isAirplay = state.playing.location.startsWith('airplay')
|
var isAirplay = state.playing.location.startsWith('airplay')
|
||||||
@@ -77,12 +125,8 @@ function renderCastScreen (state, dispatch) {
|
|||||||
if (!isChromecast && !isAirplay) throw new Error('Unimplemented cast type')
|
if (!isChromecast && !isAirplay) throw new Error('Unimplemented cast type')
|
||||||
|
|
||||||
// Show a nice title image, if possible
|
// Show a nice title image, if possible
|
||||||
var style = {}
|
var style = {
|
||||||
var infoHash = state.playing.infoHash
|
backgroundImage: cssBackgroundImagePoster(state)
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === infoHash)
|
|
||||||
if (torrentSummary && torrentSummary.posterURL) {
|
|
||||||
var cleanURL = torrentSummary.posterURL.replace(/\\/g, '/')
|
|
||||||
style.backgroundImage = `radial-gradient(circle at center, rgba(0,0,0,0.4) 0%,rgba(0,0,0,1) 100%), url(${cleanURL})`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show whether we're connected to Chromecast / Airplay
|
// Show whether we're connected to Chromecast / Airplay
|
||||||
@@ -98,8 +142,23 @@ function renderCastScreen (state, dispatch) {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the CSS background-image string for a poster image + dark vignette
|
||||||
|
function cssBackgroundImagePoster (state) {
|
||||||
|
var torrentSummary = getPlayingTorrentSummary(state)
|
||||||
|
if (!torrentSummary || !torrentSummary.posterURL) return ''
|
||||||
|
var cleanURL = torrentSummary.posterURL.replace(/\\/g, '/')
|
||||||
|
return 'radial-gradient(circle at center, ' +
|
||||||
|
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' +
|
||||||
|
`, url(${cleanURL})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayingTorrentSummary (state) {
|
||||||
|
var infoHash = state.playing.infoHash
|
||||||
|
return state.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||||
|
}
|
||||||
|
|
||||||
function renderPlayerControls (state, dispatch) {
|
function renderPlayerControls (state, dispatch) {
|
||||||
var positionPercent = 100 * state.video.currentTime / state.video.duration
|
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
||||||
|
|
||||||
var elements = [
|
var elements = [
|
||||||
@@ -174,7 +233,7 @@ function renderPlayerControls (state, dispatch) {
|
|||||||
// 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=${() => dispatch('playPause')}>
|
<i class='icon play-pause' onclick=${() => dispatch('playPause')}>
|
||||||
${state.video.isPaused ? 'play_arrow' : 'pause'}
|
${state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||||
</i>
|
</i>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
@@ -182,10 +241,10 @@ function renderPlayerControls (state, dispatch) {
|
|||||||
|
|
||||||
// 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) {
|
||||||
dispatch('videoMouseMoved')
|
dispatch('mediaMouseMoved')
|
||||||
var windowWidth = document.querySelector('body').clientWidth
|
var windowWidth = document.querySelector('body').clientWidth
|
||||||
var fraction = e.clientX / windowWidth
|
var fraction = e.clientX / windowWidth
|
||||||
var position = fraction * state.video.duration /* seconds */
|
var position = fraction * state.playing.duration /* seconds */
|
||||||
dispatch('playbackJump', position)
|
dispatch('playbackJump', position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,9 +103,10 @@ function TorrentList (state, dispatch) {
|
|||||||
// Download button toggles between torrenting (DL/seed) and paused
|
// Download button toggles between torrenting (DL/seed) and paused
|
||||||
// Play button starts streaming the torrent immediately, unpausing if needed
|
// Play button starts streaming the torrent immediately, unpausing if needed
|
||||||
function renderTorrentButtons (torrentSummary) {
|
function renderTorrentButtons (torrentSummary) {
|
||||||
var playIcon, playTooltip
|
var playIcon, playTooltip, playClass
|
||||||
if (torrentSummary.playStatus === 'unplayable') {
|
if (torrentSummary.playStatus === 'unplayable') {
|
||||||
playIcon = 'warning'
|
playIcon = 'play_arrow'
|
||||||
|
playClass = 'disabled'
|
||||||
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
|
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
|
||||||
'View details and click on individual files to open them in another program.'
|
'View details and click on individual files to open them in another program.'
|
||||||
} else if (torrentSummary.playStatus === 'timeout') {
|
} else if (torrentSummary.playStatus === 'timeout') {
|
||||||
@@ -131,13 +132,14 @@ function TorrentList (state, dispatch) {
|
|||||||
return hx`
|
return hx`
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
<i.btn.icon.play
|
<i.btn.icon.play
|
||||||
title='${playTooltip}'
|
title=${playTooltip}
|
||||||
|
class=${playClass}
|
||||||
onclick=${(e) => handleButton('play', e)}>
|
onclick=${(e) => handleButton('play', e)}>
|
||||||
${playIcon}
|
${playIcon}
|
||||||
</i>
|
</i>
|
||||||
<i.btn.icon.download
|
<i.btn.icon.download
|
||||||
class='${torrentSummary.status}'
|
class=${torrentSummary.status}
|
||||||
title='${downloadTooltip}'
|
title=${downloadTooltip}
|
||||||
onclick=${(e) => handleButton('toggleTorrent', e)}>
|
onclick=${(e) => handleButton('toggleTorrent', e)}>
|
||||||
${downloadIcon}
|
${downloadIcon}
|
||||||
</i>
|
</i>
|
||||||
@@ -153,6 +155,7 @@ function TorrentList (state, dispatch) {
|
|||||||
function handleButton (action, e) {
|
function handleButton (action, e) {
|
||||||
// Prevent propagation so that we don't select/unselect the torrent
|
// Prevent propagation so that we don't select/unselect the torrent
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
if (e.target.classList.contains('disabled')) return
|
||||||
dispatch(action, torrentSummary)
|
dispatch(action, torrentSummary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 303 KiB |
BIN
static/loading.gif
Normal file
|
After Width: | Height: | Size: 20 KiB |