Compare commits

..

53 Commits

Author SHA1 Message Date
Feross Aboukhadijeh
93a9ba11b6 App Sandboxing (OS X) 2016-05-21 01:08:42 -07:00
Feross Aboukhadijeh
b367c0b72e electron-prebuilt@1.1.1 2016-05-20 23:57:51 -07:00
Feross Aboukhadijeh
bf3b9ced74 Merge pull request #545 from feross/add-announcement
Add announcement feature
2016-05-20 16:10:27 -07:00
Feross Aboukhadijeh
9ecc12fb7f Merge pull request #544 from feross/vlc-on-top
VLC tweaks
2016-05-20 16:10:20 -07:00
Feross Aboukhadijeh
aafb1421c6 Merge pull request #543 from feross/on-open
Improve open behavior; Fix bugs in LocationHistory
2016-05-20 16:10:11 -07:00
Feross Aboukhadijeh
76c732bafb Merge pull request #541 from feross/remove-concat-stream
Use lighter-weight simple-concat instead of concat-stream
2016-05-20 16:09:46 -07:00
Feross Aboukhadijeh
ab476c9a9c Merge pull request #540 from feross/llc
WebTorrent, LLC
2016-05-20 16:09:18 -07:00
Feross Aboukhadijeh
4470310814 Merge pull request #549 from feross/nobin-debian-installer
nobin-debian-installer@0.0.10
2016-05-20 16:08:51 -07:00
Feross Aboukhadijeh
b6ba4f45c8 nobin-debian-installer@0.0.10 2016-05-20 14:37:54 -07:00
Feross Aboukhadijeh
84c860cfcb Make dialog async 2016-05-19 20:24:25 -07:00
Feross Aboukhadijeh
47c554a5ff Announcement: Support custom window title, main message, details 2016-05-19 20:17:51 -07:00
Feross Aboukhadijeh
4e46b16c13 auto updater: code style 2016-05-19 20:03:37 -07:00
Feross Aboukhadijeh
22cdcdb468 Add announcement feature
If there's a message returned by the given remote URL, then it will
show up for the user.

Useful in situations where the auto-updater is not working, or if
there's a security issue.
2016-05-19 20:03:02 -07:00
Feross Aboukhadijeh
f238b2d105 VLC tweaks
- Start video on top, so it's not obscured by other windows.

- Don't show "video title" which is just "http://localhost:xxxx"

- return after error
2016-05-19 19:43:43 -07:00
Feross Aboukhadijeh
3a81799828 Unify onOpen and onDrag, and support more cases
I don't think it matters whether the open comes from onOpen (opening
magnet, .torrent file, dragging file to dock, menu item) or from
dragging to the window.

These should use the same code path. The only relevant information is
the page of the app that we're on.

This change unifies the two methods, and supports dragging .torrent
files or creating a torrent when the player is active, if the dragged
files are not .srt or .vtt. We go back to the list, or to the create
torrent page in these situations, so it's not confusing for the user.

Always close open modals when handling an open.
2016-05-19 19:03:47 -07:00
Feross Aboukhadijeh
5dca89b61c When player is active, and magnet link is pasted, go back to list 2016-05-19 18:56:41 -07:00
Feross Aboukhadijeh
264c035ef7 After deleting torrent, remove just the player from forward stack 2016-05-19 18:56:10 -07:00
Feross Aboukhadijeh
8f39f8a23e After creating torrent, remove create torrent page from forward stack 2016-05-19 18:55:49 -07:00
Feross Aboukhadijeh
a29dbd7a71 Cancel button on create torrent page should only go back one page 2016-05-19 18:55:06 -07:00
Feross Aboukhadijeh
60a8969abc Add location.url() shorthand
location.url() === location.current().url
2016-05-19 18:54:44 -07:00
Feross Aboukhadijeh
9747d28514 Fix bugs in LocationHistory
- Handles more than 2 pages in the history robustly now!
  - When self._pending is true, all navigations are ignored.
- No more bug with back() being called twice too quickly.
- Remove "leaky abstraction" methods like clearPending() and pending()
- Add backToFirst() that properly unloads each page as it goes back to
the first one.
- Enhance clearForward() to support removing a specific page from the
forward stack, instead of nuking the whole thing.
2016-05-19 18:53:53 -07:00
Feross Aboukhadijeh
17ccd217a9 Use lighter-weight simple-concat instead of concat-stream
These modules do the same thing.

$ browserify -r simple-concat --no-builtins | wc -c
901

$ browserify -r concat-stream --no-builtins | wc -c
91998
2016-05-19 16:57:14 -07:00
Feross Aboukhadijeh
0df6198549 WebTorrent, LLC
What is WebTorrent, LLC?

WebTorrent, LLC is the legal entity that runs the WebTorrent project.
WebTorrent is still, and always will be, non-profit, open source, free
software.

There are no plans to make a profit from WebTorrent.
2016-05-19 16:43:51 -07:00
Feross Aboukhadijeh
74ada99f2b Merge pull request #538 from feross/dc/fix
Always handle when the user clicks a magnet link or torrent file, or uses File > Open Torrent
2016-05-19 16:26:56 -07:00
DC
81d5a367da Add new torrents to top and scroll to top
This means people who add a lot of torrents will always have their latest torrents at the top when they open the app, instead of having to scroll all the way down
2016-05-19 00:44:59 -07:00
DC
189e4bdc24 Always handle when the user opens a torrent
Fixes #523
2016-05-19 00:18:51 -07:00
DC
7bd30f8a16 Clean up addSubtitles (#535)
* Fix comments from #529

* Don't unlink deselected files

  I still want to do that eventually, but needs to be supported in WebTorrent

  See https://github.com/feross/webtorrent/issues/806
2016-05-18 02:07:24 -07:00
Feross Aboukhadijeh
7c6b7e4a6d changelog 2016-05-18 00:49:06 -07:00
Feross Aboukhadijeh
fe50f76619 0.5.1 2016-05-18 00:40:37 -07:00
Feross Aboukhadijeh
973a366b94 Fix the auto updater
I'm sorry.
2016-05-18 00:36:52 -07:00
Feross Aboukhadijeh
b0116deb35 appdmg@^0.4.3 2016-05-17 22:21:29 -07:00
Feross Aboukhadijeh
511382d384 package: remove unneeded 'npm prune'
prune just removes packages in node_modules that are not in
package.json, which is not necessary since we just removed node_modules
2016-05-17 22:10:43 -07:00
Feross Aboukhadijeh
cfb3a01239 0.5.0 2016-05-17 22:07:10 -07:00
Feross Aboukhadijeh
736d575ab1 changelog 2016-05-17 22:06:52 -07:00
Feross Aboukhadijeh
34a9508483 Add '...' to menu items that open dialogs 2016-05-17 22:03:17 -07:00
Feross Aboukhadijeh
21ed8797c2 Merge pull request #533 from feross/dc/select
Remove `cursor:pointer`
2016-05-17 21:31:27 -07:00
DC
454491572a Remove cursor:pointer
Apparently that's only for websites & we want to feel native
2016-05-17 21:25:31 -07:00
DC
6518a1535c Allow selecting individual files to torrent
Saves bandwidth and disk space when a torrent contains extra files you don't need

Fixes #360
2016-05-17 07:13:38 -07:00
DC
0095687bf5 Simplify subtitles code 2016-05-17 06:27:58 -07:00
DC
d466ed085a When manually adding subtitle track(s), always switch to a new track
Also fix a bug I added in the parent commit
2016-05-17 05:50:36 -07:00
DC
eeda7c17c5 Wait for the app ready event before creating windows
Fixes #524
2016-05-17 05:12:42 -07:00
DC
b89deb46db Remove debug console.logs 2016-05-16 08:35:00 -07:00
DC
951a89c6c9 Add Subtitles File menu item 2016-05-16 08:21:03 -07:00
DC
d4e6c84279 Automatically add subtitle tracks
Currently, add all .SRT and .VTT subtitle files in the same torrent as a video file
2016-05-16 08:03:21 -07:00
DC
9731d85ca3 Simplify subtitles code 2016-05-16 03:41:27 -07:00
DC
98f7ba8931 Fix a bad bug when creating multifile torrents 2016-05-16 01:09:21 -07:00
Feross Aboukhadijeh
24c775608e Merge pull request #513 from feross/detect-win32
Fix missing 'About WebTorrent' menu item
2016-05-16 03:22:50 +02:00
Feross Aboukhadijeh
f4eab12c3f Merge pull request #518 from feross/osx-magnet-exception
OS X: Fix magnet links throwing exception on launch
2016-05-16 03:04:17 +02:00
Feross Aboukhadijeh
8eeddeb4bc OS X: Fix magnet links throwing exception on launch
Push page into location right away
2016-05-15 18:02:11 -07:00
Feross Aboukhadijeh
58f1594d9e Fix missing 'About WebTorrent' menu item 2016-05-14 01:51:47 -07:00
Feross Aboukhadijeh
c126ac0a84 fix test script on windows 2016-05-13 23:11:55 -07:00
Feross Aboukhadijeh
6768be710e changelog fixes 2016-05-13 23:11:27 -07:00
Feross Aboukhadijeh
b63aa090dc fix release script 2016-05-13 23:11:23 -07:00
25 changed files with 567 additions and 228 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
View 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 () {})
})
}

View File

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

View File

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

View File

@@ -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...',

View 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()
} }

View File

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

View File

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

View File

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

View File

@@ -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 () {}

View File

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

View File

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

View File

@@ -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 () {

View File

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

View File

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

View File

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

View File

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

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