Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a15db2892 | ||
|
|
63dda10380 | ||
|
|
6e651df083 | ||
|
|
3a8fe24eec | ||
|
|
918a35e091 | ||
|
|
c76abeb8c0 | ||
|
|
d389b8ab38 | ||
|
|
a59faacbd7 | ||
|
|
12f9709601 | ||
|
|
455c9c02b9 | ||
|
|
1b49c6568b | ||
|
|
30e81c7699 | ||
|
|
2dafc68301 | ||
|
|
c310222af2 | ||
|
|
b4bb9a6603 | ||
|
|
279c621d23 | ||
|
|
eb11dbdcbd | ||
|
|
8dfdb34d31 | ||
|
|
fc9a73d67f | ||
|
|
4b5b84a0fc | ||
|
|
327c95d754 | ||
|
|
6e969e5d07 | ||
|
|
ca7c872420 | ||
|
|
8af4f42c42 | ||
|
|
ffce76a9b1 | ||
|
|
fca1d9dae4 | ||
|
|
eba09430e3 | ||
|
|
6bc8de7625 | ||
|
|
8a08ed8538 | ||
|
|
56d802f741 | ||
|
|
f7b46336fd | ||
|
|
510187c2ae | ||
|
|
ff6ff8db00 | ||
|
|
014017604d | ||
|
|
8cf544d54f | ||
|
|
870dd893fc | ||
|
|
bf3b9ced74 | ||
|
|
9ecc12fb7f | ||
|
|
aafb1421c6 | ||
|
|
76c732bafb | ||
|
|
ab476c9a9c | ||
|
|
4470310814 | ||
|
|
b6ba4f45c8 | ||
|
|
84c860cfcb | ||
|
|
47c554a5ff | ||
|
|
4e46b16c13 | ||
|
|
22cdcdb468 | ||
|
|
f238b2d105 | ||
|
|
3a81799828 | ||
|
|
5dca89b61c | ||
|
|
264c035ef7 | ||
|
|
8f39f8a23e | ||
|
|
a29dbd7a71 | ||
|
|
60a8969abc | ||
|
|
9747d28514 | ||
|
|
17ccd217a9 | ||
|
|
0df6198549 | ||
|
|
74ada99f2b | ||
|
|
81d5a367da | ||
|
|
189e4bdc24 | ||
|
|
7bd30f8a16 | ||
|
|
7c6b7e4a6d | ||
|
|
fe50f76619 | ||
|
|
973a366b94 | ||
|
|
b0116deb35 | ||
|
|
511382d384 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
@@ -18,5 +18,8 @@
|
|||||||
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
|
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
|
||||||
- gabriel <furstenheim@gmail.com>
|
- gabriel <furstenheim@gmail.com>
|
||||||
- Rolando Guedes <rolando.guedes@3gnt.net>
|
- Rolando Guedes <rolando.guedes@3gnt.net>
|
||||||
|
- Benjamin Tan <demoneaux@gmail.com>
|
||||||
|
- Mathias Rasmussen <mathiasvr@gmail.com>
|
||||||
|
- Sergey Bargamon <sergey@bargamon.ru>
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,5 +1,40 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
|
## v0.6.0 - 2016-05-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added Preferences page
|
||||||
|
- Save video position, resume playback from saved position
|
||||||
|
- Add additional video player keyboard shortcuts (#275)
|
||||||
|
- Use `poster.jpg` file as the poster image if available (#558)
|
||||||
|
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
|
||||||
|
- Add support for pasting a `instant.io` links (#559)
|
||||||
|
- Add announcement feature
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Nicer player UI
|
||||||
|
- Reduce startup jank, improve startup time (#568)
|
||||||
|
- Cleanup unsupported codec detection (#569, #570)
|
||||||
|
- Cleaner look for the torrent file list
|
||||||
|
- Improve subtitle positioning (#551)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
|
||||||
|
- Fix bugs in LocationHistory
|
||||||
|
- When player is active, and magnet link is pasted, go back to list
|
||||||
|
- After deleting torrent, remove just the player from forward stack
|
||||||
|
- After creating torrent, remove create torrent page from forward stack
|
||||||
|
- Cancel button on create torrent page should only go back one page
|
||||||
|
|
||||||
|
## v0.5.1 - 2016-05-18
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix auto-updater (OS X, Windows).
|
||||||
|
|
||||||
## v0.5.0 - 2016-05-17
|
## v0.5.0 - 2016-05-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) Feross Aboukhadijeh
|
Copyright (c) WebTorrent, LLC
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -87,4 +87,4 @@ brew install wine
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org).
|
MIT. Copyright (c) [WebTorrent, LLC](https://webtorrent.io).
|
||||||
|
|||||||
@@ -182,8 +182,6 @@ function buildDarwin (cb) {
|
|||||||
var infoPlistPath = path.join(contentsPath, 'Info.plist')
|
var infoPlistPath = path.join(contentsPath, 'Info.plist')
|
||||||
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
|
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
|
||||||
|
|
||||||
// TODO: Use new `extend-info` and `extra-resource` opts to electron-packager,
|
|
||||||
// available as of v6.
|
|
||||||
infoPlist.CFBundleDocumentTypes = [
|
infoPlist.CFBundleDocumentTypes = [
|
||||||
{
|
{
|
||||||
CFBundleTypeExtensions: [ 'torrent' ],
|
CFBundleTypeExtensions: [ 'torrent' ],
|
||||||
@@ -211,6 +209,25 @@ function buildDarwin (cb) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
infoPlist.UTExportedTypeDeclarations = [
|
||||||
|
{
|
||||||
|
UTTypeConformsTo: [
|
||||||
|
'public.data',
|
||||||
|
'public.item',
|
||||||
|
'com.bittorrent.torrent'
|
||||||
|
],
|
||||||
|
UTTypeDescription: 'BitTorrent Document',
|
||||||
|
UTTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
||||||
|
UTTypeIdentifier: 'org.bittorrent.torrent',
|
||||||
|
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
|
||||||
|
UTTypeTagSpecification: {
|
||||||
|
'com.apple.ostype': 'TORR',
|
||||||
|
'public.filename-extension': [ 'torrent' ],
|
||||||
|
'public.mime-type': 'application/x-bittorrent'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
|
fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
|
||||||
|
|
||||||
// Copy torrent file icon into app bundle
|
// Copy torrent file icon into app bundle
|
||||||
|
|||||||
@@ -6,6 +6,5 @@ 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 dedupe
|
||||||
npm test
|
npm test
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ var fs = require('fs')
|
|||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
var APP_NAME = 'WebTorrent'
|
var APP_NAME = 'WebTorrent'
|
||||||
var APP_TEAM = 'The WebTorrent Project'
|
var APP_TEAM = 'WebTorrent, LLC'
|
||||||
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')
|
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||||
|
|
||||||
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'),
|
||||||
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
|
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
|
||||||
@@ -17,8 +19,7 @@ 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' +
|
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',
|
||||||
|
|
||||||
|
|||||||
38
main/announcement.js
Normal file
38
main/announcement.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
module.exports = {
|
||||||
|
init
|
||||||
|
}
|
||||||
|
|
||||||
|
var electron = require('electron')
|
||||||
|
var get = require('simple-get')
|
||||||
|
|
||||||
|
var config = require('../config')
|
||||||
|
var log = require('./log')
|
||||||
|
|
||||||
|
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
||||||
|
'?version=' + config.APP_VERSION +
|
||||||
|
'&platform=' + process.platform
|
||||||
|
|
||||||
|
function init () {
|
||||||
|
get.concat(ANNOUNCEMENT_URL, function (err, res, data) {
|
||||||
|
if (err) return log('failed to retrieve remote message')
|
||||||
|
if (res.statusCode !== 200) return log('no remote message')
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = JSON.parse(data.toString())
|
||||||
|
} catch (err) {
|
||||||
|
data = {
|
||||||
|
title: 'WebTorrent Desktop Announcement',
|
||||||
|
message: 'WebTorrent Desktop Announcement',
|
||||||
|
detail: data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.dialog.showMessageBox({
|
||||||
|
type: 'info',
|
||||||
|
buttons: ['OK'],
|
||||||
|
title: data.title,
|
||||||
|
message: data.message,
|
||||||
|
detail: data.detail
|
||||||
|
}, function () {})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ var electron = require('electron')
|
|||||||
var app = electron.app
|
var app = electron.app
|
||||||
var ipcMain = electron.ipcMain
|
var ipcMain = electron.ipcMain
|
||||||
|
|
||||||
|
var announcement = require('./announcement')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var crashReporter = require('../crash-reporter')
|
var crashReporter = require('../crash-reporter')
|
||||||
var handlers = require('./handlers')
|
var handlers = require('./handlers')
|
||||||
@@ -91,6 +92,7 @@ function init () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function delayedInit () {
|
function delayedInit () {
|
||||||
|
announcement.init()
|
||||||
tray.init()
|
tray.init()
|
||||||
handlers.install()
|
handlers.install()
|
||||||
updater.init()
|
updater.init()
|
||||||
|
|||||||
@@ -107,11 +107,11 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('vlcPlay', function (e, url) {
|
ipcMain.on('vlcPlay', function (e, url) {
|
||||||
var args = ['--play-and-exit', '--quiet', url]
|
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url]
|
||||||
console.log('Running vlc ' + args.join(' '))
|
console.log('Running vlc ' + args.join(' '))
|
||||||
|
|
||||||
vlc.spawn(args, function (err, proc) {
|
vlc.spawn(args, function (err, proc) {
|
||||||
if (err) windows.main.send('dispatch', 'vlcNotFound')
|
if (err) return windows.main.send('dispatch', 'vlcNotFound')
|
||||||
vlcProcess = proc
|
vlcProcess = proc
|
||||||
|
|
||||||
// If it works, close the modal after a second
|
// If it works, close the modal after a second
|
||||||
|
|||||||
83
main/menu.js
83
main/menu.js
@@ -92,6 +92,35 @@ function openSubtitles () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function skipForward () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'skip', 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipBack () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'skip', -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function increasePlaybackRate () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'changePlaybackRate', 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function decreasePlaybackRate () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'changePlaybackRate', -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the preferences window
|
||||||
|
function showPreferences () {
|
||||||
|
windows.main.send('dispatch', 'preferences')
|
||||||
|
}
|
||||||
|
|
||||||
function onWindowShow () {
|
function onWindowShow () {
|
||||||
log('onWindowShow')
|
log('onWindowShow')
|
||||||
getMenuItem('Full Screen').enabled = true
|
getMenuItem('Full Screen').enabled = true
|
||||||
@@ -110,6 +139,10 @@ function onPlayerOpen () {
|
|||||||
getMenuItem('Increase Volume').enabled = true
|
getMenuItem('Increase Volume').enabled = true
|
||||||
getMenuItem('Decrease Volume').enabled = true
|
getMenuItem('Decrease Volume').enabled = true
|
||||||
getMenuItem('Add Subtitles File...').enabled = true
|
getMenuItem('Add Subtitles File...').enabled = true
|
||||||
|
getMenuItem('Step Forward').enabled = true
|
||||||
|
getMenuItem('Step Backward').enabled = true
|
||||||
|
getMenuItem('Increase Speed').enabled = true
|
||||||
|
getMenuItem('Decrease Speed').enabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function onPlayerClose () {
|
||||||
@@ -118,6 +151,10 @@ function onPlayerClose () {
|
|||||||
getMenuItem('Increase Volume').enabled = false
|
getMenuItem('Increase Volume').enabled = false
|
||||||
getMenuItem('Decrease Volume').enabled = false
|
getMenuItem('Decrease Volume').enabled = false
|
||||||
getMenuItem('Add Subtitles File...').enabled = false
|
getMenuItem('Add Subtitles File...').enabled = false
|
||||||
|
getMenuItem('Step Forward').enabled = false
|
||||||
|
getMenuItem('Step Backward').enabled = false
|
||||||
|
getMenuItem('Increase Speed').enabled = false
|
||||||
|
getMenuItem('Decrease Speed').enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleFullScreen (isFullScreen) {
|
function onToggleFullScreen (isFullScreen) {
|
||||||
@@ -237,6 +274,14 @@ function getAppMenuTemplate () {
|
|||||||
label: 'Select All',
|
label: 'Select All',
|
||||||
accelerator: 'CmdOrCtrl+A',
|
accelerator: 'CmdOrCtrl+A',
|
||||||
role: 'selectall'
|
role: 'selectall'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
accelerator: 'CmdOrCtrl+,',
|
||||||
|
click: () => showPreferences()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -307,6 +352,36 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Step Forward',
|
||||||
|
accelerator: 'CmdOrCtrl+Alt+Right',
|
||||||
|
click: skipForward,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Step Backward',
|
||||||
|
accelerator: 'CmdOrCtrl+Alt+Left',
|
||||||
|
click: skipBack,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Increase Speed',
|
||||||
|
accelerator: 'CmdOrCtrl+=',
|
||||||
|
click: increasePlaybackRate,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Decrease Speed',
|
||||||
|
accelerator: 'CmdOrCtrl+-',
|
||||||
|
click: decreasePlaybackRate,
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Add Subtitles File...',
|
label: 'Add Subtitles File...',
|
||||||
click: openSubtitles,
|
click: openSubtitles,
|
||||||
@@ -349,6 +424,14 @@ function getAppMenuTemplate () {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Preferences',
|
||||||
|
accelerator: 'Cmd+,',
|
||||||
|
click: () => showPreferences()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Services',
|
label: 'Services',
|
||||||
role: 'services',
|
role: 'services',
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ var config = require('../config')
|
|||||||
var log = require('./log')
|
var log = require('./log')
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
|
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||||
|
'?version=' + config.APP_VERSION +
|
||||||
|
'&platform=' + process.platform
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
initLinux()
|
initLinux()
|
||||||
@@ -20,7 +24,7 @@ function init () {
|
|||||||
// The Electron auto-updater does not support Linux yet, so manually check for updates and
|
// The Electron auto-updater does not support Linux yet, so manually check for updates and
|
||||||
// `show the user a modal notification.
|
// `show the user a modal notification.
|
||||||
function initLinux () {
|
function initLinux () {
|
||||||
get.concat(config.AUTO_UPDATE_URL, onResponse)
|
get.concat(AUTO_UPDATE_URL, onResponse)
|
||||||
|
|
||||||
function onResponse (err, res, data) {
|
function onResponse (err, res, data) {
|
||||||
if (err) return log(`Update error: ${err.message}`)
|
if (err) return log(`Update error: ${err.message}`)
|
||||||
@@ -67,5 +71,6 @@ function initDarwinWin32 () {
|
|||||||
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
|
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
|
||||||
)
|
)
|
||||||
|
|
||||||
electron.autoUpdater.setFeedURL(config.AUTO_UPDATE_URL)
|
electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
|
||||||
|
electron.autoUpdater.checkForUpdates()
|
||||||
}
|
}
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"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.5.0",
|
"version": "0.6.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Feross Aboukhadijeh",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@feross.org",
|
"email": "feross@feross.org",
|
||||||
"url": "http://feross.org"
|
"url": "https://webtorrent.io"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"webtorrent-desktop": "./bin/cmd.js"
|
"webtorrent-desktop": "./bin/cmd.js"
|
||||||
@@ -16,16 +16,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"airplay-js": "guerrerocarlos/node-airplay-js",
|
"airplay-js": "guerrerocarlos/node-airplay-js",
|
||||||
"application-config": "^0.2.1",
|
"application-config": "^0.2.1",
|
||||||
"async": "^2.0.0-rc.5",
|
|
||||||
"bitfield": "^1.0.2",
|
"bitfield": "^1.0.2",
|
||||||
"chromecasts": "^1.8.0",
|
"chromecasts": "^1.8.0",
|
||||||
"concat-stream": "^1.5.1",
|
|
||||||
"create-torrent": "^3.24.5",
|
"create-torrent": "^3.24.5",
|
||||||
"deep-equal": "^1.0.1",
|
"deep-equal": "^1.0.1",
|
||||||
"dlnacasts": "^0.1.0",
|
"dlnacasts": "^0.1.0",
|
||||||
"drag-drop": "^2.11.0",
|
"drag-drop": "^2.11.0",
|
||||||
"electron-localshortcut": "^0.6.0",
|
"electron-localshortcut": "^0.6.0",
|
||||||
"electron-prebuilt": "1.0.2",
|
"electron-prebuilt": "1.1.1",
|
||||||
"fs-extra": "^0.27.0",
|
"fs-extra": "^0.27.0",
|
||||||
"hyperx": "^2.0.2",
|
"hyperx": "^2.0.2",
|
||||||
"iso-639-1": "^1.2.1",
|
"iso-639-1": "^1.2.1",
|
||||||
@@ -34,12 +32,15 @@
|
|||||||
"musicmetadata": "^2.0.2",
|
"musicmetadata": "^2.0.2",
|
||||||
"network-address": "^1.1.0",
|
"network-address": "^1.1.0",
|
||||||
"prettier-bytes": "^1.0.1",
|
"prettier-bytes": "^1.0.1",
|
||||||
|
"run-parallel": "^1.1.6",
|
||||||
|
"simple-concat": "^1.0.0",
|
||||||
"simple-get": "^2.0.0",
|
"simple-get": "^2.0.0",
|
||||||
"srt-to-vtt": "^1.1.1",
|
"srt-to-vtt": "^1.1.1",
|
||||||
"virtual-dom": "^2.1.1",
|
"virtual-dom": "^2.1.1",
|
||||||
"vlc-command": "^1.0.1",
|
"vlc-command": "^1.0.1",
|
||||||
"webtorrent": "0.x",
|
"webtorrent": "0.x",
|
||||||
"winreg": "^1.2.0"
|
"winreg": "^1.2.0",
|
||||||
|
"zero-fill": "^2.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-zip": "^2.0.1",
|
"cross-zip": "^2.0.1",
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
"gh-release": "^2.0.3",
|
"gh-release": "^2.0.3",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"nobin-debian-installer": "^0.0.9",
|
"nobin-debian-installer": "^0.0.10",
|
||||||
"open": "0.0.5",
|
"open": "0.0.5",
|
||||||
"plist": "^1.2.0",
|
"plist": "^1.2.0",
|
||||||
"rimraf": "^2.5.2",
|
"rimraf": "^2.5.2",
|
||||||
@@ -70,7 +71,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"appdmg": "^0.3.6"
|
"appdmg": "^0.4.3"
|
||||||
},
|
},
|
||||||
"productName": "WebTorrent",
|
"productName": "WebTorrent",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -50,19 +50,22 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadein {
|
@keyframes fadein {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
animation: fadein 0.3s;
|
|
||||||
background: rgb(40, 40, 40);
|
background: rgb(40, 40, 40);
|
||||||
|
animation: fadein 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app:not(.is-focused) {
|
.app:not(.is-focused) {
|
||||||
@@ -94,11 +97,20 @@ table {
|
|||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon:not(.disabled):hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* UTILITY CLASSES
|
* UTILITY CLASSES
|
||||||
*/
|
*/
|
||||||
@@ -109,8 +121,8 @@ table {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled {
|
.float-left {
|
||||||
opacity: 0.3;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
@@ -144,8 +156,8 @@ table {
|
|||||||
.header {
|
.header {
|
||||||
background: rgb(40, 40, 40);
|
background: rgb(40, 40, 40);
|
||||||
border-bottom: 1px solid rgb(20, 20, 20);
|
border-bottom: 1px solid rgb(20, 20, 20);
|
||||||
height: 37px; /* vertically center OS menu buttons (OS X) */
|
height: 38px; /* vertically center OS menu buttons (OS X) */
|
||||||
padding-top: 6px;
|
padding-top: 7px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -164,7 +176,13 @@ table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app.view-player .header {
|
.app.view-player .header {
|
||||||
opacity: 0.8;
|
background: rgba(40, 40, 40, 0.8);
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app.view-player.is-win32 .header,
|
||||||
|
.app.view-player.is-linux .header {
|
||||||
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.hide-video-controls.view-player .header {
|
.app.hide-video-controls.view-player .header {
|
||||||
@@ -172,12 +190,8 @@ table {
|
|||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.hide-header .header {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .title {
|
.header .title {
|
||||||
opacity: 0.6;
|
opacity: 0.7;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
padding: 0 150px 0 150px;
|
padding: 0 150px 0 150px;
|
||||||
@@ -188,35 +202,22 @@ table {
|
|||||||
|
|
||||||
.header .nav {
|
.header .nav {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-right: 9px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .nav.left {
|
.header .nav.left {
|
||||||
float: left;
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .nav.right {
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.is-darwin:not(.is-fullscreen) .header .nav.left {
|
.app.is-darwin:not(.is-fullscreen) .header .nav.left {
|
||||||
margin-left: 78px;
|
margin-left: 78px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .nav.right {
|
.header .back,
|
||||||
float: right;
|
.header .forward {
|
||||||
}
|
|
||||||
|
|
||||||
.header .nav * {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .nav .disabled {
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .nav *:not(.disabled):hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .nav .back,
|
|
||||||
.header .nav .forward {
|
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
}
|
}
|
||||||
@@ -535,6 +536,11 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent .buttons .play.resume-position {
|
||||||
|
position: relative;
|
||||||
|
-webkit-clip-path: circle(18px at center);
|
||||||
|
}
|
||||||
|
|
||||||
.torrent .buttons .delete {
|
.torrent .buttons .delete {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
@@ -543,6 +549,10 @@ input {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent .buttons .radial-progress {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
.torrent .name {
|
.torrent .name {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -592,7 +602,7 @@ body.drag .app::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details {
|
.torrent-details {
|
||||||
padding: 8em 12px 20px 20px;
|
padding: 8em 0 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details table {
|
.torrent-details table {
|
||||||
@@ -617,7 +627,7 @@ body.drag .app::after {
|
|||||||
.torrent-details td {
|
.torrent-details td {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
vertical-align: bottom;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details td .icon {
|
.torrent-details td .icon {
|
||||||
@@ -627,7 +637,14 @@ body.drag .app::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details td.col-icon {
|
.torrent-details td.col-icon {
|
||||||
width: 2em;
|
width: 3em;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.torrent-details td.col-icon .radial-progress {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-left: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details td.col-name {
|
.torrent-details td.col-name {
|
||||||
@@ -646,7 +663,8 @@ body.drag .app::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details td.col-select {
|
.torrent-details td.col-select {
|
||||||
width: 2em;
|
width: 3em;
|
||||||
|
padding-right: 13px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,7 +696,7 @@ body.drag .app::after {
|
|||||||
* PLAYER CONTROLS
|
* PLAYER CONTROLS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.player-controls {
|
.player .controls {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: rgba(40, 40, 40, 0.8);
|
background: rgba(40, 40, 40, 0.8);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -687,7 +705,63 @@ body.drag .app::after {
|
|||||||
transition: opacity 0.15s ease-out;
|
transition: opacity 0.15s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.hide-video-controls .player-controls {
|
.player .controls .icon {
|
||||||
|
display: block;
|
||||||
|
margin: 8px;
|
||||||
|
font-size: 22px;
|
||||||
|
opacity: 0.85;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fix for overflowing captions icon
|
||||||
|
* https://github.com/feross/webtorrent-desktop/issues/467
|
||||||
|
*/
|
||||||
|
max-width: 23px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .icon:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .play-pause {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .volume-slider {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
width: 60px;
|
||||||
|
height: 3px;
|
||||||
|
margin: 18px 8px 8px 0;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0.85;
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .time,
|
||||||
|
.player .controls .rate {
|
||||||
|
font-weight: 100;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 9px 8px 8px 8px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .icon.closed-captions {
|
||||||
|
font-size: 26px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .icon.fullscreen {
|
||||||
|
font-size: 26px;
|
||||||
|
margin-right: 15px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app.hide-video-controls .player .controls {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,13 +769,16 @@ body.drag .app::after {
|
|||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.hide-video-controls .player .player-controls:hover {
|
/* TODO: find better way to handle this (that also
|
||||||
|
* keeps the header visible too).
|
||||||
|
*/
|
||||||
|
.app.hide-video-controls .player .controls:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* invisible click target for scrubbing */
|
/* invisible click target for scrubbing */
|
||||||
.player-controls .scrub-bar {
|
.player .controls .scrub-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 23px; /* 3px .loading-bar plus 10px above and below */
|
height: 23px; /* 3px .loading-bar plus 10px above and below */
|
||||||
@@ -710,7 +787,7 @@ body.drag .app::after {
|
|||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .loading-bar {
|
.player .controls .loading-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: -3px;
|
top: -3px;
|
||||||
@@ -720,14 +797,14 @@ body.drag .app::after {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .loading-bar-part {
|
.player .controls .loading-bar-part {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #dd0000;
|
background-color: #dd0000;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .playback-cursor {
|
.player .controls .playback-cursor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -3px;
|
top: -3px;
|
||||||
background-color: #FFF;
|
background-color: #FFF;
|
||||||
@@ -736,94 +813,26 @@ body.drag .app::after {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
transition-property: width, height, border-radius, margin-top, margin-left;
|
transition-property: width, height, top, margin-left;
|
||||||
transition-duration: 0.1s;
|
transition-duration: 0.1s;
|
||||||
transition-timing-function: ease-out;
|
transition-timing-function: ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .play-pause {
|
.player .controls .closed-captions.active,
|
||||||
display: block;
|
.player .controls .device.active {
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 5px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .device,
|
|
||||||
.player-controls .fullscreen,
|
|
||||||
.player-controls .closed-captions,
|
|
||||||
.player-controls .volume-icon,
|
|
||||||
.player-controls .back {
|
|
||||||
display: block;
|
|
||||||
height: 20px;
|
|
||||||
margin: 5px;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Fix for overflowing captions icon
|
|
||||||
* https://github.com/feross/webtorrent-desktop/issues/467
|
|
||||||
*/
|
|
||||||
max-width: 22px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume,
|
|
||||||
.player-controls .back {
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .device,
|
|
||||||
.player-controls .closed-captions,
|
|
||||||
.player-controls .fullscreen {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .fullscreen {
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume-icon,
|
|
||||||
.player-controls .device {
|
|
||||||
font-size: 18px; /* make the cast icons less huge */
|
|
||||||
margin-top: 8px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .closed-captions.active,
|
|
||||||
.player-controls .device.active {
|
|
||||||
color: #9af;
|
color: #9af;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .volume {
|
.player .controls .volume-slider::-webkit-slider-thumb {
|
||||||
display: block;
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume-icon {
|
|
||||||
float: left;
|
|
||||||
margin-right: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume-slider {
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
width: 50px;
|
|
||||||
height: 3px;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
vertical-align: sub;
|
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
|
||||||
|
|
||||||
.player-controls .volume-slider::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
opacity: 1.0;
|
width: 13px;
|
||||||
width: 10px;
|
height: 13px;
|
||||||
height: 10px;
|
|
||||||
border: 1px solid #303233;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .volume-slider:focus {
|
.player .controls .volume-slider:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,19 +842,27 @@ body.drag .app::after {
|
|||||||
|
|
||||||
.player .playback-bar:hover .playback-cursor {
|
.player .playback-bar:hover .playback-cursor {
|
||||||
top: -8px;
|
top: -8px;
|
||||||
|
margin-left: -5px;
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cue text position so it appears above the player controls.
|
||||||
|
*/
|
||||||
|
video::-webkit-media-text-track-container {
|
||||||
|
bottom: 60px;
|
||||||
|
transition: bottom 0.1s ease-out;
|
||||||
|
}
|
||||||
|
.app.hide-video-controls video::-webkit-media-text-track-container {
|
||||||
|
bottom: 30px;
|
||||||
|
}
|
||||||
::cue {
|
::cue {
|
||||||
background: none;
|
background: none;
|
||||||
color: #FFF;
|
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;
|
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
|
||||||
*/
|
*/
|
||||||
@@ -895,6 +912,173 @@ body.drag .app::after {
|
|||||||
margin-right: 4px !important;
|
margin-right: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Preferences page, based on Atom settings style
|
||||||
|
*/
|
||||||
|
|
||||||
|
.preferences {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: calc(10/7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .text {
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .icon {
|
||||||
|
color: rgba(170, 170, 170, 0.6);
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .btn {
|
||||||
|
display: inline-block;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-color: #cccccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #9da5b4;
|
||||||
|
text-shadow: none;
|
||||||
|
border: 1px solid #181a1f;
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
white-space: initial;
|
||||||
|
font-size: 0.889em;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .btn .icon {
|
||||||
|
margin: 0;
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .help .icon {
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.preferences .preferences-panel .control-group + .control-group {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section {
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 1px solid #181a1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section:first {
|
||||||
|
border-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section:first-child,
|
||||||
|
.preferences .section:last-child {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section.section:empty {
|
||||||
|
padding: 0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences section .section-heading,
|
||||||
|
.preferences .section .section-heading {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #dcdcdc;
|
||||||
|
font-size: 1.75em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .sub-section-heading.icon:before,
|
||||||
|
.preferences .section-heading.icon:before {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section-heading-count {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .section-body {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .sub-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .sub-section .sub-section-heading {
|
||||||
|
color: #dcdcdc;
|
||||||
|
font-size: 1.4em;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preferences-panel label {
|
||||||
|
color: #a8a8a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preferences-panel .control-group + .control-group {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preferences-panel .control-group .editor-container {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preference-title {
|
||||||
|
font-size: 1.2em;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .preference-description {
|
||||||
|
color: rgba(170, 170, 170, 0.6);
|
||||||
|
-webkit-user-select: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences input {
|
||||||
|
font-size: 1.1em;
|
||||||
|
line-height: 1.15em;
|
||||||
|
max-height: none;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #a8a8a8;
|
||||||
|
border: 1px solid #181a1f;
|
||||||
|
background-color: #1b1d23;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences input::-webkit-input-placeholder {
|
||||||
|
color: rgba(170, 170, 170, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .control-group input {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .control-group input.file-picker-text {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preferences .control-group .checkbox .icon {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MEDIA OVERLAY / AUDIO DETAILS
|
* MEDIA OVERLAY / AUDIO DETAILS
|
||||||
*/
|
*/
|
||||||
@@ -963,10 +1147,6 @@ body.drag .app::after {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app.hide-header .error-popover {
|
|
||||||
top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-popover.hidden {
|
.error-popover.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -994,3 +1174,66 @@ body.drag .app::after {
|
|||||||
.error-text {
|
.error-text {
|
||||||
color: #c44;
|
color: #c44;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* RADIAL PROGRESS BAR
|
||||||
|
*/
|
||||||
|
|
||||||
|
.radial-progress {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress .circle .mask,
|
||||||
|
.radial-progress .circle .fill {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress .circle .mask {
|
||||||
|
clip: rect(0px, 16px, 16px, 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress .circle .fill {
|
||||||
|
clip: rect(0px, 8px, 16px, 0px);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress .inset {
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin: 2px 0 0 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress-large {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress-large .circle .mask,
|
||||||
|
.radial-progress-large .circle .fill {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.radial-progress-large .circle .mask {
|
||||||
|
clip: rect(0px, 40px, 40px, 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress-large .circle .fill {
|
||||||
|
clip: rect(0px, 20px, 40px, 0px);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radial-progress-large .inset {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,12 +14,10 @@ var ipcRenderer = electron.ipcRenderer
|
|||||||
setupIpc()
|
setupIpc()
|
||||||
|
|
||||||
var appConfig = require('application-config')('WebTorrent')
|
var appConfig = require('application-config')('WebTorrent')
|
||||||
var Async = require('async')
|
|
||||||
var concat = require('concat-stream')
|
|
||||||
var dragDrop = require('drag-drop')
|
var dragDrop = require('drag-drop')
|
||||||
var fs = require('fs-extra')
|
var fs = require('fs-extra')
|
||||||
var iso639 = require('iso-639-1')
|
|
||||||
var mainLoop = require('main-loop')
|
var mainLoop = require('main-loop')
|
||||||
|
var parallel = require('run-parallel')
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
var createElement = require('virtual-dom/create-element')
|
var createElement = require('virtual-dom/create-element')
|
||||||
@@ -42,18 +40,29 @@ appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
|
|||||||
// This dependency is the slowest-loading, so we lazy load it
|
// This dependency is the slowest-loading, so we lazy load it
|
||||||
var Cast = null
|
var Cast = null
|
||||||
|
|
||||||
// For easy debugging in Developer Tools
|
|
||||||
var state = global.state = State.getInitialState()
|
|
||||||
|
|
||||||
// Push the first page into the location history
|
|
||||||
state.location.go({ url: 'home' })
|
|
||||||
|
|
||||||
var vdomLoop
|
var vdomLoop
|
||||||
|
|
||||||
|
var state = State.getInitialState()
|
||||||
|
state.location.go({ url: 'home' }) // Add first page to location history
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
||||||
|
function loadState (cb) {
|
||||||
|
appConfig.read(function (err, data) {
|
||||||
|
if (err) console.error(err)
|
||||||
|
|
||||||
|
// populate defaults if they're not there
|
||||||
|
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
|
||||||
|
state.saved.torrents.forEach(function (torrentSummary) {
|
||||||
|
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
|
||||||
|
})
|
||||||
|
|
||||||
|
if (cb) cb()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called once when the application loads. (Not once per window.)
|
* Called once when the application loads. (Not once per window.)
|
||||||
* Connects to the torrent networks, sets up the UI and OS integrations like
|
* Connects to the torrent networks, sets up the UI and OS integrations like
|
||||||
@@ -87,7 +96,7 @@ function init () {
|
|||||||
|
|
||||||
// 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', onOpen)
|
||||||
|
|
||||||
// ...same thing if you paste a torrent
|
// ...same thing if you paste a torrent
|
||||||
document.addEventListener('paste', onPaste)
|
document.addEventListener('paste', onPaste)
|
||||||
@@ -183,7 +192,7 @@ function render (state) {
|
|||||||
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
|
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
|
||||||
function update () {
|
function update () {
|
||||||
showOrHidePlayerControls()
|
showOrHidePlayerControls()
|
||||||
vdomLoop.update(state)
|
if (vdomLoop) vdomLoop.update(state)
|
||||||
updateElectron()
|
updateElectron()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,16 +261,7 @@ function dispatch (action, ...args) {
|
|||||||
setDimensions(args[0] /* dimensions */)
|
setDimensions(args[0] /* dimensions */)
|
||||||
}
|
}
|
||||||
if (action === 'backToList') {
|
if (action === 'backToList') {
|
||||||
// Exit any modals and screens with a back button
|
backToList()
|
||||||
state.modal = null
|
|
||||||
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 === 'escapeBack') {
|
if (action === 'escapeBack') {
|
||||||
if (state.modal) {
|
if (state.modal) {
|
||||||
@@ -282,19 +282,17 @@ function dispatch (action, ...args) {
|
|||||||
playPause()
|
playPause()
|
||||||
}
|
}
|
||||||
if (action === 'play') {
|
if (action === 'play') {
|
||||||
if (state.location.pending()) return
|
playFile(args[0] /* infoHash */, args[1] /* index */)
|
||||||
state.location.go({
|
|
||||||
url: 'player',
|
|
||||||
onbeforeload: function (cb) {
|
|
||||||
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
|
|
||||||
},
|
|
||||||
onbeforeunload: closePlayer
|
|
||||||
})
|
|
||||||
play()
|
|
||||||
}
|
}
|
||||||
if (action === 'playbackJump') {
|
if (action === 'playbackJump') {
|
||||||
jumpToTime(args[0] /* seconds */)
|
jumpToTime(args[0] /* seconds */)
|
||||||
}
|
}
|
||||||
|
if (action === 'skip') {
|
||||||
|
jumpToTime(state.playing.currentTime + (args[0] /* direction */ * 10))
|
||||||
|
}
|
||||||
|
if (action === 'changePlaybackRate') {
|
||||||
|
changePlaybackRate(args[0] /* direction */)
|
||||||
|
}
|
||||||
if (action === 'changeVolume') {
|
if (action === 'changeVolume') {
|
||||||
changeVolume(args[0] /* increase */)
|
changeVolume(args[0] /* increase */)
|
||||||
}
|
}
|
||||||
@@ -314,7 +312,7 @@ function dispatch (action, ...args) {
|
|||||||
state.playing.isStalled = true
|
state.playing.isStalled = true
|
||||||
}
|
}
|
||||||
if (action === 'mediaError') {
|
if (action === 'mediaError') {
|
||||||
if (state.location.current().url === 'player') {
|
if (state.location.url() === 'player') {
|
||||||
state.playing.location = 'error'
|
state.playing.location = 'error'
|
||||||
ipcRenderer.send('checkForVLC')
|
ipcRenderer.send('checkForVLC')
|
||||||
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
|
||||||
@@ -348,6 +346,26 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'exitModal') {
|
if (action === 'exitModal') {
|
||||||
state.modal = null
|
state.modal = null
|
||||||
}
|
}
|
||||||
|
if (action === 'preferences') {
|
||||||
|
state.location.go({
|
||||||
|
url: 'preferences',
|
||||||
|
onbeforeload: function (cb) {
|
||||||
|
// initialize preferences
|
||||||
|
state.window.title = 'Preferences'
|
||||||
|
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
||||||
|
cb()
|
||||||
|
},
|
||||||
|
onbeforeunload: function (cb) {
|
||||||
|
// save state after preferences
|
||||||
|
savePreferences()
|
||||||
|
state.window.title = config.APP_WINDOW_TITLE
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (action === 'updatePreferences') {
|
||||||
|
updatePreferences(args[0], args[1] /* property, value */)
|
||||||
|
}
|
||||||
if (action === 'updateAvailable') {
|
if (action === 'updateAvailable') {
|
||||||
updateAvailable(args[0] /* version */)
|
updateAvailable(args[0] /* version */)
|
||||||
}
|
}
|
||||||
@@ -394,7 +412,7 @@ function pause () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function playPause () {
|
function playPause () {
|
||||||
if (state.location.current().url !== 'player') return
|
if (state.location.url() !== 'player') return
|
||||||
if (state.playing.isPaused) {
|
if (state.playing.isPaused) {
|
||||||
play()
|
play()
|
||||||
} else {
|
} else {
|
||||||
@@ -409,7 +427,26 @@ function jumpToTime (time) {
|
|||||||
state.playing.jumpToTime = time
|
state.playing.jumpToTime = time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function changePlaybackRate (direction) {
|
||||||
|
var rate = state.playing.playbackRate
|
||||||
|
if (direction > 0 && rate >= 0.25 && rate < 2) {
|
||||||
|
rate += 0.25
|
||||||
|
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
||||||
|
rate -= 0.25
|
||||||
|
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
|
||||||
|
rate = -1
|
||||||
|
} else if (direction > 0 && rate === -1) {
|
||||||
|
rate = 0.25
|
||||||
|
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
|
||||||
|
rate *= 2
|
||||||
|
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
|
||||||
|
rate /= 2
|
||||||
|
}
|
||||||
|
state.playing.playbackRate = rate
|
||||||
|
if (lazyLoadCast().isCasting() && !Cast.setRate(rate)) {
|
||||||
|
state.playing.playbackRate = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
function changeVolume (delta) {
|
function changeVolume (delta) {
|
||||||
// change volume with delta value
|
// change volume with delta value
|
||||||
setVolume(state.playing.volume + delta)
|
setVolume(state.playing.volume + delta)
|
||||||
@@ -436,6 +473,24 @@ function openSubtitles () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quits any modal popovers and returns to the torrent list screen
|
||||||
|
function backToList () {
|
||||||
|
// Exit any modals and screens with a back button
|
||||||
|
state.modal = null
|
||||||
|
state.location.backToFirst(function () {
|
||||||
|
// If we were already on the torrent list, scroll to the top
|
||||||
|
var contentTag = document.querySelector('.content')
|
||||||
|
if (contentTag) contentTag.scrollTop = 0
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -481,22 +536,6 @@ function setupIpc () {
|
|||||||
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load state.saved from the JSON state file
|
|
||||||
function loadState (cb) {
|
|
||||||
appConfig.read(function (err, data) {
|
|
||||||
if (err) console.error(err)
|
|
||||||
console.log('loaded state from ' + appConfig.filePath)
|
|
||||||
|
|
||||||
// populate defaults if they're not there
|
|
||||||
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
|
|
||||||
state.saved.torrents.forEach(function (torrentSummary) {
|
|
||||||
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
|
|
||||||
})
|
|
||||||
|
|
||||||
if (cb) cb()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Starts all torrents that aren't paused on program startup
|
// Starts all torrents that aren't paused on program startup
|
||||||
function resumeTorrents () {
|
function resumeTorrents () {
|
||||||
state.saved.torrents
|
state.saved.torrents
|
||||||
@@ -504,6 +543,27 @@ function resumeTorrents () {
|
|||||||
.forEach((x) => startTorrentingSummary(x))
|
.forEach((x) => startTorrentingSummary(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updates a single property in the UNSAVED prefs
|
||||||
|
// For example: updatePreferences("foo.bar", "baz")
|
||||||
|
// Call savePreferences to save to config.json
|
||||||
|
function updatePreferences (property, value) {
|
||||||
|
var path = property.split('.')
|
||||||
|
var key = state.unsaved.prefs
|
||||||
|
for (var i = 0; i < path.length - 1; i++) {
|
||||||
|
if (typeof key[path[i]] === 'undefined') {
|
||||||
|
key[path[i]] = {}
|
||||||
|
}
|
||||||
|
key = key[path[i]]
|
||||||
|
}
|
||||||
|
key[path[i]] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// All unsaved prefs take effect atomically, and are saved to config.json
|
||||||
|
function savePreferences () {
|
||||||
|
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
||||||
|
saveState()
|
||||||
|
}
|
||||||
|
|
||||||
// Don't write state.saved to file more than once a second
|
// Don't write state.saved to file more than once a second
|
||||||
function saveStateThrottled () {
|
function saveStateThrottled () {
|
||||||
if (state.saveStateTimeout) return
|
if (state.saveStateTimeout) return
|
||||||
@@ -529,7 +589,7 @@ function saveState () {
|
|||||||
if (key === 'progress' || key === 'torrentKey') {
|
if (key === 'progress' || key === 'torrentKey') {
|
||||||
continue // Don't save progress info or key for the webtorrent process
|
continue // Don't save progress info or key for the webtorrent process
|
||||||
}
|
}
|
||||||
if (key === 'playStatus' && x.playStatus !== 'unplayable') {
|
if (key === 'playStatus') {
|
||||||
continue // Don't save whether a torrent is playing / pending
|
continue // Don't save whether a torrent is playing / pending
|
||||||
}
|
}
|
||||||
torrent[key] = x[key]
|
torrent[key] = x[key]
|
||||||
@@ -546,26 +606,32 @@ function saveState () {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called when the user drag-drops files onto the app
|
||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
if (!Array.isArray(files)) files = [ files ]
|
if (!Array.isArray(files)) files = [ files ]
|
||||||
|
|
||||||
// In the player, the only drag-drop function is adding subtitles
|
if (state.modal) {
|
||||||
var isInPlayer = state.location.current().url === 'player'
|
state.modal = null
|
||||||
if (isInPlayer) {
|
|
||||||
return addSubtitles(files.filter(isSubtitle), true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, you can only drag-drop onto the home screen
|
var subtitles = files.filter(isSubtitle)
|
||||||
var isHome = state.location.current().url === 'home' && !state.modal
|
|
||||||
if (isHome) {
|
if (state.location.url() === 'home' || subtitles.length === 0) {
|
||||||
if (files.every(isTorrent)) {
|
if (files.every(isTorrent)) {
|
||||||
// All .torrent files? Start downloading
|
if (state.location.url() !== 'home') {
|
||||||
|
backToList()
|
||||||
|
}
|
||||||
|
// All .torrent files? Add them.
|
||||||
files.forEach(addTorrent)
|
files.forEach(addTorrent)
|
||||||
} else {
|
} else {
|
||||||
// Show the Create Torrent screen. Let's seed those files.
|
// Show the Create Torrent screen. Let's seed those files.
|
||||||
showCreateTorrent(files)
|
showCreateTorrent(files)
|
||||||
}
|
}
|
||||||
|
} else if (state.location.url() === 'player') {
|
||||||
|
addSubtitles(subtitles, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTorrent (file) {
|
function isTorrent (file) {
|
||||||
@@ -591,13 +657,19 @@ function getTorrentSummary (torrentKey) {
|
|||||||
|
|
||||||
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
||||||
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||||
|
var instantIoRegex = /^(https:\/\/)?instant\.io\/#/
|
||||||
function addTorrent (torrentId) {
|
function addTorrent (torrentId) {
|
||||||
|
backToList()
|
||||||
var torrentKey = state.nextTorrentKey++
|
var torrentKey = state.nextTorrentKey++
|
||||||
var path = state.saved.downloadPath
|
var path = state.saved.prefs.downloadPath
|
||||||
if (torrentId.path) {
|
if (torrentId.path) {
|
||||||
// Use path string instead of W3C File object
|
// Use path string instead of W3C File object
|
||||||
torrentId = torrentId.path
|
torrentId = torrentId.path
|
||||||
}
|
}
|
||||||
|
// Allow a instant.io link to be pasted
|
||||||
|
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||||
|
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||||
|
}
|
||||||
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -606,22 +678,23 @@ function addSubtitles (files, autoSelect) {
|
|||||||
if (state.playing.type !== 'video') return
|
if (state.playing.type !== 'video') return
|
||||||
|
|
||||||
// Read the files concurrently, then add all resulting subtitle tracks
|
// Read the files concurrently, then add all resulting subtitle tracks
|
||||||
console.log(files)
|
var jobs = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||||
var subs = state.playing.subtitles
|
parallel(jobs, function (err, tracks) {
|
||||||
Async.map(files, loadSubtitle, function (err, tracks) {
|
|
||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
|
|
||||||
for (var i = 0; i < tracks.length; i++) {
|
for (var i = 0; i < tracks.length; i++) {
|
||||||
// No dupes allowed
|
// No dupes allowed
|
||||||
var track = tracks[i]
|
var track = tracks[i]
|
||||||
if (subs.tracks.some((t) => track.filePath === t.filePath)) continue
|
if (state.playing.subtitles.tracks.some(
|
||||||
|
(t) => track.filePath === t.filePath)) continue
|
||||||
|
|
||||||
// Add the track
|
// Add the track
|
||||||
subs.tracks.push(track)
|
state.playing.subtitles.tracks.push(track)
|
||||||
|
|
||||||
// If we're auto-selecting a track, try to find one in the user's language
|
// If we're auto-selecting a track, try to find one in the user's language
|
||||||
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||||
state.playing.subtitles.selectedIndex = subs.tracks.length - 1
|
state.playing.subtitles.selectedIndex =
|
||||||
|
state.playing.subtitles.tracks.length - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,31 +704,33 @@ function addSubtitles (files, autoSelect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadSubtitle (file, cb) {
|
function loadSubtitle (file, cb) {
|
||||||
var srtToVtt = require('srt-to-vtt')
|
var concat = require('simple-concat')
|
||||||
var LanguageDetect = require('languagedetect')
|
var LanguageDetect = require('languagedetect')
|
||||||
|
var srtToVtt = require('srt-to-vtt')
|
||||||
|
|
||||||
// Read the .SRT or .VTT file, parse it, add subtitle track
|
// Read the .SRT or .VTT file, parse it, add subtitle track
|
||||||
var filePath = file.path || file
|
var filePath = file.path || file
|
||||||
fs.createReadStream(filePath).pipe(srtToVtt()).pipe(concat(function (buf) {
|
|
||||||
|
var vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
||||||
|
|
||||||
|
concat(vttStream, function (err, buf) {
|
||||||
|
if (err) return onError(new Error('Error parsing subtitles file.'))
|
||||||
|
|
||||||
// Detect what language the subtitles are in
|
// Detect what language the subtitles are in
|
||||||
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
||||||
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
||||||
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
||||||
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
||||||
|
|
||||||
// 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 subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
|
|
||||||
var track = {
|
var track = {
|
||||||
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
|
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
||||||
language: langDetected,
|
language: langDetected,
|
||||||
label: langDetected,
|
label: langDetected,
|
||||||
filePath: filePath
|
filePath: filePath
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(null, track)
|
cb(null, track)
|
||||||
}))
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectSubtitle (ix) {
|
function selectSubtitle (ix) {
|
||||||
@@ -665,6 +740,7 @@ function selectSubtitle (ix) {
|
|||||||
// Checks whether a language name like "English" or "German" matches the system
|
// Checks whether a language name like "English" or "German" matches the system
|
||||||
// language, aka the current locale
|
// language, aka the current locale
|
||||||
function isSystemLanguage (language) {
|
function isSystemLanguage (language) {
|
||||||
|
var iso639 = require('iso-639-1')
|
||||||
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
|
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
|
||||||
var langIso = iso639.getCode(language) // eg "de" if language is "German"
|
var langIso = iso639.getCode(language) // eg "de" if language is "German"
|
||||||
return langIso === osLangISO
|
return langIso === osLangISO
|
||||||
@@ -707,7 +783,7 @@ function startTorrentingSummary (torrentSummary) {
|
|||||||
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
|
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
|
||||||
|
|
||||||
// Use Downloads folder by default
|
// Use Downloads folder by default
|
||||||
var path = s.path || state.saved.downloadPath
|
var path = s.path || state.saved.prefs.downloadPath
|
||||||
|
|
||||||
var torrentID
|
var torrentID
|
||||||
if (s.torrentFileName) { // Load torrent file from disk
|
if (s.torrentFileName) { // Load torrent file from disk
|
||||||
@@ -728,7 +804,6 @@ function startTorrentingSummary (torrentSummary) {
|
|||||||
// Shows the Create Torrent page with options to seed a given file or folder
|
// Shows the Create Torrent page with options to seed a given file or folder
|
||||||
function showCreateTorrent (files) {
|
function showCreateTorrent (files) {
|
||||||
if (Array.isArray(files)) {
|
if (Array.isArray(files)) {
|
||||||
if (state.location.pending() || state.location.current().url !== 'home') return
|
|
||||||
state.location.go({
|
state.location.go({
|
||||||
url: 'create-torrent',
|
url: 'create-torrent',
|
||||||
files: files
|
files: files
|
||||||
@@ -778,6 +853,9 @@ function findFilesRecursive (fileOrFolder, cb) {
|
|||||||
function createTorrent (options) {
|
function createTorrent (options) {
|
||||||
var torrentKey = state.nextTorrentKey++
|
var torrentKey = state.nextTorrentKey++
|
||||||
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||||
|
state.location.backToFirst(function () {
|
||||||
|
state.location.clearForward('create-torrent')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentInfoHash (torrentKey, infoHash) {
|
function torrentInfoHash (torrentKey, infoHash) {
|
||||||
@@ -790,7 +868,7 @@ function torrentInfoHash (torrentKey, infoHash) {
|
|||||||
torrentKey: torrentKey,
|
torrentKey: torrentKey,
|
||||||
status: 'new'
|
status: 'new'
|
||||||
}
|
}
|
||||||
state.saved.torrents.push(torrentSummary)
|
state.saved.torrents.unshift(torrentSummary)
|
||||||
sound.play('ADD')
|
sound.play('ADD')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,11 +902,17 @@ function torrentMetadata (torrentKey, torrentInfo) {
|
|||||||
torrentSummary.status = 'downloading'
|
torrentSummary.status = 'downloading'
|
||||||
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
||||||
torrentSummary.path = torrentInfo.path
|
torrentSummary.path = torrentInfo.path
|
||||||
torrentSummary.files = torrentInfo.files
|
|
||||||
torrentSummary.magnetURI = torrentInfo.magnetURI
|
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||||
|
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
||||||
|
// For now, check whether torrentSummary.files has already been set:
|
||||||
|
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
||||||
|
if (!hasDetailedFileInfo) {
|
||||||
|
torrentSummary.files = torrentInfo.files
|
||||||
|
}
|
||||||
if (!torrentSummary.selections) {
|
if (!torrentSummary.selections) {
|
||||||
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||||
}
|
}
|
||||||
|
torrentSummary.defaultPlayFileIndex = pickFileToPlay(torrentInfo.files)
|
||||||
update()
|
update()
|
||||||
|
|
||||||
// Save the .torrent file, if it hasn't been saved already
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
@@ -937,11 +1021,25 @@ function pickFileToPlay (files) {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opens the video player
|
function playFile (infoHash, index) {
|
||||||
|
state.location.go({
|
||||||
|
url: 'player',
|
||||||
|
onbeforeload: function (cb) {
|
||||||
|
play()
|
||||||
|
openPlayer(infoHash, index, cb)
|
||||||
|
},
|
||||||
|
onbeforeunload: closePlayer
|
||||||
|
}, function (err) {
|
||||||
|
if (err) onError(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opens the video player to a specific torrent
|
||||||
function openPlayer (infoHash, index, cb) {
|
function openPlayer (infoHash, index, cb) {
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
|
|
||||||
// automatically choose which file in the torrent to play, if necessary
|
// automatically choose which file in the torrent to play, if necessary
|
||||||
|
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
|
||||||
if (index === undefined) index = pickFileToPlay(torrentSummary.files)
|
if (index === undefined) index = pickFileToPlay(torrentSummary.files)
|
||||||
if (index === undefined) return cb(new errors.UnplayableError())
|
if (index === undefined) return cb(new errors.UnplayableError())
|
||||||
|
|
||||||
@@ -953,7 +1051,7 @@ function openPlayer (infoHash, index, cb) {
|
|||||||
var timeout = setTimeout(function () {
|
var timeout = setTimeout(function () {
|
||||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
||||||
sound.play('ERROR')
|
sound.play('ERROR')
|
||||||
cb(new Error('playback timed out'))
|
cb(new Error('Playback timed out. Try again.'))
|
||||||
update()
|
update()
|
||||||
}, 10000) /* give it a few seconds */
|
}, 10000) /* give it a few seconds */
|
||||||
|
|
||||||
@@ -976,6 +1074,15 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
|||||||
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||||
: 'other'
|
: 'other'
|
||||||
|
|
||||||
|
// pick up where we left off
|
||||||
|
if (fileSummary.currentTime) {
|
||||||
|
var fraction = fileSummary.currentTime / fileSummary.duration
|
||||||
|
var secondsLeft = fileSummary.duration - fileSummary.currentTime
|
||||||
|
if (fraction < 0.9 && secondsLeft > 10) {
|
||||||
|
state.playing.jumpToTime = fileSummary.currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if it's audio, parse out the metadata (artist, title, etc)
|
// if it's audio, parse out the metadata (artist, title, etc)
|
||||||
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
||||||
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
||||||
@@ -1059,7 +1166,7 @@ function deleteTorrent (infoHash) {
|
|||||||
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
var index = state.saved.torrents.findIndex((x) => x.infoHash === 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('player') // prevent user from going forward to a deleted torrent
|
||||||
sound.play('DELETE')
|
sound.play('DELETE')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1125,7 +1232,7 @@ function saveTorrentFileAs (torrentSummary) {
|
|||||||
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
|
||||||
var opts = {
|
var opts = {
|
||||||
title: 'Save Torrent File',
|
title: 'Save Torrent File',
|
||||||
defaultPath: path.join(state.saved.downloadPath, newFileName),
|
defaultPath: path.join(state.saved.prefs.downloadPath, newFileName),
|
||||||
filters: [
|
filters: [
|
||||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||||
{ name: 'All Files', extensions: ['*'] }
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
@@ -1207,7 +1314,7 @@ function showDoneNotification (torrent) {
|
|||||||
// * 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
|
||||||
function showOrHidePlayerControls () {
|
function showOrHidePlayerControls () {
|
||||||
var hideControls = state.location.current().url === 'player' &&
|
var hideControls = state.location.url() === 'player' &&
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
state.playing.mouseStationarySince !== 0 &&
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||||
!state.playing.isPaused &&
|
!state.playing.isPaused &&
|
||||||
@@ -1242,8 +1349,10 @@ function onPaste (e) {
|
|||||||
torrentIds.forEach(function (torrentId) {
|
torrentIds.forEach(function (torrentId) {
|
||||||
torrentId = torrentId.trim()
|
torrentId = torrentId.trim()
|
||||||
if (torrentId.length === 0) return
|
if (torrentId.length === 0) return
|
||||||
dispatch('addTorrent', torrentId)
|
addTorrent(torrentId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFocus (e) {
|
function onFocus (e) {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ module.exports = {
|
|||||||
play,
|
play,
|
||||||
pause,
|
pause,
|
||||||
seek,
|
seek,
|
||||||
setVolume
|
setVolume,
|
||||||
|
setRate
|
||||||
}
|
}
|
||||||
|
|
||||||
var airplay = require('airplay-js')
|
var airplay = require('airplay-js')
|
||||||
@@ -344,6 +345,22 @@ function pause () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setRate (rate) {
|
||||||
|
var device
|
||||||
|
var result = true
|
||||||
|
if (state.playing.location === 'chromecast') {
|
||||||
|
// TODO find how to control playback rate on chromecast
|
||||||
|
castCallback()
|
||||||
|
result = false
|
||||||
|
} else if (state.playing.location === 'airplay') {
|
||||||
|
device = state.devices.airplay
|
||||||
|
device.rate(rate, castCallback)
|
||||||
|
} else {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
function seek (time) {
|
function seek (time) {
|
||||||
var device = getDevice()
|
var device = getDevice()
|
||||||
if (device) {
|
if (device) {
|
||||||
|
|||||||
@@ -4,81 +4,123 @@ function LocationHistory () {
|
|||||||
if (!new.target) return new LocationHistory()
|
if (!new.target) return new LocationHistory()
|
||||||
this._history = []
|
this._history = []
|
||||||
this._forward = []
|
this._forward = []
|
||||||
this._pending = null
|
this._pending = false
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.go = function (page, cb) {
|
LocationHistory.prototype.url = function () {
|
||||||
console.log('go', page)
|
return this.current() && this.current().url
|
||||||
this.clearForward()
|
|
||||||
this._go(page, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype._go = function (page, cb) {
|
|
||||||
if (this._pending) return
|
|
||||||
if (page.onbeforeload) {
|
|
||||||
this._pending = page
|
|
||||||
page.onbeforeload((err) => {
|
|
||||||
if (this._pending !== page) return /* navigation was cancelled */
|
|
||||||
this._pending = null
|
|
||||||
if (err) {
|
|
||||||
if (cb) cb(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._history.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this._history.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype.back = function (cb) {
|
|
||||||
if (this._history.length <= 1) return
|
|
||||||
|
|
||||||
var page = this._history.pop()
|
|
||||||
|
|
||||||
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(() => {
|
|
||||||
this._forward.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this._forward.push(page)
|
|
||||||
if (cb) cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype.forward = function (cb) {
|
|
||||||
if (this._forward.length === 0) return
|
|
||||||
|
|
||||||
var page = this._forward.pop()
|
|
||||||
this._go(page, cb)
|
|
||||||
}
|
|
||||||
|
|
||||||
LocationHistory.prototype.clearForward = function () {
|
|
||||||
this._forward = []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.current = function () {
|
LocationHistory.prototype.current = function () {
|
||||||
return this._history[this._history.length - 1]
|
return this._history[this._history.length - 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.go = function (page, cb) {
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (this._pending) return cb(null)
|
||||||
|
|
||||||
|
console.log('go', page)
|
||||||
|
|
||||||
|
this.clearForward()
|
||||||
|
this._go(page, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.back = function (cb) {
|
||||||
|
var self = this
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (self._history.length <= 1 || self._pending) return cb(null)
|
||||||
|
|
||||||
|
var page = self._history.pop()
|
||||||
|
self._unload(page, done)
|
||||||
|
|
||||||
|
function done (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
self._forward.push(page)
|
||||||
|
self._load(self.current(), cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.hasBack = function () {
|
LocationHistory.prototype.hasBack = function () {
|
||||||
return this._history.length > 1
|
return this._history.length > 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype.forward = function (cb) {
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (this._forward.length === 0 || this._pending) return cb(null)
|
||||||
|
|
||||||
|
var page = this._forward.pop()
|
||||||
|
this._go(page, cb)
|
||||||
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.hasForward = function () {
|
LocationHistory.prototype.hasForward = function () {
|
||||||
return this._forward.length > 0
|
return this._forward.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.pending = function () {
|
LocationHistory.prototype.clearForward = function (url) {
|
||||||
return this._pending
|
if (url == null) {
|
||||||
|
this._forward = []
|
||||||
|
} else {
|
||||||
|
console.log(this._forward)
|
||||||
|
console.log(url)
|
||||||
|
this._forward = this._forward.filter(function (page) {
|
||||||
|
return page.url !== url
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LocationHistory.prototype.clearPending = function () {
|
LocationHistory.prototype.backToFirst = function (cb) {
|
||||||
this._pending = null
|
var self = this
|
||||||
|
if (!cb) cb = noop
|
||||||
|
if (self._history.length <= 1) return cb(null)
|
||||||
|
|
||||||
|
self.back(function (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
self.backToFirst(cb)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype._go = function (page, cb) {
|
||||||
|
var self = this
|
||||||
|
if (!cb) cb = noop
|
||||||
|
|
||||||
|
self._unload(self.current(), done1)
|
||||||
|
|
||||||
|
function done1 (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
self._load(page, done2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function done2 (err) {
|
||||||
|
if (err) return cb(err)
|
||||||
|
self._history.push(page)
|
||||||
|
cb(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype._load = function (page, cb) {
|
||||||
|
var self = this
|
||||||
|
self._pending = true
|
||||||
|
|
||||||
|
if (page && page.onbeforeload) page.onbeforeload(done)
|
||||||
|
else done(null)
|
||||||
|
|
||||||
|
function done (err) {
|
||||||
|
self._pending = false
|
||||||
|
cb(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationHistory.prototype._unload = function (page, cb) {
|
||||||
|
var self = this
|
||||||
|
self._pending = true
|
||||||
|
|
||||||
|
if (page && page.onbeforeunload) page.onbeforeunload(done)
|
||||||
|
else done(null)
|
||||||
|
|
||||||
|
function done (err) {
|
||||||
|
self._pending = false
|
||||||
|
cb(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function noop () {}
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ var captureVideoFrame = require('./capture-video-frame')
|
|||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
|
||||||
function torrentPoster (torrent, cb) {
|
function torrentPoster (torrent, cb) {
|
||||||
// First, try to use the largest video file
|
// First, try to use a poster image if available
|
||||||
|
var posterFile = torrent.files.filter(function (file) {
|
||||||
|
return /^poster\.(jpg|png|gif)$/.test(file.name)
|
||||||
|
})[0]
|
||||||
|
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
|
||||||
|
|
||||||
|
// Second, try to use the largest video file
|
||||||
// Filter out file formats that the <video> tag definitely can't play
|
// Filter out file formats that the <video> tag definitely can't play
|
||||||
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
||||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
||||||
|
|
||||||
// Second, try to use the largest image file
|
// Third, try to use the largest image file
|
||||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
|
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
|
||||||
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ var LocationHistory = require('./lib/location-history')
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getInitialState,
|
getInitialState,
|
||||||
getDefaultPlayState,
|
getDefaultPlayState,
|
||||||
getDefaultSavedState,
|
getDefaultSavedState
|
||||||
getPlayingTorrentSummary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialState () {
|
function getInitialState () {
|
||||||
@@ -63,7 +62,8 @@ function getInitialState () {
|
|||||||
/*
|
/*
|
||||||
* Getters, for convenience
|
* Getters, for convenience
|
||||||
*/
|
*/
|
||||||
getPlayingTorrentSummary
|
getPlayingTorrentSummary,
|
||||||
|
getPlayingFileSummary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +80,7 @@ function getDefaultPlayState () {
|
|||||||
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 */
|
||||||
|
playbackRate: 1,
|
||||||
subtitles: {
|
subtitles: {
|
||||||
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
||||||
selectedIndex: -1, /* current subtitle track */
|
selectedIndex: -1, /* current subtitle track */
|
||||||
@@ -265,9 +266,11 @@ function getDefaultSavedState () {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
downloadPath: config.IS_PORTABLE
|
prefs: {
|
||||||
? path.join(config.CONFIG_PATH, 'Downloads')
|
downloadPath: config.IS_PORTABLE
|
||||||
: remote.app.getPath('downloads')
|
? path.join(config.CONFIG_PATH, 'Downloads')
|
||||||
|
: remote.app.getPath('downloads')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,3 +278,9 @@ function getPlayingTorrentSummary () {
|
|||||||
var infoHash = this.playing.infoHash
|
var infoHash = this.playing.infoHash
|
||||||
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPlayingFileSummary () {
|
||||||
|
var torrentSummary = this.getPlayingTorrentSummary()
|
||||||
|
if (!torrentSummary) return null
|
||||||
|
return torrentSummary.files[this.playing.fileIndex]
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ var Header = require('./header')
|
|||||||
var Views = {
|
var Views = {
|
||||||
'home': require('./torrent-list'),
|
'home': require('./torrent-list'),
|
||||||
'player': require('./player'),
|
'player': require('./player'),
|
||||||
'create-torrent': require('./create-torrent-page')
|
'create-torrent': require('./create-torrent-page'),
|
||||||
|
'preferences': require('./preferences')
|
||||||
}
|
}
|
||||||
var Modals = {
|
var Modals = {
|
||||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
||||||
@@ -22,24 +23,20 @@ function App (state) {
|
|||||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||||
// * 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.url() === 'player' &&
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
state.playing.mouseStationarySince !== 0 &&
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
||||||
!state.playing.isPaused &&
|
!state.playing.isPaused &&
|
||||||
state.playing.location === 'local'
|
state.playing.location === 'local' &&
|
||||||
|
state.playing.playbackRate === 1
|
||||||
// Hide the header on Windows/Linux when in the player
|
|
||||||
// On OSX, the header appears as part of the title bar
|
|
||||||
var hideHeader = process.platform !== 'darwin' && state.location.current().url === 'player'
|
|
||||||
|
|
||||||
var cls = [
|
var cls = [
|
||||||
'view-' + state.location.current().url, /* e.g. view-home, view-player */
|
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
||||||
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
||||||
]
|
]
|
||||||
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
||||||
if (state.window.isFocused) cls.push('is-focused')
|
if (state.window.isFocused) cls.push('is-focused')
|
||||||
if (hideControls) cls.push('hide-video-controls')
|
if (hideControls) cls.push('hide-video-controls')
|
||||||
if (hideHeader) cls.push('hide-header')
|
|
||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='app ${cls.join(' ')}'>
|
<div class='app ${cls.join(' ')}'>
|
||||||
@@ -54,12 +51,13 @@ function App (state) {
|
|||||||
function getErrorPopover (state) {
|
function getErrorPopover (state) {
|
||||||
var now = new Date().getTime()
|
var now = new Date().getTime()
|
||||||
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
||||||
|
var hasErrors = recentErrors.length > 0
|
||||||
|
|
||||||
var errorElems = recentErrors.map(function (error) {
|
var errorElems = recentErrors.map(function (error) {
|
||||||
return hx`<div class='error'>${error.message}</div>`
|
return hx`<div class='error'>${error.message}</div>`
|
||||||
})
|
})
|
||||||
return hx`
|
return hx`
|
||||||
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
|
<div class='error-popover ${hasErrors ? 'visible' : 'hidden'}'>
|
||||||
<div class='title'>Error</div>
|
<div class='title'>Error</div>
|
||||||
${errorElems}
|
${errorElems}
|
||||||
</div>
|
</div>
|
||||||
@@ -80,6 +78,6 @@ function getModal (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getView (state) {
|
function getView (state) {
|
||||||
var url = state.location.current().url
|
var url = state.location.url()
|
||||||
return Views[url](state)
|
return Views[url](state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,11 +119,10 @@ function CreateTorrentPage (state) {
|
|||||||
comment: comment
|
comment: comment
|
||||||
}
|
}
|
||||||
dispatch('createTorrent', options)
|
dispatch('createTorrent', options)
|
||||||
dispatch('backToList')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel () {
|
function handleCancel () {
|
||||||
dispatch('backToList')
|
dispatch('back')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleToggleShowAdvanced () {
|
function handleToggleShowAdvanced () {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function Header (state) {
|
|||||||
return hx`
|
return hx`
|
||||||
<div class='header'>
|
<div class='header'>
|
||||||
${getTitle()}
|
${getTitle()}
|
||||||
<div class='nav left'>
|
<div class='nav left float-left'>
|
||||||
<i.icon.back
|
<i.icon.back
|
||||||
class=${state.location.hasBack() ? '' : 'disabled'}
|
class=${state.location.hasBack() ? '' : 'disabled'}
|
||||||
title='Back'
|
title='Back'
|
||||||
@@ -24,7 +24,7 @@ function Header (state) {
|
|||||||
chevron_right
|
chevron_right
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
<div class='nav right'>
|
<div class='nav right float-right'>
|
||||||
${getAddButton()}
|
${getAddButton()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,7 @@ function Header (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAddButton () {
|
function getAddButton () {
|
||||||
if (state.location.current().url !== 'player') {
|
if (state.location.url() === 'home') {
|
||||||
return hx`
|
return hx`
|
||||||
<i
|
<i
|
||||||
class='icon add'
|
class='icon add'
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ var h = require('virtual-dom/h')
|
|||||||
var hyperx = require('hyperx')
|
var hyperx = require('hyperx')
|
||||||
var hx = hyperx(h)
|
var hx = hyperx(h)
|
||||||
|
|
||||||
var prettyBytes = require('prettier-bytes')
|
|
||||||
var Bitfield = require('bitfield')
|
var Bitfield = require('bitfield')
|
||||||
|
var prettyBytes = require('prettier-bytes')
|
||||||
|
var zeroFill = require('zero-fill')
|
||||||
|
|
||||||
var TorrentSummary = require('../lib/torrent-summary')
|
var TorrentSummary = require('../lib/torrent-summary')
|
||||||
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
var {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
@@ -48,6 +49,9 @@ function renderMedia (state) {
|
|||||||
mediaElement.currentTime = state.playing.jumpToTime
|
mediaElement.currentTime = state.playing.jumpToTime
|
||||||
state.playing.jumpToTime = null
|
state.playing.jumpToTime = null
|
||||||
}
|
}
|
||||||
|
if (state.playing.playbackRate !== mediaElement.playbackRate) {
|
||||||
|
mediaElement.playbackRate = state.playing.playbackRate
|
||||||
|
}
|
||||||
// Set volume
|
// Set volume
|
||||||
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
|
||||||
mediaElement.volume = state.playing.setVolume
|
mediaElement.volume = state.playing.setVolume
|
||||||
@@ -60,8 +64,10 @@ function renderMedia (state) {
|
|||||||
tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden'
|
tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden'
|
||||||
}
|
}
|
||||||
|
|
||||||
state.playing.currentTime = mediaElement.currentTime
|
// Save video position
|
||||||
state.playing.duration = mediaElement.duration
|
var file = state.getPlayingFileSummary()
|
||||||
|
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
||||||
|
file.duration = state.playing.duration = mediaElement.duration
|
||||||
state.playing.volume = mediaElement.volume
|
state.playing.volume = mediaElement.volume
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,12 +131,13 @@ function renderMedia (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onCanPlay (e) {
|
function onCanPlay (e) {
|
||||||
var video = e.target
|
var elem = e.target
|
||||||
if (video.webkitVideoDecodedByteCount > 0 &&
|
if (state.playing.type === 'video' && elem.webkitVideoDecodedByteCount === 0) {
|
||||||
video.webkitAudioDecodedByteCount === 0) {
|
dispatch('mediaError', 'Video codec unsupported')
|
||||||
|
} else if (elem.webkitAudioDecodedByteCount === 0) {
|
||||||
dispatch('mediaError', 'Audio codec unsupported')
|
dispatch('mediaError', 'Audio codec unsupported')
|
||||||
} else {
|
} else {
|
||||||
video.play()
|
elem.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,8 +168,7 @@ function renderOverlay (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAudioMetadata (state) {
|
function renderAudioMetadata (state) {
|
||||||
var torrentSummary = state.getPlayingTorrentSummary()
|
var fileSummary = state.getPlayingFileSummary()
|
||||||
var fileSummary = torrentSummary.files[state.playing.fileIndex]
|
|
||||||
if (!fileSummary.audioInfo) return
|
if (!fileSummary.audioInfo) return
|
||||||
var info = fileSummary.audioInfo
|
var info = fileSummary.audioInfo
|
||||||
|
|
||||||
@@ -292,7 +298,7 @@ function renderSubtitlesOptions (state) {
|
|||||||
|
|
||||||
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 + '% - 3px)' }
|
||||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
var captionsClass = state.playing.subtitles.tracks.length === 0
|
||||||
? 'disabled'
|
? 'disabled'
|
||||||
: state.playing.subtitles.selectedIndex >= 0
|
: state.playing.subtitles.selectedIndex >= 0
|
||||||
@@ -303,15 +309,27 @@ function renderPlayerControls (state) {
|
|||||||
hx`
|
hx`
|
||||||
<div class='playback-bar'>
|
<div class='playback-bar'>
|
||||||
${renderLoadingBar(state)}
|
${renderLoadingBar(state)}
|
||||||
<div class='playback-cursor' style=${playbackCursorStyle}></div>
|
<div
|
||||||
<div class='scrub-bar'
|
class='playback-cursor'
|
||||||
|
style=${playbackCursorStyle}>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class='scrub-bar'
|
||||||
draggable='true'
|
draggable='true'
|
||||||
|
ondragstart=${handleDragStart}
|
||||||
onclick=${handleScrub},
|
onclick=${handleScrub},
|
||||||
ondrag=${handleScrub}></div>
|
ondrag=${handleScrub}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
hx`
|
hx`
|
||||||
<i class='icon fullscreen'
|
<i class='icon play-pause float-left' onclick=${dispatcher('playPause')}>
|
||||||
|
${state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||||
|
</i>
|
||||||
|
`,
|
||||||
|
hx`
|
||||||
|
<i
|
||||||
|
class='icon fullscreen float-right'
|
||||||
onclick=${dispatcher('toggleFullScreen')}>
|
onclick=${dispatcher('toggleFullScreen')}>
|
||||||
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||||
</i>
|
</i>
|
||||||
@@ -321,7 +339,7 @@ function renderPlayerControls (state) {
|
|||||||
if (state.playing.type === 'video') {
|
if (state.playing.type === 'video') {
|
||||||
// show closed captions icon
|
// show closed captions icon
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.closed-captions
|
<i.icon.closed-captions.float-right
|
||||||
class=${captionsClass}
|
class=${captionsClass}
|
||||||
onclick=${handleSubtitles}>
|
onclick=${handleSubtitles}>
|
||||||
closed_captions
|
closed_captions
|
||||||
@@ -366,7 +384,7 @@ function renderPlayerControls (state) {
|
|||||||
if (state.devices.chromecast || isOnChromecast) {
|
if (state.devices.chromecast || isOnChromecast) {
|
||||||
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
|
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.device
|
<i.icon.device.float-right
|
||||||
class=${chromecastClass}
|
class=${chromecastClass}
|
||||||
onclick=${chromecastHandler}>
|
onclick=${chromecastHandler}>
|
||||||
${castIcon}
|
${castIcon}
|
||||||
@@ -375,7 +393,7 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
if (state.devices.airplay || isOnAirplay) {
|
if (state.devices.airplay || isOnAirplay) {
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.device
|
<i.icon.device.float-right
|
||||||
class=${airplayClass}
|
class=${airplayClass}
|
||||||
onclick=${airplayHandler}>
|
onclick=${airplayHandler}>
|
||||||
airplay
|
airplay
|
||||||
@@ -384,7 +402,8 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
if (state.devices.dlna || isOnDlna) {
|
if (state.devices.dlna || isOnDlna) {
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.device
|
<i
|
||||||
|
class='icon device float-right'
|
||||||
class=${dlnaClass}
|
class=${dlnaClass}
|
||||||
onclick=${dlnaHandler}>
|
onclick=${dlnaHandler}>
|
||||||
tv
|
tv
|
||||||
@@ -392,17 +411,6 @@ function renderPlayerControls (state) {
|
|||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// On OSX, the back button is in the title bar of the window; see app.js
|
|
||||||
// On other platforms, we render one over the video on mouseover
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
elements.push(hx`
|
|
||||||
<i.icon.back
|
|
||||||
onclick=${dispatcher('back')}>
|
|
||||||
chevron_left
|
|
||||||
</i>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// render volume
|
// render volume
|
||||||
var volume = state.playing.volume
|
var volume = state.playing.volume
|
||||||
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
|
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
|
||||||
@@ -412,34 +420,54 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<div.volume>
|
<div class='volume float-left'>
|
||||||
<i.icon.volume-icon onmousedown=${handleVolumeMute}>
|
<i
|
||||||
${volumeIcon}
|
class='icon volume-icon float-left'
|
||||||
</i>
|
onmousedown=${handleVolumeMute}>
|
||||||
<input.volume-slider
|
${volumeIcon}
|
||||||
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
|
</i>
|
||||||
onmousedown=${handleVolumeScrub}
|
<input
|
||||||
onmouseup=${handleVolumeScrub}
|
class='volume-slider float-right'
|
||||||
onmousemove=${handleVolumeScrub}
|
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
|
||||||
style=${volumeStyle}
|
onmousedown=${handleVolumeScrub}
|
||||||
/>
|
onmouseup=${handleVolumeScrub}
|
||||||
|
onmousemove=${handleVolumeScrub}
|
||||||
|
style=${volumeStyle}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// Finally, the big button in the center plays or pauses the video
|
// Show video playback progress
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i class='icon play-pause' onclick=${dispatcher('playPause')}>
|
<span class='time float-left'>
|
||||||
${state.playing.isPaused ? 'play_arrow' : 'pause'}
|
${formatTime(state.playing.currentTime)} / ${formatTime(state.playing.duration)}
|
||||||
</i>
|
</span>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// render playback rate
|
||||||
|
if (state.playing.playbackRate !== 1) {
|
||||||
|
elements.push(hx`
|
||||||
|
<span class='rate float-left'>
|
||||||
|
${state.playing.playbackRate}x
|
||||||
|
</span>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='player-controls'>
|
<div class='controls'>
|
||||||
${elements}
|
${elements}
|
||||||
${renderSubtitlesOptions(state)}
|
${renderSubtitlesOptions(state)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
|
function handleDragStart (e) {
|
||||||
|
// Prevent the cursor from changing, eg to a green + icon on Mac
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
var dt = e.dataTransfer
|
||||||
|
dt.effectAllowed = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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('mediaMouseMoved')
|
dispatch('mediaMouseMoved')
|
||||||
@@ -540,3 +568,18 @@ function cssBackgroundImageDarkGradient () {
|
|||||||
return 'radial-gradient(circle at center, ' +
|
return 'radial-gradient(circle at center, ' +
|
||||||
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatTime (time) {
|
||||||
|
if (typeof time !== 'number' || Number.isNaN(time)) {
|
||||||
|
return '0:00'
|
||||||
|
}
|
||||||
|
|
||||||
|
var hours = Math.floor(time / 3600)
|
||||||
|
var minutes = Math.floor(time % 3600 / 60)
|
||||||
|
if (hours > 0) {
|
||||||
|
minutes = zeroFill(2, minutes)
|
||||||
|
}
|
||||||
|
var seconds = zeroFill(2, Math.floor(time % 60))
|
||||||
|
|
||||||
|
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
|
||||||
|
}
|
||||||
|
|||||||
104
renderer/views/preferences.js
Normal file
104
renderer/views/preferences.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
module.exports = Preferences
|
||||||
|
|
||||||
|
var h = require('virtual-dom/h')
|
||||||
|
var hyperx = require('hyperx')
|
||||||
|
var hx = hyperx(h)
|
||||||
|
var {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
var remote = require('electron').remote
|
||||||
|
var dialog = remote.dialog
|
||||||
|
|
||||||
|
function Preferences (state) {
|
||||||
|
return hx`
|
||||||
|
<div class='preferences'>
|
||||||
|
${renderGeneralSection(state)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGeneralSection (state) {
|
||||||
|
return renderSection({
|
||||||
|
title: 'General',
|
||||||
|
description: '',
|
||||||
|
icon: 'settings'
|
||||||
|
}, [
|
||||||
|
renderDownloadDirSelector(state)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDownloadDirSelector (state) {
|
||||||
|
return renderFileSelector({
|
||||||
|
label: 'Download Path',
|
||||||
|
description: 'Data from torrents will be saved here',
|
||||||
|
property: 'downloadPath',
|
||||||
|
options: {
|
||||||
|
title: 'Select download directory',
|
||||||
|
properties: [ 'openDirectory' ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state.unsaved.prefs.downloadPath,
|
||||||
|
function (filePath) {
|
||||||
|
setStateValue('downloadPath', filePath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders a prefs section.
|
||||||
|
// - definition should be {icon, title, description}
|
||||||
|
// - controls should be an array of vdom elements
|
||||||
|
function renderSection (definition, controls) {
|
||||||
|
var helpElem = !definition.description ? null : hx`
|
||||||
|
<div class='help text'>
|
||||||
|
<i.icon>help_outline</i>${definition.description}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
return hx`
|
||||||
|
<section class='section preferences-panel'>
|
||||||
|
<div class='section-container'>
|
||||||
|
<div class='section-heading'>
|
||||||
|
<i.icon>${definition.icon}</i>${definition.title}
|
||||||
|
</div>
|
||||||
|
${helpElem}
|
||||||
|
<div class='section-body'>
|
||||||
|
${controls}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a file chooser
|
||||||
|
// - defition should be {label, description, options}
|
||||||
|
// options are passed to dialog.showOpenDialog
|
||||||
|
// - value should be the current pref, a file or folder path
|
||||||
|
// - callback takes a new file or folder path
|
||||||
|
function renderFileSelector (definition, value, callback) {
|
||||||
|
return hx`
|
||||||
|
<div class='control-group'>
|
||||||
|
<div class='controls'>
|
||||||
|
<label class='control-label'>
|
||||||
|
<div class='preference-title'>${definition.label}</div>
|
||||||
|
<div class='preference-description'>${definition.description}</div>
|
||||||
|
</label>
|
||||||
|
<div class='controls'>
|
||||||
|
<input type='text' class='file-picker-text'
|
||||||
|
id=${definition.property}
|
||||||
|
disabled='disabled'
|
||||||
|
value=${value} />
|
||||||
|
<button class='btn' onclick=${handleClick}>
|
||||||
|
<i.icon>folder_open</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
function handleClick () {
|
||||||
|
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
||||||
|
if (!Array.isArray(filenames)) return
|
||||||
|
callback(filenames[0])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStateValue (property, value) {
|
||||||
|
dispatch('updatePreferences', property, value)
|
||||||
|
}
|
||||||
@@ -118,12 +118,7 @@ function TorrentList (state) {
|
|||||||
var infoHash = torrentSummary.infoHash
|
var infoHash = torrentSummary.infoHash
|
||||||
|
|
||||||
var playIcon, playTooltip, playClass
|
var playIcon, playTooltip, playClass
|
||||||
if (torrentSummary.playStatus === 'unplayable') {
|
if (torrentSummary.playStatus === 'timeout') {
|
||||||
playIcon = 'play_arrow'
|
|
||||||
playClass = 'disabled'
|
|
||||||
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.'
|
|
||||||
} else if (torrentSummary.playStatus === 'timeout') {
|
|
||||||
playIcon = 'warning'
|
playIcon = 'warning'
|
||||||
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
||||||
} else {
|
} else {
|
||||||
@@ -143,6 +138,18 @@ function TorrentList (state) {
|
|||||||
downloadTooltip = 'Click to start torrenting.'
|
downloadTooltip = 'Click to start torrenting.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do we have a saved position? Show it using a radial progress bar on top
|
||||||
|
// of the play button, unless already showing a spinner there:
|
||||||
|
var positionElem
|
||||||
|
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
||||||
|
var defaultFile = torrentSummary.files &&
|
||||||
|
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
|
||||||
|
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
|
||||||
|
var fraction = defaultFile.currentTime / defaultFile.duration
|
||||||
|
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
|
||||||
|
playClass = 'resume-position'
|
||||||
|
}
|
||||||
|
|
||||||
// Only show the play button for torrents that contain playable media
|
// Only show the play button for torrents that contain playable media
|
||||||
var playButton
|
var playButton
|
||||||
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
|
||||||
@@ -158,6 +165,7 @@ function TorrentList (state) {
|
|||||||
|
|
||||||
return hx`
|
return hx`
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
|
${positionElem}
|
||||||
${playButton}
|
${playButton}
|
||||||
<i.button-round.icon.download
|
<i.button-round.icon.download
|
||||||
class=${torrentSummary.status}
|
class=${torrentSummary.status}
|
||||||
@@ -186,11 +194,16 @@ function TorrentList (state) {
|
|||||||
filesElement = hx`<div class='files warning'>${message}</div>`
|
filesElement = hx`<div class='files warning'>${message}</div>`
|
||||||
} else {
|
} else {
|
||||||
// We do know the files. List them and show download stats for each one
|
// We do know the files. List them and show download stats for each one
|
||||||
var fileRows = torrentSummary.files.map(
|
var fileRows = torrentSummary.files
|
||||||
(file, index) => renderFileRow(torrentSummary, file, index))
|
.sort(function (a, b) {
|
||||||
|
if (a.name < b.name) return -1
|
||||||
|
if (b.name < a.name) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
.map((file, index) => renderFileRow(torrentSummary, file, index))
|
||||||
|
|
||||||
filesElement = hx`
|
filesElement = hx`
|
||||||
<div class='files'>
|
<div class='files'>
|
||||||
<strong>Files</strong>
|
|
||||||
<table>
|
<table>
|
||||||
${fileRows}
|
${fileRows}
|
||||||
</table>
|
</table>
|
||||||
@@ -217,7 +230,14 @@ function TorrentList (state) {
|
|||||||
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second, render the file as a table row
|
// Second, for media files where we saved our position, show how far we got
|
||||||
|
var positionElem
|
||||||
|
if (file.currentTime) {
|
||||||
|
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
|
||||||
|
positionElem = renderRadialProgressBar(file.currentTime / file.duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, render the file as a table row
|
||||||
var isPlayable = TorrentPlayer.isPlayable(file)
|
var isPlayable = TorrentPlayer.isPlayable(file)
|
||||||
var infoHash = torrentSummary.infoHash
|
var infoHash = torrentSummary.infoHash
|
||||||
var icon
|
var icon
|
||||||
@@ -233,17 +253,18 @@ function TorrentList (state) {
|
|||||||
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||||
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||||
return hx`
|
return hx`
|
||||||
<tr>
|
<tr onclick=${handleClick}>
|
||||||
<td class='col-icon ${rowClass}' onclick=${handleClick}>
|
<td class='col-icon ${rowClass}'>
|
||||||
|
${positionElem}
|
||||||
<i class='icon'>${icon}</i>
|
<i class='icon'>${icon}</i>
|
||||||
</td>
|
</td>
|
||||||
<td class='col-name ${rowClass}' onclick=${handleClick}>
|
<td class='col-name ${rowClass}'>
|
||||||
${file.name}
|
${file.name}
|
||||||
</td>
|
</td>
|
||||||
<td class='col-progress ${rowClass}' onclick=${handleClick}>
|
<td class='col-progress ${rowClass}'>
|
||||||
${isSelected ? progress : ''}
|
${isSelected ? progress : ''}
|
||||||
</td>
|
</td>
|
||||||
<td class='col-size ${rowClass}' onclick=${handleClick}>
|
<td class='col-size ${rowClass}'>
|
||||||
${prettyBytes(file.length)}
|
${prettyBytes(file.length)}
|
||||||
</td>
|
</td>
|
||||||
<td class='col-select'
|
<td class='col-select'
|
||||||
@@ -254,3 +275,24 @@ function TorrentList (state) {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderRadialProgressBar (fraction, cssClass) {
|
||||||
|
var rotation = 360 * fraction
|
||||||
|
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
||||||
|
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
||||||
|
|
||||||
|
return hx`
|
||||||
|
<div class="radial-progress ${cssClass}">
|
||||||
|
<div class="circle">
|
||||||
|
<div class="mask full" style=${transformFill}>
|
||||||
|
<div class="fill" style=${transformFill}></div>
|
||||||
|
</div>
|
||||||
|
<div class="mask half">
|
||||||
|
<div class="fill" style=${transformFill}></div>
|
||||||
|
<div class="fill fix" style=${transformFix}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="inset"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|||||||
@@ -349,10 +349,6 @@ function selectFiles (torrentOrInfoHash, selections) {
|
|||||||
} else {
|
} else {
|
||||||
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
|
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
|
||||||
file.deselect()
|
file.deselect()
|
||||||
|
|
||||||
// If we deselected a file, try to nuke it to save disk space
|
|
||||||
var filePath = path.join(torrent.path, file.path)
|
|
||||||
fs.unlink(filePath) // Ignore errors for now
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user