Compare commits

..

117 Commits

Author SHA1 Message Date
DC
e34223fc94 0.8.0 2016-06-23 07:31:18 -07:00
Gediminas Petrikas
15f733f11c Windows Thumbnail Bar
* While in the player view, show a play/pause toggle in the thumbnail
2016-06-23 07:12:32 -07:00
DC
7526b18507 Show which cast device you're connected to 2016-06-23 07:09:49 -07:00
DC
0af6007632 Refactor cast menu 2016-06-23 07:09:49 -07:00
DC
1bc3cd1d51 Make check-deps handle older verions of node 2016-06-23 07:09:49 -07:00
DC
92bafd695d Listen to events on new cast devices 2016-06-23 07:09:49 -07:00
DC
78a2ee4e85 Cast menu
Fixes #301
2016-06-23 07:09:49 -07:00
Feross Aboukhadijeh
8b9346d767 Prevent playback continues after minimize (#662)
Fixes #649.
2016-06-23 06:59:55 -07:00
DC
06d3bd3f93 Seeding: sort files by path (#663)
Fixes a bug where you could create duplicate torrents by adding the same folder multiple times, because the file order & therefore the infohash was nondeterministic
2016-06-23 02:14:23 -07:00
Mathias Rasmussen
1af7e4ef19 Remove torrent data support (#641)
* add moveItemToTrash to shell

* delete torrent/data + context menu items
2016-06-22 18:58:16 -07:00
DC
8e64e4120b Telemetry: add Privacy section to README 2016-06-21 21:58:15 -07:00
DC
b983559763 Telemetry: address PR comments 2016-06-21 21:58:15 -07:00
DC
e62527de23 Telemetry: limit POST to 100kb 2016-06-21 04:20:12 -07:00
DC
1f51f35f8e Telemetry: report uncaught errors 2016-06-21 03:45:34 -07:00
DC
c3686417e3 Telemetry 2016-06-20 22:33:17 -07:00
Feross Aboukhadijeh
746e10c025 author email 2016-06-15 17:41:04 -07:00
Feross Aboukhadijeh
98389fc07c Merge pull request #644 from feross/dc/ux
Support .wmv video via VLC
2016-06-15 16:35:12 -07:00
DC
aaebf93db4 Support .wmv video via VLC. Fixes #625 2016-06-15 16:22:16 -07:00
Feross Aboukhadijeh
12500dfb64 modals should stick to title bar 2016-06-13 16:15:45 -07:00
Feross Aboukhadijeh
acc8e7923a Update modal: improve buttons 2016-06-13 16:15:01 -07:00
Feross Aboukhadijeh
9aa5775528 Merge pull request #636 from mathiasvr/modals
fix modal inconsistencies
2016-06-13 16:08:05 -07:00
Mathias Rasmussen
2a2d71289a fix modal inconsistencies 2016-06-13 16:18:43 +02:00
Feross Aboukhadijeh
ae28e34fd5 Merge pull request #634 from feross/dc/fix
Make posters from jpeg files
2016-06-11 23:31:20 -07:00
DC
6b175e7d40 Make posters from jpeg files 2016-06-11 23:10:48 -07:00
Feross Aboukhadijeh
2c6d74e8ef Merge pull request #632 from mathiasvr/patch
handle play/pause when window is hidden
2016-06-10 20:23:58 -07:00
Mathias Rasmussen
3b832595fe handle play/pause when window is hidden
using `webkitvisibilitychange` event
2016-06-10 04:56:33 +02:00
Feross Aboukhadijeh
bf372029fb changelog 2016-06-03 15:10:11 -07:00
Feross Aboukhadijeh
17ce7e519c Merge pull request #623 from feross/tray
Fix Windows tray state
2016-06-02 23:24:33 -07:00
Feross Aboukhadijeh
1f6a112df7 changelog 2016-06-02 23:14:37 -07:00
Feross Aboukhadijeh
9d3e26f15a 0.7.2 2016-06-02 22:46:10 -07:00
Feross Aboukhadijeh
8a95895254 Ensure state.saved.prefs exists
Fixes a bug I introduced in the migrations
2016-06-02 22:33:22 -07:00
Feross Aboukhadijeh
5d71f9e9c6 code cleanup 2016-06-02 22:32:01 -07:00
Feross Aboukhadijeh
0ec6fb5a93 Fix Windows tray state
After this PR, the Windows tray state will be correct. "Show
WebTorrent" vs. "Hide WebTorrent"
2016-06-02 21:16:10 -07:00
Feross Aboukhadijeh
5d410457ce Merge pull request #622 from feross/about
Fix title in About Window
2016-06-02 21:12:58 -07:00
Feross Aboukhadijeh
c6cd21b8ff Fix title in About Window
Shows up as 'Electron' on Windows
2016-06-02 20:50:04 -07:00
Feross Aboukhadijeh
2235b2fa82 changelog 2016-06-02 20:10:40 -07:00
Feross Aboukhadijeh
65e0b5d6e7 0.7.1 2016-06-02 19:58:43 -07:00
Feross Aboukhadijeh
ea64411570 Merge pull request #621 from feross/f/fix
Fix v0.7 bug - refactor state
2016-06-02 19:53:53 -07:00
Feross Aboukhadijeh
9348c61a84 stray console.log 2016-06-02 19:53:37 -07:00
Feross Aboukhadijeh
d9aa3822ee Set selections by default
In case the user tries to change a file selection state before enabling
the torrent.
2016-06-02 19:52:35 -07:00
Feross Aboukhadijeh
e86bd26800 Refactor state save/load
- Fix bug where new install was relying on the migration to run on
startup to fix up the default config
- Moved save/load functions into state.js
- Removed exported getInitialState, getDefaultSavedState since that's
leaky. The state module should take care of that.
2016-06-02 19:46:29 -07:00
Feross Aboukhadijeh
6d8cec17de npm run open-config -- open folder 2016-06-02 19:40:12 -07:00
Feross Aboukhadijeh
572f084570 Merge pull request #620 from feross/skip-keys
Change step Forward/Backward shortcuts to match VLC's
2016-06-02 17:23:37 -07:00
Feross Aboukhadijeh
4a3ca5459d Change step Forward/Backward shortcuts to match VLC's
We got this feature idea from VLC, so let's align with the keyboard
shortcuts that they use :)

Closes #618.
2016-06-02 16:41:39 -07:00
Feross Aboukhadijeh
e8cb6abf0a 0.7.0 2016-06-02 16:21:56 -07:00
Feross Aboukhadijeh
94b3bc561d Packager: Move commands that can fail to post script 2016-06-02 13:50:36 -07:00
Feross Aboukhadijeh
5eb75d0250 authors 2016-06-02 13:48:07 -07:00
Feross Aboukhadijeh
b577e08053 changelog 2016-06-02 13:46:38 -07:00
Feross Aboukhadijeh
dae4840bd6 conditionally load migrations requires 2016-06-02 13:46:27 -07:00
Feross Aboukhadijeh
57eb52a606 Merge pull request #617 from feross/electron
electron-prebuilt@1.2.1
2016-06-01 19:28:48 -07:00
Feross Aboukhadijeh
6d670bdd3f electron-prebuilt@1.2.1 2016-06-01 19:19:20 -07:00
Feross Aboukhadijeh
def2209dc5 add todo comment 2016-06-01 19:18:36 -07:00
Feross Aboukhadijeh
763c573c7a Merge pull request #614 from feross/close-576
Fix subtitle controls radio button size
2016-06-01 19:17:13 -07:00
Feross Aboukhadijeh
eb61f2ac0e Merge pull request #613 from feross/fix-606
Fix IPC race condition in startup
2016-06-01 19:16:54 -07:00
Feross Aboukhadijeh
a9d1925686 Merge pull request #612 from feross/error-on-duplicate
Error on duplicate add (fixes to PR #593)
2016-06-01 19:15:34 -07:00
Feross Aboukhadijeh
0e10eba073 Merge pull request #611 from feross/close_caption
Use correct icon name
2016-06-01 19:14:41 -07:00
Feross Aboukhadijeh
0427e1f3a6 Fix subtitle controls radio button size
Closes #576.
2016-06-01 01:31:14 -07:00
gabriel
c841c94784 Preserve audio between sessions 2016-06-01 00:55:15 -07:00
gabriel
e3c6049fdb Do not use setVolume for storing global volume 2016-06-01 00:55:15 -07:00
gabriel
829206e921 Persist volume across videos 2016-06-01 00:55:15 -07:00
Feross Aboukhadijeh
f7acdffb2a Send ipcReady after state is initialized
Moving it sooner caused more than one bug, including
https://github.com/feross/webtorrent-desktop/issues/606 most recently.

The app feels no slower to start (even though the command line shows a
250ms slower start).

Closes #606.
2016-06-01 00:27:26 -07:00
Feross Aboukhadijeh
cc9ba385bf Show main window before ipcReady
It doesn't flicker anymore if we remove this option, and the app feels
no slower.
2016-06-01 00:25:18 -07:00
Feross Aboukhadijeh
e88ddd648b Cleanup argument handling in main 2016-06-01 00:20:11 -07:00
Feross Aboukhadijeh
dac34541d6 on -> once 2016-06-01 00:10:26 -07:00
Feross Aboukhadijeh
52fb378fd5 Error on duplicate add 2016-05-31 23:00:02 -07:00
Feross Aboukhadijeh
8fc61a1c90 Merge pull request #593 from mathiasvr/duplicate
Adding duplicate torrent when stopped
2016-05-31 22:30:04 -07:00
Feross Aboukhadijeh
04691ed0da Merge pull request #610 from feross/migration
Semver-based migrations
2016-05-31 17:55:52 -07:00
Feross Aboukhadijeh
f9d4e5e077 Use correct icon name
https://github.com/feross/webtorrent-desktop/issues/467

The icon name is "closed_caption", not "closed_captions"
http://jsbin.com/fenejob/1/edit?html,output

The extra space we're seeing is the icon font rendering the 's', which
renders as nothing.
2016-05-31 17:12:09 -07:00
Feross Aboukhadijeh
4ee36f459f Fix exception caused by incorrect file ordering
See
https://github.com/feross/webtorrent-desktop/pull/604#issuecomment-22280
5214
2016-05-31 13:15:48 -07:00
Feross Aboukhadijeh
2c0de25423 Semver-based migrations
- Put migrations into a separate file: migrations.js
- Add semver, for more specific version-based migrations.
- Change `state.saved.version` to semver app version.
2016-05-31 13:04:39 -07:00
Feross Aboukhadijeh
c82bdbd39d Fixes for PR #607 2016-05-31 12:12:25 -07:00
Feross Aboukhadijeh
71b08304f2 Merge branch 'pr/607' 2016-05-31 11:55:04 -07:00
Feross Aboukhadijeh
3bb3cd7c44 Merge pull request #609 from mathiasvr/menu-items
Fix 'fullscreen' and 'always on top' menu items
2016-05-31 11:41:21 -07:00
Mathias Rasmussen
41187ec43d Fix 'fullscreen' and 'always on top' menu items 2016-05-31 18:11:50 +02:00
Feross Aboukhadijeh
cf5de49deb Fix gray screen crash on config with defaultPlayFileIndex 2016-05-30 19:51:30 -07:00
Feross Aboukhadijeh
19f177f3ee remove console.log 2016-05-30 19:51:06 -07:00
Mathias Rasmussen
556d0cb1c5 Make add button accept non .torrent files 2016-05-31 03:04:02 +02:00
Mathias Rasmussen
7c7780b17e Adding duplicate torrent when stopped 2016-05-30 23:25:52 +02:00
Feross Aboukhadijeh
bd358b7692 Add comments to squirrel-win32.js 2016-05-30 02:30:20 -07:00
Feross Aboukhadijeh
1b8f180255 Merge pull request #604 from demoneaux/fix-file-sorting
Fix issue with file sorting.
2016-05-30 02:30:05 -07:00
Feross Aboukhadijeh
0bc90cea21 Merge pull request #605 from demoneaux/align-details
Align loading torrent message with torrent name.
2016-05-30 02:21:41 -07:00
Benjamin Tan
10f96ab23e Fix issue with file sorting.
Closes #591.
2016-05-30 13:18:38 +08:00
Benjamin Tan
4f0df507f4 Align loading torrent message with torrent name. 2016-05-30 13:17:42 +08:00
Feross Aboukhadijeh
256753e6ff Merge pull request #603 from demoneaux/fix-tray
Fix issue with tray on Windows.
2016-05-29 21:57:15 -07:00
Benjamin Tan
8ac42078d4 Fix issue with tray on Windows. 2016-05-30 11:29:53 +08:00
Feross Aboukhadijeh
fc83e054ea Merge pull request #597 from feross/refactor
[WIP] Major code cleanup
2016-05-29 04:06:51 -07:00
Feross Aboukhadijeh
62cb304971 Move unrelated code out of menu.js and ipc.js 2016-05-29 01:09:42 -07:00
Feross Aboukhadijeh
d4efebd694 Remove focusWindow function
Not needed -- win.show() automatically focuses, unminimizes, and shows
the window.
2016-05-28 19:10:55 -07:00
Feross Aboukhadijeh
7833f6bbc4 Dialogs on do not show a title on OS X, so the window title is used instead. 2016-05-28 19:09:05 -07:00
Feross Aboukhadijeh
8b773c5f59 Document and cleanup announcement/dialog/handlers.js 2016-05-28 18:25:25 -07:00
Feross Aboukhadijeh
5767d5b95d re-order view menu 2016-05-28 18:24:58 -07:00
Thomas Watson Steen
13f1ecdbe3 Use airplayer (#452)
* Use airplayer

* Mock AirPlay volume support

* Add AirPlay event support
2016-05-28 10:50:30 -07:00
Feross Aboukhadijeh
8ae4ac47e6 Perf: Temporarily disable dynamic subtitle detection
For
https://github.com/feross/webtorrent-desktop/pull/511#issuecomment-22119
8555
2016-05-27 18:51:42 -07:00
Feross Aboukhadijeh
001601bc5f Major refactor -- split windows into separate files 2016-05-27 00:01:30 -07:00
Feross Aboukhadijeh
3757507b18 cleanup announcement 2016-05-26 18:41:48 -07:00
Feross Aboukhadijeh
9abab7aec3 cleanup check-deps 2016-05-26 18:29:07 -07:00
Feross Aboukhadijeh
1aabd537d8 cleanup dispatcher 2016-05-26 18:12:23 -07:00
Feross Aboukhadijeh
6e240b3fd4 Misc file moving and cleanup
- Rename JS/CSS for main.html to be consistent (main.js, main.css)
- Add hx.js module to reduce virtual-dom boilerplate
- Move state.js into renderer/lib.js where it belongs
- Rename torrent-list.js -> home.js for consistency
- Rename create-torrent-page.js -> create-torrent.js for consistency
2016-05-26 17:47:16 -07:00
Feross Aboukhadijeh
501a07c386 Merge pull request #594 from feross/reenable-webrtc
Re-enable WebRTC (OS X, Windows)
2016-05-26 16:54:29 -07:00
Feross Aboukhadijeh
0d92dee14e style 2016-05-26 15:42:18 -07:00
Feross Aboukhadijeh
3a1fa25106 Re-enable WebRTC peers (OS X, Windows) 2016-05-26 15:42:12 -07:00
Feross Aboukhadijeh
b167770ea6 Merge pull request #587 from feross/electron-1.1.3
Electron 1.2.0
2016-05-26 15:39:02 -07:00
Feross Aboukhadijeh
2a8a26ac54 electron-prebuilt@1.2.0 2016-05-26 15:34:12 -07:00
Feross Aboukhadijeh
9748833ba9 Use .ico format on Windows
-  Use .ico format on Windows for best rendering quality, instead of
resized .png files
2016-05-26 15:33:33 -07:00
Feross Aboukhadijeh
bf49214790 electron-prebuilt@1.1.3 2016-05-26 15:33:33 -07:00
Feross Aboukhadijeh
2b4410a55a changelog 2016-05-26 15:26:23 -07:00
DC
bfd1b2eaf0 changelog 2016-05-26 04:00:38 -07:00
DC
44c3421e92 0.6.1 2016-05-26 03:46:32 -07:00
DC
7de3d3cc41 Clean up showCreateTorrent 2016-05-26 02:23:34 -07:00
DC
3d7f46da65 Disable WebRTC on Windows to work around Electron crash 2016-05-26 02:17:08 -07:00
DC
72d902e548 Fix selections migration
Should fix #583
2016-05-26 02:17:08 -07:00
DC
955fe76c3c Allow dropping files on dock icon
Fixes #584
2016-05-26 02:17:08 -07:00
Feross Aboukhadijeh
839bec0363 Merge pull request #588 from feross/dc/cleanup
Show error when drag-dropping hidden files
2016-05-26 01:06:46 -07:00
Feross Aboukhadijeh
9af4ce9a6b Merge pull request #589 from feross/dc/shortcuts
Simplify shortcuts. Go Back menu item
2016-05-26 00:54:30 -07:00
DC
205bf75c7e Simplify shortcuts. Go Back menu item
Fixes #585
2016-05-25 23:31:32 -07:00
DC
bafbf3d841 Show error when drag-dropping hidden files
...or anytime the user tries to create a torrent consisting only of hidden files, specifically dotfiles

Fixes #586
2016-05-25 23:15:26 -07:00
DC
1b0833fb45 Clean up player.js 2016-05-25 22:44:30 -07:00
61 changed files with 2303 additions and 1524 deletions

View File

@@ -10,7 +10,7 @@
- Romain Beaumont <romain.rom1@gmail.com> - Romain Beaumont <romain.rom1@gmail.com>
- Dan Flettre <fletd01@yahoo.com> - Dan Flettre <fletd01@yahoo.com>
- Liam Gray <liam.r.gray@gmail.com> - Liam Gray <liam.r.gray@gmail.com>
- grunjol <grunjol@argenteam.net> - grunjol <grunjol@users.noreply.github.com>
- Rémi Jouannet <remijouannet@users.noreply.github.com> - Rémi Jouannet <remijouannet@users.noreply.github.com>
- Evan Miller <miller.evan815@gmail.com> - Evan Miller <miller.evan815@gmail.com>
- Alex <alxmorais8@msn.com> - Alex <alxmorais8@msn.com>
@@ -21,5 +21,6 @@
- Benjamin Tan <demoneaux@gmail.com> - Benjamin Tan <demoneaux@gmail.com>
- Mathias Rasmussen <mathiasvr@gmail.com> - Mathias Rasmussen <mathiasvr@gmail.com>
- Sergey Bargamon <sergey@bargamon.ru> - Sergey Bargamon <sergey@bargamon.ru>
- Thomas Watson Steen <w@tson.dk>
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,15 +1,80 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.7.2 - 2016-06-02
### Fixed
- Fix exception that affects users upgrading from v0.5.1 or older
- Ensure `state.saved.prefs` configuration exists
- Fix window title on "About WebTorrent" window
## v0.7.1 - 2016-06-02
### Changed
- Change "Step Forward" keyboard shortcut to `Alt+Left` (Windows)
- Change "Step Backward" keyboard shortcut to to `Alt+Right` (Windows)
### Fixed
- First time startup bug -- invalid torrent/poster paths
## v0.7.0 - 2016-06-02
### Added
- Improved AirPlay support -- using the new [`airplayer`](https://www.npmjs.com/package/airplayer) package
- Remember volume setting in player, for as long as the app is open
### Changed
- Add (+) button now also accepts non .torrent files and creates a torrent from
those files
- Show prompt text in title bar for open dialogs (OS X)
- Upgrade Electron to 1.2.1
- Improve window resizing when aspect ratio is enforced (OS X)
- Use .ico format for better icon rendering quality (Windows)
- Fix crash reporter not working (Windows)
### Fixed
- Re-enable WebRTC (web peers)! (OS X, Windows)
- Windows support was disabled in v0.6.1 to work around a bug in Electron
- OS X support was disabled in v0.4.0 to work around a 100% CPU bug
- Fix subtitle selector radio button UI size glitch
- Fix race condition causing exeption on app startup
- Fix duplicate torrent detection in some cases
- Fix "gray screen" exception caused by incorrect file list order
- Fix torrent loading message UI misalignment
### Known issues
- When upgrading to WebTorrent Desktop v0.7.0, some torrent metadata (file list,
selected files, whether torrent is streamable) will be cleared. Just start the
torrent to re-populate the metadata.
## v0.6.1 - 2016-05-26
### Fixed
- Disable WebRTC to work around Electron crash (Windows)
- Will be re-enabled in the next version of WebTorrent, which will be based on
the next version of Electron, where the bug is fixed.
- Fix crash when updating from WebTorrent 0.5.x in some situtations (#583)
- Fix crash when dropping files onto the dock icon (OS X)
- Fix keyboard shortcuts Space and ESC being captured globally (#585)
- Fix crash, show error when drag-dropping hidden files (#586)
## v0.6.0 - 2016-05-24 ## v0.6.0 - 2016-05-24
### Added ### Added
- Added Preferences page - Added Preferences page to set Download folder
- Save video position, resume playback from saved position - Save video position, resume playback from saved position
- Add additional video player keyboard shortcuts (#275) - Add additional video player keyboard shortcuts (#275)
- Use `poster.jpg` file as the poster image if available (#558) - Use `poster.jpg` file as the poster image if available (#558)
- Associate .torrent files to WebTorrent Desktop (OS X) (#553) - Associate .torrent files to WebTorrent Desktop (OS X) (#553)
- Add support for pasting a `instant.io` links (#559) - Add support for pasting `instant.io` links (#559)
- Add announcement feature - Add announcement feature
### Changed ### Changed

View File

@@ -81,6 +81,10 @@ brew install wine
(Requires the [Homebrew](http://brew.sh/) package manager.) (Requires the [Homebrew](http://brew.sh/) package manager.)
### Privacy
WebTorrent Desktop collects some basic usage stats to help us make the app better. For example, we track what OSs are users are on, and how well the play button works (how often does it succeed? time out? show a missing codec error?). The app never sends personally identifying or other private info.
### Code Style ### Code Style
[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)

View File

@@ -3,49 +3,97 @@
var fs = require('fs') var fs = require('fs')
var cp = require('child_process') var cp = require('child_process')
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen'] // We can't use `builtin-modules` here since our TravisCI
// setup expects this file to run with no dependencies
var BUILT_IN_NODE_MODULES = [
'assert',
'buffer',
'child_process',
'cluster',
'console',
'constants',
'crypto',
'dgram',
'dns',
'domain',
'events',
'fs',
'http',
'https',
'module',
'net',
'os',
'path',
'process',
'punycode',
'querystring',
'readline',
'repl',
'stream',
'string_decoder',
'timers',
'tls',
'tty',
'url',
'util',
'v8',
'vm',
'zlib'
]
var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
var EXECUTABLE_DEPS = ['gh-release', 'standard'] var EXECUTABLE_DEPS = ['gh-release', 'standard']
main() main()
// Scans our codebase and package.json for missing or unused dependencies // Scans codebase for missing or unused dependencies. Exits with code 0 on success.
// Process returns 0 on success, prints a message and returns 1 on failure
function main () { function main () {
if (process.platform === 'win32') { if (process.platform === 'win32') {
console.log('Sorry, check-deps only works on Mac and Linux') console.error('Sorry, check-deps only works on Mac and Linux')
return return
} }
var jsDeps = findJSDeps() var usedDeps = findUsedDeps()
var packageDeps = findPackageDeps() var packageDeps = findPackageDeps()
var missingDeps = jsDeps.filter((dep) => var missingDeps = usedDeps.filter(
packageDeps.indexOf(dep) < 0 && (dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep)
BUILT_IN_DEPS.indexOf(dep) < 0) )
var unusedDeps = packageDeps.filter((dep) => var unusedDeps = packageDeps.filter(
jsDeps.indexOf(dep) < 0 && (dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
EXECUTABLE_DEPS.indexOf(dep) < 0) )
if (missingDeps.length > 0) console.log('Missing package dependencies: ' + missingDeps) if (missingDeps.length > 0) {
if (unusedDeps.length > 0) console.log('Unused package dependencies: ' + unusedDeps) console.error('Missing package dependencies: ' + missingDeps)
}
if (missingDeps.length + unusedDeps.length > 0) process.exit(1) if (unusedDeps.length > 0) {
console.error('Unused package dependencies: ' + unusedDeps)
console.log('Lookin good!') }
if (missingDeps.length + unusedDeps.length > 0) {
process.exitCode = 1
}
} }
// Finds all dependencies, required, optional, or dev, in package.json // Finds all dependencies specified in `package.json`
function findPackageDeps () { function findPackageDeps () {
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')) var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
var requiredDeps = Object.keys(pkg.dependencies)
var deps = Object.keys(pkg.dependencies)
var devDeps = Object.keys(pkg.devDependencies) var devDeps = Object.keys(pkg.devDependencies)
var optionalDeps = Object.keys(pkg.optionalDependencies) var optionalDeps = Object.keys(pkg.optionalDependencies)
return [].concat(requiredDeps, devDeps, optionalDeps) return [].concat(deps, devDeps, optionalDeps)
} }
// Finds all dependencies required() in the code // Finds all dependencies that used with `require()`
function findJSDeps () { function findUsedDeps () {
var stdout = cp.execSync('./bin/list-deps.sh') var stdout = cp.execSync('./bin/list-deps.sh')
return stdout.toString().trim().split('\n') return stdout.toString().trim().split('\n')
} }
function includes (arr, elem) {
return arr.indexOf(elem) >= 0
}

View File

@@ -6,5 +6,5 @@ var path = require('path')
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'}) var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
child.on('close', function (code) { child.on('close', function (code) {
process.exit(code) process.exitCode = code
}) })

View File

@@ -2,7 +2,5 @@
var config = require('../config') var config = require('../config')
var open = require('open') var open = require('open')
var path = require('path')
var configPath = path.join(config.CONFIG_PATH, 'config.json') open(config.CONFIG_PATH)
open(configPath)

View File

@@ -327,11 +327,11 @@ function buildDarwin (cb) {
} }
var dmg = appDmg(dmgOpts) var dmg = appDmg(dmgOpts)
dmg.on('error', cb) dmg.once('error', cb)
dmg.on('progress', function (info) { dmg.on('progress', function (info) {
if (info.type === 'step-begin') console.log(info.title + '...') if (info.type === 'step-begin') console.log(info.title + '...')
}) })
dmg.on('finish', function (info) { dmg.once('finish', function (info) {
console.log('OS X: Created dmg.') console.log('OS X: Created dmg.')
cb(null) cb(null)
}) })

View File

@@ -1,6 +1,7 @@
#!/bin/sh #!/bin/sh
set -e set -e
npm run update-authors
git diff --exit-code git diff --exit-code
npm run package -- --sign npm run package -- --sign
git push git push

View File

@@ -2,8 +2,6 @@
set -e set -e
git pull git pull
npm run update-authors
git diff --exit-code
rm -rf node_modules/ rm -rf node_modules/
npm install npm install
npm dedupe npm dedupe

View File

@@ -10,7 +10,6 @@ while (<>) {
next if $seen{$_}; next if $seen{$_};
next if /<support\@greenkeeper.io>/; next if /<support\@greenkeeper.io>/;
next if /<ungoldman\@gmail.com>/; next if /<ungoldman\@gmail.com>/;
next if /<grunjol\@users.noreply.github.com>/;
next if /<dc\@DCs-MacBook.local>/; next if /<dc\@DCs-MacBook.local>/;
next if /<rolandoguedes\@gmail.com>/; next if /<rolandoguedes\@gmail.com>/;
$seen{$_} = push @authors, "- ", $_; $seen{$_} = push @authors, "- ", $_;

View File

@@ -10,6 +10,9 @@ var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings
module.exports = { module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement', ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
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'),
@@ -19,16 +22,40 @@ 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',
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
CONFIG_PATH: getConfigPath(), CONFIG_PATH: getConfigPath(),
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'), DEFAULT_TORRENTS: [
{
name: 'Big Buck Bunny',
posterFileName: 'bigBuckBunny.jpg',
torrentFileName: 'bigBuckBunny.torrent'
},
{
name: 'Cosmos Laundromat (Preview)',
posterFileName: 'cosmosLaundromat.jpg',
torrentFileName: 'cosmosLaundromat.torrent'
},
{
name: 'Sintel',
posterFileName: 'sintel.jpg',
torrentFileName: 'sintel.torrent'
},
{
name: 'Tears of Steel',
posterFileName: 'tearsOfSteel.jpg',
torrentFileName: 'tearsOfSteel.torrent'
},
{
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
posterFileName: 'wiredCd.jpg',
torrentFileName: 'wiredCd.torrent'
}
],
DELAYED_INIT: 3000 /* 3 seconds */, DELAYED_INIT: 3000 /* 3 seconds */,
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop', GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues', GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master', GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
@@ -38,8 +65,10 @@ module.exports = {
IS_PORTABLE: isPortable(), IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(), IS_PRODUCTION: isProduction(),
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: __dirname, ROOT_PATH: __dirname,
STATIC_PATH: path.join(__dirname, 'static'), STATIC_PATH: path.join(__dirname, 'static'),
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'), WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'), WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
@@ -57,6 +86,22 @@ function getConfigPath () {
} }
} }
function getDefaultDownloadPath () {
if (!process || !process.type) {
return ''
}
if (isPortable()) {
return path.join(getConfigPath(), 'Downloads')
}
var electron = require('electron')
return process.type === 'renderer'
? electron.remote.app.getPath('downloads')
: electron.app.getPath('downloads')
}
function isPortable () { function isPortable () {
try { try {
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH) return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)

View File

@@ -3,7 +3,6 @@ module.exports = {
} }
var electron = require('electron') var electron = require('electron')
var get = require('simple-get')
var config = require('../config') var config = require('../config')
var log = require('./log') var log = require('./log')
@@ -12,27 +11,47 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
'?version=' + config.APP_VERSION + '?version=' + config.APP_VERSION +
'&platform=' + process.platform '&platform=' + process.platform
/**
* In certain situations, the WebTorrent team may need to show an announcement to
* all WebTorrent Desktop users. For example: a security notice, or an update
* notification (if the auto-updater stops working).
*
* When there is an announcement, the `ANNOUNCEMENT_URL` endpoint should return an
* HTTP 200 status code with a JSON object like this:
*
* {
* "title": "WebTorrent Desktop Announcement",
* "message": "Security Issue in v0.xx",
* "detail": "Please update to v0.xx as soon as possible..."
* }
*/
function init () { function init () {
get.concat(ANNOUNCEMENT_URL, function (err, res, data) { var get = require('simple-get')
if (err) return log('failed to retrieve remote message') get.concat(ANNOUNCEMENT_URL, onResponse)
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 () {})
})
} }
function onResponse (err, res, data) {
if (err) return log(`Failed to retrieve announcement: ${err.message}`)
if (res.statusCode !== 200) return log('No announcement exists')
try {
data = JSON.parse(data.toString())
} catch (err) {
// Support plaintext announcement messages, using a default title.
data = {
title: 'WebTorrent Desktop Announcement',
message: data.toString(),
detail: data.toString()
}
}
electron.dialog.showMessageBox({
type: 'info',
buttons: ['OK'],
title: data.title,
message: data.message,
detail: data.detail
}, noop)
}
function noop () {}

122
main/dialog.js Normal file
View File

@@ -0,0 +1,122 @@
module.exports = {
openSeedFile,
openSeedDirectory,
openTorrentFile,
openTorrentAddress,
openFiles
}
var electron = require('electron')
var config = require('../config')
var log = require('./log')
var windows = require('./windows')
/**
* Show open dialog to create a single-file torrent.
*/
function openSeedFile () {
if (!windows.main.win) return
log('openSeedFile')
var opts = {
title: 'Select a file for the torrent.',
properties: [ 'openFile' ]
}
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
}
/*
* Show open dialog to create a single-file or single-directory torrent. On
* Windows and Linux, open dialogs are for files *or* directories only, not both,
* so this function shows a directory dialog on those platforms.
*/
function openSeedDirectory () {
if (!windows.main.win) return
log('openSeedDirectory')
var opts = process.platform === 'darwin'
? {
title: 'Select a file or folder for the torrent.',
properties: [ 'openFile', 'openDirectory' ]
}
: {
title: 'Select a folder for the torrent.',
properties: [ 'openDirectory' ]
}
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
}
/*
* Show flexible open dialog that supports selecting .torrent files to add, or
* a file or folder to create a single-file or single-directory torrent.
*/
function openFiles () {
if (!windows.main.win) return
log('openFiles')
var opts = process.platform === 'darwin'
? {
title: 'Select a file or folder to add.',
properties: [ 'openFile', 'openDirectory' ]
}
: {
title: 'Select a file to add.',
properties: [ 'openFile' ]
}
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('onOpen', selectedPaths)
})
}
/*
* Show open dialog to open a .torrent file.
*/
function openTorrentFile () {
if (!windows.main.win) return
log('openTorrentFile')
var opts = {
title: 'Select a .torrent file.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ]
}
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(function (selectedPath) {
windows.main.dispatch('addTorrent', selectedPath)
})
})
}
/*
* Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.)
*/
function openTorrentAddress () {
log('openTorrentAddress')
windows.main.dispatch('openTorrentAddress')
}
/**
* Dialogs on do not show a title on OS X, so the window title is used instead.
*/
function setTitle (title) {
if (process.platform === 'darwin') {
windows.main.dispatch('setTitle', title)
}
}
function resetTitle () {
setTitle(config.APP_WINDOW_TITLE)
}

59
main/dock.js Normal file
View File

@@ -0,0 +1,59 @@
module.exports = {
downloadFinished,
init,
setBadge
}
var electron = require('electron')
var app = electron.app
var dialog = require('./dialog')
var log = require('./log')
/**
* Add a right-click menu to the dock icon. (OS X)
*/
function init () {
if (!app.dock) return
var menu = electron.Menu.buildFromTemplate(getMenuTemplate())
app.dock.setMenu(menu)
}
/**
* Bounce the Downloads stack if `path` is inside the Downloads folder. (OS X)
*/
function downloadFinished (path) {
if (!app.dock) return
log(`downloadFinished: ${path}`)
app.dock.downloadFinished(path)
}
/**
* Display string in dock badging area. (OS X)
*/
function setBadge (text) {
if (!app.dock) return
log(`setBadge: ${text}`)
app.dock.setBadge(String(text))
}
function getMenuTemplate () {
return [
{
label: 'Create New Torrent...',
accelerator: 'CmdOrCtrl+N',
click: () => dialog.openSeedDirectory()
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: () => dialog.openTorrentFile()
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: () => dialog.openTorrentAddress()
}
]
}

View File

@@ -3,9 +3,8 @@ module.exports = {
uninstall uninstall
} }
var path = require('path')
var config = require('../config') var config = require('../config')
var path = require('path')
function install () { function install () {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
@@ -35,11 +34,11 @@ function installDarwin () {
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
// On OS X, only protocols that are listed in Info.plist can be set as the default // On OS X, only protocols that are listed in `Info.plist` can be set as the
// handler at runtime. // default handler at runtime.
app.setAsDefaultProtocolClient('magnet') app.setAsDefaultProtocolClient('magnet')
// File handlers are registered in the Info.plist. // File handlers are defined in `Info.plist`.
} }
function uninstallDarwin () {} function uninstallDarwin () {}
@@ -55,10 +54,22 @@ function installWin32 () {
var log = require('./log') var log = require('./log')
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico') var iconPath = path.join(
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND) )
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND) registerProtocolHandlerWin32(
'magnet',
'URL:BitTorrent Magnet URL',
iconPath,
EXEC_COMMAND
)
registerFileHandlerWin32(
'.torrent',
'io.webtorrent.torrent',
'BitTorrent Document',
iconPath,
EXEC_COMMAND
)
/** /**
* To add a protocol handler, the following keys must be added to the Windows registry: * To add a protocol handler, the following keys must be added to the Windows registry:
@@ -265,7 +276,9 @@ function installLinux () {
installIconFile() installIconFile()
function installDesktopFile () { function installDesktopFile () {
var templatePath = path.join(config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop') var templatePath = path.join(
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
)
fs.readFile(templatePath, 'utf8', writeDesktopFile) fs.readFile(templatePath, 'utf8', writeDesktopFile)
} }

View File

@@ -8,11 +8,12 @@ var ipcMain = electron.ipcMain
var announcement = require('./announcement') var announcement = require('./announcement')
var config = require('../config') var config = require('../config')
var crashReporter = require('../crash-reporter') var crashReporter = require('../crash-reporter')
var dialog = require('./dialog')
var dock = require('./dock')
var handlers = require('./handlers') var handlers = require('./handlers')
var ipc = require('./ipc') var ipc = require('./ipc')
var log = require('./log') var log = require('./log')
var menu = require('./menu') var menu = require('./menu')
var shortcuts = require('./shortcuts')
var squirrelWin32 = require('./squirrel-win32') var squirrelWin32 = require('./squirrel-win32')
var tray = require('./tray') var tray = require('./tray')
var updater = require('./updater') var updater = require('./updater')
@@ -54,23 +55,29 @@ function init () {
ipc.init() ipc.init()
app.on('will-finish-launching', function () { app.once('will-finish-launching', function () {
crashReporter.init() crashReporter.init()
}) })
app.on('ready', function () { app.on('ready', function () {
isReady = true isReady = true
windows.createMainWindow() windows.main.init()
windows.createWebTorrentHiddenWindow() windows.webtorrent.init()
menu.init() menu.init()
shortcuts.init()
// To keep app startup fast, some code is delayed. // To keep app startup fast, some code is delayed.
setTimeout(delayedInit, config.DELAYED_INIT) setTimeout(delayedInit, config.DELAYED_INIT)
// Report uncaught exceptions
process.on('uncaughtException', (err) => {
console.error(err)
var errJSON = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', errJSON)
})
}) })
app.on('ipcReady', function () { app.once('ipcReady', function () {
log('Command line args:', argv) log('Command line args:', argv)
processArgv(argv) processArgv(argv)
console.timeEnd('init') console.timeEnd('init')
@@ -81,20 +88,21 @@ function init () {
app.isQuitting = true app.isQuitting = true
e.preventDefault() e.preventDefault()
windows.main.send('dispatch', 'saveState') /* try to save state on exit */ windows.main.dispatch('saveState') // try to save state on exit
ipcMain.once('savedState', () => app.quit()) ipcMain.once('savedState', () => app.quit())
setTimeout(() => app.quit(), 2000) /* quit after 2 secs, at most */ setTimeout(() => app.quit(), 2000) // quit after 2 secs, at most
}) })
app.on('activate', function () { app.on('activate', function () {
if (isReady) windows.createMainWindow() if (isReady) windows.main.show()
}) })
} }
function delayedInit () { function delayedInit () {
announcement.init() announcement.init()
tray.init() dock.init()
handlers.install() handlers.install()
tray.init()
updater.init() updater.init()
} }
@@ -102,13 +110,12 @@ function onOpen (e, torrentId) {
e.preventDefault() e.preventDefault()
if (app.ipcReady) { if (app.ipcReady) {
windows.main.send('dispatch', 'onOpen', torrentId) // Magnet links opened from Chrome won't focus the app without a setTimeout.
// Magnet links opened from Chrome won't focus the app without a setTimeout. The // The confirmation dialog Chrome shows causes Chrome to steal back the focus.
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
// Electron issue: https://github.com/atom/electron/issues/4338 // Electron issue: https://github.com/atom/electron/issues/4338
setTimeout(function () { setTimeout(() => windows.main.show(), 100)
windows.focusWindow(windows.main)
}, 100) processArgv([ torrentId ])
} else { } else {
argv.push(torrentId) argv.push(torrentId)
} }
@@ -119,7 +126,7 @@ function onAppOpen (newArgv) {
if (app.ipcReady) { if (app.ipcReady) {
log('Second app instance opened, but was prevented:', newArgv) log('Second app instance opened, but was prevented:', newArgv)
windows.focusWindow(windows.main) windows.main.show()
processArgv(newArgv) processArgv(newArgv)
} else { } else {
@@ -132,18 +139,22 @@ function sliceArgv (argv) {
} }
function processArgv (argv) { function processArgv (argv) {
var torrentIds = []
argv.forEach(function (arg) { argv.forEach(function (arg) {
if (arg === '-n') { if (arg === '-n') {
menu.showOpenSeedFiles() dialog.openSeedDirectory()
} else if (arg === '-o') { } else if (arg === '-o') {
menu.showOpenTorrentFile() dialog.openTorrentFile()
} else if (arg === '-u') { } else if (arg === '-u') {
menu.showOpenTorrentAddress() dialog.openTorrentAddress()
} else if (arg.startsWith('-psn')) { } else if (arg.startsWith('-psn')) {
// Ignore OS X launchd "process serial number" argument // Ignore OS X launchd "process serial number" argument
// More: https://github.com/feross/webtorrent-desktop/issues/214 // Issue: https://github.com/feross/webtorrent-desktop/issues/214
} else { } else {
windows.main.send('dispatch', 'onOpen', arg) torrentIds.push(arg)
} }
}) })
if (torrentIds.length > 0) {
windows.main.dispatch('onOpen', torrentIds)
}
} }

View File

@@ -5,31 +5,33 @@ module.exports = {
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
var ipcMain = electron.ipcMain
var dialog = require('./dialog')
var dock = require('./dock')
var log = require('./log') var log = require('./log')
var menu = require('./menu') var menu = require('./menu')
var windows = require('./windows') var powerSaveBlocker = require('./power-save-blocker')
var shell = require('./shell')
var shortcuts = require('./shortcuts') var shortcuts = require('./shortcuts')
var vlc = require('./vlc') var vlc = require('./vlc')
var windows = require('./windows')
var thumbnail = require('./thumbnail')
// has to be a number, not a boolean, and undefined throws an error // Messages from the main process, to be sent once the WebTorrent process starts
var powerSaveBlockerId = 0
// messages from the main process, to be sent once the WebTorrent process starts
var messageQueueMainToWebTorrent = [] var messageQueueMainToWebTorrent = []
// holds a ChildProcess while we're playing a video in VLC, null otherwise // holds a ChildProcess while we're playing a video in VLC, null otherwise
var vlcProcess var vlcProcess
function init () { function init () {
ipcMain.on('ipcReady', function (e) { var ipc = electron.ipcMain
windows.main.show()
ipc.once('ipcReady', function (e) {
app.ipcReady = true app.ipcReady = true
app.emit('ipcReady') app.emit('ipcReady')
}) })
ipcMain.on('ipcReadyWebTorrent', function (e) { ipc.once('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window', log('sending %d queued messages from the main win to the webtorrent window',
messageQueueMainToWebTorrent.length) messageQueueMainToWebTorrent.length)
@@ -39,113 +41,117 @@ function init () {
}) })
}) })
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile) /**
* Dialog
*/
ipcMain.on('setBounds', function (e, bounds, maximize) { ipc.on('openTorrentFile', () => dialog.openTorrentFile())
setBounds(bounds, maximize) ipc.on('openFiles', () => dialog.openFiles())
})
ipcMain.on('setAspectRatio', function (e, aspectRatio) { /**
setAspectRatio(aspectRatio) * Dock
}) */
ipcMain.on('setBadge', function (e, text) { ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
setBadge(text) ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
})
ipcMain.on('setProgress', function (e, progress) { /**
setProgress(progress) * Events
}) */
ipcMain.on('toggleFullScreen', function (e, flag) { ipc.on('onPlayerOpen', function () {
menu.toggleFullScreen(flag)
})
ipcMain.on('setTitle', function (e, title) {
windows.main.setTitle(title)
})
ipcMain.on('openItem', function (e, path) {
log('open item: ' + path)
electron.shell.openItem(path)
})
ipcMain.on('showItemInFolder', function (e, path) {
log('show item in folder: ' + path)
electron.shell.showItemInFolder(path)
})
ipcMain.on('blockPowerSave', blockPowerSave)
ipcMain.on('unblockPowerSave', unblockPowerSave)
ipcMain.on('onPlayerOpen', function () {
menu.onPlayerOpen() menu.onPlayerOpen()
shortcuts.onPlayerOpen() shortcuts.onPlayerOpen()
}) })
ipcMain.on('onPlayerClose', function () { ipc.on('onPlayerClose', function () {
menu.onPlayerClose() menu.onPlayerClose()
shortcuts.onPlayerOpen() shortcuts.onPlayerOpen()
}) })
ipcMain.on('focusWindow', function (e, windowName) { ipc.on('updateThumbnailBar', function (e, isPaused) {
windows.focusWindow(windows[windowName]) thumbnail.updateThumbarButtons(isPaused)
}) })
ipcMain.on('downloadFinished', function (e, filePath) { /**
if (app.dock) { * Power Save Blocker
// Bounces the Downloads stack if the filePath is inside the Downloads folder. */
app.dock.downloadFinished(filePath)
}
})
ipcMain.on('checkForVLC', function (e) { ipc.on('blockPowerSave', () => powerSaveBlocker.start())
ipc.on('unblockPowerSave', () => powerSaveBlocker.stop())
/**
* Shell
*/
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
/**
* Windows: Main
*/
var main = windows.main
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
ipc.on('setProgress', (e, ...args) => main.setProgress(...args))
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
ipc.on('show', () => main.show())
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
/**
* VLC
* TODO: Move most of this code to vlc.js
*/
ipc.on('checkForVLC', function (e) {
vlc.checkForVLC(function (isInstalled) { vlc.checkForVLC(function (isInstalled) {
windows.main.send('checkForVLC', isInstalled) windows.main.send('checkForVLC', isInstalled)
}) })
}) })
ipcMain.on('vlcPlay', function (e, url) { ipc.on('vlcPlay', function (e, url) {
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url] var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url]
console.log('Running vlc ' + args.join(' ')) log('Running vlc ' + args.join(' '))
vlc.spawn(args, function (err, proc) { vlc.spawn(args, function (err, proc) {
if (err) return windows.main.send('dispatch', 'vlcNotFound') if (err) return windows.main.dispatch('vlcNotFound')
vlcProcess = proc vlcProcess = proc
// If it works, close the modal after a second // If it works, close the modal after a second
var closeModalTimeout = setTimeout(() => var closeModalTimeout = setTimeout(() =>
windows.main.send('dispatch', 'exitModal'), 1000) windows.main.dispatch('exitModal'), 1000)
vlcProcess.on('close', function (code) { vlcProcess.on('close', function (code) {
clearTimeout(closeModalTimeout) clearTimeout(closeModalTimeout)
if (!vlcProcess) return // Killed if (!vlcProcess) return // Killed
console.log('VLC exited with code ', code) log('VLC exited with code ', code)
if (code === 0) { if (code === 0) {
windows.main.send('dispatch', 'backToList') windows.main.dispatch('backToList')
} else { } else {
windows.main.send('dispatch', 'vlcNotFound') windows.main.dispatch('vlcNotFound')
} }
vlcProcess = null vlcProcess = null
}) })
vlcProcess.on('error', function (e) { vlcProcess.on('error', function (e) {
console.log('VLC error', e) log('VLC error', e)
}) })
}) })
}) })
ipcMain.on('vlcQuit', function () { ipc.on('vlcQuit', function () {
if (!vlcProcess) return if (!vlcProcess) return
console.log('Killing VLC, pid ' + vlcProcess.pid) log('Killing VLC, pid ' + vlcProcess.pid)
vlcProcess.kill('SIGKILL') // kill -9 vlcProcess.kill('SIGKILL') // kill -9
vlcProcess = null vlcProcess = null
}) })
// Capture all events // Capture all events
var oldEmit = ipcMain.emit var oldEmit = ipc.emit
ipcMain.emit = function (name, e, ...args) { ipc.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window // Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) { if (name.startsWith('wt-') && !app.isQuitting) {
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') { if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
@@ -168,82 +174,6 @@ function init () {
} }
// Emit all other events normally // Emit all other events normally
oldEmit.call(ipcMain, name, e, ...args) oldEmit.call(ipc, name, e, ...args)
}
}
function setBounds (bounds, maximize) {
// Do nothing in fullscreen
if (!windows.main || windows.main.isFullScreen()) {
log('setBounds: not setting bounds because we\'re in full screen')
return
}
// Maximize or minimize, if the second argument is present
var willBeMaximized
if (maximize === true) {
if (!windows.main.isMaximized()) {
log('setBounds: maximizing')
windows.main.maximize()
}
willBeMaximized = true
} else if (maximize === false) {
if (windows.main.isMaximized()) {
log('setBounds: unmaximizing')
windows.main.unmaximize()
}
willBeMaximized = false
} else {
willBeMaximized = windows.main.isMaximized()
}
// Assuming we're not maximized or maximizing, set the window size
if (!willBeMaximized) {
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen
var scr = electron.screen.getDisplayMatching(windows.main.getBounds())
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
log('setBounds: centered to ' + JSON.stringify(bounds))
}
windows.main.setBounds(bounds, true)
} else {
log('setBounds: not setting bounds because of window maximization')
}
}
function setAspectRatio (aspectRatio) {
log('setAspectRatio %o', aspectRatio)
if (windows.main) {
windows.main.setAspectRatio(aspectRatio)
}
}
// Display string in dock badging area (OS X)
function setBadge (text) {
log('setBadge %s', text)
if (app.dock) {
app.dock.setBadge(String(text))
}
}
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
function setProgress (progress) {
log('setProgress %s', progress)
if (windows.main) {
windows.main.setProgressBar(progress)
}
}
function blockPowerSave () {
powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep')
log('blockPowerSave %d', powerSaveBlockerId)
}
function unblockPowerSave () {
if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) {
electron.powerSaveBlocker.stop(powerSaveBlockerId)
log('unblockPowerSave %d', powerSaveBlockerId)
} }
} }

View File

@@ -9,7 +9,6 @@ module.exports.error = error
*/ */
var electron = require('electron') var electron = require('electron')
var windows = require('./windows') var windows = require('./windows')
var app = electron.app var app = electron.app
@@ -18,7 +17,7 @@ function log (...args) {
if (app.ipcReady) { if (app.ipcReady) {
windows.main.send('log', ...args) windows.main.send('log', ...args)
} else { } else {
app.on('ipcReady', () => windows.main.send('log', ...args)) app.once('ipcReady', () => windows.main.send('log', ...args))
} }
} }
@@ -26,6 +25,6 @@ function error (...args) {
if (app.ipcReady) { if (app.ipcReady) {
windows.main.send('error', ...args) windows.main.send('error', ...args)
} else { } else {
app.on('ipcReady', () => windows.main.send('error', ...args)) app.once('ipcReady', () => windows.main.send('error', ...args))
} }
} }

View File

@@ -2,15 +2,10 @@ module.exports = {
init, init,
onPlayerClose, onPlayerClose,
onPlayerOpen, onPlayerOpen,
onToggleAlwaysOnTop,
onToggleFullScreen, onToggleFullScreen,
onWindowHide, onWindowBlur,
onWindowShow, onWindowFocus
// TODO: move these out of menu.js -- they don't belong here
showOpenSeedFiles,
showOpenTorrentAddress,
showOpenTorrentFile,
toggleFullScreen
} }
var electron = require('electron') var electron = require('electron')
@@ -18,207 +13,72 @@ var electron = require('electron')
var app = electron.app var app = electron.app
var config = require('../config') var config = require('../config')
var log = require('./log') var dialog = require('./dialog')
var shell = require('./shell')
var windows = require('./windows') var windows = require('./windows')
var thumbnail = require('./thumbnail')
var appMenu var menu
function init () { function init () {
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate()) menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(appMenu) electron.Menu.setApplicationMenu(menu)
if (app.dock) {
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
app.dock.setMenu(dockMenu)
}
}
function toggleFullScreen (flag) {
log('toggleFullScreen %s', flag)
if (windows.main && windows.main.isVisible()) {
flag = flag != null ? flag : !windows.main.isFullScreen()
if (flag) {
// Allows the window to use the full screen in fullscreen mode (OS X).
windows.main.setAspectRatio(0)
}
windows.main.setFullScreen(flag)
}
}
// Sets whether the window should always show on top of other windows
function toggleFloatOnTop (flag) {
log('toggleFloatOnTop %s', flag)
if (windows.main) {
flag = flag != null ? flag : !windows.main.isAlwaysOnTop()
windows.main.setAlwaysOnTop(flag)
getMenuItem('Float on Top').checked = flag
}
}
function toggleDevTools () {
log('toggleDevTools')
if (windows.main) {
windows.main.toggleDevTools()
}
}
function showWebTorrentWindow () {
log('showWebTorrentWindow')
windows.webtorrent.show()
windows.webtorrent.webContents.openDevTools({ detach: true })
}
function playPause () {
if (windows.main) {
windows.main.send('dispatch', 'playPause')
}
}
function increaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', 0.1)
}
}
function decreaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', -0.1)
}
}
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 () {
log('onWindowShow')
getMenuItem('Full Screen').enabled = true
getMenuItem('Float on Top').enabled = true
}
function onWindowHide () {
log('onWindowHide')
getMenuItem('Full Screen').enabled = false
getMenuItem('Float on Top').enabled = false
}
function onPlayerOpen () {
log('onPlayerOpen')
getMenuItem('Play/Pause').enabled = true
getMenuItem('Increase 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 () {
log('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 Forward').enabled = false
getMenuItem('Step Backward').enabled = false getMenuItem('Step Backward').enabled = false
getMenuItem('Increase Speed').enabled = false getMenuItem('Increase Speed').enabled = false
getMenuItem('Decrease Speed').enabled = false getMenuItem('Decrease Speed').enabled = false
getMenuItem('Add Subtitles File...').enabled = false
thumbnail.showPlayerThumbnailBar()
} }
function onToggleFullScreen (isFullScreen) { function onPlayerOpen () {
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen() getMenuItem('Play/Pause').enabled = true
windows.main.setMenuBarVisibility(!isFullScreen) getMenuItem('Increase Volume').enabled = true
getMenuItem('Full Screen').checked = isFullScreen getMenuItem('Decrease Volume').enabled = true
windows.main.send('fullscreenChanged', isFullScreen) getMenuItem('Step Forward').enabled = true
getMenuItem('Step Backward').enabled = true
getMenuItem('Increase Speed').enabled = true
getMenuItem('Decrease Speed').enabled = true
getMenuItem('Add Subtitles File...').enabled = true
thumbnail.hidePlayerThumbnailBar()
}
function onToggleAlwaysOnTop (flag) {
getMenuItem('Float on Top').checked = flag
}
function onToggleFullScreen (flag) {
getMenuItem('Full Screen').checked = flag
}
function onWindowBlur () {
getMenuItem('Full Screen').enabled = false
getMenuItem('Float on Top').enabled = false
}
function onWindowFocus () {
getMenuItem('Full Screen').enabled = true
getMenuItem('Float on Top').enabled = true
} }
function getMenuItem (label) { function getMenuItem (label) {
for (var i = 0; i < appMenu.items.length; i++) { for (var i = 0; i < menu.items.length; i++) {
var menuItem = appMenu.items[i].submenu.items.find(function (item) { var menuItem = menu.items[i].submenu.items.find(function (item) {
return item.label === label return item.label === label
}) })
if (menuItem) return menuItem if (menuItem) return menuItem
} }
} }
// Prompts the user for a file, then creates a torrent. Only allows a single file function getMenuTemplate () {
// selection.
function showOpenSeedFile () {
electron.dialog.showOpenDialog({
title: 'Select a file for the torrent file.',
properties: [ 'openFile' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
var selectedPath = selectedPaths[0]
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
})
}
// Prompts the user for a file or directory, then creates a torrent. Only allows a single
// selection. To create a multi-file torrent, the user must select a directory.
function showOpenSeedFiles () {
electron.dialog.showOpenDialog({
title: 'Select a file or folder for the torrent file.',
properties: [ 'openFile', 'openDirectory' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
var selectedPath = selectedPaths[0]
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
})
}
// Prompts the user to choose a torrent file, then adds it to the app
function showOpenTorrentFile () {
electron.dialog.showOpenDialog(windows.main, {
title: 'Select a .torrent file to open.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(function (selectedPath) {
windows.main.send('dispatch', 'addTorrent', selectedPath)
})
})
}
// Prompts the user for the URL of a torrent file, then downloads and adds it
function showOpenTorrentAddress () {
windows.main.send('showOpenTorrentAddress')
}
function getAppMenuTemplate () {
var template = [ var template = [
{ {
label: 'File', label: 'File',
@@ -228,17 +88,17 @@ function getAppMenuTemplate () {
? 'Create New Torrent...' ? 'Create New Torrent...'
: 'Create New Torrent from Folder...', : 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N', accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles click: () => dialog.openSeedDirectory()
}, },
{ {
label: 'Open Torrent File...', label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O', accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile click: () => dialog.openTorrentFile()
}, },
{ {
label: 'Open Torrent Address...', label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U', accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress click: () => dialog.openTorrentAddress()
}, },
{ {
type: 'separator' type: 'separator'
@@ -281,7 +141,7 @@ function getAppMenuTemplate () {
{ {
label: 'Preferences', label: 'Preferences',
accelerator: 'CmdOrCtrl+,', accelerator: 'CmdOrCtrl+,',
click: () => showPreferences() click: () => windows.main.dispatch('preferences')
} }
] ]
}, },
@@ -294,12 +154,20 @@ function getAppMenuTemplate () {
accelerator: process.platform === 'darwin' accelerator: process.platform === 'darwin'
? 'Ctrl+Command+F' ? 'Ctrl+Command+F'
: 'F11', : 'F11',
click: () => toggleFullScreen() click: () => windows.main.toggleFullScreen()
}, },
{ {
label: 'Float on Top', label: 'Float on Top',
type: 'checkbox', type: 'checkbox',
click: () => toggleFloatOnTop() click: () => windows.main.toggleAlwaysOnTop()
},
{
type: 'separator'
},
{
label: 'Go Back',
accelerator: 'Esc',
click: () => windows.main.dispatch('escapeBack')
}, },
{ {
type: 'separator' type: 'separator'
@@ -312,14 +180,14 @@ function getAppMenuTemplate () {
accelerator: process.platform === 'darwin' accelerator: process.platform === 'darwin'
? 'Alt+Command+I' ? 'Alt+Command+I'
: 'Ctrl+Shift+I', : 'Ctrl+Shift+I',
click: toggleDevTools click: () => windows.main.toggleDevTools()
}, },
{ {
label: 'Show WebTorrent Process', label: 'Show WebTorrent Process',
accelerator: process.platform === 'darwin' accelerator: process.platform === 'darwin'
? 'Alt+Command+P' ? 'Alt+Command+P'
: 'Ctrl+Shift+P', : 'Ctrl+Shift+P',
click: showWebTorrentWindow click: () => windows.webtorrent.toggleDevTools()
} }
] ]
} }
@@ -330,8 +198,8 @@ function getAppMenuTemplate () {
submenu: [ submenu: [
{ {
label: 'Play/Pause', label: 'Play/Pause',
accelerator: 'CmdOrCtrl+P', accelerator: 'Space',
click: playPause, click: () => windows.main.dispatch('playPause'),
enabled: false enabled: false
}, },
{ {
@@ -340,13 +208,13 @@ function getAppMenuTemplate () {
{ {
label: 'Increase Volume', label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up', accelerator: 'CmdOrCtrl+Up',
click: increaseVolume, click: () => windows.main.dispatch('changeVolume', 0.1),
enabled: false enabled: false
}, },
{ {
label: 'Decrease Volume', label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down', accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume, click: () => windows.main.dispatch('changeVolume', -0.1),
enabled: false enabled: false
}, },
{ {
@@ -354,14 +222,18 @@ function getAppMenuTemplate () {
}, },
{ {
label: 'Step Forward', label: 'Step Forward',
accelerator: 'CmdOrCtrl+Alt+Right', accelerator: process.platform === 'darwin'
click: skipForward, ? 'CmdOrCtrl+Alt+Right'
: 'Alt+Right',
click: () => windows.main.dispatch('skip', 1),
enabled: false enabled: false
}, },
{ {
label: 'Step Backward', label: 'Step Backward',
accelerator: 'CmdOrCtrl+Alt+Left', accelerator: process.platform === 'darwin'
click: skipBack, ? 'CmdOrCtrl+Alt+Left'
: 'Alt+Left',
click: () => windows.main.dispatch('skip', -1),
enabled: false enabled: false
}, },
{ {
@@ -370,13 +242,13 @@ function getAppMenuTemplate () {
{ {
label: 'Increase Speed', label: 'Increase Speed',
accelerator: 'CmdOrCtrl+=', accelerator: 'CmdOrCtrl+=',
click: increasePlaybackRate, click: () => windows.main.dispatch('changePlaybackRate', 1),
enabled: false enabled: false
}, },
{ {
label: 'Decrease Speed', label: 'Decrease Speed',
accelerator: 'CmdOrCtrl+-', accelerator: 'CmdOrCtrl+-',
click: decreasePlaybackRate, click: () => windows.main.dispatch('changePlaybackRate', -1),
enabled: false enabled: false
}, },
{ {
@@ -384,7 +256,7 @@ function getAppMenuTemplate () {
}, },
{ {
label: 'Add Subtitles File...', label: 'Add Subtitles File...',
click: openSubtitles, click: () => windows.main.dispatch('openSubtitles'),
enabled: false enabled: false
} }
] ]
@@ -395,18 +267,18 @@ function getAppMenuTemplate () {
submenu: [ submenu: [
{ {
label: 'Learn more about ' + config.APP_NAME, label: 'Learn more about ' + config.APP_NAME,
click: () => electron.shell.openExternal(config.HOME_PAGE_URL) click: () => shell.openExternal(config.HOME_PAGE_URL)
}, },
{ {
label: 'Contribute on GitHub', label: 'Contribute on GitHub',
click: () => electron.shell.openExternal(config.GITHUB_URL) click: () => shell.openExternal(config.GITHUB_URL)
}, },
{ {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Report an Issue...', label: 'Report an Issue...',
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES) click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
} }
] ]
} }
@@ -427,7 +299,7 @@ function getAppMenuTemplate () {
{ {
label: 'Preferences', label: 'Preferences',
accelerator: 'Cmd+,', accelerator: 'Cmd+,',
click: () => showPreferences() click: () => windows.main.dispatch('preferences')
}, },
{ {
type: 'separator' type: 'separator'
@@ -486,12 +358,13 @@ function getAppMenuTemplate () {
}) })
} }
// In Linux and Windows it is not possible to open both folders and files // On Windows and Linux, open dialogs do not support selecting both files and
// folders and files, so add an extra menu item so there is one for each type.
if (process.platform === 'linux' || process.platform === 'win32') { 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...',
click: showOpenSeedFile click: () => dialog.openSeedFile()
}) })
// Help menu (Windows, Linux) // Help menu (Windows, Linux)
@@ -501,12 +374,12 @@ function getAppMenuTemplate () {
}, },
{ {
label: 'About ' + config.APP_NAME, label: 'About ' + config.APP_NAME,
click: windows.createAboutWindow click: () => windows.about.init()
} }
) )
} }
// Add "File > Quit" menu item so Linux distros where the system tray icon is missing // Add "File > Quit" menu item so Linux distros where the system tray icon is
// will have a way to quit the app. // missing will have a way to quit the app.
if (process.platform === 'linux') { if (process.platform === 'linux') {
// File menu (Linux) // File menu (Linux)
template[0].submenu.push({ template[0].submenu.push({
@@ -517,23 +390,3 @@ function getAppMenuTemplate () {
return template return template
} }
function getDockMenuTemplate () {
return [
{
label: 'Create New Torrent...',
accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
}
]
}

View File

@@ -0,0 +1,30 @@
module.exports = {
start,
stop
}
var electron = require('electron')
var log = require('./log')
var blockId = 0
/**
* Block the system from entering low-power (sleep) mode or turning off the
* display.
*/
function start () {
stop() // Stop the previous power saver block, if one exists.
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
log(`powerSaveBlocker.start: ${blockId}`)
}
/**
* Stop blocking the system from entering low-power mode.
*/
function stop () {
if (!electron.powerSaveBlocker.isStarted(blockId)) {
return
}
electron.powerSaveBlocker.stop(blockId)
log(`powerSaveBlocker.stop: ${blockId}`)
}

41
main/shell.js Normal file
View File

@@ -0,0 +1,41 @@
module.exports = {
openExternal,
openItem,
showItemInFolder,
moveItemToTrash
}
var electron = require('electron')
var log = require('./log')
/**
* Open the given external protocol URL in the desktops default manner.
*/
function openExternal (url) {
log(`openExternal: ${url}`)
electron.shell.openExternal(url)
}
/**
* Open the given file in the desktops default manner.
*/
function openItem (path) {
log(`openItem: ${path}`)
electron.shell.openItem(path)
}
/**
* Show the given file in a file manager. If possible, select the file.
*/
function showItemInFolder (path) {
log(`showItemInFolder: ${path}`)
electron.shell.showItemInFolder(path)
}
/**
* Move the given file to trash and returns a boolean status for the operation.
*/
function moveItemToTrash (path) {
log(`moveItemToTrash: ${path}`)
electron.shell.moveItemToTrash(path)
}

View File

@@ -1,34 +1,20 @@
module.exports = { module.exports = {
init,
onPlayerClose, onPlayerClose,
onPlayerOpen onPlayerOpen
} }
var electron = require('electron') var electron = require('electron')
var menu = require('./menu')
var windows = require('./windows') var windows = require('./windows')
function init () {
var localShortcut = require('electron-localshortcut')
// Alternate shortcuts. Most shortcuts are registered in menu,js, but Electron
// does not support multiple shortcuts for a single menu item.
localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen)
localShortcut.register('Space', () => windows.main.send('dispatch', 'playPause'))
// Hidden shortcuts, i.e. not shown in the menu
localShortcut.register('Esc', () => windows.main.send('dispatch', 'escapeBack'))
}
function onPlayerOpen () { function onPlayerOpen () {
// Register special "media key" for play/pause, available on some keyboards // Register play/pause media key, available on some keyboards.
electron.globalShortcut.register( electron.globalShortcut.register(
'MediaPlayPause', 'MediaPlayPause',
() => windows.main.send('dispatch', 'playPause') () => windows.main.dispatch('playPause')
) )
} }
function onPlayerClose () { function onPlayerClose () {
// Return the media key to the OS, so other apps can use it.
electron.globalShortcut.unregister('MediaPlayPause') electron.globalShortcut.unregister('MediaPlayPause')
} }

View File

@@ -12,8 +12,8 @@ var app = electron.app
var handlers = require('./handlers') var handlers = require('./handlers')
var exeName = path.basename(process.execPath) var EXE_NAME = path.basename(process.execPath)
var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe') var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
function handleEvent (cmd) { function handleEvent (cmd) {
if (cmd === '--squirrel-install') { if (cmd === '--squirrel-install') {
@@ -61,15 +61,17 @@ function handleEvent (cmd) {
} }
if (cmd === '--squirrel-firstrun') { if (cmd === '--squirrel-firstrun') {
// This is called on the app's first run. Do not quit, allow startup to continue. // App is running for the first time. Do not quit, allow startup to continue.
return false return false
} }
return false return false
} }
// Spawn a command and invoke the callback when it completes with an error and the output /**
// from standard out. * Spawn a command and invoke the callback when it completes with an error and
* the output from standard out.
*/
function spawn (command, args, cb) { function spawn (command, args, cb) {
var stdout = '' var stdout = ''
@@ -99,24 +101,31 @@ function spawn (command, args, cb) {
}) })
} }
// Spawn Squirrel's Update.exe with the given arguments and invoke the callback when the /**
// command completes. * Spawn the Squirrel `Update.exe` command with the given arguments and invoke
* the callback when the command completes.
*/
function spawnUpdate (args, cb) { function spawnUpdate (args, cb) {
spawn(updateDotExe, args, cb) spawn(UPDATE_EXE, args, cb)
} }
// Create desktop/start menu shortcuts using the Squirrel Update.exe command line API /**
* Create desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function createShortcuts (cb) { function createShortcuts (cb) {
spawnUpdate(['--createShortcut', exeName], cb) spawnUpdate(['--createShortcut', EXE_NAME], cb)
} }
// Update desktop/start menu shortcuts using the Squirrel Update.exe command line API /**
* Update desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function updateShortcuts (cb) { function updateShortcuts (cb) {
var homeDir = os.homedir() var homeDir = os.homedir()
if (homeDir) { if (homeDir) {
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk') var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
// Check if the desktop shortcut has been previously deleted and and keep it deleted // If the desktop shortcut was deleted by the user, then keep it deleted.
// if it was
fs.access(desktopShortcutPath, function (err) { fs.access(desktopShortcutPath, function (err) {
var desktopShortcutExists = !err var desktopShortcutExists = !err
createShortcuts(function () { createShortcuts(function () {
@@ -133,7 +142,10 @@ function updateShortcuts (cb) {
} }
} }
// Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API /**
* Remove desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function removeShortcuts (cb) { function removeShortcuts (cb) {
spawnUpdate(['--removeShortcut', exeName], cb) spawnUpdate(['--removeShortcut', EXE_NAME], cb)
} }

35
main/thumbnail.js Normal file
View File

@@ -0,0 +1,35 @@
module.exports = {
showPlayerThumbnailBar,
hidePlayerThumbnailBar,
updateThumbarButtons
}
var path = require('path')
var config = require('../config')
var windows = require('./windows')
// gets called on player open
function showPlayerThumbnailBar () {
updateThumbarButtons(false)
}
// gets called on player close
function hidePlayerThumbnailBar () {
windows.main.win.setThumbarButtons([])
}
function updateThumbarButtons (isPaused) {
var icon = isPaused ? 'PlayThumbnailBarButton.png' : 'PauseThumbnailBarButton.png'
var tooltip = isPaused ? 'Play' : 'Pause'
var buttons = [
{
tooltip: tooltip,
icon: path.join(config.STATIC_PATH, icon),
click: function () {
windows.main.send('dispatch', 'playPause')
}
}
]
windows.main.win.setThumbarButtons(buttons)
}

View File

@@ -1,51 +1,62 @@
module.exports = { module.exports = {
hasTray,
init, init,
hasTray onWindowBlur,
onWindowFocus
} }
var cp = require('child_process')
var path = require('path')
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
var config = require('../config')
var windows = require('./windows') var windows = require('./windows')
var trayIcon var tray
function init () { function init () {
// OS X has no tray icon
if (process.platform === 'darwin') return
// On Linux, asynchronously check for libappindicator1
if (process.platform === 'linux') { if (process.platform === 'linux') {
checkLinuxTraySupport(function (supportsTray) { initLinux()
if (supportsTray) createTrayIcon()
})
} }
if (process.platform === 'win32') {
// Windows always supports minimize-to-tray initWin32()
if (process.platform === 'win32') createTrayIcon() }
// OS X apps generally do not have menu bar icons
} }
/**
* Returns true if there a tray icon is active.
*/
function hasTray () { function hasTray () {
return !!trayIcon return !!tray
} }
function createTrayIcon () { function onWindowBlur () {
trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png')) if (!tray) return
// On Windows, left click to open the app, right click for context menu
// On Linux, any click (right or left) opens the context menu
trayIcon.on('click', showApp)
// Show the tray context menu, and keep the available commands up to date
updateTrayMenu() updateTrayMenu()
windows.main.on('show', updateTrayMenu)
windows.main.on('hide', updateTrayMenu)
} }
function onWindowFocus () {
if (!tray) return
updateTrayMenu()
}
function initLinux () {
checkLinuxTraySupport(function (supportsTray) {
if (supportsTray) createTray()
})
}
function initWin32 () {
createTray()
}
/**
* Check for libappindicator1 support before creating tray icon
*/
function checkLinuxTraySupport (cb) { function checkLinuxTraySupport (cb) {
var cp = require('child_process')
// Check that we're on Ubuntu (or another debian system) and that we have // Check that we're on Ubuntu (or another debian system) and that we have
// libappindicator1. If WebTorrent was installed from the deb file, we should // libappindicator1. If WebTorrent was installed from the deb file, we should
// always have it. If it was installed from the zip file, we might not. // always have it. If it was installed from the zip file, we might not.
@@ -57,25 +68,48 @@ function checkLinuxTraySupport (cb) {
}) })
} }
function createTray () {
tray = new electron.Tray(getIconPath())
// On Windows, left click opens the app, right click opens the context menu.
// On Linux, any click (left or right) opens the context menu.
tray.on('click', () => windows.main.show())
// Show the tray context menu, and keep the available commands up to date
updateTrayMenu()
}
function updateTrayMenu () { function updateTrayMenu () {
var showHideMenuItem var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
if (windows.main.isVisible()) { tray.setContextMenu(contextMenu)
showHideMenuItem = { label: 'Hide to tray', click: hideApp } }
} else {
showHideMenuItem = { label: 'Show', click: showApp } function getMenuTemplate () {
return [
getToggleItem(),
{
label: 'Quit',
click: () => app.quit()
}
]
function getToggleItem () {
if (windows.main.win.isVisible()) {
return {
label: 'Hide to tray',
click: () => windows.main.hide()
}
} else {
return {
label: 'Show WebTorrent',
click: () => windows.main.show()
}
}
} }
var contextMenu = electron.Menu.buildFromTemplate([
showHideMenuItem,
{ label: 'Quit', click: () => app.quit() }
])
trayIcon.setContextMenu(contextMenu)
} }
function showApp () { function getIconPath () {
windows.main.show() return process.platform === 'win32'
} ? config.APP_ICON + '.ico'
: config.APP_ICON + '.png'
function hideApp () {
windows.main.hide()
windows.main.send('dispatch', 'backToList')
} }

View File

@@ -21,27 +21,27 @@ 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
// `show the user a modal notification. // updates and show the user a modal notification.
function initLinux () { function initLinux () {
get.concat(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}`)
if (res.statusCode === 200) { if (res.statusCode === 200) {
// Update available // Update available
try { try {
data = JSON.parse(data) data = JSON.parse(data)
} catch (err) { } catch (err) {
return log(`Update error: Invalid JSON response: ${err.message}`) return log(`Update error: Invalid JSON response: ${err.message}`)
}
windows.main.send('dispatch', 'updateAvailable', data.version)
} else if (res.statusCode === 204) {
// No update available
} else {
// Unexpected status code
log(`Update error: Unexpected status code: ${res.statusCode}`)
} }
windows.main.dispatch('updateAvailable', data.version)
} else if (res.statusCode === 204) {
// No update available
} else {
// Unexpected status code
log(`Update error: Unexpected status code: ${res.statusCode}`)
} }
} }

View File

@@ -1,139 +0,0 @@
var windows = module.exports = {
about: null,
main: null,
createAboutWindow,
createWebTorrentHiddenWindow,
createMainWindow,
focusWindow
}
var electron = require('electron')
var app = electron.app
var config = require('../config')
var menu = require('./menu')
var tray = require('./tray')
function createAboutWindow () {
if (windows.about) {
return focusWindow(windows.about)
}
var win = windows.about = new electron.BrowserWindow({
backgroundColor: '#ECECEC',
show: false,
center: true,
resizable: false,
icon: config.APP_ICON + '.png',
title: process.platform !== 'darwin'
? 'About ' + config.APP_WINDOW_TITLE
: '',
useContentSize: true, // Specify web page size without OS chrome
width: 300,
height: 170,
minimizable: false,
maximizable: false,
fullscreen: false,
skipTaskbar: true
})
win.loadURL(config.WINDOW_ABOUT)
// No window menu
win.setMenu(null)
win.webContents.on('did-finish-load', function () {
win.show()
})
win.once('closed', function () {
windows.about = null
})
}
function createWebTorrentHiddenWindow () {
var win = windows.webtorrent = new electron.BrowserWindow({
backgroundColor: '#1E1E1E',
show: false,
center: true,
title: 'webtorrent-hidden-window',
useContentSize: true,
width: 150,
height: 150,
minimizable: false,
maximizable: false,
resizable: false,
fullscreenable: false,
fullscreen: false,
skipTaskbar: true
})
win.loadURL(config.WINDOW_WEBTORRENT)
// Prevent killing the WebTorrent process
win.on('close', function (e) {
if (!app.isQuitting) {
e.preventDefault()
win.hide()
}
})
win.once('closed', function () {
windows.webtorrent = null
})
}
var HEADER_HEIGHT = 37
var TORRENT_HEIGHT = 100
function createMainWindow () {
if (windows.main) {
return focusWindow(windows.main)
}
var win = windows.main = new electron.BrowserWindow({
backgroundColor: '#1E1E1E',
darkTheme: true, // Forces dark theme (GTK+3)
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
minWidth: config.WINDOW_MIN_WIDTH,
minHeight: config.WINDOW_MIN_HEIGHT,
show: false, // Hide window until renderer sends 'ipcReady' event
title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
useContentSize: true, // Specify web page size without OS chrome
width: 500,
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
})
win.loadURL(config.WINDOW_MAIN)
if (process.platform === 'darwin') {
win.setSheetOffset(HEADER_HEIGHT)
}
win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen()
})
win.on('blur', menu.onWindowHide)
win.on('focus', menu.onWindowShow)
win.on('enter-full-screen', () => menu.onToggleFullScreen(true))
win.on('leave-full-screen', () => menu.onToggleFullScreen(false))
win.on('close', function (e) {
if (process.platform !== 'darwin' && !tray.hasTray()) {
app.quit()
} else if (!app.isQuitting) {
e.preventDefault()
win.hide()
win.send('dispatch', 'backToList')
}
})
win.once('closed', function () {
windows.main = null
})
}
function focusWindow (win) {
if (win.isMinimized()) {
win.restore()
}
win.show() // shows and gives focus
}

48
main/windows/about.js Normal file
View File

@@ -0,0 +1,48 @@
var about = module.exports = {
init,
win: null
}
var config = require('../../config')
var electron = require('electron')
function init () {
if (about.win) {
return about.win.show()
}
var win = about.win = new electron.BrowserWindow({
backgroundColor: '#ECECEC',
center: true,
fullscreen: false,
height: 170,
icon: getIconPath(),
maximizable: false,
minimizable: false,
resizable: false,
show: false,
skipTaskbar: true,
title: 'About ' + config.APP_WINDOW_TITLE,
useContentSize: true,
width: 300
})
win.loadURL(config.WINDOW_ABOUT)
// No menu on the About window
win.setMenu(null)
win.webContents.once('did-finish-load', function () {
win.show()
})
win.once('closed', function () {
about.win = null
})
}
function getIconPath () {
return process.platform === 'win32'
? config.APP_ICON + '.ico'
: config.APP_ICON + '.png'
}

3
main/windows/index.js Normal file
View File

@@ -0,0 +1,3 @@
exports.about = require('./about')
exports.main = require('./main')
exports.webtorrent = require('./webtorrent')

220
main/windows/main.js Normal file
View File

@@ -0,0 +1,220 @@
var main = module.exports = {
dispatch,
hide,
init,
send,
setAspectRatio,
setBounds,
setProgress,
setTitle,
show,
toggleAlwaysOnTop,
toggleDevTools,
toggleFullScreen,
win: null
}
var electron = require('electron')
var app = electron.app
var config = require('../../config')
var log = require('../log')
var menu = require('../menu')
var tray = require('../tray')
var HEADER_HEIGHT = 37
var TORRENT_HEIGHT = 100
function init () {
if (main.win) {
return main.win.show()
}
var win = main.win = new electron.BrowserWindow({
backgroundColor: '#1E1E1E',
darkTheme: true, // Forces dark theme (GTK+3)
icon: getIconPath(), // Window icon (Windows, Linux)
minWidth: config.WINDOW_MIN_WIDTH,
minHeight: config.WINDOW_MIN_HEIGHT,
title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide title bar (OS X)
useContentSize: true, // Specify web page size without OS chrome
width: 500,
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
})
win.loadURL(config.WINDOW_MAIN)
if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT)
win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen(main.win.isFullScreen())
})
win.on('blur', onWindowBlur)
win.on('focus', onWindowFocus)
win.on('hide', onWindowBlur)
win.on('show', onWindowFocus)
win.on('enter-full-screen', function () {
menu.onToggleFullScreen(true)
send('fullscreenChanged', true)
win.setMenuBarVisibility(false)
})
win.on('leave-full-screen', function () {
menu.onToggleFullScreen(false)
send('fullscreenChanged', false)
win.setMenuBarVisibility(true)
})
win.on('close', function (e) {
if (process.platform !== 'darwin' && !tray.hasTray()) {
app.quit()
} else if (!app.isQuitting) {
e.preventDefault()
hide()
}
})
}
function dispatch (...args) {
send('dispatch', ...args)
}
function hide () {
if (!main.win) return
main.win.send('dispatch', 'backToList')
main.win.hide()
}
function send (...args) {
if (!main.win) return
main.win.send(...args)
}
/**
* Enforce window aspect ratio. Remove with 0. (OS X)
*/
function setAspectRatio (aspectRatio) {
if (!main.win) return
main.win.setAspectRatio(aspectRatio)
}
/**
* Change the size of the window.
* TODO: Clean this up? Seems overly complicated.
*/
function setBounds (bounds, maximize) {
// Do nothing in fullscreen
if (!main.win || main.win.isFullScreen()) {
log('setBounds: not setting bounds because we\'re in full screen')
return
}
// Maximize or minimize, if the second argument is present
var willBeMaximized
if (maximize === true) {
if (!main.win.isMaximized()) {
log('setBounds: maximizing')
main.win.maximize()
}
willBeMaximized = true
} else if (maximize === false) {
if (main.win.isMaximized()) {
log('setBounds: unmaximizing')
main.win.unmaximize()
}
willBeMaximized = false
} else {
willBeMaximized = main.win.isMaximized()
}
// Assuming we're not maximized or maximizing, set the window size
if (!willBeMaximized) {
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen
var scr = electron.screen.getDisplayMatching(main.win.getBounds())
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
log('setBounds: centered to ' + JSON.stringify(bounds))
}
main.win.setBounds(bounds, true)
} else {
log('setBounds: not setting bounds because of window maximization')
}
}
/**
* Set progress bar to [0, 1]. Indeterminate when > 1. Remove with < 0.
*/
function setProgress (progress) {
if (!main.win) return
main.win.setProgressBar(progress)
}
function setTitle (title) {
if (!main.win) return
main.win.setTitle(title)
}
function show () {
if (!main.win) return
main.win.show()
}
// Sets whether the window should always show on top of other windows
function toggleAlwaysOnTop (flag) {
if (!main.win) return
if (flag == null) {
flag = !main.win.isAlwaysOnTop()
}
log(`toggleAlwaysOnTop ${flag}`)
main.win.setAlwaysOnTop(flag)
menu.onToggleAlwaysOnTop(flag)
}
function toggleDevTools () {
if (!main.win) return
log('toggleDevTools')
if (main.win.webContents.isDevToolsOpened()) {
main.win.webContents.closeDevTools()
} else {
main.win.webContents.openDevTools({ detach: true })
}
}
function toggleFullScreen (flag) {
if (!main.win || !main.win.isVisible()) {
return
}
if (flag == null) flag = !main.win.isFullScreen()
log(`toggleFullScreen ${flag}`)
if (flag) {
// Fullscreen and aspect ratio do not play well together. (OS X)
main.win.setAspectRatio(0)
}
main.win.setFullScreen(flag)
}
function onWindowBlur () {
menu.onWindowBlur()
tray.onWindowBlur()
}
function onWindowFocus () {
menu.onWindowFocus()
tray.onWindowFocus()
}
function getIconPath () {
return process.platform === 'win32'
? config.APP_ICON + '.ico'
: config.APP_ICON + '.png'
}

View File

@@ -0,0 +1,62 @@
var webtorrent = module.exports = {
init,
send,
show,
toggleDevTools,
win: null
}
var electron = require('electron')
var config = require('../../config')
var log = require('../log')
function init () {
var win = webtorrent.win = new electron.BrowserWindow({
backgroundColor: '#1E1E1E',
center: true,
fullscreen: false,
fullscreenable: false,
height: 150,
maximizable: false,
minimizable: false,
resizable: false,
show: false,
skipTaskbar: true,
title: 'webtorrent-hidden-window',
useContentSize: true,
width: 150
})
win.loadURL(config.WINDOW_WEBTORRENT)
// Prevent killing the WebTorrent process
win.on('close', function (e) {
if (electron.app.isQuitting) {
return
}
e.preventDefault()
win.hide()
})
}
function show () {
if (!webtorrent.win) return
webtorrent.win.show()
}
function send (...args) {
if (!webtorrent.win) return
webtorrent.win.send(...args)
}
function toggleDevTools () {
if (!webtorrent.win) return
log('toggleDevTools')
if (webtorrent.win.webContents.isDevToolsOpened()) {
webtorrent.win.webContents.closeDevTools()
webtorrent.win.hide()
} else {
webtorrent.win.webContents.openDevTools({ detach: true })
}
}

View File

@@ -1,10 +1,10 @@
{ {
"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.6.0", "version": "0.8.0",
"author": { "author": {
"name": "WebTorrent, LLC", "name": "WebTorrent, LLC",
"email": "feross@feross.org", "email": "feross@webtorrent.io",
"url": "https://webtorrent.io" "url": "https://webtorrent.io"
}, },
"bin": { "bin": {
@@ -14,7 +14,7 @@
"url": "https://github.com/feross/webtorrent-desktop/issues" "url": "https://github.com/feross/webtorrent-desktop/issues"
}, },
"dependencies": { "dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js", "airplayer": "^2.0.0",
"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",
@@ -22,8 +22,7 @@
"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-prebuilt": "1.2.1",
"electron-prebuilt": "1.1.1",
"fs-extra": "^0.27.0", "fs-extra": "^0.27.0",
"hyperx": "^2.0.2", "hyperx": "^2.0.2",
"iso-639-1": "^1.2.1", "iso-639-1": "^1.2.1",
@@ -31,8 +30,10 @@
"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",
"parse-torrent": "^5.7.3",
"prettier-bytes": "^1.0.1", "prettier-bytes": "^1.0.1",
"run-parallel": "^1.1.6", "run-parallel": "^1.1.6",
"semver": "^5.1.0",
"simple-concat": "^1.0.0", "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",

View File

@@ -3,8 +3,9 @@
// * Starts and stops casting, provides remote video controls // * Starts and stops casting, provides remote video controls
module.exports = { module.exports = {
init, init,
open, toggleMenu,
close, selectDevice,
stop,
play, play,
pause, pause,
seek, seek,
@@ -12,7 +13,7 @@ module.exports = {
setRate setRate
} }
var airplay = require('airplay-js') var airplayer = require('airplayer')()
var chromecasts = require('chromecasts')() var chromecasts = require('chromecasts')()
var dlnacasts = require('dlnacasts')() var dlnacasts = require('dlnacasts')()
@@ -32,25 +33,49 @@ function init (appState, callback) {
state = appState state = appState
update = callback update = callback
state.devices.chromecast = chromecastPlayer()
state.devices.dlna = dlnaPlayer()
state.devices.airplay = airplayPlayer()
// Listen for devices: Chromecast, DLNA and Airplay // Listen for devices: Chromecast, DLNA and Airplay
chromecasts.on('update', function (player) { chromecasts.on('update', function (device) {
state.devices.chromecast = chromecastPlayer(player) // TODO: how do we tell if there are *no longer* any Chromecasts available?
// From looking at the code, chromecasts.players only grows, never shrinks
state.devices.chromecast.addDevice(device)
}) })
dlnacasts.on('update', function (player) { dlnacasts.on('update', function (device) {
state.devices.dlna = dlnaPlayer(player) state.devices.dlna.addDevice(device)
}) })
var browser = airplay.createBrowser() airplayer.on('update', function (device) {
browser.on('deviceOn', function (player) { state.devices.airplay.addDevice(device)
state.devices.airplay = airplayPlayer(player) })
}).start()
} }
// chromecast player implementation // chromecast player implementation
function chromecastPlayer (player) { function chromecastPlayer () {
function addEvents () { var ret = {
player.on('error', function (err) { device: null,
addDevice,
getDevices,
open,
play,
pause,
stop,
status,
seek,
volume
}
return ret
function getDevices () {
return chromecasts.players
}
function addDevice (device) {
device.on('error', function (err) {
if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
time: new Date().getTime(), time: new Date().getTime(),
@@ -58,7 +83,8 @@ function chromecastPlayer (player) {
}) })
update() update()
}) })
player.on('disconnect', function () { device.on('disconnect', function () {
if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
}) })
@@ -66,7 +92,7 @@ function chromecastPlayer (player) {
function open () { function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
player.play(state.server.networkURL, { ret.device.play(state.server.networkURL, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) { }, function (err) {
@@ -84,19 +110,19 @@ function chromecastPlayer (player) {
} }
function play (callback) { function play (callback) {
player.play(null, null, callback) ret.device.play(null, null, callback)
} }
function pause (callback) { function pause (callback) {
player.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
player.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
player.status(function (err, status) { ret.device.status(function (err, status) {
if (err) return console.log('error getting %s status: %o', state.playing.location, err) if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED' state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime state.playing.currentTime = status.currentTime
@@ -106,36 +132,59 @@ function chromecastPlayer (player) {
} }
function seek (time, callback) { function seek (time, callback) {
player.seek(time, callback) ret.device.seek(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
player.volume(volume, callback) ret.device.volume(volume, callback)
}
addEvents()
return {
player: player,
open: open,
play: play,
pause: pause,
stop: stop,
status: status,
seek: seek,
volume: volume
} }
} }
// airplay player implementation // airplay player implementation
function airplayPlayer (player) { function airplayPlayer () {
var ret = {
device: null,
addDevice,
getDevices,
open,
play,
pause,
stop,
status,
seek,
volume
}
return ret
function addDevice (player) {
player.on('event', function (event) {
switch (event.state) {
case 'loading':
break
case 'playing':
state.playing.isPaused = false
break
case 'paused':
state.playing.isPaused = true
break
case 'stopped':
break
}
update()
})
}
function getDevices () {
return airplayer.players
}
function open () { function open () {
player.play(state.server.networkURL, 0, function (res) { ret.device.play(state.server.networkURL, function (err, res) {
if (res.statusCode !== 200) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
time: new Date().getTime(), time: new Date().getTime(),
message: 'Could not connect to AirPlay.' message: 'Could not connect to AirPlay. ' + err.message
}) })
} else { } else {
state.playing.location = 'airplay' state.playing.location = 'airplay'
@@ -145,55 +194,67 @@ function airplayPlayer (player) {
} }
function play (callback) { function play (callback) {
player.rate(1, callback) ret.device.resume(callback)
} }
function pause (callback) { function pause (callback) {
player.rate(0, callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
player.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
player.status(function (status) { ret.device.playbackInfo(function (err, res, status) {
state.playing.isPaused = status.rate === 0 if (err) {
state.playing.currentTime = status.position state.playing.location = 'local'
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume state.errors.push({
// According to docs is in [-30 - 0] (db) range time: new Date().getTime(),
// should be converted to [0 - 1] using (val / 30 + 1) message: 'Could not connect to AirPlay. ' + err.message
update() })
} else {
state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position
update()
}
}) })
} }
function seek (time, callback) { function seek (time, callback) {
player.scrub(time, callback) ret.device.scrub(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
// TODO remove line below once we can fetch the information in status update // AirPlay doesn't support volume
// TODO: We should just disable the volume slider
state.playing.volume = volume state.playing.volume = volume
volume = (volume - 1) * 30
player.volume(volume, callback)
}
return {
player: player,
open: open,
play: play,
pause: pause,
stop: stop,
status: status,
seek: seek,
volume: volume
} }
} }
// DLNA player implementation // DLNA player implementation
function dlnaPlayer (player) { function dlnaPlayer (player) {
function addEvents () { var ret = {
player.on('error', function (err) { device: null,
addDevice,
getDevices,
open,
play,
pause,
stop,
status,
seek,
volume
}
return ret
function getDevices () {
return dlnacasts.players
}
function addDevice (device) {
device.on('error', function (err) {
if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
time: new Date().getTime(), time: new Date().getTime(),
@@ -201,7 +262,8 @@ function dlnaPlayer (player) {
}) })
update() update()
}) })
player.on('disconnect', function () { device.on('disconnect', function () {
if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
}) })
@@ -209,7 +271,7 @@ function dlnaPlayer (player) {
function open () { function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
player.play(state.server.networkURL, { ret.device.play(state.server.networkURL, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0 seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
@@ -228,19 +290,19 @@ function dlnaPlayer (player) {
} }
function play (callback) { function play (callback) {
player.play(null, null, callback) ret.device.play(null, null, callback)
} }
function pause (callback) { function pause (callback) {
player.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
player.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
player.status(function (err, status) { ret.device.status(function (err, status) {
if (err) return console.log('error getting %s status: %o', state.playing.location, err) if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED' state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime state.playing.currentTime = status.currentTime
@@ -250,61 +312,78 @@ function dlnaPlayer (player) {
} }
function seek (time, callback) { function seek (time, callback) {
player.seek(time, callback) ret.device.seek(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
player.volume(volume, function (err) { ret.device.volume(volume, function (err) {
// quick volume update // quick volume update
state.playing.volume = volume state.playing.volume = volume
callback(err) callback(err)
}) })
} }
addEvents()
return {
player: player,
open: open,
play: play,
pause: pause,
stop: stop,
status: status,
seek: seek,
volume: volume
}
} }
// Start polling cast device state, whenever we're connected // Start polling cast device state, whenever we're connected
function startStatusInterval () { function startStatusInterval () {
statusInterval = setInterval(function () { statusInterval = setInterval(function () {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.status()
device.status()
}
}, 1000) }, 1000)
} }
function open (location) { /*
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
* The menu lists eg. all Chromecasts detected; the user can click one to cast.
* If the menu was already showing for that type, hides the menu.
*/
function toggleMenu (location) {
// If the menu is already showing, hide it
if (state.devices.castMenu && state.devices.castMenu.location === location) {
state.devices.castMenu = null
return
}
// Never cast to two devices at the same time
if (state.playing.location !== 'local') { if (state.playing.location !== 'local') {
throw new Error('You can\'t connect to ' + location + ' when already connected to another device') throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
} }
state.playing.location = location + '-pending' // Find all cast devices of the given type
var device = getDevice(location) var player = getPlayer(location)
if (device) { var devices = player ? player.getDevices() : []
getDevice(location).open() if (devices.length === 0) throw new Error('No ' + location + ' devices available')
startStatusInterval()
}
// Show a menu
state.devices.castMenu = {location, devices}
}
function selectDevice (index) {
var {location, devices} = state.devices.castMenu
// Start casting
var player = getPlayer(location)
player.device = devices[index]
player.open()
// Poll the casting device's status every few seconds
startStatusInterval()
// Show the Connecting... screen
state.devices.castMenu = null
state.playing.castName = devices[index].name
state.playing.location = location + '-pending'
update() update()
} }
// Stops casting, move video back to local screen // Stops casting, move video back to local screen
function close () { function stop () {
var device = getDevice() var player = getPlayer()
if (device) { if (player) {
device.stop(stoppedCasting) player.stop(function () {
player.device = null
stoppedCasting()
})
clearInterval(statusInterval) clearInterval(statusInterval)
} else { } else {
stoppedCasting() stoppedCasting()
@@ -317,8 +396,8 @@ function stoppedCasting () {
update() update()
} }
function getDevice (location) { function getPlayer (location) {
if (location && state.devices[location]) { if (location) {
return state.devices[location] return state.devices[location]
} else if (state.playing.location === 'chromecast') { } else if (state.playing.location === 'chromecast') {
return state.devices.chromecast return state.devices.chromecast
@@ -332,29 +411,25 @@ function getDevice (location) {
} }
function play () { function play () {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.play(castCallback)
device.play(castCallback)
}
} }
function pause () { function pause () {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.pause(castCallback)
device.pause(castCallback)
}
} }
function setRate (rate) { function setRate (rate) {
var device var player
var result = true var result = true
if (state.playing.location === 'chromecast') { if (state.playing.location === 'chromecast') {
// TODO find how to control playback rate on chromecast // TODO find how to control playback rate on chromecast
castCallback() castCallback()
result = false result = false
} else if (state.playing.location === 'airplay') { } else if (state.playing.location === 'airplay') {
device = state.devices.airplay player = state.devices.airplay
device.rate(rate, castCallback) player.rate(rate, castCallback)
} else { } else {
result = false result = false
} }
@@ -362,17 +437,13 @@ function setRate (rate) {
} }
function seek (time) { function seek (time) {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.seek(time, castCallback)
device.seek(time, castCallback)
}
} }
function setVolume (volume) { function setVolume (volume) {
var device = getDevice() var player = getPlayer()
if (device) { if (player) player.volume(volume, castCallback)
device.volume(volume, castCallback)
}
} }
function castCallback () { function castCallback () {

View File

@@ -1,36 +1,39 @@
module.exports = { module.exports = {
setDispatch,
dispatch, dispatch,
dispatcher dispatcher,
setDispatch
} }
// Memoize most of our event handlers, which are functions in the form var dispatchers = {}
// () => dispatch(<args>) var _dispatch = function () {}
// ... this prevents virtual-dom from updating every listener on every update()
var _dispatchers = {}
var _dispatch = () => {}
function setDispatch (dispatch) { function setDispatch (dispatch) {
_dispatch = dispatch _dispatch = dispatch
} }
// Get a _memoized event handler that calls dispatch() function dispatch (...args) {
// All args must be JSON-able _dispatch(...args)
}
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler
// function. This prevents virtual-dom from updating the listener functions on
// each update().
function dispatcher (...args) { function dispatcher (...args) {
var json = JSON.stringify(args) var str = JSON.stringify(args)
var handler = _dispatchers[json] var handler = dispatchers[str]
if (!handler) { if (!handler) {
handler = _dispatchers[json] = (e) => { handler = dispatchers[str] = function (e) {
// Don't click on whatever is below the button // Do not propagate click to elements below the button
e.stopPropagation() e.stopPropagation()
// Don't regisiter clicks on disabled buttons
if (e.currentTarget.classList.contains('disabled')) return if (e.currentTarget.classList.contains('disabled')) {
_dispatch.apply(null, args) // Ignore clicks on disabled elements
return
}
dispatch(...args)
} }
} }
return handler return handler
} }
function dispatch (...args) {
_dispatch.apply(null, args)
}

5
renderer/lib/hx.js Normal file
View File

@@ -0,0 +1,5 @@
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
module.exports = hx

View File

@@ -0,0 +1,95 @@
/* eslint-disable camelcase */
module.exports = {
run
}
var semver = require('semver')
var config = require('../../config')
// Change `state.saved` (which will be saved back to config.json on exit) as
// needed, for example to deal with config.json format changes across versions
function run (state) {
// Replace "{ version: 1 }" with app version (semver)
if (!semver.valid(state.saved.version)) {
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
}
var version = state.saved.version
if (semver.lt(version, '0.7.0')) {
migrate_0_7_0(state.saved)
}
if (semver.lt(version, '0.7.2')) {
migrate_0_7_2(state.saved)
}
// Config is now on the new version
state.saved.version = config.APP_VERSION
}
function migrate_0_7_0 (saved) {
console.log('migrate to 0.7.0')
var fs = require('fs-extra')
var path = require('path')
saved.torrents.forEach(function (ts) {
var infoHash = ts.infoHash
// Replace torrentPath with torrentFileName
// There are a number of cases to handle here:
// * Originally we used absolute paths
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
// * Finally, now we're getting rid of torrentPath altogether
var src, dst
if (ts.torrentPath) {
console.log('replacing torrentPath %s', ts.torrentPath)
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
src = ts.torrentPath
} else {
src = path.join(config.STATIC_PATH, ts.torrentPath)
}
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
// Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once
if (src !== dst) fs.copySync(src, dst)
delete ts.torrentPath
ts.torrentFileName = infoHash + '.torrent'
}
// Replace posterURL with posterFileName
if (ts.posterURL) {
console.log('replacing posterURL %s', ts.posterURL)
var extension = path.extname(ts.posterURL)
src = path.isAbsolute(ts.posterURL)
? ts.posterURL
: path.join(config.STATIC_PATH, ts.posterURL)
dst = path.join(config.POSTER_PATH, infoHash + extension)
// Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once
if (src !== dst) fs.copySync(src, dst)
delete ts.posterURL
ts.posterFileName = infoHash + extension
}
// Fix exception caused by incorrect file ordering.
// https://github.com/feross/webtorrent-desktop/pull/604#issuecomment-222805214
delete ts.defaultPlayFileIndex
delete ts.files
delete ts.selections
delete ts.fileModtimes
})
}
function migrate_0_7_2 (saved) {
if (!saved.prefs) {
saved.prefs = {
downloadPath: config.DEFAULT_DOWNLOAD_PATH
}
}
}

212
renderer/lib/state.js Normal file
View File

@@ -0,0 +1,212 @@
module.exports = {
getDefaultPlayState,
load,
save
}
var appConfig = require('application-config')('WebTorrent')
var path = require('path')
var config = require('../../config')
var migrations = require('./migrations')
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
function getDefaultState () {
var LocationHistory = require('./location-history')
return {
/*
* Temporary state disappears once the program exits.
* It can contain complex objects like open connections, etc.
*/
client: null, /* the WebTorrent client */
server: null, /* local WebTorrent-to-HTTP server */
prev: {}, /* used for state diffing in updateElectron() */
location: new LocationHistory(),
window: {
bounds: null, /* {x, y, width, height } */
isFocused: true,
isFullScreen: false,
title: config.APP_WINDOW_TITLE
},
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
devices: {}, /* playback devices like Chromecast and AppleTV */
dock: {
badge: 0,
progress: 0
},
modal: null, /* modal popover */
errors: [], /* user-facing errors */
nextTorrentKey: 1, /* identify torrents for IPC between the main and webtorrent windows */
/*
* Saved state is read from and written to a file every time the app runs.
* It should be simple and minimal and must be JSON.
* It must never contain absolute paths since we have a portable app.
*
* Config path:
*
* OS X ~/Library/Application Support/WebTorrent/config.json
* Linux (XDG) $XDG_CONFIG_HOME/WebTorrent/config.json
* Linux (Legacy) ~/.config/WebTorrent/config.json
* Windows (> Vista) %LOCALAPPDATA%/WebTorrent/config.json
* Windows (XP, 2000) %USERPROFILE%/Local Settings/Application Data/WebTorrent/config.json
*
* Also accessible via `require('application-config')('WebTorrent').filePath`
*/
saved: {},
/*
* Getters, for convenience
*/
getPlayingTorrentSummary,
getPlayingFileSummary
}
}
/* Whenever we stop playing video or audio, here's what we reset state.playing to */
function getDefaultPlayState () {
return {
infoHash: null, /* the info hash of the torrent we're playing */
fileIndex: null, /* the zero-based index within the torrent */
location: 'local', /* 'local', 'chromecast', 'airplay' */
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
currentTime: 0, /* seconds */
duration: 1, /* seconds */
isPaused: true,
isStalled: false,
lastTimeUpdate: 0, /* Unix time in ms */
mouseStationarySince: 0, /* Unix time in ms */
playbackRate: 1,
subtitles: {
tracks: [], /* subtitle tracks, each {label, language, ...} */
selectedIndex: -1, /* current subtitle track */
showMenu: false /* popover menu, above the video */
},
aspectRatio: 0 /* aspect ratio of the video */
}
}
/* If the saved state file doesn't exist yet, here's what we use instead */
function setupSavedState (cb) {
var fs = require('fs-extra')
var parseTorrent = require('parse-torrent')
var parallel = require('run-parallel')
var saved = {
prefs: {
downloadPath: config.DEFAULT_DOWNLOAD_PATH
},
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
}
var tasks = []
config.DEFAULT_TORRENTS.map(function (t, i) {
var infoHash = saved.torrents[i].infoHash
tasks.push(function (cb) {
fs.copy(
path.join(config.STATIC_PATH, t.posterFileName),
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)),
cb
)
})
tasks.push(function (cb) {
fs.copy(
path.join(config.STATIC_PATH, t.torrentFileName),
path.join(config.TORRENT_PATH, infoHash + '.torrent'),
cb
)
})
})
parallel(tasks, function (err) {
if (err) return cb(err)
cb(null, saved)
})
function createTorrentObject (t) {
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
var parsedTorrent = parseTorrent(torrent)
return {
status: 'paused',
infoHash: parsedTorrent.infoHash,
name: t.name,
displayName: t.name,
posterFileName: parsedTorrent.infoHash + path.extname(t.posterFileName),
torrentFileName: parsedTorrent.infoHash + '.torrent',
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
files: parsedTorrent.files,
selections: parsedTorrent.files.map((x) => true)
}
}
}
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]
}
function load (cb) {
var state = getDefaultState()
appConfig.read(function (err, saved) {
if (err || !saved.version) {
console.log('Missing config file: Creating new one')
setupSavedState(onSaved)
} else {
onSaved(null, saved)
}
})
function onSaved (err, saved) {
if (err) return cb(err)
state.saved = saved
migrations.run(state)
cb(null, state)
}
}
// Write state.saved to the JSON state file
function save (state, cb) {
console.log('Saving state to ' + appConfig.filePath)
var electron = require('electron')
// Clean up, so that we're not saving any pending state
var copy = Object.assign({}, state.saved)
// Remove torrents pending addition to the list, where we haven't finished
// reading the torrent file or file(s) to seed & don't have an infohash
copy.torrents = copy.torrents
.filter((x) => x.infoHash)
.map(function (x) {
var torrent = {}
for (var key in x) {
if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process
}
if (key === 'playStatus') {
continue // Don't save whether a torrent is playing / pending
}
torrent[key] = x[key]
}
return torrent
})
appConfig.write(copy, function (err) {
if (err) console.error(err)
// TODO: this doesn't belong here
electron.ipcRenderer.send('savedState')
})
}

140
renderer/lib/telemetry.js Normal file
View File

@@ -0,0 +1,140 @@
// Collects anonymous usage stats and uncaught errors
// Reports back so that we can improve WebTorrent Desktop
module.exports = {
init,
logUncaughtError,
logPlayAttempt
}
const crypto = require('crypto')
const electron = require('electron')
const https = require('https')
const os = require('os')
const url = require('url')
const config = require('../../config')
var telemetry
function init (state) {
telemetry = state.saved.telemetry
if (!telemetry) {
telemetry = state.saved.telemetry = createSummary()
reset()
}
var now = new Date()
telemetry.timestamp = now.toISOString()
telemetry.localTime = now.toTimeString()
telemetry.screens = getScreenInfo()
telemetry.system = getSystemInfo()
telemetry.approxNumTorrents = getApproxNumTorrents(state)
postToServer(telemetry)
}
function reset () {
telemetry.uncaughtErrors = []
telemetry.playAttempts = {
total: 0,
success: 0,
timeout: 0,
error: 0,
abandoned: 0
}
}
function postToServer () {
// Serialize the telemetry summary
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
// POST to our server
var options = url.parse(config.TELEMETRY_URL)
options.method = 'POST'
options.headers = {
'Content-Type': 'application/json',
'Content-Length': payload.length
}
var req = https.request(options, function (res) {
if (res.statusCode === 200) {
console.log('Successfully posted telemetry summary')
reset()
} else {
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
}
})
req.on('error', function (e) {
console.error('Couldn\'t post telemetry summary', e)
})
req.write(payload)
req.end()
}
// Creates a new telemetry summary. Gives the user a unique ID,
// collects screen resolution, etc
function createSummary () {
// Make a 256-bit random unique ID
var userID = crypto.randomBytes(32).toString('hex')
return { userID }
}
// Track screen resolution
function getScreenInfo () {
return electron.screen.getAllDisplays().map((screen) => ({
width: screen.size.width,
height: screen.size.height,
scaleFactor: screen.scaleFactor
}))
}
// Track basic system info like OS version and amount of RAM
function getSystemInfo () {
return {
osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(),
architecture: os.arch(),
totalMemoryMB: os.totalmem() / (1 << 20),
numCores: os.cpus().length
}
}
// Get the number of torrents, rounded to the nearest power of two
function getApproxNumTorrents (state) {
var exactNum = state.saved.torrents.length
if (exactNum === 0) return 0
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
var log2 = Math.log(exactNum) / Math.log(2)
return 1 << Math.round(log2)
}
// An uncaught error happened in the main process or in one of the windows
function logUncaughtError (procName, err) {
var message, stack
if (typeof err === 'string') {
message = err
stack = ''
} else {
message = err.message
stack = err.stack
}
// We need to POST the telemetry object, make sure it stays < 100kb
if (telemetry.uncaughtErrors.length > 20) return
if (message.length > 1000) message = message.substring(0, 1000)
if (stack.length > 1000) stack = stack.substring(0, 1000)
telemetry.uncaughtErrors.push({process: procName, message, stack})
}
// The user pressed play. It either worked, timed out, or showed the
// "Play in VLC" codec error
function logPlayAttempt (result) {
if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) {
return console.error('Unknown play attempt result', result)
}
var attempts = telemetry.playAttempts
attempts.total = (attempts.total || 0) + 1
attempts[result] = (attempts[result] || 0) + 1
}

View File

@@ -24,7 +24,8 @@ function isVideo (file) {
'.mp4', '.mp4',
'.mpg', '.mpg',
'.ogv', '.ogv',
'.webm' '.webm',
'.wmv'
].includes(ext) ].includes(ext)
} }

View File

@@ -16,7 +16,7 @@ function torrentPoster (torrent, cb) {
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb) if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
// Third, 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', '.jpeg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb) if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
// TODO: generate a waveform from the largest sound file // TODO: generate a waveform from the largest sound file

View File

@@ -10,14 +10,14 @@ var config = require('../../config')
// Returns an absolute path to the torrent file, or null if unavailable // Returns an absolute path to the torrent file, or null if unavailable
function getTorrentPath (torrentSummary) { function getTorrentPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.torrentFileName) return null if (!torrentSummary || !torrentSummary.torrentFileName) return null
return path.join(config.CONFIG_TORRENT_PATH, torrentSummary.torrentFileName) return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
} }
// Expects a torrentSummary // Expects a torrentSummary
// Returns an absolute path to the poster image, or null if unavailable // Returns an absolute path to the poster image, or null if unavailable
function getPosterPath (torrentSummary) { function getPosterPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.posterFileName) return null if (!torrentSummary || !torrentSummary.posterFileName) return null
var posterPath = path.join(config.CONFIG_POSTER_PATH, torrentSummary.posterFileName) var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron): // Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
// Backslashes in URLS in CSS cause bizarre string encoding issues // Backslashes in URLS in CSS cause bizarre string encoding issues
return posterPath.replace(/\\/g, '/') return posterPath.replace(/\\/g, '/')

View File

@@ -65,7 +65,7 @@ table {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
background: rgb(40, 40, 40); background: rgb(40, 40, 40);
animation: fadein 1s; animation: fadein 0.5s;
} }
.app:not(.is-focused) { .app:not(.is-focused) {
@@ -260,7 +260,7 @@ table {
.modal .modal-content { .modal .modal-content {
position: fixed; position: fixed;
top: 45px; top: 38px;
left: 0; left: 0;
right: 0; right: 0;
margin: 0 auto; margin: 0 auto;
@@ -280,36 +280,36 @@ table {
width: 100%; width: 100%;
} }
.create-torrent-page { .create-torrent {
padding: 10px 25px; padding: 10px 25px;
overflow: hidden; overflow: hidden;
} }
.create-torrent-page .torrent-attribute { .create-torrent .torrent-attribute {
white-space: nowrap; white-space: nowrap;
} }
.create-torrent-page .torrent-attribute>* { .create-torrent .torrent-attribute>* {
display: inline-block; display: inline-block;
} }
.create-torrent-page .torrent-attribute label { .create-torrent .torrent-attribute label {
width: 60px; width: 60px;
margin-right: 10px; margin-right: 10px;
vertical-align: top; vertical-align: top;
} }
.create-torrent-page .torrent-attribute>div { .create-torrent .torrent-attribute>div {
width: calc(100% - 90px); width: calc(100% - 90px);
} }
.create-torrent-page .torrent-attribute div { .create-torrent .torrent-attribute div {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.create-torrent-page .torrent-attribute textarea { .create-torrent .torrent-attribute textarea {
width: calc(100% - 80px); width: calc(100% - 80px);
height: 80px; height: 80px;
color: #eee; color: #eee;
@@ -321,11 +321,11 @@ table {
padding: 4px 6px; padding: 4px 6px;
} }
.create-torrent-page textarea.torrent-trackers { .create-torrent textarea.torrent-trackers {
height: 200px; height: 200px;
} }
.create-torrent-page input.torrent-is-private { .create-torrent input.torrent-is-private {
width: initial; width: initial;
margin: 0; margin: 0;
} }
@@ -605,6 +605,10 @@ body.drag .app::after {
padding: 8em 0 20px 0; padding: 8em 0 20px 0;
} }
.torrent-details .warning {
padding-left: 15px;
}
.torrent-details table { .torrent-details table {
width: 100%; width: 100%;
white-space: nowrap; white-space: nowrap;
@@ -711,10 +715,7 @@ body.drag .app::after {
font-size: 22px; font-size: 22px;
opacity: 0.85; opacity: 0.85;
/* /* Make all icons have uniform spacing */
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
max-width: 23px; max-width: 23px;
overflow: hidden; overflow: hidden;
} }
@@ -750,7 +751,7 @@ body.drag .app::after {
opacity: 0.8; opacity: 0.8;
} }
.player .controls .icon.closed-captions { .player .controls .icon.closed-caption {
font-size: 26px; font-size: 26px;
margin-top: 6px; margin-top: 6px;
} }
@@ -818,7 +819,7 @@ body.drag .app::after {
transition-timing-function: ease-out; transition-timing-function: ease-out;
} }
.player .controls .closed-captions.active, .player .controls .closed-caption.active,
.player .controls .device.active { .player .controls .device.active {
color: #9af; color: #9af;
} }
@@ -847,6 +848,28 @@ body.drag .app::after {
height: 14px; height: 14px;
} }
.player .controls .options-list {
position: fixed;
background: rgba(40, 40, 40, 0.8);
min-width: 100px;
bottom: 45px;
right: 3px;
transition: opacity 0.15s ease-out;
padding: 5px 10px;
border-radius: 3px;
margin: 0;
list-style-type: none;
color: rgba(255, 255, 255, 0.8);
}
.player .controls .options-list .icon {
display: inline;
font-size: 17px;
vertical-align: bottom;
line-height: 21px;
margin: 4px;
}
/** /**
* Set the cue text position so it appears above the player controls. * Set the cue text position so it appears above the player controls.
*/ */
@@ -889,29 +912,6 @@ video::-webkit-media-text-track-container {
font-weight: bold; font-weight: bold;
} }
/*
* Subtitles list
*/
.subtitles-list {
position: fixed;
background: rgba(40, 40, 40, 0.8);
min-width: 100px;
bottom: 45px;
right: 3px;
transition: opacity 0.15s ease-out;
padding: 5px 10px;
border-radius: 3px;
margin: 0;
list-style-type: none;
color: rgba(255, 255, 255, 0.8);
}
.subtitles-list i {
font-size: 11px; /* make the cast icons less huge */
margin-right: 4px !important;
}
/* /*
* Preferences page, based on Atom settings style * Preferences page, based on Atom settings style
*/ */

View File

@@ -3,9 +3,9 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="index.css" charset="utf-8"> <link rel="stylesheet" href="main.css" charset="utf-8">
</head> </head>
<body> <body>
<script async src="index.js"></script> <script async src="main.js"></script>
</body> </body>
</html> </html>

View File

@@ -3,18 +3,8 @@ console.time('init')
var crashReporter = require('../crash-reporter') var crashReporter = require('../crash-reporter')
crashReporter.init() crashReporter.init()
var electron = require('electron')
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
// Listen for messages from the main process
setupIpc()
var appConfig = require('application-config')('WebTorrent')
var dragDrop = require('drag-drop') var dragDrop = require('drag-drop')
var electron = require('electron')
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 parallel = require('run-parallel')
@@ -24,43 +14,34 @@ var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff') var diff = require('virtual-dom/diff')
var patch = require('virtual-dom/patch') var patch = require('virtual-dom/patch')
var App = require('./views/app')
var config = require('../config') var config = require('../config')
var App = require('./views/app')
var telemetry = require('./lib/telemetry')
var errors = require('./lib/errors') var errors = require('./lib/errors')
var sound = require('./lib/sound') var sound = require('./lib/sound')
var State = require('./state') var State = require('./lib/state')
var TorrentPlayer = require('./lib/torrent-player') var TorrentPlayer = require('./lib/torrent-player')
var TorrentSummary = require('./lib/torrent-summary') var TorrentSummary = require('./lib/torrent-summary')
var {setDispatch} = require('./lib/dispatcher') var {setDispatch} = require('./lib/dispatcher')
setDispatch(dispatch)
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json') // Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
var state, vdomLoop
// 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
var vdomLoop init()
var state = State.getInitialState() function init () {
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 other state is ephemeral. First we load state.saved then initialize the app.
State.load(onState)
// All state lives in state.js. `state.saved` is read from and written to a file. setDispatch(dispatch)
// All other state is ephemeral. First we load state.saved then initialize the app.
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()
})
} }
/** /**
@@ -68,9 +49,12 @@ function loadState (cb) {
* 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
* the dock icon and drag+drop. * the dock icon and drag+drop.
*/ */
function init () { function onState (err, _state) {
// Clean up the freshly-loaded config file, which may be from an older version if (err) return onError(err)
cleanUpConfig() state = _state
// Add first page to 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()
@@ -89,6 +73,9 @@ function init () {
}) })
document.body.appendChild(vdomLoop.target) document.body.appendChild(vdomLoop.target)
// Listen for messages from the main process
setupIpc()
// Calling update() updates the UI given the current state // Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary // Do this at least once a second to give every file in every torrentSummary
// a progress bar and to keep the cursor in sync when playing a video // a progress bar and to keep the cursor in sync when playing a video
@@ -105,69 +92,22 @@ function init () {
window.addEventListener('focus', onFocus) window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur) window.addEventListener('blur', onBlur)
// Done! Ideally we want to get here <100ms after the user clicks the app // ...window visibility state.
sound.play('STARTUP') document.addEventListener('webkitvisibilitychange', onVisibilityChange)
// Log uncaught JS errors
window.addEventListener('error',
(e) => telemetry.logUncaughtError('window', e.error), true)
// Done! Ideally we want to get here < 500ms after the user clicks the app
sound.play('STARTUP')
console.timeEnd('init') console.timeEnd('init')
} }
function delayedInit () { function delayedInit () {
lazyLoadCast() lazyLoadCast()
sound.preload() sound.preload()
} telemetry.init(state)
// Change `state.saved` (which will be saved back to config.json on exit) as
// needed, for example to deal with config.json format changes across versions
function cleanUpConfig () {
state.saved.torrents.forEach(function (ts) {
var infoHash = ts.infoHash
// Migration: replace torrentPath with torrentFileName
var src, dst
if (ts.torrentPath) {
// There are a number of cases to handle here:
// * Originally we used absolute paths
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
// * Finally, now we're getting rid of torrentPath altogether
console.log('migration: replacing torrentPath %s', ts.torrentPath)
if (path.isAbsolute(ts.torrentPath)) {
src = ts.torrentPath
} else if (ts.torrentPath.startsWith('..')) {
src = ts.torrentPath
} else {
src = path.join(config.STATIC_PATH, ts.torrentPath)
}
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
// Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once
if (src !== dst) fs.copySync(src, dst)
delete ts.torrentPath
ts.torrentFileName = infoHash + '.torrent'
}
// Migration: replace posterURL with posterFileName
if (ts.posterURL) {
console.log('migration: replacing posterURL %s', ts.posterURL)
var extension = path.extname(ts.posterURL)
src = path.isAbsolute(ts.posterURL)
? ts.posterURL
: path.join(config.STATIC_PATH, ts.posterURL)
dst = path.join(config.CONFIG_POSTER_PATH, infoHash + extension)
// Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once
if (src !== dst) fs.copySync(src, dst)
delete ts.posterURL
ts.posterFileName = infoHash + extension
}
// Migration: add per-file selections
if (!ts.selections) {
ts.selections = ts.files.map((x) => true)
}
})
} }
// Lazily loads Chromecast and Airplay support // Lazily loads Chromecast and Airplay support
@@ -192,7 +132,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()
if (vdomLoop) vdomLoop.update(state) vdomLoop.update(state)
updateElectron() updateElectron()
} }
@@ -224,17 +164,23 @@ function dispatch (action, ...args) {
if (action === 'addTorrent') { if (action === 'addTorrent') {
addTorrent(args[0] /* torrent */) addTorrent(args[0] /* torrent */)
} }
if (action === 'showOpenTorrentFile') { if (action === 'openTorrentFile') {
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */ ipcRenderer.send('openTorrentFile') /* open torrent file */
}
if (action === 'openFiles') {
ipcRenderer.send('openFiles') /* add files with dialog */
} }
if (action === 'showCreateTorrent') { if (action === 'showCreateTorrent') {
showCreateTorrent(args[0] /* fileOrFolder */) showCreateTorrent(args[0] /* paths */)
}
if (action === 'openTorrentAddress') {
state.modal = { id: 'open-torrent-address-modal' }
} }
if (action === 'createTorrent') { if (action === 'createTorrent') {
createTorrent(args[0] /* options */) createTorrent(args[0] /* options */)
} }
if (action === 'openFile') { if (action === 'openItem') {
openFile(args[0] /* infoHash */, args[1] /* index */) openItem(args[0] /* infoHash */, args[1] /* index */)
} }
if (action === 'toggleTorrent') { if (action === 'toggleTorrent') {
toggleTorrent(args[0] /* infoHash */) toggleTorrent(args[0] /* infoHash */)
@@ -251,11 +197,14 @@ function dispatch (action, ...args) {
if (action === 'openTorrentContextMenu') { if (action === 'openTorrentContextMenu') {
openTorrentContextMenu(args[0] /* infoHash */) openTorrentContextMenu(args[0] /* infoHash */)
} }
if (action === 'openDevice') { if (action === 'toggleCastMenu') {
lazyLoadCast().open(args[0] /* deviceType */) lazyLoadCast().toggleMenu(args[0] /* deviceType */)
} }
if (action === 'closeDevice') { if (action === 'selectCastDevice') {
lazyLoadCast().close() lazyLoadCast().selectDevice(args[0] /* index */)
}
if (action === 'stopCasting') {
lazyLoadCast().stop()
} }
if (action === 'setDimensions') { if (action === 'setDimensions') {
setDimensions(args[0] /* dimensions */) setDimensions(args[0] /* dimensions */)
@@ -313,6 +262,7 @@ function dispatch (action, ...args) {
} }
if (action === 'mediaError') { if (action === 'mediaError') {
if (state.location.url() === 'player') { if (state.location.url() === 'player') {
state.playing.result = 'error'
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) {
@@ -324,6 +274,9 @@ function dispatch (action, ...args) {
}) })
} }
} }
if (action === 'mediaSuccess') {
state.playing.result = 'success'
}
if (action === 'mediaTimeUpdate') { if (action === 'mediaTimeUpdate') {
state.playing.lastTimeUpdate = new Date().getTime() state.playing.lastTimeUpdate = new Date().getTime()
state.playing.isStalled = false state.playing.isStalled = false
@@ -375,7 +328,13 @@ function dispatch (action, ...args) {
saveStateThrottled() saveStateThrottled()
} }
if (action === 'saveState') { if (action === 'saveState') {
saveState() State.save(state)
}
if (action === 'setTitle') {
state.window.title = args[0] /* title */
}
if (action === 'uncaughtError') {
telemetry.logUncaughtError(args[0] /* process */, args[1] /* error */)
} }
// Update the virtual-dom, unless it's just a mouse move event // Update the virtual-dom, unless it's just a mouse move event
@@ -413,11 +372,18 @@ function pause () {
function playPause () { function playPause () {
if (state.location.url() !== 'player') return if (state.location.url() !== 'player') return
if (state.playing.isPaused) { if (state.playing.isPaused) {
play() play()
} else { } else {
pause() pause()
} }
// force rerendering if window is hidden,
// in order to bypass `raf` and play/pause media immediately
if (!state.window.isVisible) render(state)
ipcRenderer.send('updateThumbnailBar', state.playing.isPaused)
} }
function jumpToTime (time) { function jumpToTime (time) {
@@ -501,18 +467,11 @@ function isCasting () {
} }
function setupIpc () { function setupIpc () {
ipcRenderer.send('ipcReady')
ipcRenderer.on('log', (e, ...args) => console.log(...args)) ipcRenderer.on('log', (e, ...args) => console.log(...args))
ipcRenderer.on('error', (e, ...args) => console.error(...args)) ipcRenderer.on('error', (e, ...args) => console.error(...args))
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args)) ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
ipcRenderer.on('showOpenTorrentAddress', function (e) {
state.modal = { id: 'open-torrent-address-modal' }
update()
})
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) { ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
state.window.isFullScreen = isFullScreen state.window.isFullScreen = isFullScreen
if (!isFullScreen) { if (!isFullScreen) {
@@ -534,13 +493,17 @@ function setupIpc () {
ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args)) ipcRenderer.on('wt-poster', (e, ...args) => torrentPosterSaved(...args))
ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args)) ipcRenderer.on('wt-audio-metadata', (e, ...args) => torrentAudioMetadata(...args))
ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args)) ipcRenderer.on('wt-server-running', (e, ...args) => torrentServerRunning(...args))
ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err))
ipcRenderer.send('ipcReady')
} }
// 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
.filter((x) => x.status !== 'paused') .filter((torrentSummary) => torrentSummary.status !== 'paused')
.forEach((x) => startTorrentingSummary(x)) .forEach((torrentSummary) => startTorrentingSummary(torrentSummary))
} }
// Updates a single property in the UNSAVED prefs // Updates a single property in the UNSAVED prefs
@@ -561,7 +524,8 @@ function updatePreferences (property, value) {
// All unsaved prefs take effect atomically, and are saved to config.json // All unsaved prefs take effect atomically, and are saved to config.json
function savePreferences () { function savePreferences () {
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs) state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
saveState() State.save(state)
update()
} }
// Don't write state.saved to file more than once a second // Don't write state.saved to file more than once a second
@@ -569,44 +533,13 @@ function saveStateThrottled () {
if (state.saveStateTimeout) return if (state.saveStateTimeout) return
state.saveStateTimeout = setTimeout(function () { state.saveStateTimeout = setTimeout(function () {
delete state.saveStateTimeout delete state.saveStateTimeout
saveState() State.save(state)
update()
}, 1000) }, 1000)
} }
// Write state.saved to the JSON state file // Called when the user adds files (.torrent, files to seed, subtitles) to the app
function saveState () { // via any method (drag-drop, drag to app icon, command line)
console.log('saving state to ' + appConfig.filePath)
// Clean up, so that we're not saving any pending state
var copy = Object.assign({}, state.saved)
// Remove torrents pending addition to the list, where we haven't finished
// reading the torrent file or file(s) to seed & don't have an infohash
copy.torrents = copy.torrents
.filter((x) => x.infoHash)
.map(function (x) {
var torrent = {}
for (var key in x) {
if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process
}
if (key === 'playStatus') {
continue // Don't save whether a torrent is playing / pending
}
torrent[key] = x[key]
}
return torrent
})
appConfig.write(copy, function (err) {
if (err) console.error(err)
ipcRenderer.send('savedState')
})
// Update right away, don't wait for the state to save
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 ]
@@ -667,6 +600,7 @@ function addTorrent (torrentId) {
torrentId = torrentId.path torrentId = torrentId.path
} }
// Allow a instant.io link to be pasted // Allow a instant.io link to be pasted
// TODO: remove this once support is added to webtorrent core
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) { if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1) torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
} }
@@ -674,12 +608,13 @@ function addTorrent (torrentId) {
} }
function addSubtitles (files, autoSelect) { function addSubtitles (files, autoSelect) {
// Subtitles are only supported while playing video // Subtitles are only supported when playing video files
if (state.playing.type !== 'video') return if (state.playing.type !== 'video') return
if (files.length === 0) return
// Read the files concurrently, then add all resulting subtitle tracks // Read the files concurrently, then add all resulting subtitle tracks
var jobs = files.map((file) => (cb) => loadSubtitle(file, cb)) var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(jobs, function (err, tracks) { parallel(tasks, function (err, tracks) {
if (err) return onError(err) if (err) return onError(err)
for (var i = 0; i < tracks.length; i++) { for (var i = 0; i < tracks.length; i++) {
@@ -803,7 +738,9 @@ 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)) { // Files will either be an array of file objects, which we can send directly
// to the create-torrent screen
if (files.length === 0 || typeof files[0] !== 'string') {
state.location.go({ state.location.go({
url: 'create-torrent', url: 'create-torrent',
files: files files: files
@@ -811,13 +748,30 @@ function showCreateTorrent (files) {
return return
} }
var fileOrFolder = files // ... or it will be an array of mixed file and folder paths. We have to walk
findFilesRecursive(fileOrFolder, showCreateTorrent) // through all the folders and find the files
findFilesRecursive(files, showCreateTorrent)
} }
// Recursively finds {name, path, size} for all files in a folder // Recursively finds {name, path, size} for all files in a folder
// Calls `cb` on success, calls `onError` on failure // Calls `cb` on success, calls `onError` on failure
function findFilesRecursive (fileOrFolder, cb) { function findFilesRecursive (paths, cb) {
if (paths.length > 1) {
var numComplete = 0
var ret = []
paths.forEach(function (path) {
findFilesRecursive([path], function (fileObjs) {
ret = ret.concat(fileObjs)
if (++numComplete === paths.length) {
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
cb(ret)
}
})
})
return
}
var fileOrFolder = paths[0]
fs.stat(fileOrFolder, function (err, stat) { fs.stat(fileOrFolder, function (err, stat) {
if (err) return onError(err) if (err) return onError(err)
@@ -835,16 +789,8 @@ function findFilesRecursive (fileOrFolder, cb) {
var folderPath = fileOrFolder var folderPath = fileOrFolder
fs.readdir(folderPath, function (err, fileNames) { fs.readdir(folderPath, function (err, fileNames) {
if (err) return onError(err) if (err) return onError(err)
var numComplete = 0 var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
var ret = [] findFilesRecursive(paths, cb)
fileNames.forEach(function (fileName) {
findFilesRecursive(path.join(folderPath, fileName), function (fileObjs) {
ret = ret.concat(fileObjs)
if (++numComplete === fileNames.length) {
cb(ret)
}
})
})
}) })
}) })
} }
@@ -864,6 +810,12 @@ function torrentInfoHash (torrentKey, infoHash) {
torrentSummary ? 'existing' : 'new', torrentKey) torrentSummary ? 'existing' : 'new', torrentKey)
if (!torrentSummary) { if (!torrentSummary) {
// Check if an existing (non-active) torrent has the same info hash
if (state.saved.torrents.find((t) => t.infoHash === infoHash)) {
ipcRenderer.send('wt-stop-torrenting', infoHash)
return onError(new Error('Cannot add duplicate torrent'))
}
torrentSummary = { torrentSummary = {
torrentKey: torrentKey, torrentKey: torrentKey,
status: 'new' status: 'new'
@@ -964,7 +916,10 @@ function torrentProgress (progressInfo) {
torrentSummary.progress = p torrentSummary.progress = p
}) })
checkForSubtitles() // TODO: Find an efficient way to re-enable this line, which allows subtitle
// files which are completed after a video starts to play to be added
// dynamically to the list of subtitles.
// checkForSubtitles()
update() update()
} }
@@ -1045,10 +1000,13 @@ function openPlayer (infoHash, index, cb) {
// update UI to show pending playback // update UI to show pending playback
if (torrentSummary.progress !== 1) sound.play('PLAY') if (torrentSummary.progress !== 1) sound.play('PLAY')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'requested' torrentSummary.playStatus = 'requested'
update() update()
var timeout = setTimeout(function () { var timeout = setTimeout(function () {
telemetry.logPlayAttempt('timeout')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'timeout' /* no seeders available? */ torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR') sound.play('ERROR')
cb(new Error('Playback timed out. Try again.')) cb(new Error('Playback timed out. Try again.'))
@@ -1114,22 +1072,41 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
} }
function closePlayer (cb) { function closePlayer (cb) {
console.log('closePlayer')
// Quit any external players, like Chromecast/Airplay/etc or VLC
if (isCasting()) { if (isCasting()) {
Cast.close() Cast.stop()
} }
if (state.playing.location === 'vlc') { if (state.playing.location === 'vlc') {
ipcRenderer.send('vlcQuit') ipcRenderer.send('vlcQuit')
} }
// Save volume (this session only, not in state.saved)
state.previousVolume = state.playing.volume
// Telemetry: track what happens after the user clicks play
var result = state.playing.result // 'success' or 'error'
if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed
else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc
else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame
else console.error('Unknown state.playing.result', state.playing.result)
// Reset the window contents back to the home screen
state.window.title = config.APP_WINDOW_TITLE state.window.title = config.APP_WINDOW_TITLE
state.playing = State.getDefaultPlayState() state.playing = State.getDefaultPlayState()
state.server = null state.server = null
// Reset the window size and location back to where it was
if (state.window.isFullScreen) { if (state.window.isFullScreen) {
dispatch('toggleFullScreen', false) dispatch('toggleFullScreen', false)
} }
restoreBounds() restoreBounds()
// Tell the WebTorrent process to kill the torrent-to-HTTP server
ipcRenderer.send('wt-stop-server') ipcRenderer.send('wt-stop-server')
// Tell the OS we're no longer playing media, laptops allowed to sleep again
ipcRenderer.send('unblockPowerSave') ipcRenderer.send('unblockPowerSave')
ipcRenderer.send('onPlayerClose') ipcRenderer.send('onPlayerClose')
@@ -1137,7 +1114,7 @@ function closePlayer (cb) {
cb() cb()
} }
function openFile (infoHash, index) { function openItem (infoHash, index) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
var filePath = path.join( var filePath = path.join(
torrentSummary.path, torrentSummary.path,
@@ -1160,9 +1137,14 @@ function toggleTorrent (infoHash) {
} }
// TODO: use torrentKey, not infoHash // TODO: use torrentKey, not infoHash
function deleteTorrent (infoHash) { function deleteTorrent (infoHash, deleteData) {
ipcRenderer.send('wt-stop-torrenting', infoHash) ipcRenderer.send('wt-stop-torrenting', infoHash)
if (deleteData) {
var torrentSummary = getTorrentSummary(infoHash)
moveItemToTrash(torrentSummary)
}
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()
@@ -1188,6 +1170,20 @@ function openTorrentContextMenu (infoHash) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
var menu = new electron.remote.Menu() var menu = new electron.remote.Menu()
menu.append(new electron.remote.MenuItem({
label: 'Remove From List',
click: () => deleteTorrent(torrentSummary.infoHash, false)
}))
menu.append(new electron.remote.MenuItem({
label: 'Remove Data File',
click: () => deleteTorrent(torrentSummary.infoHash, true)
}))
menu.append(new electron.remote.MenuItem({
type: 'separator'
}))
if (torrentSummary.files) { if (torrentSummary.files) {
menu.append(new electron.remote.MenuItem({ menu.append(new electron.remote.MenuItem({
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder', label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
@@ -1228,6 +1224,10 @@ function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary)) ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
} }
function moveItemToTrash (torrentSummary) {
ipcRenderer.send('moveItemToTrash', getTorrentPath(torrentSummary))
}
function saveTorrentFileAs (torrentSummary) { function saveTorrentFileAs (torrentSummary) {
var newFileName = `${path.parse(torrentSummary.name).name}.torrent` var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
var opts = { var opts = {
@@ -1302,7 +1302,7 @@ function showDoneNotification (torrent) {
}) })
notif.onclick = function () { notif.onclick = function () {
ipcRenderer.send('focusWindow', 'main') ipcRenderer.send('show')
} }
sound.play('DONE') sound.play('DONE')
@@ -1365,3 +1365,7 @@ function onBlur () {
state.window.isFocused = false state.window.isFocused = false
update() update()
} }
function onVisibilityChange () {
state.window.isVisible = !document.webkitHidden
}

View File

@@ -1,286 +0,0 @@
var electron = require('electron')
var path = require('path')
var remote = electron.remote
var config = require('../config')
var LocationHistory = require('./lib/location-history')
module.exports = {
getInitialState,
getDefaultPlayState,
getDefaultSavedState
}
function getInitialState () {
return {
/*
* Temporary state disappears once the program exits.
* It can contain complex objects like open connections, etc.
*/
client: null, /* the WebTorrent client */
server: null, /* local WebTorrent-to-HTTP server */
prev: {}, /* used for state diffing in updateElectron() */
location: new LocationHistory(),
window: {
bounds: null, /* {x, y, width, height } */
isFocused: true,
isFullScreen: false,
title: config.APP_WINDOW_TITLE
},
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
playing: getDefaultPlayState(), /* the media (audio or video) that we're currently playing */
devices: { /* playback devices like Chromecast and AppleTV */
airplay: null, /* airplay client. finds and manages AppleTVs */
chromecast: null /* chromecast client. finds and manages Chromecasts */
},
dock: {
badge: 0,
progress: 0
},
modal: null, /* modal popover */
errors: [], /* user-facing errors */
nextTorrentKey: 1, /* identify torrents for IPC between the main and webtorrent windows */
/*
* Saved state is read from and written to a file every time the app runs.
* It should be simple and minimal and must be JSON.
* It must never contain absolute paths since we have a portable app.
*
* Config path:
*
* OS X ~/Library/Application Support/WebTorrent/config.json
* Linux (XDG) $XDG_CONFIG_HOME/WebTorrent/config.json
* Linux (Legacy) ~/.config/WebTorrent/config.json
* Windows (> Vista) %LOCALAPPDATA%/WebTorrent/config.json
* Windows (XP, 2000) %USERPROFILE%/Local Settings/Application Data/WebTorrent/config.json
*
* Also accessible via `require('application-config')('WebTorrent').filePath`
*/
saved: {},
/*
* Getters, for convenience
*/
getPlayingTorrentSummary,
getPlayingFileSummary
}
}
/* Whenever we stop playing video or audio, here's what we reset state.playing to */
function getDefaultPlayState () {
return {
infoHash: null, /* the info hash of the torrent we're playing */
fileIndex: null, /* the zero-based index within the torrent */
location: 'local', /* 'local', 'chromecast', 'airplay' */
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
currentTime: 0, /* seconds */
duration: 1, /* seconds */
isPaused: true,
isStalled: false,
lastTimeUpdate: 0, /* Unix time in ms */
mouseStationarySince: 0, /* Unix time in ms */
playbackRate: 1,
subtitles: {
tracks: [], /* subtitle tracks, each {label, language, ...} */
selectedIndex: -1, /* current subtitle track */
showMenu: false /* popover menu, above the video */
},
aspectRatio: 0 /* aspect ratio of the video */
}
}
/* If the saved state file doesn't exist yet, here's what we use instead */
function getDefaultSavedState () {
return {
version: 1, /* make sure we can upgrade gracefully later */
torrents: [
{
status: 'paused',
infoHash: '88594aaacbde40ef3e2510c47374ec0aa396c08e',
magnetURI: 'magnet:?xt=urn:btih:88594aaacbde40ef3e2510c47374ec0aa396c08e&dn=bbb_sunflower_1080p_30fps_normal.mp4&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fdistribution.bbb3d.renderfarming.net%2Fvideo%2Fmp4%2Fbbb_sunflower_1080p_30fps_normal.mp4',
displayName: 'Big Buck Bunny',
posterURL: 'bigBuckBunny.jpg',
torrentPath: 'bigBuckBunny.torrent',
files: [
{
length: 276134947,
name: 'bbb_sunflower_1080p_30fps_normal.mp4'
}
]
},
{
status: 'paused',
infoHash: '6a9759bffd5c0af65319979fb7832189f4f3c35d',
magnetURI: 'magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d&dn=sintel.mp4&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel-1024-surround.mp4',
displayName: 'Sintel',
posterURL: 'sintel.jpg',
torrentPath: 'sintel.torrent',
files: [
{
length: 129241752,
name: 'sintel.mp4'
}
]
},
{
status: 'paused',
infoHash: '02767050e0be2fd4db9a2ad6c12416ac806ed6ed',
magnetURI: 'magnet:?xt=urn:btih:02767050e0be2fd4db9a2ad6c12416ac806ed6ed&dn=tears_of_steel_1080p.webm&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io',
displayName: 'Tears of Steel',
posterURL: 'tearsOfSteel.jpg',
torrentPath: 'tearsOfSteel.torrent',
files: [
{
length: 571346576,
name: 'tears_of_steel_1080p.webm'
}
]
},
{
status: 'paused',
infoHash: '6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5',
magnetURI: 'magnet:?xt=urn:btih:6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5&dn=CosmosLaundromatFirstCycle&tr=http%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=http%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fia601508.us.archive.org%2F14%2Fitems%2F&ws=http%3A%2F%2Fia801508.us.archive.org%2F14%2Fitems%2F&ws=https%3A%2F%2Farchive.org%2Fdownload%2F',
displayName: 'Cosmos Laundromat (Preview)',
posterURL: 'cosmosLaundromat.jpg',
torrentPath: 'cosmosLaundromat.torrent',
files: [
{
length: 223580,
name: 'Cosmos Laundromat - First Cycle (1080p).gif'
},
{
length: 220087570,
name: 'Cosmos Laundromat - First Cycle (1080p).mp4'
},
{
length: 56832560,
name: 'Cosmos Laundromat - First Cycle (1080p).ogv'
},
{
length: 3949,
name: 'CosmosLaundromat-FirstCycle1080p.en.srt'
},
{
length: 3907,
name: 'CosmosLaundromat-FirstCycle1080p.es.srt'
},
{
length: 4119,
name: 'CosmosLaundromat-FirstCycle1080p.fr.srt'
},
{
length: 3941,
name: 'CosmosLaundromat-FirstCycle1080p.it.srt'
},
{
length: 11264,
name: 'CosmosLaundromatFirstCycle_meta.sqlite'
},
{
length: 1204,
name: 'CosmosLaundromatFirstCycle_meta.xml'
}
]
},
{
status: 'paused',
infoHash: '3ba219a8634bf7bae3d848192b2da75ae995589d',
magnetURI: 'magnet:?xt=urn:btih:3ba219a8634bf7bae3d848192b2da75ae995589d&dn=The+WIRED+CD+-+Rip.+Sample.+Mash.+Share.&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2F',
displayName: 'The WIRED CD - Rip. Sample. Mash. Share.',
posterURL: 'wired-cd.jpg',
torrentPath: 'wired-cd.torrent',
files: [
{
length: 1964275,
name: '01 - Beastie Boys - Now Get Busy.mp3'
},
{
length: 3610523,
name: '02 - David Byrne - My Fair Lady.mp3'
},
{
length: 2759377,
name: '03 - Zap Mama - Wadidyusay.mp3'
},
{
length: 5816537,
name: '04 - My Morning Jacket - One Big Holiday.mp3'
},
{
length: 2106421,
name: '05 - Spoon - Revenge!.mp3'
},
{
length: 3347550,
name: '06 - Gilberto Gil - Oslodum.mp3'
},
{
length: 2107577,
name: '07 - Dan The Automator - Relaxation Spa Treatment.mp3'
},
{
length: 3108130,
name: '08 - Thievery Corporation - Dc 3000.mp3'
},
{
length: 3051528,
name: '09 - Le Tigre - Fake French.mp3'
},
{
length: 3270259,
name: '10 - Paul Westerberg - Looking Up In Heaven.mp3'
},
{
length: 3263528,
name: '11 - Chuck D - No Meaning No (feat. Fine Arts Militia).mp3'
},
{
length: 6380952,
name: '12 - The Rapture - Sister Saviour (Blackstrobe Remix).mp3'
},
{
length: 6550396,
name: '13 - Cornelius - Wataridori 2.mp3'
},
{
length: 3034692,
name: '14 - DJ Danger Mouse - What U Sittin\' On (feat. Jemini, Cee Lo And Tha Alkaholiks).mp3'
},
{
length: 3854611,
name: '15 - DJ Dolores - Oslodum 2004.mp3'
},
{
length: 1762120,
name: '16 - Matmos - Action At A Distance.mp3'
},
{
length: 4071,
name: 'README.md'
},
{
length: 78163,
name: 'poster.jpg'
}
]
}
],
prefs: {
downloadPath: config.IS_PORTABLE
? 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

@@ -1,16 +1,15 @@
module.exports = App module.exports = App
var h = require('virtual-dom/h') var hx = require('../lib/hx')
var hyperx = require('hyperx')
var hx = hyperx(h)
var Header = require('./header') var Header = require('./header')
var Views = { var Views = {
'home': require('./torrent-list'), 'home': require('./home'),
'player': require('./player'), 'player': require('./player'),
'create-torrent': require('./create-torrent-page'), 'create-torrent': require('./create-torrent'),
'preferences': require('./preferences') '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'),
'update-available-modal': require('./update-available-modal'), 'update-available-modal': require('./update-available-modal'),

View File

@@ -1,14 +1,11 @@
module.exports = CreateTorrentPage module.exports = CreateTorrentPage
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var createTorrent = require('create-torrent') var createTorrent = require('create-torrent')
var path = require('path') var path = require('path')
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var {dispatch} = require('../lib/dispatcher') var {dispatch, dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function CreateTorrentPage (state) { function CreateTorrentPage (state) {
var info = state.location.current() var info = state.location.current()
@@ -17,17 +14,14 @@ function CreateTorrentPage (state) {
var files = info.files var files = info.files
.filter((f) => !f.name.startsWith('.')) .filter((f) => !f.name.startsWith('.'))
.map((f) => ({name: f.name, path: f.path, size: f.size})) .map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return CreateTorrentErrorPage()
// First, extract the base folder that the files are all in // First, extract the base folder that the files are all in
var pathPrefix = info.folderPath var pathPrefix = info.folderPath
if (!pathPrefix) { if (!pathPrefix) {
if (files.length > 0) { pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix) if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) { pathPrefix = path.dirname(pathPrefix)
pathPrefix = path.dirname(pathPrefix)
}
} else {
pathPrefix = files[0]
} }
} }
@@ -62,7 +56,7 @@ function CreateTorrentPage (state) {
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed' var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
return hx` return hx`
<div class='create-torrent-page'> <div class='create-torrent'>
<h2>Create torrent ${defaultName}</h2> <h2>Create torrent ${defaultName}</h2>
<p class="torrent-info"> <p class="torrent-info">
${torrentInfo} ${torrentInfo}
@@ -133,6 +127,27 @@ function CreateTorrentPage (state) {
} }
} }
function CreateTorrentErrorPage () {
return hx`
<div class='create-torrent'>
<h2>Create torrent</h2>
<p class="torrent-info">
<p>
Sorry, you must select at least one file that is not a hidden file.
</p>
<p>
Hidden files, starting with a . character, are not included.
</p>
</p>
<p class="float-right">
<button class='button-flat light' onclick=${dispatcher('back')}>
Cancel
</button>
</p>
</div>
`
}
// Finds the longest common prefix // Finds the longest common prefix
function findCommonPrefix (a, b) { function findCommonPrefix (a, b) {
for (var i = 0; i < a.length && i < b.length; i++) { for (var i = 0; i < a.length && i < b.length; i++) {

View File

@@ -1,10 +1,7 @@
module.exports = Header module.exports = Header
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatcher} = require('../lib/dispatcher') var {dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function Header (state) { function Header (state) {
return hx` return hx`
@@ -42,7 +39,7 @@ function Header (state) {
<i <i
class='icon add' class='icon add'
title='Add torrent' title='Add torrent'
onclick=${dispatcher('showOpenTorrentFile')}> onclick=${dispatcher('openFiles')}>
add add
</i> </i>
` `

View File

@@ -1,17 +1,17 @@
module.exports = TorrentList module.exports = TorrentList
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var hx = require('../lib/hx')
var TorrentSummary = require('../lib/torrent-summary') var TorrentSummary = require('../lib/torrent-summary')
var TorrentPlayer = require('../lib/torrent-player') var TorrentPlayer = require('../lib/torrent-player')
var {dispatcher} = require('../lib/dispatcher') var {dispatcher} = require('../lib/dispatcher')
function TorrentList (state) { function TorrentList (state) {
var torrentRows = state.saved.torrents.map( var torrentRows = state.saved.torrents.map(
(torrentSummary) => renderTorrent(torrentSummary)) (torrentSummary) => renderTorrent(torrentSummary)
)
return hx` return hx`
<div class='torrent-list'> <div class='torrent-list'>
${torrentRows} ${torrentRows}
@@ -20,11 +20,7 @@ function TorrentList (state) {
</div> </div>
</div>` </div>`
// Renders a torrent in the torrent list
// Includes name, download status, play button, background image
// May be expanded for additional info, including the list of files inside
function renderTorrent (torrentSummary) { function renderTorrent (torrentSummary) {
// Get ephemeral data (like progress %) directly from the WebTorrent handle
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var isSelected = infoHash && state.selectedInfoHash === infoHash var isSelected = infoHash && state.selectedInfoHash === infoHash
@@ -195,12 +191,13 @@ function TorrentList (state) {
} 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 var fileRows = torrentSummary.files
.map((file, index) => ({ file, index }))
.sort(function (a, b) { .sort(function (a, b) {
if (a.name < b.name) return -1 if (a.file.name < b.file.name) return -1
if (b.name < a.name) return 1 if (b.file.name < a.file.name) return 1
return 0 return 0
}) })
.map((file, index) => renderFileRow(torrentSummary, file, index)) .map((object) => renderFileRow(torrentSummary, object.file, object.index))
filesElement = hx` filesElement = hx`
<div class='files'> <div class='files'>
@@ -221,7 +218,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 isSelected = torrentSummary.selections[index] // Are we even torrenting it? // Are we even torrenting it?
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
var isDone = false // Are we finished 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) {
@@ -247,7 +245,7 @@ function TorrentList (state) {
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 */
handleClick = dispatcher('openFile', infoHash, index) handleClick = dispatcher('openItem', infoHash, index)
} }
var rowClass = '' var rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented

View File

@@ -1,10 +1,7 @@
module.exports = OpenTorrentAddressModal module.exports = OpenTorrentAddressModal
var h = require('virtual-dom/h') var {dispatch, dispatcher} = require('../lib/dispatcher')
var hyperx = require('hyperx') var hx = require('../lib/hx')
var hx = hyperx(h)
var {dispatch} = require('../lib/dispatcher')
function OpenTorrentAddressModal (state) { function OpenTorrentAddressModal (state) {
return hx` return hx`
@@ -14,8 +11,8 @@ function OpenTorrentAddressModal (state) {
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} /> <input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
</p> </p>
<p class='float-right'> <p class='float-right'>
<button class='button button-flat' onclick=${handleCancel}>CANCEL</button> <button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
<button class='button button-raised' onclick=${handleOK}>OK</button> <button class='button button-raised' onclick=${handleOK}>OK</button>
</p> </p>
<script>document.querySelector('#add-torrent-url').focus()</script> <script>document.querySelector('#add-torrent-url').focus()</script>
</div> </div>
@@ -30,7 +27,3 @@ function handleOK () {
dispatch('exitModal') dispatch('exitModal')
dispatch('addTorrent', document.querySelector('#add-torrent-url').value) dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
} }
function handleCancel () {
dispatch('exitModal')
}

View File

@@ -1,13 +1,10 @@
module.exports = Player module.exports = Player
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var Bitfield = require('bitfield') var Bitfield = require('bitfield')
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var zeroFill = require('zero-fill') var zeroFill = require('zero-fill')
var hx = require('../lib/hx')
var TorrentSummary = require('../lib/torrent-summary') var TorrentSummary = require('../lib/torrent-summary')
var {dispatch, dispatcher} = require('../lib/dispatcher') var {dispatch, dispatcher} = require('../lib/dispatcher')
@@ -37,7 +34,8 @@ function renderMedia (state) {
// Unfortunately, play/pause can't be done just by modifying HTML. // Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary // Instead, grab the DOM node and play/pause it if necessary
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */ // Get the <video> or <audio> tag
var mediaElement = document.querySelector(state.playing.type)
if (mediaElement !== null) { if (mediaElement !== null) {
if (state.playing.isPaused && !mediaElement.paused) { if (state.playing.isPaused && !mediaElement.paused) {
mediaElement.pause() mediaElement.pause()
@@ -52,6 +50,12 @@ function renderMedia (state) {
if (state.playing.playbackRate !== mediaElement.playbackRate) { if (state.playing.playbackRate !== mediaElement.playbackRate) {
mediaElement.playbackRate = state.playing.playbackRate mediaElement.playbackRate = state.playing.playbackRate
} }
// Recover previous volume
if (state.previousVolume !== null && isFinite(state.previousVolume)) {
mediaElement.volume = state.previousVolume
state.previousVolume = null
}
// 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
@@ -59,9 +63,10 @@ function renderMedia (state) {
} }
// Switch to the newly added subtitle track, if available // Switch to the newly added subtitle track, if available
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' var isSelectedTrack = j === state.playing.subtitles.selectedIndex
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
} }
// Save video position // Save video position
@@ -114,7 +119,7 @@ function renderMedia (state) {
</div> </div>
` `
// As soon as the video loads enough to know the video dimensions, resize the window // As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) { function onLoadedMetadata (e) {
if (state.playing.type !== 'video') return if (state.playing.type !== 'video') return
var video = e.target var video = e.target
@@ -132,11 +137,13 @@ function renderMedia (state) {
function onCanPlay (e) { function onCanPlay (e) {
var elem = e.target var elem = e.target
if (state.playing.type === 'video' && elem.webkitVideoDecodedByteCount === 0) { if (state.playing.type === 'video' &&
elem.webkitVideoDecodedByteCount === 0) {
dispatch('mediaError', 'Video codec unsupported') dispatch('mediaError', 'Video codec unsupported')
} else if (elem.webkitAudioDecodedByteCount === 0) { } else if (elem.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported') dispatch('mediaError', 'Audio codec unsupported')
} else { } else {
dispatch('mediaSuccess')
elem.play() elem.play()
} }
} }
@@ -157,7 +164,8 @@ function renderOverlay (state) {
} else if (elems.length !== 0) { } else if (elems.length !== 0) {
style = { backgroundImage: cssBackgroundImageDarkGradient() } style = { backgroundImage: cssBackgroundImageDarkGradient() }
} else { } else {
return /* Video, not audio, and it isn't stalled, so no spinner. No overlay needed. */ // Video playing, so no spinner. No overlay needed
return
} }
return hx` return hx`
@@ -187,15 +195,37 @@ function renderAudioMetadata (state) {
track = info.track.no + ' of ' + info.track.of track = info.track.no + ' of ' + info.track.of
} }
// Show a small info box in the middle of the screen with title/album/artist/etc // Show a small info box in the middle of the screen with title/album/etc
var elems = [] var elems = []
if (artist) elems.push(hx`<div class='audio-artist'><label>Artist</label>${artist}</div>`) if (artist) {
if (album) elems.push(hx`<div class='audio-album'><label>Album</label>${album}</div>`) elems.push(hx`
if (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`) <div class='audio-artist'>
<label>Artist</label>${artist}
</div>
`)
}
if (album) {
elems.push(hx`
<div class='audio-album'>
<label>Album</label>${album}
</div>
`)
}
if (track) {
elems.push(hx`
<div class='audio-track'>
<label>Track</label>${track}
</div>
`)
}
// Align the title with the artist/etc info, if available. Otherwise, center the title // Align the title with the other info, if available. Otherwise, center title
var emptyLabel = hx`<label></label>` var emptyLabel = hx`<label></label>`
elems.unshift(hx`<div class='audio-title'>${elems.length ? emptyLabel : undefined}${title}</div>`) elems.unshift(hx`
<div class='audio-title'>
${elems.length ? emptyLabel : undefined}${title}
</div>
`)
return hx`<div class='audio-metadata'>${elems}</div>` return hx`<div class='audio-metadata'>${elems}</div>`
} }
@@ -250,8 +280,10 @@ function renderCastScreen (state) {
} }
var isStarting = state.playing.location.endsWith('-pending') var isStarting = state.playing.location.endsWith('-pending')
var castName = state.playing.castName
var castStatus var castStatus
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected' if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
else castStatus = '' else castStatus = ''
// Show a nice title image, if possible // Show a nice title image, if possible
@@ -270,6 +302,30 @@ function renderCastScreen (state) {
` `
} }
function renderCastOptions (state) {
if (!state.devices.castMenu) return
var {location, devices} = state.devices.castMenu
var player = state.devices[location]
var items = devices.map(function (device, ix) {
var isSelected = player.device === device
var name = device.name
return hx`
<li onclick=${dispatcher('selectCastDevice', ix)}>
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
${name}
</li>
`
})
return hx`
<ul.options-list>
${items}
</ul>
`
}
function renderSubtitlesOptions (state) { function renderSubtitlesOptions (state) {
var subtitles = state.playing.subtitles var subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return if (!subtitles.tracks.length || !subtitles.showMenu) return
@@ -278,18 +334,19 @@ function renderSubtitlesOptions (state) {
var isSelected = state.playing.subtitles.selectedIndex === ix var isSelected = state.playing.subtitles.selectedIndex === ix
return hx` return hx`
<li onclick=${dispatcher('selectSubtitle', ix)}> <li onclick=${dispatcher('selectSubtitle', ix)}>
<i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i> <i.icon>${'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
${track.label} ${track.label}
</li> </li>
` `
}) })
var noneSelected = state.playing.subtitles.selectedIndex === -1 var noneSelected = state.playing.subtitles.selectedIndex === -1
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
return hx` return hx`
<ul.subtitles-list> <ul.options-list>
${items} ${items}
<li onclick=${dispatcher('selectSubtitle', -1)}> <li onclick=${dispatcher('selectSubtitle', -1)}>
<i.icon>${noneSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i> <i.icon>${noneClass}</i>
None None
</li> </li>
</ul> </ul>
@@ -339,84 +396,68 @@ 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.float-right <i.icon.closed-caption.float-right
class=${captionsClass} class=${captionsClass}
onclick=${handleSubtitles}> onclick=${handleSubtitles}>
closed_captions closed_caption
</i> </i>
`) `)
} }
// If we've detected a Chromecast or AppleTV, the user can play video there // If we've detected a Chromecast or AppleTV, the user can play video there
var isOnChromecast = state.playing.location.startsWith('chromecast') var castTypes = ['chromecast', 'airplay', 'dlna']
var isOnAirplay = state.playing.location.startsWith('airplay') var isCastingAnywhere = castTypes.some(
var isOnDlna = state.playing.location.startsWith('dlna') (castType) => state.playing.location.startsWith(castType))
var chromecastClass, chromecastHandler, airplayClass, airplayHandler, dlnaClass, dlnaHandler
if (isOnChromecast) {
chromecastClass = 'active'
dlnaClass = 'disabled'
airplayClass = 'disabled'
chromecastHandler = dispatcher('closeDevice')
airplayHandler = undefined
dlnaHandler = undefined
} else if (isOnAirplay) {
chromecastClass = 'disabled'
dlnaClass = 'disabled'
airplayClass = 'active'
chromecastHandler = undefined
airplayHandler = dispatcher('closeDevice')
dlnaHandler = undefined
} else if (isOnDlna) {
chromecastClass = 'disabled'
dlnaClass = 'active'
airplayClass = 'disabled'
chromecastHandler = undefined
airplayHandler = undefined
dlnaHandler = dispatcher('closeDevice')
} else {
chromecastClass = ''
airplayClass = ''
dlnaClass = ''
chromecastHandler = dispatcher('openDevice', 'chromecast')
airplayHandler = dispatcher('openDevice', 'airplay')
dlnaHandler = dispatcher('openDevice', 'dlna')
}
if (state.devices.chromecast || isOnChromecast) {
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
elements.push(hx`
<i.icon.device.float-right
class=${chromecastClass}
onclick=${chromecastHandler}>
${castIcon}
</i>
`)
}
if (state.devices.airplay || isOnAirplay) {
elements.push(hx`
<i.icon.device.float-right
class=${airplayClass}
onclick=${airplayHandler}>
airplay
</i>
`)
}
if (state.devices.dlna || isOnDlna) {
elements.push(hx`
<i
class='icon device float-right'
class=${dlnaClass}
onclick=${dlnaHandler}>
tv
</i>
`)
}
// render volume // Add the cast buttons. Icons for each cast type, connected/disconnected:
var buttonIcons = {
'chromecast': {true: 'cast_connected', false: 'cast'},
'airplay': {true: 'airplay', false: 'airplay'},
'dnla': {true: 'tv', false: 'tv'}
}
castTypes.forEach(function (castType) {
// Do we show this button (eg. the Chromecast button) at all?
var isCasting = state.playing.location.startsWith(castType)
var player = state.devices[castType]
if ((!player || player.getDevices().length === 0) && !isCasting) return
// Show the button. Three options for eg the Chromecast button:
var buttonClass, buttonHandler
if (isCasting) {
// Option 1: we are currently connected to Chromecast. Button stops the cast.
buttonClass = 'active'
buttonHandler = dispatcher('stopCasting')
} else if (isCastingAnywhere) {
// Option 2: we are currently connected somewhere else. Button disabled.
buttonClass = 'disabled'
buttonHandler = undefined
} else {
// Option 3: we are not connected anywhere. Button opens Chromecast menu.
buttonClass = ''
buttonHandler = dispatcher('toggleCastMenu', castType)
}
var buttonIcon = buttonIcons[castType][isCasting]
elements.push(hx`
<i.icon.device.float-right
class=${buttonClass}
onclick=${buttonHandler}>
${buttonIcon}
</i>
`)
})
// Render volume slider
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_' + (
var volumeStyle = { background: '-webkit-gradient(linear, left top, right top, ' + volume === 0 ? 'off'
'color-stop(' + (volume * 100) + '%, #eee), ' + : volume < 0.3 ? 'mute'
'color-stop(' + (volume * 100) + '%, #727272))' : volume < 0.6 ? 'down'
: 'up')
var volumeStyle = {
background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' +
'color-stop(' + (volume * 100) + '%, #727272))'
} }
elements.push(hx` elements.push(hx`
@@ -428,7 +469,8 @@ function renderPlayerControls (state) {
</i> </i>
<input <input
class='volume-slider float-right' class='volume-slider float-right'
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume} type='range' min='0' max='1' step='0.05'
value=${volumeChanging !== false ? volumeChanging : volume}
onmousedown=${handleVolumeScrub} onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub} onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub} onmousemove=${handleVolumeScrub}
@@ -438,9 +480,11 @@ function renderPlayerControls (state) {
`) `)
// Show video playback progress // Show video playback progress
var currentTimeStr = formatTime(state.playing.currentTime)
var durationStr = formatTime(state.playing.duration)
elements.push(hx` elements.push(hx`
<span class='time float-left'> <span class='time float-left'>
${formatTime(state.playing.currentTime)} / ${formatTime(state.playing.duration)} ${currentTimeStr} / ${durationStr}
</span> </span>
`) `)
@@ -456,6 +500,7 @@ function renderPlayerControls (state) {
return hx` return hx`
<div class='controls'> <div class='controls'>
${elements} ${elements}
${renderCastOptions(state)}
${renderSubtitlesOptions(state)} ${renderSubtitlesOptions(state)}
</div> </div>
` `

View File

@@ -1,8 +1,6 @@
module.exports = Preferences module.exports = Preferences
var h = require('virtual-dom/h') var hx = require('../lib/hx')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatch} = require('../lib/dispatcher') var {dispatch} = require('../lib/dispatcher')
var remote = require('electron').remote var remote = require('electron').remote

View File

@@ -1,12 +1,9 @@
module.exports = UnsupportedMediaModal module.exports = UnsupportedMediaModal
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var electron = require('electron') var electron = require('electron')
var {dispatch, dispatcher} = require('../lib/dispatcher') var {dispatch, dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function UnsupportedMediaModal (state) { function UnsupportedMediaModal (state) {
var err = state.modal.error var err = state.modal.error

View File

@@ -1,21 +1,18 @@
module.exports = UpdateAvailableModal module.exports = UpdateAvailableModal
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var electron = require('electron') var electron = require('electron')
var {dispatch} = require('../lib/dispatcher') var {dispatch} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function UpdateAvailableModal (state) { function UpdateAvailableModal (state) {
return hx` return hx`
<div class='update-available-modal'> <div class='update-available-modal'>
<p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p> <p><strong>A new version of WebTorrent is available: v${state.modal.version}</strong></p>
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p> <p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
<p> <p class='float-right'>
<button class='primary' onclick=${handleOK}>Show Download Page</button> <button class='button button-flat' onclick=${handleCancel}>Skip This Release</button>
<button class='cancel' onclick=${handleCancel}>Skip This Release</button> <button class='button button-raised' onclick=${handleOK}>Show Download Page</button>
</p> </p>
</div> </div>
` `

View File

@@ -28,20 +28,13 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid // Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq // client, as explained here: https://webtorrent.io/faq
var client = window.client = new WebTorrent({ var client = new WebTorrent()
tracker: {
// HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome bug.
// Fixed in Chrome 51, so we can remove this hack once Electron updates Chrome.
// Issue: https://github.com/feross/webtorrent-desktop/issues/353
wrtc: process.platform !== 'darwin'
}
})
// WebTorrent-to-HTTP streaming sever // WebTorrent-to-HTTP streaming sever
var server = window.server = null var server = null
// Used for diffing, so we only send progress updates when necessary // Used for diffing, so we only send progress updates when necessary
var prevProgress = window.prevProgress = null var prevProgress = null
init() init()
@@ -70,6 +63,10 @@ function init () {
ipc.send('ipcReadyWebTorrent') ipc.send('ipcReadyWebTorrent')
window.addEventListener('error', (e) =>
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
true)
setInterval(updateTorrentProgress, 1000) setInterval(updateTorrentProgress, 1000)
} }
@@ -180,7 +177,7 @@ function saveTorrentFile (torrentKey) {
} }
// Otherwise, save the .torrent file, under the app config folder // Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) { fs.mkdir(config.TORRENT_PATH, function (_) {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) { fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err) if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath) console.log('saved torrent file %s', torrentPath)
@@ -193,7 +190,7 @@ function saveTorrentFile (torrentKey) {
// Checks whether we've already resolved a given infohash to a torrent file // Checks whether we've already resolved a given infohash to a torrent file
// Calls back with (torrentPath, exists). Logs, does not call back on error // Calls back with (torrentPath, exists). Logs, does not call back on error
function checkIfTorrentFileExists (infoHash, cb) { function checkIfTorrentFileExists (infoHash, cb) {
var torrentPath = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent') var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
fs.exists(torrentPath, function (exists) { fs.exists(torrentPath, function (exists) {
cb(torrentPath, exists) cb(torrentPath, exists)
}) })
@@ -206,10 +203,10 @@ function generateTorrentPoster (torrentKey) {
torrentPoster(torrent, function (err, buf, extension) { torrentPoster(torrent, function (err, buf, extension) {
if (err) return console.log('error generating poster: %o', err) if (err) return console.log('error generating poster: %o', err)
// save it for next time // save it for next time
fs.mkdirp(config.CONFIG_POSTER_PATH, function (err) { fs.mkdirp(config.POSTER_PATH, function (err) {
if (err) return console.log('error creating poster dir: %o', err) if (err) return console.log('error creating poster dir: %o', err)
var posterFileName = torrent.infoHash + extension var posterFileName = torrent.infoHash + extension
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, posterFileName) var posterFilePath = path.join(config.POSTER_PATH, posterFileName)
fs.writeFile(posterFilePath, buf, function (err) { fs.writeFile(posterFilePath, buf, function (err) {
if (err) return console.log('error saving poster: %o', err) if (err) return console.log('error saving poster: %o', err)
// show the poster // show the poster
@@ -277,7 +274,7 @@ function getTorrentProgress () {
function startServer (infoHash, index) { function startServer (infoHash, index) {
var torrent = client.get(infoHash) var torrent = client.get(infoHash)
if (torrent.ready) startServerFromReadyTorrent(torrent, index) if (torrent.ready) startServerFromReadyTorrent(torrent, index)
else torrent.on('ready', () => startServerFromReadyTorrent(torrent, index)) else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
} }
function startServerFromReadyTorrent (torrent, index, cb) { function startServerFromReadyTorrent (torrent, index, cb) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB