Compare commits

...

87 Commits

Author SHA1 Message Date
DC
0a15db2892 0.6.0 2016-05-24 03:05:31 -07:00
DC
63dda10380 changelog 2016-05-24 03:03:42 -07:00
DC
6e651df083 authors 2016-05-24 02:42:29 -07:00
DC
3a8fe24eec Fix scrub cursor 2016-05-24 02:39:28 -07:00
DC
918a35e091 Fix scrub button position 2016-05-24 02:09:17 -07:00
DC
c76abeb8c0 Remove cursor:pointer 2016-05-24 01:59:51 -07:00
DC
d389b8ab38 Bugfix: window title shouldn't be stuck on 'Preferences' 2016-05-24 01:56:39 -07:00
DC
a59faacbd7 Simplify prefs window 2016-05-24 01:52:31 -07:00
grunjol
12f9709601 Add preferences page
* For now, the prefs page has just a single option, Downloads Folder
* For now, you can't type in a folder, you must use the chooser
* Further fixes coming om master
* Written by @ChrisMorrisOrg and @grunjol, rebased by @dcposch
2016-05-23 22:31:09 -07:00
Feross Aboukhadijeh
455c9c02b9 Reduce startup jank, improve startup time (#568)
* Reduce jank on app startup

This feels a lot better on my 12" macbook (underpowered machine)

* Defer loading iso-639-1 and simple-concat
2016-05-23 22:12:04 -07:00
Feross Aboukhadijeh
1b49c6568b Cleanup unsupported codec detection (pt 2) (#570) 2016-05-23 22:03:38 -07:00
Feross Aboukhadijeh
30e81c7699 Cleanup for PR #571 2016-05-23 15:15:52 -07:00
Feross Aboukhadijeh
2dafc68301 Merge pull request #571 from Lurk/242
#242 add sort by file name
2016-05-23 15:08:31 -07:00
Feross Aboukhadijeh
c310222af2 Merge pull request #566 from feross/ui-improvements
More consistent controls
2016-05-23 14:49:49 -07:00
Feross Aboukhadijeh
b4bb9a6603 Fix rate UX for new design 2016-05-23 14:47:02 -07:00
Feross Aboukhadijeh
279c621d23 More consistent controls, delete verbose css 2016-05-23 14:16:51 -07:00
DC
eb11dbdcbd Fix error handling in dispatch('play') 2016-05-23 06:17:17 -07:00
DC
8dfdb34d31 Bugfix: default file to play on default torrents 2016-05-23 05:42:37 -07:00
Sergey Bargamon
fc9a73d67f #242 add sort by file name 2016-05-23 10:43:55 +03:00
DC
4b5b84a0fc Resume playback from saved position, even if we had to restart the torrent 2016-05-23 00:33:17 -07:00
DC
327c95d754 Show video position on circular progress bars 2016-05-23 00:33:17 -07:00
DC
6e969e5d07 Cleaner look for the torrent file list 2016-05-23 00:33:17 -07:00
DC
ca7c872420 Save video position 2016-05-23 00:33:17 -07:00
Sergey Bargamon
8af4f42c42 Add additional video player keyboard shortcuts (#275)
*  Skip forward 10 seconds ((CMD OR CTRL) ALT right)
 Skip back 10 seconds ((CMD OR CTRL) ALT left)
 Increase video speed ((CMD OR CTRL) +)
 Decrease video speed ((CMD OR CTRL) -)

* Codestyle fix

* The 'steps' should be implemented in base2, standard players use 1x, 2x, 4x, 8x, 16x

fixed bug with shift + "=" which is "+"

* resolve conflicts

* remove ide specific data
make playback rate more granular
add to menu skip and speed entries

* intendation fix

* conflict resolve

* rename setPlaybackRate to changePlaybackRate
setRate return boolean depending on whether this cast target supports setting the playback rate.
if setRate returns false - don`t change state
redundant else if statement in changePlaybackRate function
2016-05-23 00:15:57 -07:00
Feross Aboukhadijeh
ffce76a9b1 Cleanup unsupported codec detection (#569)
Review: @dcposch
2016-05-22 23:35:29 -07:00
Feross Aboukhadijeh
fca1d9dae4 Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
Closes #539.
2016-05-22 23:20:30 -07:00
Feross Aboukhadijeh
eba09430e3 Merge pull request #563 from mathiasvr/patch
Handle unsupported video codec (e.g. H.265)
2016-05-22 23:13:50 -07:00
Feross Aboukhadijeh
6bc8de7625 Merge pull request #562 from demoneaux/video-progress
Add support for video progress time in player controls.
2016-05-22 19:51:59 -07:00
Benjamin Tan
8a08ed8538 Add support for video progress time in player controls.
Closes #351.
2016-05-23 10:39:58 +08:00
Mathias Rasmussen
56d802f741 Handle unsupported video codec (e.g. H.265) 2016-05-22 23:23:45 +02:00
Benjamin Tan
f7b46336fd Use poster.jpg file as the poster image if available. (#558)
Closes #501.
2016-05-22 02:14:48 -07:00
Feross Aboukhadijeh
510187c2ae electron-prebuilt@1.1.1 (#555) 2016-05-21 17:00:46 -07:00
Feross Aboukhadijeh
ff6ff8db00 Fewer click handlers (#552)
One more bit of cleanup for PR #529.

We can register `handleClick` once on the `<tr>` tag and just have the
onclick handler on `col-select` too. Because dispatcher calls
`event.stopPropagation()` we don't need to worry about event bubbling
up to the `<tr>`.
2016-05-21 16:50:18 -07:00
Feross Aboukhadijeh
014017604d Improve subtitle positioning (#551)
Before this commit, we tweaked the subtitle position by modifying the
VTT file, line by line with a regex because I did not know it was
possible to use CSS for it.

But apparently there are Shadow DOM elements that we can use instead.

This new approach improves:

- Wrapping long lines. Before, the text would go off the edge of the
screen. Now it wraps intelligently.

- The subtitles move up to get out of the way of the controls when
those are visible.
2016-05-21 16:49:30 -07:00
Feross Aboukhadijeh
8cf544d54f Associate .torrent files to WebTorrent Desktop (OS X) (#553)
Before this change, .torrent files would only be associated to
WebTorrent Desktop if another torrent client (like Transmission) was
installed on the system.

That's because one of the user's apps needs to define
"UTExportedTypeDeclarations".

On a fresh machine, without Transmission, WebTorrent Desktop now
associates .torrent files correctly.

So it will appear in the "Open With..." menu and the webtorrent
.torrent icon will be used for .torrent files.

Closes #542.
2016-05-21 16:47:57 -07:00
Benjamin Tan
870dd893fc Add support for pasting a instant.io link. (#559)
Closes #547.
2016-05-21 16:23:16 -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
28 changed files with 1434 additions and 450 deletions

2
.gitignore vendored
View File

@@ -1,2 +1,2 @@
node_modules node_modules
dist dist

View File

@@ -18,5 +18,8 @@
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com> - Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
- gabriel <furstenheim@gmail.com> - gabriel <furstenheim@gmail.com>
- Rolando Guedes <rolando.guedes@3gnt.net> - Rolando Guedes <rolando.guedes@3gnt.net>
- Benjamin Tan <demoneaux@gmail.com>
- Mathias Rasmussen <mathiasvr@gmail.com>
- Sergey Bargamon <sergey@bargamon.ru>
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,5 +1,59 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.6.0 - 2016-05-24
### Added
- Added Preferences page
- Save video position, resume playback from saved position
- Add additional video player keyboard shortcuts (#275)
- Use `poster.jpg` file as the poster image if available (#558)
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
- Add support for pasting a `instant.io` links (#559)
- Add announcement feature
### Changed
- Nicer player UI
- Reduce startup jank, improve startup time (#568)
- Cleanup unsupported codec detection (#569, #570)
- Cleaner look for the torrent file list
- Improve subtitle positioning (#551)
### Fixed
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
- Fix bugs in LocationHistory
- When player is active, and magnet link is pasted, go back to list
- After deleting torrent, remove just the player from forward stack
- After creating torrent, remove create torrent page from forward stack
- Cancel button on create torrent page should only go back one page
## v0.5.1 - 2016-05-18
### Fixed
- Fix auto-updater (OS X, Windows).
## v0.5.0 - 2016-05-17
### 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 +92,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 +105,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

@@ -182,8 +182,6 @@ function buildDarwin (cb) {
var infoPlistPath = path.join(contentsPath, 'Info.plist') var infoPlistPath = path.join(contentsPath, 'Info.plist')
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
// TODO: Use new `extend-info` and `extra-resource` opts to electron-packager,
// available as of v6.
infoPlist.CFBundleDocumentTypes = [ infoPlist.CFBundleDocumentTypes = [
{ {
CFBundleTypeExtensions: [ 'torrent' ], CFBundleTypeExtensions: [ 'torrent' ],
@@ -211,6 +209,25 @@ function buildDarwin (cb) {
} }
] ]
infoPlist.UTExportedTypeDeclarations = [
{
UTTypeConformsTo: [
'public.data',
'public.item',
'com.bittorrent.torrent'
],
UTTypeDescription: 'BitTorrent Document',
UTTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
UTTypeIdentifier: 'org.bittorrent.torrent',
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
UTTypeTagSpecification: {
'com.apple.ostype': 'TORR',
'public.filename-extension': [ 'torrent' ],
'public.mime-type': 'application/x-bittorrent'
}
}
]
fs.writeFileSync(infoPlistPath, plist.build(infoPlist)) fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
// Copy torrent file icon into app bundle // Copy torrent file icon into app bundle

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')
@@ -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,41 @@ function decreaseVolume () {
} }
} }
function openSubtitles () {
if (windows.main) {
windows.main.send('dispatch', 'openSubtitles')
}
}
function skipForward () {
if (windows.main) {
windows.main.send('dispatch', 'skip', 1)
}
}
function skipBack () {
if (windows.main) {
windows.main.send('dispatch', 'skip', -1)
}
}
function increasePlaybackRate () {
if (windows.main) {
windows.main.send('dispatch', 'changePlaybackRate', 1)
}
}
function decreasePlaybackRate () {
if (windows.main) {
windows.main.send('dispatch', 'changePlaybackRate', -1)
}
}
// Open the preferences window
function showPreferences () {
windows.main.send('dispatch', 'preferences')
}
function onWindowShow () { function onWindowShow () {
log('onWindowShow') log('onWindowShow')
getMenuItem('Full Screen').enabled = true getMenuItem('Full Screen').enabled = true
@@ -103,6 +138,11 @@ 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
getMenuItem('Step Forward').enabled = true
getMenuItem('Step Backward').enabled = true
getMenuItem('Increase Speed').enabled = true
getMenuItem('Decrease Speed').enabled = true
} }
function onPlayerClose () { function onPlayerClose () {
@@ -110,6 +150,11 @@ 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
getMenuItem('Step Forward').enabled = false
getMenuItem('Step Backward').enabled = false
getMenuItem('Increase Speed').enabled = false
getMenuItem('Decrease Speed').enabled = false
} }
function onToggleFullScreen (isFullScreen) { function onToggleFullScreen (isFullScreen) {
@@ -199,7 +244,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',
@@ -229,6 +274,14 @@ function getAppMenuTemplate () {
label: 'Select All', label: 'Select All',
accelerator: 'CmdOrCtrl+A', accelerator: 'CmdOrCtrl+A',
role: 'selectall' role: 'selectall'
},
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => showPreferences()
} }
] ]
}, },
@@ -295,6 +348,44 @@ function getAppMenuTemplate () {
accelerator: 'CmdOrCtrl+Down', accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume, click: decreaseVolume,
enabled: false enabled: false
},
{
type: 'separator'
},
{
label: 'Step Forward',
accelerator: 'CmdOrCtrl+Alt+Right',
click: skipForward,
enabled: false
},
{
label: 'Step Backward',
accelerator: 'CmdOrCtrl+Alt+Left',
click: skipBack,
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Speed',
accelerator: 'CmdOrCtrl+=',
click: increasePlaybackRate,
enabled: false
},
{
label: 'Decrease Speed',
accelerator: 'CmdOrCtrl+-',
click: decreasePlaybackRate,
enabled: false
},
{
type: 'separator'
},
{
label: 'Add Subtitles File...',
click: openSubtitles,
enabled: false
} }
] ]
}, },
@@ -333,6 +424,14 @@ function getAppMenuTemplate () {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => showPreferences()
},
{
type: 'separator'
},
{ {
label: 'Services', label: 'Services',
role: 'services', role: 'services',
@@ -388,7 +487,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.6.0",
"author": { "author": {
"name": "Feross Aboukhadijeh", "name": "WebTorrent, LLC",
"email": "feross@feross.org", "email": "feross@feross.org",
"url": "http://feross.org" "url": "https://webtorrent.io"
}, },
"bin": { "bin": {
"webtorrent-desktop": "./bin/cmd.js" "webtorrent-desktop": "./bin/cmd.js"
@@ -18,26 +18,29 @@
"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",
"vlc-command": "^1.0.1", "vlc-command": "^1.0.1",
"webtorrent": "0.x", "webtorrent": "0.x",
"winreg": "^1.2.0" "winreg": "^1.2.0",
"zero-fill": "^2.2.3"
}, },
"devDependencies": { "devDependencies": {
"cross-zip": "^2.0.1", "cross-zip": "^2.0.1",
@@ -47,7 +50,7 @@
"gh-release": "^2.0.3", "gh-release": "^2.0.3",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"nobin-debian-installer": "^0.0.9", "nobin-debian-installer": "^0.0.10",
"open": "0.0.5", "open": "0.0.5",
"plist": "^1.2.0", "plist": "^1.2.0",
"rimraf": "^2.5.2", "rimraf": "^2.5.2",
@@ -68,7 +71,7 @@
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"optionalDependencies": { "optionalDependencies": {
"appdmg": "^0.3.6" "appdmg": "^0.4.3"
}, },
"productName": "WebTorrent", "productName": "WebTorrent",
"repository": { "repository": {
@@ -80,7 +83,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

@@ -50,19 +50,22 @@ table {
} }
@keyframes fadein { @keyframes fadein {
from { opacity: 0; } from {
to { opacity: 1; } opacity: 0;
}
to {
opacity: 1;
}
} }
.app { .app {
-webkit-user-select: none; -webkit-user-select: none;
-webkit-app-region: drag; -webkit-app-region: drag;
height: 100%; height: 100%;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
animation: fadein 0.3s;
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
animation: fadein 1s;
} }
.app:not(.is-focused) { .app:not(.is-focused) {
@@ -94,11 +97,20 @@ table {
word-wrap: normal; word-wrap: normal;
white-space: nowrap; white-space: nowrap;
direction: ltr; direction: ltr;
opacity: 0.85;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
.icon.disabled {
opacity: 0.3;
}
.icon:not(.disabled):hover {
opacity: 1;
}
/* /*
* UTILITY CLASSES * UTILITY CLASSES
*/ */
@@ -109,18 +121,14 @@ table {
white-space: nowrap; white-space: nowrap;
} }
.disabled { .float-left {
opacity: 0.3; float: left;
} }
.float-right { .float-right {
float: right; float: right;
} }
.expand-collapse {
cursor: pointer;
}
.expand-collapse.expanded::before { .expand-collapse.expanded::before {
content: '▲' content: '▲'
} }
@@ -148,8 +156,8 @@ table {
.header { .header {
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
border-bottom: 1px solid rgb(20, 20, 20); border-bottom: 1px solid rgb(20, 20, 20);
height: 37px; /* vertically center OS menu buttons (OS X) */ height: 38px; /* vertically center OS menu buttons (OS X) */
padding-top: 6px; padding-top: 7px;
overflow: hidden; overflow: hidden;
flex: 0 1 auto; flex: 0 1 auto;
opacity: 1; opacity: 1;
@@ -168,7 +176,13 @@ table {
} }
.app.view-player .header { .app.view-player .header {
opacity: 0.8; background: rgba(40, 40, 40, 0.8);
border-bottom: none;
}
.app.view-player.is-win32 .header,
.app.view-player.is-linux .header {
background: none;
} }
.app.hide-video-controls.view-player .header { .app.hide-video-controls.view-player .header {
@@ -176,12 +190,8 @@ table {
cursor: none; cursor: none;
} }
.app.hide-header .header {
display: none;
}
.header .title { .header .title {
opacity: 0.6; opacity: 0.7;
position: absolute; position: absolute;
margin-top: 1px; margin-top: 1px;
padding: 0 150px 0 150px; padding: 0 150px 0 150px;
@@ -192,35 +202,22 @@ table {
.header .nav { .header .nav {
font-weight: bold; font-weight: bold;
margin-right: 9px;
} }
.header .nav.left { .header .nav.left {
float: left; margin-left: 10px;
}
.header .nav.right {
margin-right: 10px;
} }
.app.is-darwin:not(.is-fullscreen) .header .nav.left { .app.is-darwin:not(.is-fullscreen) .header .nav.left {
margin-left: 78px; margin-left: 78px;
} }
.header .nav.right { .header .back,
float: right; .header .forward {
}
.header .nav * {
opacity: 0.6;
}
.header .nav .disabled {
opacity: 0.1;
}
.header .nav *:not(.disabled):hover {
opacity: 1;
}
.header .nav .back,
.header .nav .forward {
font-size: 30px; font-size: 30px;
margin-top: -3px; margin-top: -3px;
} }
@@ -366,7 +363,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;
} }
@@ -540,6 +536,11 @@ input {
} }
} }
.torrent .buttons .play.resume-position {
position: relative;
-webkit-clip-path: circle(18px at center);
}
.torrent .buttons .delete { .torrent .buttons .delete {
opacity: 0.5; opacity: 0.5;
} }
@@ -548,6 +549,10 @@ input {
opacity: 0.7; opacity: 0.7;
} }
.torrent .buttons .radial-progress {
position: absolute;
}
.torrent .name { .torrent .name {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
@@ -597,7 +602,7 @@ body.drag .app::after {
} }
.torrent-details { .torrent-details {
padding: 8em 20px 20px 20px; padding: 8em 0 20px 0;
} }
.torrent-details table { .torrent-details table {
@@ -611,6 +616,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);
} }
@@ -618,19 +627,26 @@ body.drag .app::after {
.torrent-details td { .torrent-details td {
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
vertical-align: bottom; vertical-align: middle;
} }
.torrent-details td.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: 3em;
padding-left: 16px;
}
.torrent-details td.col-icon .radial-progress {
position: absolute;
margin-top: 4px;
margin-left: 0.5px;
}
.torrent-details td.col-name { .torrent-details td.col-name {
width: auto; width: auto;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -646,6 +662,12 @@ body.drag .app::after {
text-align: right; text-align: right;
} }
.torrent-details td.col-select {
width: 3em;
padding-right: 13px;
text-align: right;
}
/* /*
* PLAYER * PLAYER
*/ */
@@ -674,7 +696,7 @@ body.drag .app::after {
* PLAYER CONTROLS * PLAYER CONTROLS
*/ */
.player-controls { .player .controls {
position: fixed; position: fixed;
background: rgba(40, 40, 40, 0.8); background: rgba(40, 40, 40, 0.8);
width: 100%; width: 100%;
@@ -683,7 +705,63 @@ body.drag .app::after {
transition: opacity 0.15s ease-out; transition: opacity 0.15s ease-out;
} }
.app.hide-video-controls .player-controls { .player .controls .icon {
display: block;
margin: 8px;
font-size: 22px;
opacity: 0.85;
/*
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
max-width: 23px;
overflow: hidden;
}
.player .controls .icon:hover {
opacity: 1;
}
.player .controls .play-pause {
font-size: 28px;
margin-top: 5px;
margin-right: 10px;
margin-left: 15px;
}
.player .controls .volume-slider {
-webkit-appearance: none;
-webkit-app-region: no-drag;
width: 60px;
height: 3px;
margin: 18px 8px 8px 0;
border: none;
padding: 0;
opacity: 0.85;
vertical-align: sub;
}
.player .controls .time,
.player .controls .rate {
font-weight: 100;
font-size: 13px;
margin: 9px 8px 8px 8px;
opacity: 0.8;
}
.player .controls .icon.closed-captions {
font-size: 26px;
margin-top: 6px;
}
.player .controls .icon.fullscreen {
font-size: 26px;
margin-right: 15px;
margin-top: 6px;
}
.app.hide-video-controls .player .controls {
opacity: 0; opacity: 0;
} }
@@ -691,13 +769,16 @@ body.drag .app::after {
cursor: none; cursor: none;
} }
.app.hide-video-controls .player .player-controls:hover { /* TODO: find better way to handle this (that also
* keeps the header visible too).
*/
.app.hide-video-controls .player .controls:hover {
opacity: 1; opacity: 1;
cursor: default; cursor: default;
} }
/* invisible click target for scrubbing */ /* invisible click target for scrubbing */
.player-controls .scrub-bar { .player .controls .scrub-bar {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 23px; /* 3px .loading-bar plus 10px above and below */ height: 23px; /* 3px .loading-bar plus 10px above and below */
@@ -706,7 +787,7 @@ body.drag .app::after {
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
} }
.player-controls .loading-bar { .player .controls .loading-bar {
position: relative; position: relative;
width: 100%; width: 100%;
top: -3px; top: -3px;
@@ -716,14 +797,14 @@ body.drag .app::after {
position: absolute; position: absolute;
} }
.player-controls .loading-bar-part { .player .controls .loading-bar-part {
position: absolute; position: absolute;
background-color: #dd0000; background-color: #dd0000;
top: 0; top: 0;
height: 100%; height: 100%;
} }
.player-controls .playback-cursor { .player .controls .playback-cursor {
position: absolute; position: absolute;
top: -3px; top: -3px;
background-color: #FFF; background-color: #FFF;
@@ -732,94 +813,26 @@ body.drag .app::after {
border-radius: 50%; border-radius: 50%;
margin-top: 0; margin-top: 0;
margin-left: 0; margin-left: 0;
transition-property: width, height, border-radius, margin-top, margin-left; transition-property: width, height, top, margin-left;
transition-duration: 0.1s; transition-duration: 0.1s;
transition-timing-function: ease-out; transition-timing-function: ease-out;
} }
.player-controls .play-pause { .player .controls .closed-captions.active,
display: block; .player .controls .device.active {
width: 30px;
height: 30px;
padding: 5px;
margin: 0 auto;
}
.player-controls .device,
.player-controls .fullscreen,
.player-controls .closed-captions,
.player-controls .volume-icon,
.player-controls .back {
display: block;
height: 20px;
margin: 5px;
/*
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
max-width: 22px;
overflow: hidden;
}
.player-controls .volume,
.player-controls .back {
float: left;
}
.player-controls .device,
.player-controls .closed-captions,
.player-controls .fullscreen {
float: right;
}
.player-controls .fullscreen {
margin-right: 15px;
}
.player-controls .volume-icon,
.player-controls .device {
font-size: 18px; /* make the cast icons less huge */
margin-top: 8px !important;
}
.player-controls .closed-captions.active,
.player-controls .device.active {
color: #9af; color: #9af;
} }
.player-controls .volume { .player .controls .volume-slider::-webkit-slider-thumb {
display: block;
width: 90px;
}
.player-controls .volume-icon {
float: left;
margin-right: 0px;
}
.player-controls .volume-slider {
-webkit-appearance: none; -webkit-appearance: none;
width: 50px;
height: 3px;
border: none;
padding: 0;
vertical-align: sub;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
}
.player-controls .volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: #fff; background-color: #fff;
opacity: 1.0; width: 13px;
width: 10px; height: 13px;
height: 10px;
border: 1px solid #303233;
border-radius: 50%; border-radius: 50%;
-webkit-app-region: no-drag;
} }
.player-controls .volume-slider:focus { .player .controls .volume-slider:focus {
outline: none; outline: none;
} }
@@ -829,19 +842,27 @@ body.drag .app::after {
.player .playback-bar:hover .playback-cursor { .player .playback-bar:hover .playback-cursor {
top: -8px; top: -8px;
margin-left: -5px;
width: 14px; width: 14px;
height: 14px; height: 14px;
} }
/**
* Set the cue text position so it appears above the player controls.
*/
video::-webkit-media-text-track-container {
bottom: 60px;
transition: bottom 0.1s ease-out;
}
.app.hide-video-controls video::-webkit-media-text-track-container {
bottom: 30px;
}
::cue { ::cue {
background: none; background: none;
color: #FFF; color: #FFF;
font: 24px;
line-height: 1.3em;
text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0; text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0;
} }
/* /*
* CHROMECAST / AIRPLAY CONTROLS * CHROMECAST / AIRPLAY CONTROLS
*/ */
@@ -891,6 +912,173 @@ body.drag .app::after {
margin-right: 4px !important; margin-right: 4px !important;
} }
/*
* Preferences page, based on Atom settings style
*/
.preferences {
font-size: 12px;
line-height: calc(10/7);
}
.preferences .text {
color: #a8a8a8;
}
.preferences .icon {
color: rgba(170, 170, 170, 0.6);
font-size: 16px;
margin-right: 0.2em;
}
.preferences .btn {
display: inline-block;
-webkit-appearance: button;
margin: 0;
font-weight: normal;
text-align: center;
vertical-align: middle;
border-color: #cccccc;
border-radius: 3px;
color: #9da5b4;
text-shadow: none;
border: 1px solid #181a1f;
background-color: #3d3d3d;
white-space: initial;
font-size: 0.889em;
line-height: 1;
padding: 0.5em 0.75em;
}
.preferences .btn .icon {
margin: 0;
color: #a8a8a8;
}
.preferences .help .icon {
vertical-align: sub;
}
.preferences .preferences-panel .control-group + .control-group {
margin-top: 1.5em;
}
.preferences .section {
padding: 20px;
border-top: 1px solid #181a1f;
}
.preferences .section:first {
border-top: 0px;
}
.preferences .section:first-child,
.preferences .section:last-child {
padding: 20px;
}
.preferences .section.section:empty {
padding: 0;
border-top: none;
}
.preferences .section-container {
width: 100%;
max-width: 800px;
}
.preferences section .section-heading,
.preferences .section .section-heading {
margin-bottom: 10px;
color: #dcdcdc;
font-size: 1.75em;
font-weight: bold;
line-height: 1;
-webkit-user-select: none;
cursor: default;
}
.preferences .sub-section-heading.icon:before,
.preferences .section-heading.icon:before {
margin-right: 8px;
}
.preferences .section-heading-count {
margin-left: .5em;
}
.preferences .section-body {
margin-top: 20px;
}
.preferences .sub-section {
margin: 20px 0;
}
.preferences .sub-section .sub-section-heading {
color: #dcdcdc;
font-size: 1.4em;
font-weight: bold;
line-height: 1;
-webkit-user-select: none;
}
.preferences .preferences-panel label {
color: #a8a8a8;
}
.preferences .preferences-panel .control-group + .control-group {
margin-top: 1.5em;
}
.preferences .preferences-panel .control-group .editor-container {
margin: 0;
}
.preferences .preference-title {
font-size: 1.2em;
-webkit-user-select: none;
display: inline-block;
}
.preferences .preference-description {
color: rgba(170, 170, 170, 0.6);
-webkit-user-select: none;
cursor: default;
}
.preferences input {
font-size: 1.1em;
line-height: 1.15em;
max-height: none;
width: 100%;
padding-left: 0.5em;
border-radius: 3px;
color: #a8a8a8;
border: 1px solid #181a1f;
background-color: #1b1d23;
}
.preferences input::-webkit-input-placeholder {
color: rgba(170, 170, 170, 0.6);
}
.preferences .control-group input {
margin-top: 0.2em;
}
.preferences .control-group input.file-picker-text {
width: calc(100% - 40px);
}
.preferences .control-group .checkbox .icon {
font-size: 1.5em;
margin: 0;
vertical-align: text-bottom;
}
/* /*
* MEDIA OVERLAY / AUDIO DETAILS * MEDIA OVERLAY / AUDIO DETAILS
*/ */
@@ -959,10 +1147,6 @@ body.drag .app::after {
z-index: 1; z-index: 1;
} }
.app.hide-header .error-popover {
top: 0px;
}
.error-popover.hidden { .error-popover.hidden {
display: none; display: none;
} }
@@ -990,3 +1174,66 @@ body.drag .app::after {
.error-text { .error-text {
color: #c44; color: #c44;
} }
/*
* RADIAL PROGRESS BAR
*/
.radial-progress {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #888;
}
.radial-progress .circle .mask,
.radial-progress .circle .fill {
width: 16px;
height: 16px;
position: absolute;
border-radius: 50%;
-webkit-backface-visibility: hidden;
}
.radial-progress .circle .mask {
clip: rect(0px, 16px, 16px, 8px);
}
.radial-progress .circle .fill {
clip: rect(0px, 8px, 16px, 0px);
background-color: white;
}
.radial-progress .inset {
position: absolute;
width: 12px;
height: 12px;
margin: 2px 0 0 2px;
border-radius: 50%;
background-color: black;
}
.radial-progress-large {
width: 40px;
height: 40px;
}
.radial-progress-large .circle .mask,
.radial-progress-large .circle .fill {
width: 40px;
height: 40px;
}
.radial-progress-large .circle .mask {
clip: rect(0px, 40px, 40px, 20px);
}
.radial-progress-large .circle .fill {
clip: rect(0px, 20px, 40px, 0px);
background-color: white;
}
.radial-progress-large .inset {
width: 32px;
height: 32px;
margin: 4px 0 0 4px;
}

View File

@@ -14,10 +14,10 @@ var ipcRenderer = electron.ipcRenderer
setupIpc() setupIpc()
var appConfig = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
var concat = require('concat-stream')
var dragDrop = require('drag-drop') var dragDrop = require('drag-drop')
var fs = require('fs-extra') var fs = require('fs-extra')
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')
@@ -40,15 +40,29 @@ appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
// This dependency is the slowest-loading, so we lazy load it // This dependency is the slowest-loading, so we lazy load it
var Cast = null var Cast = null
// For easy debugging in Developer Tools
var state = global.state = State.getInitialState()
var vdomLoop var vdomLoop
var state = State.getInitialState()
state.location.go({ url: 'home' }) // Add first page to location history
// All state lives in state.js. `state.saved` is read from and written to a file. // All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app. // All other state is ephemeral. First we load state.saved then initialize the app.
loadState(init) loadState(init)
function loadState (cb) {
appConfig.read(function (err, data) {
if (err) console.error(err)
// populate defaults if they're not there
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
state.saved.torrents.forEach(function (torrentSummary) {
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
})
if (cb) cb()
})
}
/** /**
* Called once when the application loads. (Not once per window.) * Called once when the application loads. (Not once per window.)
* Connects to the torrent networks, sets up the UI and OS integrations like * Connects to the torrent networks, sets up the UI and OS integrations like
@@ -58,9 +72,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 +96,7 @@ function init () {
// OS integrations: // OS integrations:
// ...drag and drop a torrent or video file to play or seed // ...drag and drop a torrent or video file to play or seed
dragDrop('body', (files) => dispatch('onOpen', files)) dragDrop('body', onOpen)
// ...same thing if you paste a torrent // ...same thing if you paste a torrent
document.addEventListener('paste', onPaste) document.addEventListener('paste', onPaste)
@@ -151,6 +162,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)
}
}) })
} }
@@ -176,7 +192,7 @@ function render (state) {
// Calls render() to go from state -> UI, then applies to vdom to the real DOM. // Calls render() to go from state -> UI, then applies to vdom to the real DOM.
function update () { function update () {
showOrHidePlayerControls() showOrHidePlayerControls()
vdomLoop.update(state) if (vdomLoop) vdomLoop.update(state)
updateElectron() updateElectron()
} }
@@ -229,6 +245,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 +261,7 @@ function dispatch (action, ...args) {
setDimensions(args[0] /* dimensions */) setDimensions(args[0] /* dimensions */)
} }
if (action === 'backToList') { if (action === 'backToList') {
// Exit any modals and screens with a back button backToList()
state.modal = null
while (state.location.hasBack()) state.location.back()
// Work around virtual-dom issue: it doesn't expose its redraw function,
// and only redraws on requestAnimationFrame(). That means when the user
// closes the window (hide window / minimize to tray) and we want to pause
// the video, we update the vdom but it keeps playing until you reopen!
var mediaTag = document.querySelector('video,audio')
if (mediaTag) mediaTag.pause()
} }
if (action === 'escapeBack') { if (action === 'escapeBack') {
if (state.modal) { if (state.modal) {
@@ -272,19 +282,17 @@ function dispatch (action, ...args) {
playPause() playPause()
} }
if (action === 'play') { if (action === 'play') {
if (state.location.pending()) return playFile(args[0] /* infoHash */, args[1] /* index */)
state.location.go({
url: 'player',
onbeforeload: function (cb) {
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
},
onbeforeunload: closePlayer
})
play()
} }
if (action === 'playbackJump') { if (action === 'playbackJump') {
jumpToTime(args[0] /* seconds */) jumpToTime(args[0] /* seconds */)
} }
if (action === 'skip') {
jumpToTime(state.playing.currentTime + (args[0] /* direction */ * 10))
}
if (action === 'changePlaybackRate') {
changePlaybackRate(args[0] /* direction */)
}
if (action === 'changeVolume') { if (action === 'changeVolume') {
changeVolume(args[0] /* increase */) changeVolume(args[0] /* increase */)
} }
@@ -295,16 +303,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) {
@@ -338,6 +346,26 @@ function dispatch (action, ...args) {
if (action === 'exitModal') { if (action === 'exitModal') {
state.modal = null state.modal = null
} }
if (action === 'preferences') {
state.location.go({
url: 'preferences',
onbeforeload: function (cb) {
// initialize preferences
state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
cb()
},
onbeforeunload: function (cb) {
// save state after preferences
savePreferences()
state.window.title = config.APP_WINDOW_TITLE
cb()
}
})
}
if (action === 'updatePreferences') {
updatePreferences(args[0], args[1] /* property, value */)
}
if (action === 'updateAvailable') { if (action === 'updateAvailable') {
updateAvailable(args[0] /* version */) updateAvailable(args[0] /* version */)
} }
@@ -384,7 +412,7 @@ function pause () {
} }
function playPause () { function playPause () {
if (state.location.current().url !== 'player') return if (state.location.url() !== 'player') return
if (state.playing.isPaused) { if (state.playing.isPaused) {
play() play()
} else { } else {
@@ -399,7 +427,26 @@ function jumpToTime (time) {
state.playing.jumpToTime = time state.playing.jumpToTime = time
} }
} }
function changePlaybackRate (direction) {
var rate = state.playing.playbackRate
if (direction > 0 && rate >= 0.25 && rate < 2) {
rate += 0.25
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
rate -= 0.25
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
rate = -1
} else if (direction > 0 && rate === -1) {
rate = 0.25
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
rate *= 2
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
rate /= 2
}
state.playing.playbackRate = rate
if (lazyLoadCast().isCasting() && !Cast.setRate(rate)) {
state.playing.playbackRate = 1
}
}
function changeVolume (delta) { function changeVolume (delta) {
// change volume with delta value // change volume with delta value
setVolume(state.playing.volume + delta) setVolume(state.playing.volume + delta)
@@ -422,7 +469,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()
}) })
} }
@@ -471,22 +536,6 @@ function setupIpc () {
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args)) ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
} }
// Load state.saved from the JSON state file
function loadState (cb) {
appConfig.read(function (err, data) {
if (err) console.error(err)
console.log('loaded state from ' + appConfig.filePath)
// populate defaults if they're not there
state.saved = Object.assign({}, State.getDefaultSavedState(), data)
state.saved.torrents.forEach(function (torrentSummary) {
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
})
if (cb) cb()
})
}
// Starts all torrents that aren't paused on program startup // Starts all torrents that aren't paused on program startup
function resumeTorrents () { function resumeTorrents () {
state.saved.torrents state.saved.torrents
@@ -494,6 +543,27 @@ function resumeTorrents () {
.forEach((x) => startTorrentingSummary(x)) .forEach((x) => startTorrentingSummary(x))
} }
// Updates a single property in the UNSAVED prefs
// For example: updatePreferences("foo.bar", "baz")
// Call savePreferences to save to config.json
function updatePreferences (property, value) {
var path = property.split('.')
var key = state.unsaved.prefs
for (var i = 0; i < path.length - 1; i++) {
if (typeof key[path[i]] === 'undefined') {
key[path[i]] = {}
}
key = key[path[i]]
}
key[path[i]] = value
}
// All unsaved prefs take effect atomically, and are saved to config.json
function savePreferences () {
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
saveState()
}
// Don't write state.saved to file more than once a second // Don't write state.saved to file more than once a second
function saveStateThrottled () { function saveStateThrottled () {
if (state.saveStateTimeout) return if (state.saveStateTimeout) return
@@ -519,7 +589,7 @@ function saveState () {
if (key === 'progress' || key === 'torrentKey') { if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process continue // Don't save progress info or key for the webtorrent process
} }
if (key === 'playStatus' && x.playStatus !== 'unplayable') { if (key === 'playStatus') {
continue // Don't save whether a torrent is playing / pending continue // Don't save whether a torrent is playing / pending
} }
torrent[key] = x[key] torrent[key] = x[key]
@@ -536,26 +606,32 @@ function saveState () {
update() update()
} }
// Called when the user drag-drops files onto the app
function onOpen (files) { function onOpen (files) {
if (!Array.isArray(files)) files = [ files ] if (!Array.isArray(files)) files = [ files ]
// In the player, the only drag-drop function is adding subtitles if (state.modal) {
var isInPlayer = state.location.current().url === 'player' state.modal = null
if (isInPlayer) {
return 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) {
@@ -581,60 +657,122 @@ function getTorrentSummary (torrentKey) {
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a // Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent- // magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
var instantIoRegex = /^(https:\/\/)?instant\.io\/#/
function addTorrent (torrentId) { function addTorrent (torrentId) {
backToList()
var torrentKey = state.nextTorrentKey++ var torrentKey = state.nextTorrentKey++
var path = state.saved.downloadPath var path = state.saved.prefs.downloadPath
if (torrentId.path) { if (torrentId.path) {
// Use path string instead of W3C File object // Use path string instead of W3C File object
torrentId = torrentId.path torrentId = torrentId.path
} }
// Allow a instant.io link to be pasted
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
}
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path) ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
} }
function addSubtitle (file) { function addSubtitles (files, autoSelect) {
var srtToVtt = require('srt-to-vtt') // Subtitles are only supported while playing video
var LanguageDetect = require('languagedetect')
if (state.playing.type !== 'video') return if (state.playing.type !== 'video') return
fs.createReadStream(file.path || file).pipe(srtToVtt()).pipe(concat(function (buf) {
// Set the cue text position so it appears above the player controls. // Read the files concurrently, then add all resulting subtitle tracks
// The only way to change cue text position is by modifying the VTT. It is not var jobs = files.map((file) => (cb) => loadSubtitle(file, cb))
// possible via CSS. parallel(jobs, function (err, tracks) {
var langDetected = (new LanguageDetect()).detect(buf.toString().replace(/(.*-->.*)/g, ''), 2) 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 concat = require('simple-concat')
var LanguageDetect = require('languagedetect')
var srtToVtt = require('srt-to-vtt')
// Read the .SRT or .VTT file, parse it, add subtitle track
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.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1) langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
var track = { var track = {
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'), buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected,
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 iso639 = require('iso-639-1')
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
@@ -645,7 +783,7 @@ function startTorrentingSummary (torrentSummary) {
if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++ if (!s.torrentKey) s.torrentKey = state.nextTorrentKey++
// Use Downloads folder by default // Use Downloads folder by default
var path = s.path || state.saved.downloadPath var path = s.path || state.saved.prefs.downloadPath
var torrentID var torrentID
if (s.torrentFileName) { // Load torrent file from disk if (s.torrentFileName) { // Load torrent file from disk
@@ -655,7 +793,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 +804,6 @@ function startTorrentingSummary (torrentSummary) {
// Shows the Create Torrent page with options to seed a given file or folder // Shows the Create Torrent page with options to seed a given file or folder
function showCreateTorrent (files) { function showCreateTorrent (files) {
if (Array.isArray(files)) { if (Array.isArray(files)) {
if (state.location.pending() || state.location.current().url !== 'home') return
state.location.go({ state.location.go({
url: 'create-torrent', url: 'create-torrent',
files: files files: files
@@ -716,6 +853,9 @@ function findFilesRecursive (fileOrFolder, cb) {
function createTorrent (options) { function createTorrent (options) {
var torrentKey = state.nextTorrentKey++ var torrentKey = state.nextTorrentKey++
ipcRenderer.send('wt-create-torrent', torrentKey, options) ipcRenderer.send('wt-create-torrent', torrentKey, options)
state.location.backToFirst(function () {
state.location.clearForward('create-torrent')
})
} }
function torrentInfoHash (torrentKey, infoHash) { function torrentInfoHash (torrentKey, infoHash) {
@@ -728,7 +868,7 @@ function torrentInfoHash (torrentKey, infoHash) {
torrentKey: torrentKey, torrentKey: torrentKey,
status: 'new' status: 'new'
} }
state.saved.torrents.push(torrentSummary) state.saved.torrents.unshift(torrentSummary)
sound.play('ADD') sound.play('ADD')
} }
@@ -762,8 +902,17 @@ function torrentMetadata (torrentKey, torrentInfo) {
torrentSummary.status = 'downloading' torrentSummary.status = 'downloading'
torrentSummary.name = torrentSummary.displayName || torrentInfo.name torrentSummary.name = torrentSummary.displayName || torrentInfo.name
torrentSummary.path = torrentInfo.path torrentSummary.path = torrentInfo.path
torrentSummary.files = torrentInfo.files
torrentSummary.magnetURI = torrentInfo.magnetURI torrentSummary.magnetURI = torrentInfo.magnetURI
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
// For now, check whether torrentSummary.files has already been set:
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
if (!hasDetailedFileInfo) {
torrentSummary.files = torrentInfo.files
}
if (!torrentSummary.selections) {
torrentSummary.selections = torrentSummary.files.map((x) => true)
}
torrentSummary.defaultPlayFileIndex = pickFileToPlay(torrentInfo.files)
update() update()
// Save the .torrent file, if it hasn't been saved already // Save the .torrent file, if it hasn't been saved already
@@ -815,6 +964,8 @@ function torrentProgress (progressInfo) {
torrentSummary.progress = p torrentSummary.progress = p
}) })
checkForSubtitles()
update() update()
} }
@@ -870,11 +1021,25 @@ function pickFileToPlay (files) {
return undefined return undefined
} }
// Opens the video player function playFile (infoHash, index) {
state.location.go({
url: 'player',
onbeforeload: function (cb) {
play()
openPlayer(infoHash, index, cb)
},
onbeforeunload: closePlayer
}, function (err) {
if (err) onError(err)
})
}
// Opens the video player to a specific torrent
function openPlayer (infoHash, index, cb) { function openPlayer (infoHash, index, cb) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
// automatically choose which file in the torrent to play, if necessary // automatically choose which file in the torrent to play, if necessary
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
if (index === undefined) index = pickFileToPlay(torrentSummary.files) if (index === undefined) index = pickFileToPlay(torrentSummary.files)
if (index === undefined) return cb(new errors.UnplayableError()) if (index === undefined) return cb(new errors.UnplayableError())
@@ -886,7 +1051,7 @@ function openPlayer (infoHash, index, cb) {
var timeout = setTimeout(function () { var timeout = setTimeout(function () {
torrentSummary.playStatus = 'timeout' /* no seeders available? */ torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR') sound.play('ERROR')
cb(new Error('playback timed out')) cb(new Error('Playback timed out. Try again.'))
update() update()
}, 10000) /* give it a few seconds */ }, 10000) /* give it a few seconds */
@@ -909,11 +1074,23 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
: TorrentPlayer.isAudio(fileSummary) ? 'audio' : TorrentPlayer.isAudio(fileSummary) ? 'audio'
: 'other' : 'other'
// pick up where we left off
if (fileSummary.currentTime) {
var fraction = fileSummary.currentTime / fileSummary.duration
var secondsLeft = fileSummary.duration - fileSummary.currentTime
if (fraction < 0.9 && secondsLeft > 10) {
state.playing.jumpToTime = fileSummary.currentTime
}
}
// if it's audio, parse out the metadata (artist, title, etc) // if it's audio, parse out the metadata (artist, title, etc)
if (state.playing.type === 'audio' && !fileSummary.audioInfo) { if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index) ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
} }
// 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 +1166,7 @@ function deleteTorrent (infoHash) {
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash) var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) state.saved.torrents.splice(index, 1) if (index > -1) state.saved.torrents.splice(index, 1)
saveStateThrottled() saveStateThrottled()
state.location.clearForward() // prevent user from going forward to a deleted torrent state.location.clearForward('player') // prevent user from going forward to a deleted torrent
sound.play('DELETE') sound.play('DELETE')
} }
@@ -999,6 +1176,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()
@@ -1047,7 +1232,7 @@ function saveTorrentFileAs (torrentSummary) {
var newFileName = `${path.parse(torrentSummary.name).name}.torrent` var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
var opts = { var opts = {
title: 'Save Torrent File', title: 'Save Torrent File',
defaultPath: path.join(state.saved.downloadPath, newFileName), defaultPath: path.join(state.saved.prefs.downloadPath, newFileName),
filters: [ filters: [
{ name: 'Torrent Files', extensions: ['torrent'] }, { name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] } { name: 'All Files', extensions: ['*'] }
@@ -1129,7 +1314,7 @@ function showDoneNotification (torrent) {
// * The video is paused // * The video is paused
// * The video is playing remotely on Chromecast or Airplay // * The video is playing remotely on Chromecast or Airplay
function showOrHidePlayerControls () { function showOrHidePlayerControls () {
var hideControls = state.location.current().url === 'player' && var hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 && state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 && new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused && !state.playing.isPaused &&
@@ -1164,8 +1349,10 @@ function onPaste (e) {
torrentIds.forEach(function (torrentId) { torrentIds.forEach(function (torrentId) {
torrentId = torrentId.trim() torrentId = torrentId.trim()
if (torrentId.length === 0) return if (torrentId.length === 0) return
dispatch('addTorrent', torrentId) addTorrent(torrentId)
}) })
update()
} }
function onFocus (e) { function onFocus (e) {

View File

@@ -8,7 +8,8 @@ module.exports = {
play, play,
pause, pause,
seek, seek,
setVolume setVolume,
setRate
} }
var airplay = require('airplay-js') var airplay = require('airplay-js')
@@ -344,6 +345,22 @@ function pause () {
} }
} }
function setRate (rate) {
var device
var result = true
if (state.playing.location === 'chromecast') {
// TODO find how to control playback rate on chromecast
castCallback()
result = false
} else if (state.playing.location === 'airplay') {
device = state.devices.airplay
device.rate(rate, castCallback)
} else {
result = false
}
return result
}
function seek (time) { function seek (time) {
var device = getDevice() var device = getDevice()
if (device) { if (device) {

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

@@ -4,12 +4,18 @@ var captureVideoFrame = require('./capture-video-frame')
var path = require('path') var path = require('path')
function torrentPoster (torrent, cb) { function torrentPoster (torrent, cb) {
// First, try to use the largest video file // First, try to use a poster image if available
var posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
// Second, try to use the largest video file
// Filter out file formats that the <video> tag definitely can't play // Filter out file formats that the <video> tag definitely can't play
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv']) var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb) if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
// Second, try to use the largest image file // Third, try to use the largest image file
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png']) var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb) if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)

View File

@@ -57,7 +57,13 @@ function getInitialState () {
* *
* Also accessible via `require('application-config')('WebTorrent').filePath` * Also accessible via `require('application-config')('WebTorrent').filePath`
*/ */
saved: {} saved: {},
/*
* Getters, for convenience
*/
getPlayingTorrentSummary,
getPlayingFileSummary
} }
} }
@@ -74,9 +80,11 @@ function getDefaultPlayState () {
isStalled: false, isStalled: false,
lastTimeUpdate: 0, /* Unix time in ms */ lastTimeUpdate: 0, /* Unix time in ms */
mouseStationarySince: 0, /* Unix time in ms */ mouseStationarySince: 0, /* Unix time in ms */
playbackRate: 1,
subtitles: { subtitles: {
tracks: [], /* 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 */
} }
@@ -258,8 +266,21 @@ function getDefaultSavedState () {
] ]
} }
], ],
downloadPath: config.IS_PORTABLE prefs: {
? path.join(config.CONFIG_PATH, 'Downloads') downloadPath: config.IS_PORTABLE
: remote.app.getPath('downloads') ? path.join(config.CONFIG_PATH, 'Downloads')
: remote.app.getPath('downloads')
}
} }
} }
function getPlayingTorrentSummary () {
var infoHash = this.playing.infoHash
return this.saved.torrents.find((x) => x.infoHash === infoHash)
}
function getPlayingFileSummary () {
var torrentSummary = this.getPlayingTorrentSummary()
if (!torrentSummary) return null
return torrentSummary.files[this.playing.fileIndex]
}

View File

@@ -8,7 +8,8 @@ var Header = require('./header')
var Views = { var Views = {
'home': require('./torrent-list'), 'home': require('./torrent-list'),
'player': require('./player'), 'player': require('./player'),
'create-torrent': require('./create-torrent-page') 'create-torrent': require('./create-torrent-page'),
'preferences': require('./preferences')
} }
var Modals = { var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal'), 'open-torrent-address-modal': require('./open-torrent-address-modal'),
@@ -22,24 +23,20 @@ function App (state) {
// * The mouse is over the controls or we're scrubbing (see CSS) // * The mouse is over the controls or we're scrubbing (see CSS)
// * The video is paused // * The video is paused
// * The video is playing remotely on Chromecast or Airplay // * The video is playing remotely on Chromecast or Airplay
var hideControls = state.location.current().url === 'player' && var hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 && state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 && new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused && !state.playing.isPaused &&
state.playing.location === 'local' state.playing.location === 'local' &&
state.playing.playbackRate === 1
// Hide the header on Windows/Linux when in the player
// On OSX, the header appears as part of the title bar
var hideHeader = process.platform !== 'darwin' && state.location.current().url === 'player'
var cls = [ var cls = [
'view-' + state.location.current().url, /* e.g. view-home, view-player */ 'view-' + state.location.url(), /* e.g. view-home, view-player */
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */ 'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
] ]
if (state.window.isFullScreen) cls.push('is-fullscreen') if (state.window.isFullScreen) cls.push('is-fullscreen')
if (state.window.isFocused) cls.push('is-focused') if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls') if (hideControls) cls.push('hide-video-controls')
if (hideHeader) cls.push('hide-header')
return hx` return hx`
<div class='app ${cls.join(' ')}'> <div class='app ${cls.join(' ')}'>
@@ -54,12 +51,13 @@ function App (state) {
function getErrorPopover (state) { function getErrorPopover (state) {
var now = new Date().getTime() var now = new Date().getTime()
var recentErrors = state.errors.filter((x) => now - x.time < 5000) var recentErrors = state.errors.filter((x) => now - x.time < 5000)
var hasErrors = recentErrors.length > 0
var errorElems = recentErrors.map(function (error) { var errorElems = recentErrors.map(function (error) {
return hx`<div class='error'>${error.message}</div>` return hx`<div class='error'>${error.message}</div>`
}) })
return hx` return hx`
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'> <div class='error-popover ${hasErrors ? 'visible' : 'hidden'}'>
<div class='title'>Error</div> <div class='title'>Error</div>
${errorElems} ${errorElems}
</div> </div>
@@ -80,6 +78,6 @@ function getModal (state) {
} }
function getView (state) { function getView (state) {
var url = state.location.current().url var url = state.location.url()
return Views[url](state) return Views[url](state)
} }

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

@@ -10,7 +10,7 @@ function Header (state) {
return hx` return hx`
<div class='header'> <div class='header'>
${getTitle()} ${getTitle()}
<div class='nav left'> <div class='nav left float-left'>
<i.icon.back <i.icon.back
class=${state.location.hasBack() ? '' : 'disabled'} class=${state.location.hasBack() ? '' : 'disabled'}
title='Back' title='Back'
@@ -24,7 +24,7 @@ function Header (state) {
chevron_right chevron_right
</i> </i>
</div> </div>
<div class='nav right'> <div class='nav right float-right'>
${getAddButton()} ${getAddButton()}
</div> </div>
</div> </div>
@@ -37,7 +37,7 @@ function Header (state) {
} }
function getAddButton () { function getAddButton () {
if (state.location.current().url !== 'player') { if (state.location.url() === 'home') {
return hx` return hx`
<i <i
class='icon add' class='icon add'

View File

@@ -4,8 +4,9 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx') var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var prettyBytes = require('prettier-bytes')
var Bitfield = require('bitfield') var Bitfield = require('bitfield')
var prettyBytes = require('prettier-bytes')
var zeroFill = require('zero-fill')
var TorrentSummary = require('../lib/torrent-summary') var TorrentSummary = require('../lib/torrent-summary')
var {dispatch, dispatcher} = require('../lib/dispatcher') var {dispatch, dispatcher} = require('../lib/dispatcher')
@@ -48,36 +49,37 @@ function renderMedia (state) {
mediaElement.currentTime = state.playing.jumpToTime mediaElement.currentTime = state.playing.jumpToTime
state.playing.jumpToTime = null state.playing.jumpToTime = null
} }
if (state.playing.playbackRate !== mediaElement.playbackRate) {
mediaElement.playbackRate = state.playing.playbackRate
}
// Set volume // Set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) { if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
mediaElement.volume = state.playing.setVolume mediaElement.volume = state.playing.setVolume
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 // Save video position
state.playing.duration = mediaElement.duration var file = state.getPlayingFileSummary()
file.currentTime = state.playing.currentTime = mediaElement.currentTime
file.duration = state.playing.duration = mediaElement.duration
state.playing.volume = mediaElement.volume state.playing.volume = mediaElement.volume
} }
// 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}>
@@ -129,12 +131,13 @@ function renderMedia (state) {
} }
function onCanPlay (e) { function onCanPlay (e) {
var video = e.target var elem = e.target
if (video.webkitVideoDecodedByteCount > 0 && if (state.playing.type === 'video' && elem.webkitVideoDecodedByteCount === 0) {
video.webkitAudioDecodedByteCount === 0) { dispatch('mediaError', 'Video codec unsupported')
} else if (elem.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported') dispatch('mediaError', 'Audio codec unsupported')
} else { } else {
video.play() elem.play()
} }
} }
} }
@@ -165,8 +168,7 @@ function renderOverlay (state) {
} }
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
var torrentSummary = getPlayingTorrentSummary(state) var fileSummary = state.getPlayingFileSummary()
var fileSummary = torrentSummary.files[state.playing.fileIndex]
if (!fileSummary.audioInfo) return if (!fileSummary.audioInfo) return
var info = fileSummary.audioInfo var info = fileSummary.audioInfo
@@ -204,7 +206,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 +272,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>
@@ -294,10 +298,10 @@ function renderSubtitlesOptions (state) {
function renderPlayerControls (state) { function renderPlayerControls (state) {
var positionPercent = 100 * state.playing.currentTime / state.playing.duration var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' } var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0 var captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled' ? 'disabled'
: state.playing.subtitles.enabled : state.playing.subtitles.selectedIndex >= 0
? 'active' ? 'active'
: '' : ''
@@ -305,15 +309,27 @@ function renderPlayerControls (state) {
hx` hx`
<div class='playback-bar'> <div class='playback-bar'>
${renderLoadingBar(state)} ${renderLoadingBar(state)}
<div class='playback-cursor' style=${playbackCursorStyle}></div> <div
<div class='scrub-bar' class='playback-cursor'
style=${playbackCursorStyle}>
</div>
<div
class='scrub-bar'
draggable='true' draggable='true'
ondragstart=${handleDragStart}
onclick=${handleScrub}, onclick=${handleScrub},
ondrag=${handleScrub}></div> ondrag=${handleScrub}>
</div>
</div> </div>
`, `,
hx` hx`
<i class='icon fullscreen' <i class='icon play-pause float-left' onclick=${dispatcher('playPause')}>
${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>
`,
hx`
<i
class='icon fullscreen float-right'
onclick=${dispatcher('toggleFullScreen')}> onclick=${dispatcher('toggleFullScreen')}>
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'} ${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i> </i>
@@ -323,7 +339,7 @@ function renderPlayerControls (state) {
if (state.playing.type === 'video') { if (state.playing.type === 'video') {
// show closed captions icon // show closed captions icon
elements.push(hx` elements.push(hx`
<i.icon.closed-captions <i.icon.closed-captions.float-right
class=${captionsClass} class=${captionsClass}
onclick=${handleSubtitles}> onclick=${handleSubtitles}>
closed_captions closed_captions
@@ -368,7 +384,7 @@ function renderPlayerControls (state) {
if (state.devices.chromecast || isOnChromecast) { if (state.devices.chromecast || isOnChromecast) {
var castIcon = isOnChromecast ? 'cast_connected' : 'cast' var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
elements.push(hx` elements.push(hx`
<i.icon.device <i.icon.device.float-right
class=${chromecastClass} class=${chromecastClass}
onclick=${chromecastHandler}> onclick=${chromecastHandler}>
${castIcon} ${castIcon}
@@ -377,7 +393,7 @@ function renderPlayerControls (state) {
} }
if (state.devices.airplay || isOnAirplay) { if (state.devices.airplay || isOnAirplay) {
elements.push(hx` elements.push(hx`
<i.icon.device <i.icon.device.float-right
class=${airplayClass} class=${airplayClass}
onclick=${airplayHandler}> onclick=${airplayHandler}>
airplay airplay
@@ -386,7 +402,8 @@ function renderPlayerControls (state) {
} }
if (state.devices.dlna || isOnDlna) { if (state.devices.dlna || isOnDlna) {
elements.push(hx` elements.push(hx`
<i.icon.device <i
class='icon device float-right'
class=${dlnaClass} class=${dlnaClass}
onclick=${dlnaHandler}> onclick=${dlnaHandler}>
tv tv
@@ -394,17 +411,6 @@ function renderPlayerControls (state) {
`) `)
} }
// On OSX, the back button is in the title bar of the window; see app.js
// On other platforms, we render one over the video on mouseover
if (process.platform !== 'darwin') {
elements.push(hx`
<i.icon.back
onclick=${dispatcher('back')}>
chevron_left
</i>
`)
}
// render volume // render volume
var volume = state.playing.volume var volume = state.playing.volume
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up') var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
@@ -414,34 +420,54 @@ function renderPlayerControls (state) {
} }
elements.push(hx` elements.push(hx`
<div.volume> <div class='volume float-left'>
<i.icon.volume-icon onmousedown=${handleVolumeMute}> <i
${volumeIcon} class='icon volume-icon float-left'
</i> onmousedown=${handleVolumeMute}>
<input.volume-slider ${volumeIcon}
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume} </i>
onmousedown=${handleVolumeScrub} <input
onmouseup=${handleVolumeScrub} class='volume-slider float-right'
onmousemove=${handleVolumeScrub} type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
style=${volumeStyle} onmousedown=${handleVolumeScrub}
/> onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub}
style=${volumeStyle}
/>
</div> </div>
`) `)
// Finally, the big button in the center plays or pauses the video // Show video playback progress
elements.push(hx` elements.push(hx`
<i class='icon play-pause' onclick=${dispatcher('playPause')}> <span class='time float-left'>
${state.playing.isPaused ? 'play_arrow' : 'pause'} ${formatTime(state.playing.currentTime)} / ${formatTime(state.playing.duration)}
</i> </span>
`) `)
// render playback rate
if (state.playing.playbackRate !== 1) {
elements.push(hx`
<span class='rate float-left'>
${state.playing.playbackRate}x
</span>
`)
}
return hx` return hx`
<div class='player-controls'> <div class='controls'>
${elements} ${elements}
${renderSubtitlesOptions(state)} ${renderSubtitlesOptions(state)}
</div> </div>
` `
function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) {
var dt = e.dataTransfer
dt.effectAllowed = 'none'
}
}
// Handles a click or drag to scrub (jump to another position in the video) // Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) { function handleScrub (e) {
dispatch('mediaMouseMoved') dispatch('mediaMouseMoved')
@@ -484,7 +510,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 +521,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 +558,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})`
@@ -543,7 +569,17 @@ function cssBackgroundImageDarkGradient () {
'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) { function formatTime (time) {
var infoHash = state.playing.infoHash if (typeof time !== 'number' || Number.isNaN(time)) {
return state.saved.torrents.find((x) => x.infoHash === infoHash) return '0:00'
}
var hours = Math.floor(time / 3600)
var minutes = Math.floor(time % 3600 / 60)
if (hours > 0) {
minutes = zeroFill(2, minutes)
}
var seconds = zeroFill(2, Math.floor(time % 60))
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
} }

View File

@@ -0,0 +1,104 @@
module.exports = Preferences
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatch} = require('../lib/dispatcher')
var remote = require('electron').remote
var dialog = remote.dialog
function Preferences (state) {
return hx`
<div class='preferences'>
${renderGeneralSection(state)}
</div>
`
}
function renderGeneralSection (state) {
return renderSection({
title: 'General',
description: '',
icon: 'settings'
}, [
renderDownloadDirSelector(state)
])
}
function renderDownloadDirSelector (state) {
return renderFileSelector({
label: 'Download Path',
description: 'Data from torrents will be saved here',
property: 'downloadPath',
options: {
title: 'Select download directory',
properties: [ 'openDirectory' ]
}
},
state.unsaved.prefs.downloadPath,
function (filePath) {
setStateValue('downloadPath', filePath)
})
}
// Renders a prefs section.
// - definition should be {icon, title, description}
// - controls should be an array of vdom elements
function renderSection (definition, controls) {
var helpElem = !definition.description ? null : hx`
<div class='help text'>
<i.icon>help_outline</i>${definition.description}
</div>
`
return hx`
<section class='section preferences-panel'>
<div class='section-container'>
<div class='section-heading'>
<i.icon>${definition.icon}</i>${definition.title}
</div>
${helpElem}
<div class='section-body'>
${controls}
</div>
</div>
</section>
`
}
// Creates a file chooser
// - defition should be {label, description, options}
// options are passed to dialog.showOpenDialog
// - value should be the current pref, a file or folder path
// - callback takes a new file or folder path
function renderFileSelector (definition, value, callback) {
return hx`
<div class='control-group'>
<div class='controls'>
<label class='control-label'>
<div class='preference-title'>${definition.label}</div>
<div class='preference-description'>${definition.description}</div>
</label>
<div class='controls'>
<input type='text' class='file-picker-text'
id=${definition.property}
disabled='disabled'
value=${value} />
<button class='btn' onclick=${handleClick}>
<i.icon>folder_open</i>
</button>
</div>
</div>
</div>
`
function handleClick () {
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
if (!Array.isArray(filenames)) return
callback(filenames[0])
})
}
}
function setStateValue (property, value) {
dispatch('updatePreferences', property, value)
}

View File

@@ -118,12 +118,7 @@ function TorrentList (state) {
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'unplayable') { if (torrentSummary.playStatus === 'timeout') {
playIcon = 'play_arrow'
playClass = 'disabled'
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
'View details and click on individual files to open them in another program.'
} else if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning' playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.' playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else { } else {
@@ -143,6 +138,18 @@ function TorrentList (state) {
downloadTooltip = 'Click to start torrenting.' downloadTooltip = 'Click to start torrenting.'
} }
// Do we have a saved position? Show it using a radial progress bar on top
// of the play button, unless already showing a spinner there:
var positionElem
var willShowSpinner = torrentSummary.playStatus === 'requested'
var defaultFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
var fraction = defaultFile.currentTime / defaultFile.duration
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position'
}
// Only show the play button for torrents that contain playable media // Only show the play button for torrents that contain playable media
var playButton var playButton
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) { if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
@@ -158,6 +165,7 @@ function TorrentList (state) {
return hx` return hx`
<div class='buttons'> <div class='buttons'>
${positionElem}
${playButton} ${playButton}
<i.button-round.icon.download <i.button-round.icon.download
class=${torrentSummary.status} class=${torrentSummary.status}
@@ -186,11 +194,16 @@ function TorrentList (state) {
filesElement = hx`<div class='files warning'>${message}</div>` filesElement = hx`<div class='files warning'>${message}</div>`
} else { } else {
// We do know the files. List them and show download stats for each one // We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files.map( var fileRows = torrentSummary.files
(file, index) => renderFileRow(torrentSummary, file, index)) .sort(function (a, b) {
if (a.name < b.name) return -1
if (b.name < a.name) return 1
return 0
})
.map((file, index) => renderFileRow(torrentSummary, file, index))
filesElement = hx` filesElement = hx`
<div class='files'> <div class='files'>
<strong>Files</strong>
<table> <table>
${fileRows} ${fileRows}
</table> </table>
@@ -208,7 +221,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]
@@ -216,28 +230,69 @@ function TorrentList (state) {
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%' progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
} }
// Second, render the file as a table row // Second, for media files where we saved our position, show how far we got
var positionElem
if (file.currentTime) {
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
positionElem = renderRadialProgressBar(file.currentTime / file.duration)
}
// Finally, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file)
var 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 onclick=${handleClick}>
<td class='col-icon'> <td class='col-icon ${rowClass}'>
${positionElem}
<i class='icon'>${icon}</i> <i class='icon'>${icon}</i>
</td> </td>
<td class='col-name'>${file.name}</td> <td class='col-name ${rowClass}'>
<td class='col-progress'>${progress}</td> ${file.name}
<td class='col-size'>${prettyBytes(file.length)}</td> </td>
<td class='col-progress ${rowClass}'>
${isSelected ? progress : ''}
</td>
<td class='col-size ${rowClass}'>
${prettyBytes(file.length)}
</td>
<td class='col-select'
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
</td>
</tr> </tr>
` `
} }
} }
function renderRadialProgressBar (fraction, cssClass) {
var rotation = 360 * fraction
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return hx`
<div class="radial-progress ${cssClass}">
<div class="circle">
<div class="mask full" style=${transformFill}>
<div class="fill" style=${transformFill}></div>
</div>
<div class="mask half">
<div class="fill" style=${transformFill}></div>
<div class="fill fix" style=${transformFix}></div>
</div>
</div>
<div class="inset"></div>
</div>
`
}

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