Compare commits
53 Commits
v0.4.0
...
entitlemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93a9ba11b6 | ||
|
|
b367c0b72e | ||
|
|
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 | ||
|
|
cfb3a01239 | ||
|
|
736d575ab1 | ||
|
|
34a9508483 | ||
|
|
21ed8797c2 | ||
|
|
454491572a | ||
|
|
6518a1535c | ||
|
|
0095687bf5 | ||
|
|
d466ed085a | ||
|
|
eeda7c17c5 | ||
|
|
b89deb46db | ||
|
|
951a89c6c9 | ||
|
|
d4e6c84279 | ||
|
|
9731d85ca3 | ||
|
|
98f7ba8931 | ||
|
|
24c775608e | ||
|
|
f4eab12c3f | ||
|
|
8eeddeb4bc | ||
|
|
58f1594d9e | ||
|
|
c126ac0a84 | ||
|
|
6768be710e | ||
|
|
b63aa090dc |
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,5 +1,30 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
|
## v0.5.1 - 2016-05-18
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix auto-updater (OS X, Windows).
|
||||||
|
|
||||||
|
## v0.5.0 - 2016-05-17
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Select/deselect individual files to torrent.
|
||||||
|
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
|
||||||
|
- "Add Subtitle File..." menu item.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- When manually adding subtitle track(s), always switch to the new track.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Magnet links throw exception on app launch. (OS X)
|
||||||
|
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
|
||||||
|
- Missing 'About WebTorrent' menu item. (Windows)
|
||||||
|
- Rare exception. ("Cannot create BrowserWindow before app is ready")
|
||||||
|
|
||||||
## v0.4.0 - 2016-05-13
|
## v0.4.0 - 2016-05-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -38,7 +63,8 @@
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is temporary. (OS X)
|
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is
|
||||||
|
temporary. (OS X)
|
||||||
- When fullscreen, make controls use the full window. (OS X)
|
- When fullscreen, make controls use the full window. (OS X)
|
||||||
- Support creating torrents that contain .torrent files.
|
- Support creating torrents that contain .torrent files.
|
||||||
- Block power save while casting to a remote device.
|
- Block power save while casting to a remote device.
|
||||||
@@ -50,10 +76,14 @@
|
|||||||
- Do not stop music when tabbing to another program (OS X)
|
- Do not stop music when tabbing to another program (OS X)
|
||||||
- Properly size the Windows volume mixer icon.
|
- Properly size the Windows volume mixer icon.
|
||||||
- Default to the user's OS-defined, localized "Downloads" folder.
|
- Default to the user's OS-defined, localized "Downloads" folder.
|
||||||
- Enforce minimimum window size when resizing player, to prevent window disappearing.
|
- Enforce minimimum window size when resizing player to prevent window disappearing.
|
||||||
- Fix rare race condition error on app quit.
|
- Fix rare race condition error on app quit.
|
||||||
- Don't use zero-byte torrent "poster" images.
|
- Don't use zero-byte torrent "poster" images.
|
||||||
|
|
||||||
|
Thanks to @grunjol, @rguedes, @furstenheim, @karloluis, @DiegoRBaquero, @alxhotel,
|
||||||
|
@AgentEpsilon, @remijouannet, Rolando Guedes, @dcposch, and @feross for contributing
|
||||||
|
to this release!
|
||||||
|
|
||||||
## v0.3.3 - 2016-04-07
|
## v0.3.3 - 2016-04-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
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).
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ var all = {
|
|||||||
|
|
||||||
var darwin = {
|
var darwin = {
|
||||||
// Build for OS X
|
// Build for OS X
|
||||||
platform: 'darwin',
|
platform: 'mas',
|
||||||
|
|
||||||
// Build 64 bit binaries only.
|
// Build 64 bit binaries only.
|
||||||
arch: 'x64',
|
arch: 'x64',
|
||||||
@@ -211,6 +211,8 @@ function buildDarwin (cb) {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
infoPlist.ElectronTeamID = '5MAMC8G3L8'
|
||||||
|
|
||||||
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
|
||||||
@@ -248,8 +250,9 @@ function buildDarwin (cb) {
|
|||||||
*/
|
*/
|
||||||
var signOpts = {
|
var signOpts = {
|
||||||
app: appPath,
|
app: appPath,
|
||||||
platform: 'darwin',
|
entitlements: path.join(config.STATIC_PATH, 'parent.entitlements'),
|
||||||
verbose: true
|
'entitlements-inherit': path.join(config.STATIC_PATH, 'child.entitlements'),
|
||||||
|
platform: 'mas'
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('OS X: Signing app...')
|
console.log('OS X: Signing app...')
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ npm run package -- --sign
|
|||||||
git push
|
git push
|
||||||
git push --tags
|
git push --tags
|
||||||
npm publish
|
npm publish
|
||||||
gh-release
|
./node_modules/.bin/gh-release
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -25,14 +26,14 @@ if (process.platform === 'win32') {
|
|||||||
argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1)
|
argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldQuit) {
|
// if (!shouldQuit) {
|
||||||
// Prevent multiple instances of app from running at same time. New instances signal
|
// // Prevent multiple instances of app from running at same time. New instances signal
|
||||||
// this instance and quit.
|
// // this instance and quit.
|
||||||
shouldQuit = app.makeSingleInstance(onAppOpen)
|
// shouldQuit = app.makeSingleInstance(onAppOpen)
|
||||||
if (shouldQuit) {
|
// if (shouldQuit) {
|
||||||
app.quit()
|
// app.quit()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!shouldQuit) {
|
if (!shouldQuit) {
|
||||||
init()
|
init()
|
||||||
@@ -43,6 +44,7 @@ function init () {
|
|||||||
app.setPath('userData', config.CONFIG_PATH)
|
app.setPath('userData', config.CONFIG_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isReady = false // app ready, windows can be created
|
||||||
app.ipcReady = false // main window has finished loading and IPC is ready
|
app.ipcReady = false // main window has finished loading and IPC is ready
|
||||||
app.isQuitting = false
|
app.isQuitting = false
|
||||||
|
|
||||||
@@ -57,6 +59,8 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.on('ready', function () {
|
app.on('ready', function () {
|
||||||
|
isReady = true
|
||||||
|
|
||||||
windows.createMainWindow()
|
windows.createMainWindow()
|
||||||
windows.createWebTorrentHiddenWindow()
|
windows.createWebTorrentHiddenWindow()
|
||||||
menu.init()
|
menu.init()
|
||||||
@@ -83,11 +87,12 @@ function init () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
windows.createMainWindow()
|
if (isReady) windows.createMainWindow()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
20
main/menu.js
20
main/menu.js
@@ -86,6 +86,12 @@ function decreaseVolume () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSubtitles () {
|
||||||
|
if (windows.main) {
|
||||||
|
windows.main.send('dispatch', 'openSubtitles')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onWindowShow () {
|
function onWindowShow () {
|
||||||
log('onWindowShow')
|
log('onWindowShow')
|
||||||
getMenuItem('Full Screen').enabled = true
|
getMenuItem('Full Screen').enabled = true
|
||||||
@@ -103,6 +109,7 @@ function onPlayerOpen () {
|
|||||||
getMenuItem('Play/Pause').enabled = true
|
getMenuItem('Play/Pause').enabled = true
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerClose () {
|
function onPlayerClose () {
|
||||||
@@ -110,6 +117,7 @@ function onPlayerClose () {
|
|||||||
getMenuItem('Play/Pause').enabled = false
|
getMenuItem('Play/Pause').enabled = false
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleFullScreen (isFullScreen) {
|
function onToggleFullScreen (isFullScreen) {
|
||||||
@@ -199,7 +207,7 @@ function getAppMenuTemplate () {
|
|||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: process.platform === 'windows'
|
label: process.platform === 'win32'
|
||||||
? 'Close'
|
? 'Close'
|
||||||
: 'Close Window',
|
: 'Close Window',
|
||||||
accelerator: 'CmdOrCtrl+W',
|
accelerator: 'CmdOrCtrl+W',
|
||||||
@@ -295,6 +303,14 @@ function getAppMenuTemplate () {
|
|||||||
accelerator: 'CmdOrCtrl+Down',
|
accelerator: 'CmdOrCtrl+Down',
|
||||||
click: decreaseVolume,
|
click: decreaseVolume,
|
||||||
enabled: false
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add Subtitles File...',
|
||||||
|
click: openSubtitles,
|
||||||
|
enabled: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -388,7 +404,7 @@ function getAppMenuTemplate () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// In Linux and Windows it is not possible to open both folders and files
|
// In Linux and Windows it is not possible to open both folders and files
|
||||||
if (process.platform === 'linux' || process.platform === 'windows') {
|
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||||
// File menu (Windows, Linux)
|
// File menu (Windows, Linux)
|
||||||
template[0].submenu.unshift({
|
template[0].submenu.unshift({
|
||||||
label: 'Create New Torrent from File...',
|
label: 'Create New Torrent from File...',
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
18
package.json
18
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.4.0",
|
"version": "0.5.1",
|
||||||
"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"
|
||||||
@@ -18,20 +18,22 @@
|
|||||||
"application-config": "^0.2.1",
|
"application-config": "^0.2.1",
|
||||||
"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",
|
||||||
"languagedetect": "^1.1.1",
|
"languagedetect": "^1.1.1",
|
||||||
"main-loop": "^3.2.0",
|
"main-loop": "^3.2.0",
|
||||||
"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",
|
||||||
@@ -47,7 +49,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",
|
||||||
@@ -68,7 +70,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": {
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
"open-config": "node ./bin/open-config.js",
|
"open-config": "node ./bin/open-config.js",
|
||||||
"package": "node ./bin/package.js",
|
"package": "node ./bin/package.js",
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"test": "standard && ./bin/check-deps.js",
|
"test": "standard && node ./bin/check-deps.js",
|
||||||
"update-authors": "./bin/update-authors.sh"
|
"update-authors": "./bin/update-authors.sh"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,10 +117,6 @@ table {
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-collapse {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-collapse.expanded::before {
|
.expand-collapse.expanded::before {
|
||||||
content: '▲'
|
content: '▲'
|
||||||
}
|
}
|
||||||
@@ -366,7 +362,6 @@ button { /* Rectangular text buttons */
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@@ -597,7 +592,7 @@ body.drag .app::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details {
|
.torrent-details {
|
||||||
padding: 8em 20px 20px 20px;
|
padding: 8em 12px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details table {
|
.torrent-details table {
|
||||||
@@ -611,6 +606,10 @@ body.drag .app::after {
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent-details td {
|
||||||
|
vertical-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.torrent-details tr:hover {
|
.torrent-details tr:hover {
|
||||||
background-color: rgba(200, 200, 200, 0.3);
|
background-color: rgba(200, 200, 200, 0.3);
|
||||||
}
|
}
|
||||||
@@ -621,16 +620,16 @@ body.drag .app::after {
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-details td.col-icon {
|
.torrent-details td .icon {
|
||||||
width: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.torrent-details td.col-icon .icon {
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent-details td.col-icon {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
.torrent-details td.col-name {
|
.torrent-details td.col-name {
|
||||||
width: auto;
|
width: auto;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -646,6 +645,11 @@ body.drag .app::after {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent-details td.col-select {
|
||||||
|
width: 2em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* PLAYER
|
* PLAYER
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ var ipcRenderer = electron.ipcRenderer
|
|||||||
setupIpc()
|
setupIpc()
|
||||||
|
|
||||||
var appConfig = require('application-config')('WebTorrent')
|
var appConfig = require('application-config')('WebTorrent')
|
||||||
var concat = require('concat-stream')
|
var concat = require('simple-concat')
|
||||||
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')
|
||||||
@@ -43,6 +45,9 @@ var Cast = null
|
|||||||
// For easy debugging in Developer Tools
|
// For easy debugging in Developer Tools
|
||||||
var state = global.state = State.getInitialState()
|
var state = global.state = State.getInitialState()
|
||||||
|
|
||||||
|
// Push the first page into the location history
|
||||||
|
state.location.go({ url: 'home' })
|
||||||
|
|
||||||
var vdomLoop
|
var vdomLoop
|
||||||
|
|
||||||
// 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.
|
||||||
@@ -58,9 +63,6 @@ function init () {
|
|||||||
// Clean up the freshly-loaded config file, which may be from an older version
|
// Clean up the freshly-loaded config file, which may be from an older version
|
||||||
cleanUpConfig()
|
cleanUpConfig()
|
||||||
|
|
||||||
// Push the first page into the location history
|
|
||||||
state.location.go({ url: 'home' })
|
|
||||||
|
|
||||||
// Restart everything we were torrenting last time the app ran
|
// Restart everything we were torrenting last time the app ran
|
||||||
resumeTorrents()
|
resumeTorrents()
|
||||||
|
|
||||||
@@ -85,7 +87,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)
|
||||||
@@ -151,6 +153,11 @@ function cleanUpConfig () {
|
|||||||
delete ts.posterURL
|
delete ts.posterURL
|
||||||
ts.posterFileName = infoHash + extension
|
ts.posterFileName = infoHash + extension
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: add per-file selections
|
||||||
|
if (!ts.selections) {
|
||||||
|
ts.selections = ts.files.map((x) => true)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,6 +236,9 @@ function dispatch (action, ...args) {
|
|||||||
if (action === 'toggleSelectTorrent') {
|
if (action === 'toggleSelectTorrent') {
|
||||||
toggleSelectTorrent(args[0] /* infoHash */)
|
toggleSelectTorrent(args[0] /* infoHash */)
|
||||||
}
|
}
|
||||||
|
if (action === 'toggleTorrentFile') {
|
||||||
|
toggleTorrentFile(args[0] /* infoHash */, args[1] /* index */)
|
||||||
|
}
|
||||||
if (action === 'openTorrentContextMenu') {
|
if (action === 'openTorrentContextMenu') {
|
||||||
openTorrentContextMenu(args[0] /* infoHash */)
|
openTorrentContextMenu(args[0] /* infoHash */)
|
||||||
}
|
}
|
||||||
@@ -242,16 +252,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) {
|
||||||
@@ -272,15 +273,14 @@ function dispatch (action, ...args) {
|
|||||||
playPause()
|
playPause()
|
||||||
}
|
}
|
||||||
if (action === 'play') {
|
if (action === 'play') {
|
||||||
if (state.location.pending()) return
|
|
||||||
state.location.go({
|
state.location.go({
|
||||||
url: 'player',
|
url: 'player',
|
||||||
onbeforeload: function (cb) {
|
onbeforeload: function (cb) {
|
||||||
|
play()
|
||||||
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
|
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
|
||||||
},
|
},
|
||||||
onbeforeunload: closePlayer
|
onbeforeunload: closePlayer
|
||||||
})
|
})
|
||||||
play()
|
|
||||||
}
|
}
|
||||||
if (action === 'playbackJump') {
|
if (action === 'playbackJump') {
|
||||||
jumpToTime(args[0] /* seconds */)
|
jumpToTime(args[0] /* seconds */)
|
||||||
@@ -295,16 +295,16 @@ function dispatch (action, ...args) {
|
|||||||
openSubtitles()
|
openSubtitles()
|
||||||
}
|
}
|
||||||
if (action === 'selectSubtitle') {
|
if (action === 'selectSubtitle') {
|
||||||
selectSubtitle(args[0] /* label */)
|
selectSubtitle(args[0] /* index */)
|
||||||
}
|
}
|
||||||
if (action === 'showSubtitles') {
|
if (action === 'toggleSubtitlesMenu') {
|
||||||
showSubtitles()
|
toggleSubtitlesMenu()
|
||||||
}
|
}
|
||||||
if (action === 'mediaStalled') {
|
if (action === 'mediaStalled') {
|
||||||
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) {
|
||||||
@@ -384,7 +384,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 {
|
||||||
@@ -422,7 +422,25 @@ function openSubtitles () {
|
|||||||
properties: [ 'openFile' ]
|
properties: [ 'openFile' ]
|
||||||
}, function (filenames) {
|
}, function (filenames) {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(filenames)) return
|
||||||
addSubtitle({path: filenames[0]})
|
addSubtitles(filenames, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,26 +554,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 files.filter(isSubtitle).forEach(addSubtitle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -582,6 +606,7 @@ 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-
|
||||||
function addTorrent (torrentId) {
|
function addTorrent (torrentId) {
|
||||||
|
backToList()
|
||||||
var torrentKey = state.nextTorrentKey++
|
var torrentKey = state.nextTorrentKey++
|
||||||
var path = state.saved.downloadPath
|
var path = state.saved.downloadPath
|
||||||
if (torrentId.path) {
|
if (torrentId.path) {
|
||||||
@@ -591,50 +616,108 @@ function addTorrent (torrentId) {
|
|||||||
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSubtitle (file) {
|
function addSubtitles (files, autoSelect) {
|
||||||
|
// Subtitles are only supported while playing video
|
||||||
|
if (state.playing.type !== 'video') return
|
||||||
|
|
||||||
|
// Read the files concurrently, then add all resulting subtitle tracks
|
||||||
|
var jobs = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||||
|
parallel(jobs, function (err, tracks) {
|
||||||
|
if (err) return onError(err)
|
||||||
|
|
||||||
|
for (var i = 0; i < tracks.length; i++) {
|
||||||
|
// No dupes allowed
|
||||||
|
var track = tracks[i]
|
||||||
|
if (state.playing.subtitles.tracks.some(
|
||||||
|
(t) => track.filePath === t.filePath)) continue
|
||||||
|
|
||||||
|
// Add the track
|
||||||
|
state.playing.subtitles.tracks.push(track)
|
||||||
|
|
||||||
|
// If we're auto-selecting a track, try to find one in the user's language
|
||||||
|
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||||
|
state.playing.subtitles.selectedIndex =
|
||||||
|
state.playing.subtitles.tracks.length - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, make sure no two tracks have the same label
|
||||||
|
relabelSubtitles()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSubtitle (file, cb) {
|
||||||
var srtToVtt = require('srt-to-vtt')
|
var srtToVtt = require('srt-to-vtt')
|
||||||
var LanguageDetect = require('languagedetect')
|
var LanguageDetect = require('languagedetect')
|
||||||
|
|
||||||
if (state.playing.type !== 'video') return
|
// Read the .SRT or .VTT file, parse it, add subtitle track
|
||||||
fs.createReadStream(file.path || file).pipe(srtToVtt()).pipe(concat(function (buf) {
|
var filePath = file.path || file
|
||||||
|
|
||||||
|
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
|
||||||
|
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
||||||
|
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
||||||
|
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
||||||
|
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
||||||
|
|
||||||
// Set the cue text position so it appears above the player controls.
|
// 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
|
// The only way to change cue text position is by modifying the VTT. It is not
|
||||||
// possible via CSS.
|
// possible via CSS.
|
||||||
var langDetected = (new LanguageDetect()).detect(buf.toString().replace(/(.*-->.*)/g, ''), 2)
|
|
||||||
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
|
||||||
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
|
||||||
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
|
var 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,' + subtitles.toString('base64'),
|
||||||
|
language: langDetected,
|
||||||
label: langDetected,
|
label: langDetected,
|
||||||
selected: true
|
filePath: filePath
|
||||||
}
|
}
|
||||||
state.playing.subtitles.tracks.forEach(function (trackItem) {
|
|
||||||
trackItem.selected = false
|
|
||||||
if (trackItem.label === track.label) {
|
|
||||||
var labelParts = /([^\d]+)(\d+)$/.exec(track.label)
|
|
||||||
track.label = labelParts
|
|
||||||
? labelParts[1] + (parseInt(labelParts[2]) + 1)
|
|
||||||
: track.label + ' 2'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
state.playing.subtitles.change = track.label
|
|
||||||
state.playing.subtitles.tracks.push(track)
|
|
||||||
state.playing.subtitles.enabled = true
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectSubtitle (label) {
|
cb(null, track)
|
||||||
state.playing.subtitles.tracks.forEach(function (track) {
|
|
||||||
track.selected = (track.label === label)
|
|
||||||
})
|
})
|
||||||
state.playing.subtitles.enabled = !!label
|
|
||||||
state.playing.subtitles.change = label
|
|
||||||
state.playing.subtitles.show = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSubtitles () {
|
function selectSubtitle (ix) {
|
||||||
state.playing.subtitles.show = !state.playing.subtitles.show
|
state.playing.subtitles.selectedIndex = ix
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks whether a language name like "English" or "German" matches the system
|
||||||
|
// language, aka the current locale
|
||||||
|
function isSystemLanguage (language) {
|
||||||
|
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
|
||||||
|
var langIso = iso639.getCode(language) // eg "de" if language is "German"
|
||||||
|
return langIso === osLangISO
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure we don't have two subtitle tracks with the same label
|
||||||
|
// Labels each track by language, eg "German", "English", "English 2", ...
|
||||||
|
function relabelSubtitles () {
|
||||||
|
var counts = {}
|
||||||
|
state.playing.subtitles.tracks.forEach(function (track) {
|
||||||
|
var lang = track.language
|
||||||
|
counts[lang] = (counts[lang] || 0) + 1
|
||||||
|
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForSubtitles () {
|
||||||
|
if (state.playing.type !== 'video') return
|
||||||
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
|
if (!torrentSummary || !torrentSummary.progress) return
|
||||||
|
|
||||||
|
torrentSummary.progress.files.forEach(function (fp, ix) {
|
||||||
|
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
|
||||||
|
var file = torrentSummary.files[ix]
|
||||||
|
if (!isSubtitle(file.name)) return
|
||||||
|
var filePath = path.join(torrentSummary.path, file.path)
|
||||||
|
addSubtitles([filePath], false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSubtitlesMenu () {
|
||||||
|
state.playing.subtitles.showMenu = !state.playing.subtitles.showMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
|
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
|
||||||
@@ -655,7 +738,7 @@ function startTorrentingSummary (torrentSummary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('start torrenting %s %s', s.torrentKey, torrentID)
|
console.log('start torrenting %s %s', s.torrentKey, torrentID)
|
||||||
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes)
|
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes, s.selections)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -666,7 +749,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
|
||||||
@@ -716,6 +798,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) {
|
||||||
@@ -728,7 +813,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')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,6 +849,9 @@ function torrentMetadata (torrentKey, torrentInfo) {
|
|||||||
torrentSummary.path = torrentInfo.path
|
torrentSummary.path = torrentInfo.path
|
||||||
torrentSummary.files = torrentInfo.files
|
torrentSummary.files = torrentInfo.files
|
||||||
torrentSummary.magnetURI = torrentInfo.magnetURI
|
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||||
|
if (!torrentSummary.selections) {
|
||||||
|
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||||
|
}
|
||||||
update()
|
update()
|
||||||
|
|
||||||
// Save the .torrent file, if it hasn't been saved already
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
@@ -815,6 +903,8 @@ function torrentProgress (progressInfo) {
|
|||||||
torrentSummary.progress = p
|
torrentSummary.progress = p
|
||||||
})
|
})
|
||||||
|
|
||||||
|
checkForSubtitles()
|
||||||
|
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -914,6 +1004,9 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
|||||||
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if it's video, check for subtitles files that are done downloading
|
||||||
|
checkForSubtitles()
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
||||||
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, function (e, info) {
|
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, function (e, info) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
@@ -989,7 +1082,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')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -999,6 +1092,14 @@ function toggleSelectTorrent (infoHash) {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleTorrentFile (infoHash, index) {
|
||||||
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
|
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
||||||
|
|
||||||
|
// Let the WebTorrent process know to start or stop fetching that file
|
||||||
|
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||||
|
}
|
||||||
|
|
||||||
function openTorrentContextMenu (infoHash) {
|
function openTorrentContextMenu (infoHash) {
|
||||||
var torrentSummary = getTorrentSummary(infoHash)
|
var torrentSummary = getTorrentSummary(infoHash)
|
||||||
var menu = new electron.remote.Menu()
|
var menu = new electron.remote.Menu()
|
||||||
@@ -1129,7 +1230,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 &&
|
||||||
@@ -1164,8 +1265,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) {
|
||||||
|
|||||||
@@ -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 () {}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ var LocationHistory = require('./lib/location-history')
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
getInitialState,
|
getInitialState,
|
||||||
getDefaultPlayState,
|
getDefaultPlayState,
|
||||||
getDefaultSavedState
|
getDefaultSavedState,
|
||||||
|
getPlayingTorrentSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialState () {
|
function getInitialState () {
|
||||||
@@ -57,7 +58,12 @@ function getInitialState () {
|
|||||||
*
|
*
|
||||||
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
* Also accessible via `require('application-config')('WebTorrent').filePath`
|
||||||
*/
|
*/
|
||||||
saved: {}
|
saved: {},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Getters, for convenience
|
||||||
|
*/
|
||||||
|
getPlayingTorrentSummary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +81,9 @@ function getDefaultPlayState () {
|
|||||||
lastTimeUpdate: 0, /* Unix time in ms */
|
lastTimeUpdate: 0, /* Unix time in ms */
|
||||||
mouseStationarySince: 0, /* Unix time in ms */
|
mouseStationarySince: 0, /* Unix time in ms */
|
||||||
subtitles: {
|
subtitles: {
|
||||||
tracks: [], /* subtitles file (Buffer) */
|
tracks: [], /* subtitle tracks, each {label, language, ...} */
|
||||||
enabled: false
|
selectedIndex: -1, /* current subtitle track */
|
||||||
|
showMenu: false /* popover menu, above the video */
|
||||||
},
|
},
|
||||||
aspectRatio: 0 /* aspect ratio of the video */
|
aspectRatio: 0 /* aspect ratio of the video */
|
||||||
}
|
}
|
||||||
@@ -263,3 +270,8 @@ function getDefaultSavedState () {
|
|||||||
: remote.app.getPath('downloads')
|
: remote.app.getPath('downloads')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPlayingTorrentSummary () {
|
||||||
|
var infoHash = this.playing.infoHash
|
||||||
|
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ 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 &&
|
||||||
@@ -30,10 +30,10 @@ function App (state) {
|
|||||||
|
|
||||||
// Hide the header on Windows/Linux when in the player
|
// Hide the header on Windows/Linux when in the player
|
||||||
// On OSX, the header appears as part of the title bar
|
// On OSX, the header appears as part of the title bar
|
||||||
var hideHeader = process.platform !== 'darwin' && state.location.current().url === 'player'
|
var hideHeader = process.platform !== 'darwin' && state.location.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')
|
||||||
@@ -54,12 +54,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 +81,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ function CreateTorrentPage (state) {
|
|||||||
basePath = pathPrefix
|
basePath = pathPrefix
|
||||||
} else {
|
} else {
|
||||||
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a"
|
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a"
|
||||||
defaultName = files[0].name
|
defaultName = path.basename(pathPrefix)
|
||||||
basePath = path.basename(pathPrefix)
|
basePath = path.dirname(pathPrefix)
|
||||||
}
|
}
|
||||||
var maxFileElems = 100
|
var maxFileElems = 100
|
||||||
var fileElems = files.slice(0, maxFileElems).map(function (file) {
|
var fileElems = files.slice(0, maxFileElems).map(function (file) {
|
||||||
@@ -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 () {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function Header (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getAddButton () {
|
function getAddButton () {
|
||||||
if (state.location.current().url !== 'player') {
|
if (state.location.url() !== 'player') {
|
||||||
return hx`
|
return hx`
|
||||||
<i
|
<i
|
||||||
class='icon add'
|
class='icon add'
|
||||||
|
|||||||
@@ -54,14 +54,10 @@ function renderMedia (state) {
|
|||||||
state.playing.setVolume = null
|
state.playing.setVolume = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix textTrack cues not been removed <track> rerender
|
// Switch to the newly added subtitle track, if available
|
||||||
if (state.playing.subtitles.change) {
|
var tracks = mediaElement.textTracks
|
||||||
var tracks = mediaElement.textTracks
|
for (var j = 0; j < tracks.length; j++) {
|
||||||
for (var j = 0; j < tracks.length; j++) {
|
tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden'
|
||||||
// mode is not an <track> attribute, only available on DOM
|
|
||||||
tracks[j].mode = (tracks[j].label === state.playing.subtitles.change) ? 'showing' : 'hidden'
|
|
||||||
}
|
|
||||||
state.playing.subtitles.change = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.playing.currentTime = mediaElement.currentTime
|
state.playing.currentTime = mediaElement.currentTime
|
||||||
@@ -71,13 +67,13 @@ function renderMedia (state) {
|
|||||||
|
|
||||||
// Add subtitles to the <video> tag
|
// Add subtitles to the <video> tag
|
||||||
var trackTags = []
|
var trackTags = []
|
||||||
|
if (state.playing.subtitles.selectedIndex >= 0) {
|
||||||
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
|
|
||||||
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||||
var track = state.playing.subtitles.tracks[i]
|
var track = state.playing.subtitles.tracks[i]
|
||||||
|
var isSelected = state.playing.subtitles.selectedIndex === i
|
||||||
trackTags.push(hx`
|
trackTags.push(hx`
|
||||||
<track
|
<track
|
||||||
${track.selected ? 'default' : ''}
|
${isSelected ? 'default' : ''}
|
||||||
label=${track.label}
|
label=${track.label}
|
||||||
type='subtitles'
|
type='subtitles'
|
||||||
src=${track.buffer}>
|
src=${track.buffer}>
|
||||||
@@ -165,7 +161,7 @@ function renderOverlay (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAudioMetadata (state) {
|
function renderAudioMetadata (state) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
var fileSummary = torrentSummary.files[state.playing.fileIndex]
|
var fileSummary = torrentSummary.files[state.playing.fileIndex]
|
||||||
if (!fileSummary.audioInfo) return
|
if (!fileSummary.audioInfo) return
|
||||||
var info = fileSummary.audioInfo
|
var info = fileSummary.audioInfo
|
||||||
@@ -204,7 +200,7 @@ function renderLoadingSpinner (state) {
|
|||||||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
||||||
if (!isProbablyStalled) return
|
if (!isProbablyStalled) return
|
||||||
|
|
||||||
var prog = getPlayingTorrentSummary(state).progress || {}
|
var prog = state.getPlayingTorrentSummary().progress || {}
|
||||||
var fileProgress = 0
|
var fileProgress = 0
|
||||||
if (prog.files) {
|
if (prog.files) {
|
||||||
var file = prog.files[state.playing.fileIndex]
|
var file = prog.files[state.playing.fileIndex]
|
||||||
@@ -270,22 +266,24 @@ function renderCastScreen (state) {
|
|||||||
|
|
||||||
function renderSubtitlesOptions (state) {
|
function renderSubtitlesOptions (state) {
|
||||||
var subtitles = state.playing.subtitles
|
var subtitles = state.playing.subtitles
|
||||||
if (!subtitles.tracks.length || !subtitles.show) return
|
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||||
|
|
||||||
var items = subtitles.tracks.map(function (track) {
|
var items = subtitles.tracks.map(function (track, ix) {
|
||||||
|
var isSelected = state.playing.subtitles.selectedIndex === ix
|
||||||
return hx`
|
return hx`
|
||||||
<li onclick=${dispatcher('selectSubtitle', track.label)}>
|
<li onclick=${dispatcher('selectSubtitle', ix)}>
|
||||||
<i.icon>${track.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||||
${track.label}
|
${track.label}
|
||||||
</li>
|
</li>
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||||
return hx`
|
return hx`
|
||||||
<ul.subtitles-list>
|
<ul.subtitles-list>
|
||||||
${items}
|
${items}
|
||||||
<li onclick=${dispatcher('selectSubtitle', '')}>
|
<li onclick=${dispatcher('selectSubtitle', -1)}>
|
||||||
<i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
<i.icon>${noneSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||||
None
|
None
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -297,7 +295,7 @@ function renderPlayerControls (state) {
|
|||||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
||||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
var captionsClass = state.playing.subtitles.tracks.length === 0
|
||||||
? 'disabled'
|
? 'disabled'
|
||||||
: state.playing.subtitles.enabled
|
: state.playing.subtitles.selectedIndex >= 0
|
||||||
? 'active'
|
? 'active'
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
@@ -484,7 +482,7 @@ function renderPlayerControls (state) {
|
|||||||
// if no subtitles available select it
|
// if no subtitles available select it
|
||||||
dispatch('openSubtitles')
|
dispatch('openSubtitles')
|
||||||
} else {
|
} else {
|
||||||
dispatch('showSubtitles')
|
dispatch('toggleSubtitlesMenu')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,7 +493,7 @@ var volumeChanging = false
|
|||||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||||
// can be "spongey" / non-contiguous
|
// can be "spongey" / non-contiguous
|
||||||
function renderLoadingBar (state) {
|
function renderLoadingBar (state) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary.progress) {
|
if (!torrentSummary.progress) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -532,7 +530,7 @@ function renderLoadingBar (state) {
|
|||||||
|
|
||||||
// Returns the CSS background-image string for a poster image + dark vignette
|
// Returns the CSS background-image string for a poster image + dark vignette
|
||||||
function cssBackgroundImagePoster (state) {
|
function cssBackgroundImagePoster (state) {
|
||||||
var torrentSummary = getPlayingTorrentSummary(state)
|
var torrentSummary = state.getPlayingTorrentSummary()
|
||||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
if (!posterPath) return ''
|
if (!posterPath) return ''
|
||||||
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
||||||
@@ -542,8 +540,3 @@ 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 getPlayingTorrentSummary (state) {
|
|
||||||
var infoHash = state.playing.infoHash
|
|
||||||
return state.saved.torrents.find((x) => x.infoHash === infoHash)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -208,7 +208,8 @@ function TorrentList (state) {
|
|||||||
// Show a single torrentSummary file in the details view for a single torrent
|
// Show a single torrentSummary file in the details view for a single torrent
|
||||||
function renderFileRow (torrentSummary, file, index) {
|
function renderFileRow (torrentSummary, file, index) {
|
||||||
// First, find out how much of the file we've downloaded
|
// First, find out how much of the file we've downloaded
|
||||||
var isDone = false
|
var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
|
||||||
|
var isDone = false // Are we finished torrenting it?
|
||||||
var progress = ''
|
var progress = ''
|
||||||
if (torrentSummary.progress && torrentSummary.progress.files) {
|
if (torrentSummary.progress && torrentSummary.progress.files) {
|
||||||
var fileProg = torrentSummary.progress.files[index]
|
var fileProg = torrentSummary.progress.files[index]
|
||||||
@@ -217,26 +218,38 @@ function TorrentList (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Second, render the file as a table row
|
// Second, render the file as a table row
|
||||||
|
var isPlayable = TorrentPlayer.isPlayable(file)
|
||||||
var infoHash = torrentSummary.infoHash
|
var infoHash = torrentSummary.infoHash
|
||||||
var icon
|
var icon
|
||||||
var rowClass = ''
|
|
||||||
var handleClick
|
var handleClick
|
||||||
if (TorrentPlayer.isPlayable(file)) {
|
if (isPlayable) {
|
||||||
icon = 'play_arrow' /* playable? add option to play */
|
icon = 'play_arrow' /* playable? add option to play */
|
||||||
handleClick = dispatcher('play', infoHash, index)
|
handleClick = dispatcher('play', infoHash, index)
|
||||||
} else {
|
} else {
|
||||||
icon = 'description' /* file icon, opens in OS default app */
|
icon = 'description' /* file icon, opens in OS default app */
|
||||||
rowClass = isDone ? '' : 'disabled'
|
|
||||||
handleClick = dispatcher('openFile', infoHash, index)
|
handleClick = dispatcher('openFile', infoHash, index)
|
||||||
}
|
}
|
||||||
|
var rowClass = ''
|
||||||
|
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||||
|
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||||
return hx`
|
return hx`
|
||||||
<tr onclick=${handleClick} class='${rowClass}'>
|
<tr>
|
||||||
<td class='col-icon'>
|
<td class='col-icon ${rowClass}' onclick=${handleClick}>
|
||||||
<i class='icon'>${icon}</i>
|
<i class='icon'>${icon}</i>
|
||||||
</td>
|
</td>
|
||||||
<td class='col-name'>${file.name}</td>
|
<td class='col-name ${rowClass}' onclick=${handleClick}>
|
||||||
<td class='col-progress'>${progress}</td>
|
${file.name}
|
||||||
<td class='col-size'>${prettyBytes(file.length)}</td>
|
</td>
|
||||||
|
<td class='col-progress ${rowClass}' onclick=${handleClick}>
|
||||||
|
${isSelected ? progress : ''}
|
||||||
|
</td>
|
||||||
|
<td class='col-size ${rowClass}' onclick=${handleClick}>
|
||||||
|
${prettyBytes(file.length)}
|
||||||
|
</td>
|
||||||
|
<td class='col-select'
|
||||||
|
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
|
||||||
|
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ function init () {
|
|||||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||||
|
|
||||||
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) =>
|
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
||||||
startTorrenting(torrentKey, torrentID, path, fileModtimes))
|
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
||||||
ipc.on('wt-stop-torrenting', (e, infoHash) =>
|
ipc.on('wt-stop-torrenting', (e, infoHash) =>
|
||||||
stopTorrenting(infoHash))
|
stopTorrenting(infoHash))
|
||||||
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
|
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
|
||||||
@@ -65,6 +65,8 @@ function init () {
|
|||||||
startServer(infoHash, index))
|
startServer(infoHash, index))
|
||||||
ipc.on('wt-stop-server', (e) =>
|
ipc.on('wt-stop-server', (e) =>
|
||||||
stopServer())
|
stopServer())
|
||||||
|
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
||||||
|
selectFiles(infoHash, selections))
|
||||||
|
|
||||||
ipc.send('ipcReadyWebTorrent')
|
ipc.send('ipcReadyWebTorrent')
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ function init () {
|
|||||||
|
|
||||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
||||||
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
||||||
function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
|
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
||||||
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
||||||
|
|
||||||
var torrent = client.add(torrentID, {
|
var torrent = client.add(torrentID, {
|
||||||
@@ -81,8 +83,13 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
|
|||||||
fileModtimes: fileModtimes
|
fileModtimes: fileModtimes
|
||||||
})
|
})
|
||||||
torrent.key = torrentKey
|
torrent.key = torrentKey
|
||||||
|
|
||||||
|
// Listen for ready event, progress notifications, etc
|
||||||
addTorrentEvents(torrent)
|
addTorrentEvents(torrent)
|
||||||
|
|
||||||
|
// Only download the files the user wants, not necessarily all files
|
||||||
|
torrent.once('ready', () => selectFiles(torrent, selections))
|
||||||
|
|
||||||
return torrent
|
return torrent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,9 +164,7 @@ function getTorrentFileInfo (file) {
|
|||||||
return {
|
return {
|
||||||
name: file.name,
|
name: file.name,
|
||||||
length: file.length,
|
length: file.length,
|
||||||
path: file.path,
|
path: file.path
|
||||||
numPiecesPresent: 0,
|
|
||||||
numPieces: null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +315,44 @@ function getAudioMetadata (infoHash, index) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectFiles (torrentOrInfoHash, selections) {
|
||||||
|
// Get the torrent object
|
||||||
|
var torrent
|
||||||
|
if (typeof torrentOrInfoHash === 'string') {
|
||||||
|
torrent = client.get(torrentOrInfoHash)
|
||||||
|
} else {
|
||||||
|
torrent = torrentOrInfoHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selections not specified?
|
||||||
|
// Load all files. We still need to replace the default whole-torrent
|
||||||
|
// selection with individual selections for each file, so we can
|
||||||
|
// select/deselect files later on
|
||||||
|
if (!selections) {
|
||||||
|
selections = torrent.files.map((x) => true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selections specified incorrectly?
|
||||||
|
if (selections.length !== torrent.files.length) {
|
||||||
|
throw new Error('got ' + selections.length + ' file selections, ' +
|
||||||
|
'but the torrent contains ' + torrent.files.length + ' files')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove default selection (whole torrent)
|
||||||
|
torrent.deselect(0, torrent.pieces.length - 1, false)
|
||||||
|
|
||||||
|
// Add selections (individual files)
|
||||||
|
for (var i = 0; i < selections.length; i++) {
|
||||||
|
var file = torrent.files[i]
|
||||||
|
if (selections[i]) {
|
||||||
|
file.select()
|
||||||
|
} else {
|
||||||
|
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
|
||||||
|
file.deselect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Gets a WebTorrent handle by torrentKey
|
// Gets a WebTorrent handle by torrentKey
|
||||||
// Throws an Error if we're not currently torrenting anything w/ that key
|
// Throws an Error if we're not currently torrenting anything w/ that key
|
||||||
function getTorrent (torrentKey) {
|
function getTorrent (torrentKey) {
|
||||||
|
|||||||
10
static/child.entitlements
Normal file
10
static/child.entitlements
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.inherit</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
20
static/parent.entitlements
Normal file
20
static/parent.entitlements
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.client</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.network.server</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>5MAMC8G3L8.io.webtorrent.webtorrent</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Reference in New Issue
Block a user