Compare commits

...

246 Commits

Author SHA1 Message Date
DC
3cfdf857cf Fix scrubbing bug: don't skip to 0s after drag 2016-07-21 01:09:43 -07:00
DC
c59abb251b Fix play/pause toggle bug
Sometimes, while casting, WT thinks the video isn't visible and the play/pause toggle stops working
2016-07-21 01:03:45 -07:00
DC
2fc1034cc5 changelog 2016-07-21 00:17:00 -07:00
DC
a8fd60f46e authors 2016-07-20 23:47:32 -07:00
DC
0cbae6b4d5 0.9.0 2016-07-20 23:18:23 -07:00
DC
d0733d3370 Fix bug in PlaybackController 2016-07-17 17:34:58 -07:00
DC
7b8751312a Refactor main.js: torrent controller 2016-07-17 15:52:09 -07:00
DC
6d664f2086 Refactor main.js: TorrentPlayer.isTorrent 2016-07-17 15:52:09 -07:00
Mathias Rasmussen
4ebf7e25b7 fix showItemInFolder (#723) 2016-07-17 15:51:50 -07:00
Mathias Rasmussen
54e70e7158 Remove torrent/data confirmation modal 2016-07-16 14:33:01 -07:00
DC
b950829de3 TorrentSummary.getFileOrFolder 2016-07-16 12:09:49 -07:00
Mathias Rasmussen
a489397f84 remove torrent data (single file / folder) 2016-07-16 12:09:49 -07:00
DC
897dac354d onError -> error (#716) 2016-07-16 12:08:42 -07:00
Mathias Rasmussen
beb4af1311 Remove torrent file and poster (#711)
* remove torrent file and poster

* Delete file instead of moving to trash
2016-07-14 15:41:05 -07:00
Mathias Rasmussen
f0aeab0207 Fix unhandled 'error' dispatch (#708)
* fix 'error' dispatch

* directly call functions defined in main
2016-07-14 14:04:25 -07:00
Mathias Rasmussen
be1314422d Improve error logging (#707)
e.g. undefined <video> errors
2016-07-14 14:01:58 -07:00
Mathias Rasmussen
c15711aae8 Save selected subtitle (#702) 2016-07-08 18:26:30 -07:00
DC
1668c4c614 Refactor main.js: fix Create Torrent modal 2016-07-08 11:58:15 -07:00
DC
7050ee849b Refactor main.js: fix state save on exit 2016-07-08 11:58:15 -07:00
DC
dfe1e3b631 Fix Cast lazy loading
Move all the heavy initialization, which takes ~300ms, from require('./lib/cast') to Cast.init()
2016-07-08 11:58:15 -07:00
Adam Gotlib
50c47dd657 Refactor main.js: controllers.playback.skip() (#706)
* Fixes bug with Step Forward/Backward commands not working

* Fix 'invalid torrent identifier' error
2016-07-08 11:58:15 -07:00
DC
a373141a93 Refactor main.js: replace dispatch() if-else with hash 2016-07-08 11:58:15 -07:00
DC
24f5856649 Refactor main.js: playback and subtitles controllers 2016-07-08 11:58:15 -07:00
DC
f85e0a61b1 Refactor main.js: torrent list controller 2016-07-08 11:58:15 -07:00
DC
4319ef2853 Refactor main.js: prefs controller 2016-07-08 11:58:15 -07:00
DC
c3a27dbebe Refactor main.js: media and update controllers 2016-07-08 11:58:15 -07:00
DC
bac43509d2 Refactoring main.js: simplify startup 2016-07-08 11:58:15 -07:00
DC
59b012e527 Fix README 2016-07-08 11:58:15 -07:00
Rémi Jouannet
c615e285db add debian jessie dependencies (#601)
* add debian jessie dependencies

* update dep for deb package

* gconf2, libgtk2.0-0, libnss3, libxss1
2016-07-05 19:54:59 -03:00
DC
1aca9fe753 Only send telemetry from production (#668) 2016-07-04 00:37:57 -07:00
Feross Aboukhadijeh
349c5ee22e Clean up thumbar (thumbnail) code (#670)
* Cleanup thumbnail bar code

- rename thumbnail method names for succinctness
- Get rid of 'updateThumbnailBar' event -- use existing events
- Get rid of 'blockPowerSave' and 'unblockPowerSave' events -- use a
new combined 'onPlayerPlay' and 'onPlayerPause' events which apply to
power save and updating the thumbbar

* Consistent naming for enable/disable methods
2016-06-28 06:32:28 -07:00
Adam Gotlib
c44943cef7 Fix state.playing.jumpToTime behavior (#672)
Previously, state.playing.jumpToTime = 0 didn't do anything.
This commit fixes that.
2016-06-28 06:25:26 -07:00
DC
7a61b52d64 changelog 2016-06-27 02:42:04 -07:00
DC
e5df96c82e authors 2016-06-27 02:39:53 -07:00
DC
770327c3fa 0.8.1 2016-06-27 02:38:45 -07:00
Adam Gotlib
4bdc6e3d65 Fix typo in renderer/views/player.js (#673) 2016-06-27 00:28:19 -07:00
Feross Aboukhadijeh
4799a032e5 Fixes for PR #640 2016-06-23 18:57:08 -07:00
Feross Aboukhadijeh
b2d2a6a7a5 Merge pull request #640 from anonymlol/master
new protocol handler: stream-magnet
2016-06-23 18:37:53 -07:00
DC
7676106914 changelog 2016-06-23 07:45:02 -07:00
DC
fe5ea31f2c authors 2016-06-23 07:32:28 -07:00
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
anonymlol
8f03ecedaa fix 'isMagnet' is already defined error 2016-06-14 13:53:01 +02:00
anonymlol
db20bd8eaf New Handler: stream-magnet
only tested on windows
2016-06-14 13:30:38 +02: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
DC
0a15db2892 0.6.0 2016-05-24 03:05:31 -07:00
DC
63dda10380 changelog 2016-05-24 03:03:42 -07:00
DC
6e651df083 authors 2016-05-24 02:42:29 -07:00
DC
3a8fe24eec Fix scrub cursor 2016-05-24 02:39:28 -07:00
DC
918a35e091 Fix scrub button position 2016-05-24 02:09:17 -07:00
DC
c76abeb8c0 Remove cursor:pointer 2016-05-24 01:59:51 -07:00
DC
d389b8ab38 Bugfix: window title shouldn't be stuck on 'Preferences' 2016-05-24 01:56:39 -07:00
DC
a59faacbd7 Simplify prefs window 2016-05-24 01:52:31 -07:00
grunjol
12f9709601 Add preferences page
* For now, the prefs page has just a single option, Downloads Folder
* For now, you can't type in a folder, you must use the chooser
* Further fixes coming om master
* Written by @ChrisMorrisOrg and @grunjol, rebased by @dcposch
2016-05-23 22:31:09 -07:00
Feross Aboukhadijeh
455c9c02b9 Reduce startup jank, improve startup time (#568)
* Reduce jank on app startup

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

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

* Codestyle fix

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

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

* resolve conflicts

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

* intendation fix

* conflict resolve

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

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

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

This new approach improves:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Don't unlink deselected files

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

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

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

2
.gitignore vendored
View File

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

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>
@@ -18,5 +18,13 @@
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com> - Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
- gabriel <furstenheim@gmail.com> - gabriel <furstenheim@gmail.com>
- Rolando Guedes <rolando.guedes@3gnt.net> - Rolando Guedes <rolando.guedes@3gnt.net>
- Benjamin Tan <demoneaux@gmail.com>
- Mathias Rasmussen <mathiasvr@gmail.com>
- Sergey Bargamon <sergey@bargamon.ru>
- Thomas Watson Steen <w@tson.dk>
- anonymlol <anonymlol7@gmail.com>
- Gediminas Petrikas <gedas18@gmail.com>
- Adam Gotlib <gotlib.adam+dev@gmail.com>
- Rémi Jouannet <remijouannet@gmail.com>
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,5 +1,167 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## v0.9.0 - 2016-07-20
### Added
- Save selected subtitles
- Ask for confirmation before deleting torrents
- Support Debian Jessie
### Changed
- Only send telemetry in production
- Clean up the code. Split main.js, refactor lots of things
### Fixed
- Fix state.playing.jumpToTime behavior
- Remove torrent file and poster image when deleting a torrent
## v0.8.1 - 2016-06-24
### Added
- New URI handler: stream-magnet
### Fixed
- DLNA crashing bug
## v0.8.0 - 2016-06-23
### Added
- Cast menu: choose which Chromecast, Airplay, or DLNA device you want to use
- Telemetry: send basic data, plus stats on how often the play button works
- Make posters from jpeg files, not just jpg
- Support .wmv video via Play in VLC
- Windows thumbnail bar with a play/pause button
### Changed
- Nicer modal styles
### Fixed
- Windows tray icon now stays in the right state
## 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
### Added
- Added Preferences page to set Download folder
- Save video position, resume playback from saved position
- Add additional video player keyboard shortcuts (#275)
- Use `poster.jpg` file as the poster image if available (#558)
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
- Add support for pasting `instant.io` links (#559)
- Add announcement feature
### Changed
- Nicer player UI
- Reduce startup jank, improve startup time (#568)
- Cleanup unsupported codec detection (#569, #570)
- Cleaner look for the torrent file list
- Improve subtitle positioning (#551)
### Fixed
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
- Fix bugs in LocationHistory
- When player is active, and magnet link is pasted, go back to list
- After deleting torrent, remove just the player from forward stack
- After creating torrent, remove create torrent page from forward stack
- Cancel button on create torrent page should only go back one page
## v0.5.1 - 2016-05-18
### Fixed
- Fix auto-updater (OS X, Windows).
## v0.5.0 - 2016-05-17
### Added
- Select/deselect individual files to torrent.
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
- "Add Subtitle File..." menu item.
### Changed
- When manually adding subtitle track(s), always switch to the new track.
### Fixed
- Magnet links throw exception on app launch. (OS X)
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
- Missing 'About WebTorrent' menu item. (Windows)
- Rare exception. ("Cannot create BrowserWindow before app is ready")
## v0.4.0 - 2016-05-13 ## v0.4.0 - 2016-05-13
### Added ### Added
@@ -38,7 +200,8 @@
### Fixed ### Fixed
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is temporary. (OS X) - Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is
temporary. (OS X)
- When fullscreen, make controls use the full window. (OS X) - When fullscreen, make controls use the full window. (OS X)
- Support creating torrents that contain .torrent files. - Support creating torrents that contain .torrent files.
- Block power save while casting to a remote device. - Block power save while casting to a remote device.
@@ -50,10 +213,14 @@
- Do not stop music when tabbing to another program (OS X) - Do not stop music when tabbing to another program (OS X)
- Properly size the Windows volume mixer icon. - Properly size the Windows volume mixer icon.
- Default to the user's OS-defined, localized "Downloads" folder. - Default to the user's OS-defined, localized "Downloads" folder.
- Enforce minimimum window size when resizing player, to prevent window disappearing. - Enforce minimimum window size when resizing player to prevent window disappearing.
- Fix rare race condition error on app quit. - Fix rare race condition error on app quit.
- Don't use zero-byte torrent "poster" images. - Don't use zero-byte torrent "poster" images.
Thanks to @grunjol, @rguedes, @furstenheim, @karloluis, @DiegoRBaquero, @alxhotel,
@AgentEpsilon, @remijouannet, Rolando Guedes, @dcposch, and @feross for contributing
to this release!
## v0.3.3 - 2016-04-07 ## v0.3.3 - 2016-04-07
### Fixed ### Fixed

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) Feross Aboukhadijeh Copyright (c) WebTorrent, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in

View File

@@ -81,10 +81,16 @@ 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 how well the play button works. How often does it succeed? Time out? Show a missing codec error?
The app never sends personally identifying information, nor does it track which swarms you join.
### 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)
## License ## License
MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). MIT. Copyright (c) [WebTorrent, LLC](https://webtorrent.io).

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

@@ -182,8 +182,6 @@ function buildDarwin (cb) {
var infoPlistPath = path.join(contentsPath, 'Info.plist') var infoPlistPath = path.join(contentsPath, 'Info.plist')
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
// TODO: Use new `extend-info` and `extra-resource` opts to electron-packager,
// available as of v6.
infoPlist.CFBundleDocumentTypes = [ infoPlist.CFBundleDocumentTypes = [
{ {
CFBundleTypeExtensions: [ 'torrent' ], CFBundleTypeExtensions: [ 'torrent' ],
@@ -208,6 +206,31 @@ function buildDarwin (cb) {
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns', CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
CFBundleURLName: 'BitTorrent Magnet URL', CFBundleURLName: 'BitTorrent Magnet URL',
CFBundleURLSchemes: [ 'magnet' ] CFBundleURLSchemes: [ 'magnet' ]
},
{
CFBundleTypeRole: 'Editor',
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
CFBundleURLName: 'BitTorrent Stream-Magnet URL',
CFBundleURLSchemes: [ 'stream-magnet' ]
}
]
infoPlist.UTExportedTypeDeclarations = [
{
UTTypeConformsTo: [
'public.data',
'public.item',
'com.bittorrent.torrent'
],
UTTypeDescription: 'BitTorrent Document',
UTTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
UTTypeIdentifier: 'org.bittorrent.torrent',
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
UTTypeTagSpecification: {
'com.apple.ostype': 'TORR',
'public.filename-extension': [ 'torrent' ],
'public.mime-type': 'application/x-bittorrent'
}
} }
] ]
@@ -310,11 +333,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)
}) })
@@ -443,7 +466,7 @@ function buildLinux (cb) {
info: { info: {
arch: destArch === 'x64' ? 'amd64' : 'i386', arch: destArch === 'x64' ? 'amd64' : 'i386',
targetDir: DIST_PATH, targetDir: DIST_PATH,
depends: 'libc6 (>= 2.4)', depends: 'gconf2, libgtk2.0-0, libnss3, libxss1',
scripts: { scripts: {
postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'), postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'),
prerm: path.join(config.STATIC_PATH, 'linux', 'prerm') prerm: path.join(config.STATIC_PATH, 'linux', 'prerm')

View File

@@ -1,9 +1,10 @@
#!/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
git push --tags git push --tags
npm publish npm publish
gh-release ./node_modules/.bin/gh-release

View File

@@ -2,10 +2,7 @@
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 prune
npm dedupe npm dedupe
npm test npm test

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

@@ -3,12 +3,17 @@ var fs = require('fs')
var path = require('path') var path = require('path')
var APP_NAME = 'WebTorrent' var APP_NAME = 'WebTorrent'
var APP_TEAM = 'The WebTorrent Project' var APP_TEAM = 'WebTorrent, LLC'
var APP_VERSION = require('./package.json').version var APP_VERSION = require('./package.json').version
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings') var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
module.exports = { module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
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'),
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'), APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
@@ -17,17 +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' +
'?version=' + APP_VERSION + '&platform=' + process.platform,
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',
@@ -37,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'),
@@ -56,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)

57
main/announcement.js Normal file
View File

@@ -0,0 +1,57 @@
module.exports = {
init
}
var electron = require('electron')
var config = require('../config')
var log = require('./log')
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
'?version=' + config.APP_VERSION +
'&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 () {
var get = require('simple-get')
get.concat(ANNOUNCEMENT_URL, onResponse)
}
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,12 @@ 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')
app.setAsDefaultProtocolClient('stream-magnet')
// File handlers are registered in the Info.plist. // File handlers are defined in `Info.plist`.
} }
function uninstallDarwin () {} function uninstallDarwin () {}
@@ -55,10 +55,28 @@ 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
)
registerProtocolHandlerWin32(
'stream-magnet',
'URL:BitTorrent Stream-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:
@@ -190,6 +208,7 @@ function uninstallWin32 () {
var Registry = require('winreg') var Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND) unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
function unregisterProtocolHandlerWin32 (protocol, command) { function unregisterProtocolHandlerWin32 (protocol, command) {
@@ -265,7 +284,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

@@ -5,13 +5,15 @@ var electron = require('electron')
var app = electron.app var app = electron.app
var ipcMain = electron.ipcMain var ipcMain = electron.ipcMain
var announcement = require('./announcement')
var config = require('../config') var config = require('../config')
var crashReporter = require('../crash-reporter') var crashReporter = require('../crash-reporter')
var 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')
@@ -43,6 +45,7 @@ function init () {
app.setPath('userData', config.CONFIG_PATH) app.setPath('userData', config.CONFIG_PATH)
} }
var isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false app.isQuitting = false
@@ -52,21 +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 () {
windows.createMainWindow() isReady = true
windows.createWebTorrentHiddenWindow()
windows.main.init()
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')
@@ -77,19 +88,24 @@ 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(() => {
console.error('Saving state took too long. Quitting.')
app.quit()
}, 2000) // quit after 2 secs, at most
}) })
app.on('activate', function () { app.on('activate', function () {
windows.createMainWindow() if (isReady) windows.main.show()
}) })
} }
function delayedInit () { function delayedInit () {
tray.init() announcement.init()
dock.init()
handlers.install() handlers.install()
tray.init()
updater.init() updater.init()
} }
@@ -97,13 +113,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)
} }
@@ -114,7 +129,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 {
@@ -127,18 +142,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 thumbar = require('./thumbar')
// 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,120 @@ 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() powerSaveBlocker.enable()
shortcuts.enable()
thumbar.enable()
}) })
ipcMain.on('onPlayerClose', function () { ipc.on('onPlayerClose', function () {
menu.onPlayerClose() menu.onPlayerClose()
shortcuts.onPlayerOpen() powerSaveBlocker.disable()
shortcuts.disable()
thumbar.disable()
}) })
ipcMain.on('focusWindow', function (e, windowName) { ipc.on('onPlayerPlay', function () {
windows.focusWindow(windows[windowName]) powerSaveBlocker.enable()
thumbar.onPlayerPlay()
}) })
ipcMain.on('downloadFinished', function (e, filePath) { ipc.on('onPlayerPause', function () {
if (app.dock) { powerSaveBlocker.disable()
// Bounces the Downloads stack if the filePath is inside the Downloads folder. thumbar.onPlayerPause()
app.dock.downloadFinished(filePath)
}
}) })
ipcMain.on('checkForVLC', function (e) { /**
* 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', '--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) 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 +177,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,162 +13,67 @@ 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 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) { function onPlayerClose () {
log('toggleFullScreen %s', flag) getMenuItem('Play/Pause').enabled = false
if (windows.main && windows.main.isVisible()) { getMenuItem('Increase Volume').enabled = false
flag = flag != null ? flag : !windows.main.isFullScreen() getMenuItem('Decrease Volume').enabled = false
if (flag) { getMenuItem('Step Forward').enabled = false
// Allows the window to use the full screen in fullscreen mode (OS X). getMenuItem('Step Backward').enabled = false
windows.main.setAspectRatio(0) getMenuItem('Increase Speed').enabled = false
} getMenuItem('Decrease Speed').enabled = false
windows.main.setFullScreen(flag) getMenuItem('Add Subtitles File...').enabled = false
}
} }
// Sets whether the window should always show on top of other windows function onPlayerOpen () {
function toggleFloatOnTop (flag) { getMenuItem('Play/Pause').enabled = true
log('toggleFloatOnTop %s', flag) getMenuItem('Increase Volume').enabled = true
if (windows.main) { getMenuItem('Decrease Volume').enabled = true
flag = flag != null ? flag : !windows.main.isAlwaysOnTop() getMenuItem('Step Forward').enabled = true
windows.main.setAlwaysOnTop(flag) getMenuItem('Step Backward').enabled = true
getMenuItem('Float on Top').checked = flag getMenuItem('Increase Speed').enabled = true
} getMenuItem('Decrease Speed').enabled = true
getMenuItem('Add Subtitles File...').enabled = true
} }
function toggleDevTools () { function onToggleAlwaysOnTop (flag) {
log('toggleDevTools') getMenuItem('Float on Top').checked = flag
if (windows.main) {
windows.main.toggleDevTools()
}
} }
function showWebTorrentWindow () { function onToggleFullScreen (flag) {
log('showWebTorrentWindow') getMenuItem('Full Screen').checked = flag
windows.webtorrent.show()
windows.webtorrent.webContents.openDevTools({ detach: true })
} }
function playPause () { function onWindowBlur () {
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 onWindowShow () {
log('onWindowShow')
getMenuItem('Full Screen').enabled = true
getMenuItem('Float on Top').enabled = true
}
function onWindowHide () {
log('onWindowHide')
getMenuItem('Full Screen').enabled = false getMenuItem('Full Screen').enabled = false
getMenuItem('Float on Top').enabled = false getMenuItem('Float on Top').enabled = false
} }
function onPlayerOpen () { function onWindowFocus () {
log('onPlayerOpen') getMenuItem('Full Screen').enabled = true
getMenuItem('Play/Pause').enabled = true getMenuItem('Float on Top').enabled = true
getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true
}
function onPlayerClose () {
log('onPlayerClose')
getMenuItem('Play/Pause').enabled = false
getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false
}
function onToggleFullScreen (isFullScreen) {
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen()
windows.main.setMenuBarVisibility(!isFullScreen)
getMenuItem('Full Screen').checked = isFullScreen
windows.main.send('fullscreenChanged', isFullScreen)
} }
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',
@@ -183,23 +83,23 @@ 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'
}, },
{ {
label: process.platform === 'windows' label: process.platform === 'win32'
? 'Close' ? 'Close'
: 'Close Window', : 'Close Window',
accelerator: 'CmdOrCtrl+W', accelerator: 'CmdOrCtrl+W',
@@ -229,6 +129,14 @@ function getAppMenuTemplate () {
label: 'Select All', label: 'Select All',
accelerator: 'CmdOrCtrl+A', accelerator: 'CmdOrCtrl+A',
role: 'selectall' role: 'selectall'
},
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => windows.main.dispatch('preferences')
} }
] ]
}, },
@@ -241,12 +149,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'
@@ -259,14 +175,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()
} }
] ]
} }
@@ -277,8 +193,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
}, },
{ {
@@ -287,13 +203,55 @@ 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
},
{
type: 'separator'
},
{
label: 'Step Forward',
accelerator: process.platform === 'darwin'
? 'CmdOrCtrl+Alt+Right'
: 'Alt+Right',
click: () => windows.main.dispatch('skip', 10),
enabled: false
},
{
label: 'Step Backward',
accelerator: process.platform === 'darwin'
? 'CmdOrCtrl+Alt+Left'
: 'Alt+Left',
click: () => windows.main.dispatch('skip', -10),
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Speed',
accelerator: 'CmdOrCtrl+=',
click: () => windows.main.dispatch('changePlaybackRate', 1),
enabled: false
},
{
label: 'Decrease Speed',
accelerator: 'CmdOrCtrl+-',
click: () => windows.main.dispatch('changePlaybackRate', -1),
enabled: false
},
{
type: 'separator'
},
{
label: 'Add Subtitles File...',
click: () => windows.main.dispatch('openSubtitles'),
enabled: false enabled: false
} }
] ]
@@ -304,18 +262,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)
} }
] ]
} }
@@ -333,6 +291,14 @@ function getAppMenuTemplate () {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => windows.main.dispatch('preferences')
},
{
type: 'separator'
},
{ {
label: 'Services', label: 'Services',
role: 'services', role: 'services',
@@ -387,12 +353,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
if (process.platform === 'linux' || process.platform === 'windows') { // folders and files, so add an extra menu item so there is one for each type.
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)
@@ -402,12 +369,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({
@@ -418,23 +385,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 = {
enable,
disable
}
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 enable () {
disable() // Stop the previous power saver block, if one exists.
blockId = electron.powerSaveBlocker.start('prevent-display-sleep')
log(`powerSaveBlocker.enable: ${blockId}`)
}
/**
* Stop blocking the system from entering low-power mode.
*/
function disable () {
if (!electron.powerSaveBlocker.isStarted(blockId)) {
return
}
electron.powerSaveBlocker.stop(blockId)
log(`powerSaveBlocker.disable: ${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, disable,
onPlayerClose, enable
onPlayerOpen
} }
var electron = require('electron') var electron = require('electron')
var menu = require('./menu')
var windows = require('./windows') var windows = require('./windows')
function init () { function enable () {
var localShortcut = require('electron-localshortcut') // Register play/pause media key, available on some keyboards.
// 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 () {
// Register special "media key" for play/pause, available on some keyboards
electron.globalShortcut.register( electron.globalShortcut.register(
'MediaPlayPause', 'MediaPlayPause',
() => windows.main.send('dispatch', 'playPause') () => windows.main.dispatch('playPause')
) )
} }
function onPlayerClose () { function disable () {
// 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)
} }

54
main/thumbar.js Normal file
View File

@@ -0,0 +1,54 @@
module.exports = {
disable,
enable,
onPlayerPause,
onPlayerPlay
}
/**
* On Windows, add a "thumbnail toolbar" with a play/pause button in the taskbar.
* This provides users a way to access play/pause functionality without restoring
* or activating the window.
*/
var path = require('path')
var config = require('../config')
var windows = require('./windows')
/**
* Show the Windows thumbnail toolbar buttons.
*/
function enable () {
update(false)
}
/**
* Hide the Windows thumbnail toolbar buttons.
*/
function disable () {
windows.main.win.setThumbarButtons([])
}
function onPlayerPause () {
update(true)
}
function onPlayerPlay () {
update(false)
}
function update (isPaused) {
var icon = isPaused
? 'PlayThumbnailBarButton.png'
: 'PauseThumbnailBarButton.png'
var buttons = [
{
tooltip: isPaused ? 'Play' : 'Pause',
icon: path.join(config.STATIC_PATH, icon),
click: () => windows.main.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

@@ -9,6 +9,10 @@ var config = require('../config')
var log = require('./log') var log = require('./log')
var windows = require('./windows') var windows = require('./windows')
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform
function init () { function init () {
if (process.platform === 'linux') { if (process.platform === 'linux') {
initLinux() initLinux()
@@ -17,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(config.AUTO_UPDATE_URL, onResponse) get.concat(AUTO_UPDATE_URL, onResponse)
}
function onResponse (err, res, data) { function onResponse (err, res, data) {
if (err) return log(`Update error: ${err.message}`) if (err) return log(`Update error: ${err.message}`)
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}`)
} }
} }
@@ -67,5 +71,6 @@ function initDarwinWin32 () {
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`) (e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
) )
electron.autoUpdater.setFeedURL(config.AUTO_UPDATE_URL) electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
electron.autoUpdater.checkForUpdates()
} }

View File

@@ -1,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,11 +1,11 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.4.0", "version": "0.9.0",
"author": { "author": {
"name": "Feross Aboukhadijeh", "name": "WebTorrent, LLC",
"email": "feross@feross.org", "email": "feross@webtorrent.io",
"url": "http://feross.org" "url": "https://webtorrent.io"
}, },
"bin": { "bin": {
"webtorrent-desktop": "./bin/cmd.js" "webtorrent-desktop": "./bin/cmd.js"
@@ -14,30 +14,34 @@
"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",
"concat-stream": "^1.5.1",
"create-torrent": "^3.24.5", "create-torrent": "^3.24.5",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0", "dlnacasts": "^0.1.0",
"drag-drop": "^2.11.0", "drag-drop": "^2.11.0",
"electron-localshortcut": "^0.6.0", "electron-prebuilt": "1.2.1",
"electron-prebuilt": "1.0.2",
"fs-extra": "^0.27.0", "fs-extra": "^0.27.0",
"hyperx": "^2.0.2", "hyperx": "^2.0.2",
"iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1", "languagedetect": "^1.1.1",
"main-loop": "^3.2.0", "main-loop": "^3.2.0",
"musicmetadata": "^2.0.2", "musicmetadata": "^2.0.2",
"network-address": "^1.1.0", "network-address": "^1.1.0",
"parse-torrent": "^5.7.3",
"prettier-bytes": "^1.0.1", "prettier-bytes": "^1.0.1",
"run-parallel": "^1.1.6",
"semver": "^5.1.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",
"virtual-dom": "^2.1.1", "virtual-dom": "^2.1.1",
"vlc-command": "^1.0.1", "vlc-command": "^1.0.1",
"webtorrent": "0.x", "webtorrent": "0.x",
"winreg": "^1.2.0" "winreg": "^1.2.0",
"zero-fill": "^2.2.3"
}, },
"devDependencies": { "devDependencies": {
"cross-zip": "^2.0.1", "cross-zip": "^2.0.1",
@@ -47,7 +51,7 @@
"gh-release": "^2.0.3", "gh-release": "^2.0.3",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"nobin-debian-installer": "^0.0.9", "nobin-debian-installer": "^0.0.10",
"open": "0.0.5", "open": "0.0.5",
"plist": "^1.2.0", "plist": "^1.2.0",
"rimraf": "^2.5.2", "rimraf": "^2.5.2",
@@ -68,7 +72,7 @@
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"optionalDependencies": { "optionalDependencies": {
"appdmg": "^0.3.6" "appdmg": "^0.4.3"
}, },
"productName": "WebTorrent", "productName": "WebTorrent",
"repository": { "repository": {
@@ -80,7 +84,7 @@
"open-config": "node ./bin/open-config.js", "open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js", "package": "node ./bin/package.js",
"start": "electron .", "start": "electron .",
"test": "standard && ./bin/check-deps.js", "test": "standard && node ./bin/check-deps.js",
"update-authors": "./bin/update-authors.sh" "update-authors": "./bin/update-authors.sh"
} }
} }

View File

@@ -0,0 +1,56 @@
const electron = require('electron')
const ipcRenderer = electron.ipcRenderer
// Controls local play back: the <video>/<audio> tag and VLC
// Does not control remote casting (Chromecast etc)
module.exports = class MediaController {
constructor (state) {
this.state = state
}
mediaSuccess () {
this.state.playing.result = 'success'
}
mediaStalled () {
this.state.playing.isStalled = true
}
mediaError (error) {
var state = this.state
if (state.location.url() === 'player') {
state.playing.result = 'error'
state.playing.location = 'error'
ipcRenderer.send('checkForVLC')
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
state.modal = {
id: 'unsupported-media-modal',
error: error,
vlcInstalled: isInstalled
}
})
}
}
mediaTimeUpdate () {
this.state.playing.lastTimeUpdate = new Date().getTime()
this.state.playing.isStalled = false
}
mediaMouseMoved () {
this.state.playing.mouseStationarySince = new Date().getTime()
}
vlcPlay () {
ipcRenderer.send('vlcPlay', this.state.server.localURL)
this.state.playing.location = 'vlc'
}
vlcNotFound () {
var modal = this.state.modal
if (modal && modal.id === 'unsupported-media-modal') {
modal.vlcNotFound = true
}
}
}

View File

@@ -0,0 +1,309 @@
const electron = require('electron')
const path = require('path')
const Cast = require('../lib/cast')
const {dispatch} = require('../lib/dispatcher')
const telemetry = require('../lib/telemetry')
const errors = require('../lib/errors')
const sound = require('../lib/sound')
const TorrentPlayer = require('../lib/torrent-player')
const TorrentSummary = require('../lib/torrent-summary')
const State = require('../lib/state')
const ipcRenderer = electron.ipcRenderer
// Controls playback of torrents and files within torrents
// both local (<video>,<audio>,VLC) and remote (cast)
module.exports = class PlaybackController {
constructor (state, config, update) {
this.state = state
this.config = config
this.update = update
}
// Play a file in a torrent.
// * Start torrenting, if necessary
// * Stream, if not already fully downloaded
// * If no file index is provided, pick the default file to play
playFile (infoHash, index /* optional */) {
this.state.location.go({
url: 'player',
onbeforeload: (cb) => {
this.play()
openPlayer(this.state, infoHash, index, cb)
},
onbeforeunload: (cb) => closePlayer(this.state, this.config, cb)
}, (err) => {
if (err) dispatch('error', err)
})
}
// Show a file in the OS, eg in Finder on a Mac
openItem (infoHash, index) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var filePath = path.join(
torrentSummary.path,
torrentSummary.files[index].path)
ipcRenderer.send('openItem', filePath)
}
// Toggle (play or pause) the currently playing media
playPause () {
var state = this.state
if (state.location.url() !== 'player') return
// force rerendering if window is hidden,
// in order to bypass `raf` and play/pause media immediately
var mediaTag = document.querySelector('video,audio')
if (!state.window.isVisible && mediaTag) {
if (state.playing.isPaused) mediaTag.play()
else mediaTag.pause()
}
if (state.playing.isPaused) this.play()
else this.pause()
}
// Play (unpause) the current media
play () {
var state = this.state
if (!state.playing.isPaused) return
state.playing.isPaused = false
if (isCasting(state)) {
Cast.play()
}
ipcRenderer.send('onPlayerPlay')
}
// Pause the currently playing media
pause () {
var state = this.state
if (state.playing.isPaused) return
state.playing.isPaused = true
if (isCasting(state)) {
Cast.pause()
}
ipcRenderer.send('onPlayerPause')
}
// Skip specified number of seconds (backwards if negative)
skip (time) {
this.skipTo(this.state.playing.currentTime + time)
}
// Skip (aka seek) to a specific point, in seconds
skipTo (time) {
if (isCasting(this.state)) Cast.seek(time)
else this.state.playing.jumpToTime = time
}
// Change playback speed. 1 = faster, -1 = slower
// Playback speed ranges from 16 (fast forward) to 1 (normal playback)
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
// until -16 (fast rewind)
changePlaybackRate (direction) {
var state = this.state
var rate = state.playing.playbackRate
if (direction > 0 && rate >= 0.25 && rate < 2) {
rate += 0.25
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
rate -= 0.25
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
rate = -1
} else if (direction > 0 && rate === -1) {
rate = 0.25
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
rate *= 2
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
rate /= 2
}
state.playing.playbackRate = rate
if (isCasting(state) && !Cast.setRate(rate)) {
state.playing.playbackRate = 1
}
}
// Change the volume, in range [0, 1], by some amount
// For example, volume muted (0), changeVolume (0.3) increases to 30% volume
changeVolume (delta) {
// change volume with delta value
this.setVolume(this.state.playing.volume + delta)
}
// Set the volume to some value in [0, 1]
setVolume (volume) {
// check if its in [0.0 - 1.0] range
volume = Math.max(0, Math.min(1, volume))
var state = this.state
if (isCasting(state)) {
Cast.setVolume(volume)
} else {
state.playing.setVolume = volume
}
}
// Hide player controls while playing video, if the mouse stays still for a while
// Never hide the controls when:
// * The mouse is over the controls or we're scrubbing (see CSS)
// * The video is paused
// * The video is playing remotely on Chromecast or Airplay
showOrHidePlayerControls () {
var state = this.state
var hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local'
if (hideControls !== state.playing.hideControls) {
state.playing.hideControls = hideControls
return true
}
return false
}
}
// Opens the video player to a specific torrent
function openPlayer (state, infoHash, index, cb) {
var torrentSummary = TorrentSummary.getByKey(state, infoHash)
// automatically choose which file in the torrent to play, if necessary
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
if (index === undefined) return cb(new errors.UnplayableError())
// update UI to show pending playback
if (torrentSummary.progress !== 1) sound.play('PLAY')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'requested'
this.update()
var timeout = setTimeout(() => {
telemetry.logPlayAttempt('timeout')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR')
cb(new Error('Playback timed out. Try again.'))
this.update()
}, 10000) /* give it a few seconds */
if (torrentSummary.status === 'paused') {
dispatch('startTorrentingSummary', torrentSummary)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
() => openPlayerFromActiveTorrent(state, torrentSummary, index, timeout, cb))
} else {
openPlayerFromActiveTorrent(state, torrentSummary, index, timeout, cb)
}
}
function openPlayerFromActiveTorrent (state, torrentSummary, index, timeout, cb) {
var fileSummary = torrentSummary.files[index]
// update state
state.playing.infoHash = torrentSummary.infoHash
state.playing.fileIndex = index
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
: 'other'
// pick up where we left off
if (fileSummary.currentTime) {
var fraction = fileSummary.currentTime / fileSummary.duration
var secondsLeft = fileSummary.duration - fileSummary.currentTime
if (fraction < 0.9 && secondsLeft > 10) {
state.playing.jumpToTime = fileSummary.currentTime
}
}
// if it's audio, parse out the metadata (artist, title, etc)
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
}
// if it's video, check for subtitles files that are done downloading
dispatch('checkForSubtitles')
// enable previously selected subtitle track
if (fileSummary.selectedSubtitle) {
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
}
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
clearTimeout(timeout)
// if we timed out (user clicked play a long time ago), don't autoplay
var timedOut = torrentSummary.playStatus === 'timeout'
delete torrentSummary.playStatus
if (timedOut) {
ipcRenderer.send('wt-stop-server')
return this.update()
}
// otherwise, play the video
state.window.title = torrentSummary.files[state.playing.fileIndex].name
this.update()
ipcRenderer.send('onPlayerOpen')
cb()
})
}
function closePlayer (state, config, cb) {
console.log('closePlayer')
// Quit any external players, like Chromecast/Airplay/etc or VLC
if (isCasting(state)) {
Cast.stop()
}
if (state.playing.location === 'vlc') {
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.playing = State.getDefaultPlayState()
state.server = null
// Reset the window size and location back to where it was
if (state.window.isFullScreen) {
dispatch('toggleFullScreen', false)
}
restoreBounds(state)
// Tell the WebTorrent process to kill the torrent-to-HTTP server
ipcRenderer.send('wt-stop-server')
ipcRenderer.send('onPlayerClose')
this.update()
cb()
}
// Checks whether we are connected and already casting
// Returns false if we not casting (state.playing.location === 'local')
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
function isCasting (state) {
return state.playing.location === 'chromecast' ||
state.playing.location === 'airplay' ||
state.playing.location === 'dlna'
}
function restoreBounds (state) {
ipcRenderer.send('setAspectRatio', 0)
if (state.window.bounds) {
ipcRenderer.send('setBounds', state.window.bounds, false)
}
}

View File

@@ -0,0 +1,51 @@
const State = require('../lib/state')
// Controls the Preferences screen
module.exports = class PrefsController {
constructor (state, config) {
this.state = state
this.config = config
}
// Goes to the Preferences screen
show () {
var state = this.state
state.location.go({
url: 'preferences',
onbeforeload: function (cb) {
// initialize preferences
state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
cb()
},
onbeforeunload: (cb) => {
// save state after preferences
this.save()
state.window.title = this.config.APP_WINDOW_TITLE
cb()
}
})
}
// Updates a single property in the UNSAVED prefs
// For example: updatePreferences("foo.bar", "baz")
// Call savePreferences to save to config.json
update (property, value) {
var path = property.split('.')
var key = this.state.unsaved.prefs
for (var i = 0; i < path.length - 1; i++) {
if (typeof key[path[i]] === 'undefined') {
key[path[i]] = {}
}
key = key[path[i]]
}
key[path[i]] = value
}
// All unsaved prefs take effect atomically, and are saved to config.json
save () {
var state = this.state
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
State.save(state)
}
}

View File

@@ -0,0 +1,137 @@
const electron = require('electron')
const fs = require('fs-extra')
const path = require('path')
const parallel = require('run-parallel')
const {dispatch} = require('../lib/dispatcher')
module.exports = class SubtitlesController {
constructor (state) {
this.state = state
}
openSubtitles () {
electron.remote.dialog.showOpenDialog({
title: 'Select a subtitles file.',
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
properties: [ 'openFile' ]
}, (filenames) => {
if (!Array.isArray(filenames)) return
this.addSubtitles(filenames, true)
})
}
selectSubtitle (ix) {
this.state.playing.subtitles.selectedIndex = ix
}
toggleSubtitlesMenu () {
var subtitles = this.state.playing.subtitles
subtitles.showMenu = !subtitles.showMenu
}
addSubtitles (files, autoSelect) {
var state = this.state
// Subtitles are only supported when playing video files
if (state.playing.type !== 'video') return
if (files.length === 0) return
var subtitles = state.playing.subtitles
// Read the files concurrently, then add all resulting subtitle tracks
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(tasks, function (err, tracks) {
if (err) return dispatch('error', err)
for (var i = 0; i < tracks.length; i++) {
// No dupes allowed
var track = tracks[i]
var trackIndex = state.playing.subtitles.tracks
.findIndex((t) => track.filePath === t.filePath)
// Add the track
if (trackIndex === -1) {
trackIndex = state.playing.subtitles.tracks.push(track) - 1
}
// If we're auto-selecting a track, try to find one in the user's language
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
state.playing.subtitles.selectedIndex = trackIndex
}
}
// Finally, make sure no two tracks have the same label
relabelSubtitles(subtitles)
})
}
checkForSubtitles () {
if (this.state.playing.type !== 'video') return
var torrentSummary = this.state.getPlayingTorrentSummary()
if (!torrentSummary || !torrentSummary.progress) return
torrentSummary.progress.files.forEach((fp, ix) => {
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
var file = torrentSummary.files[ix]
if (!this.isSubtitle(file.name)) return
var filePath = path.join(torrentSummary.path, file.path)
this.addSubtitles([filePath], false)
})
}
isSubtitle (file) {
var name = typeof file === 'string' ? file : file.name
var ext = path.extname(name).toLowerCase()
return ext === '.srt' || ext === '.vtt'
}
}
function loadSubtitle (file, cb) {
// Lazy load to keep startup fast
var concat = require('simple-concat')
var LanguageDetect = require('languagedetect')
var srtToVtt = require('srt-to-vtt')
// Read the .SRT or .VTT file, parse it, add subtitle track
var filePath = file.path || file
var vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
concat(vttStream, function (err, buf) {
if (err) return dispatch('error', 'Can\'t parse subtitles file.')
// Detect what language the subtitles are in
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
var track = {
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
language: langDetected,
label: langDetected,
filePath: filePath
}
cb(null, track)
})
}
// Checks whether a language name like "English" or "German" matches the system
// language, aka the current locale
function isSystemLanguage (language) {
var iso639 = require('iso-639-1')
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
var langIso = iso639.getCode(language) // eg "de" if language is "German"
return langIso === osLangISO
}
// Make sure we don't have two subtitle tracks with the same label
// Labels each track by language, eg "German", "English", "English 2", ...
function relabelSubtitles (subtitles) {
var counts = {}
subtitles.tracks.forEach(function (track) {
var lang = track.language
counts[lang] = (counts[lang] || 0) + 1
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
})
}

View File

@@ -0,0 +1,192 @@
const path = require('path')
const ipcRenderer = require('electron').ipcRenderer
const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player')
const sound = require('../lib/sound')
const {dispatch} = require('../lib/dispatcher')
module.exports = class TorrentController {
constructor (state) {
this.state = state
}
torrentInfoHash (torrentKey, infoHash) {
var torrentSummary = this.getTorrentSummary(torrentKey)
console.log('got infohash for %s torrent %s',
torrentSummary ? 'existing' : 'new', torrentKey)
if (!torrentSummary) {
var torrents = this.state.saved.torrents
// Check if an existing (non-active) torrent has the same info hash
if (torrents.find((t) => t.infoHash === infoHash)) {
ipcRenderer.send('wt-stop-torrenting', infoHash)
return dispatch('error', 'Cannot add duplicate torrent')
}
torrentSummary = {
torrentKey: torrentKey,
status: 'new'
}
torrents.unshift(torrentSummary)
sound.play('ADD')
}
torrentSummary.infoHash = infoHash
dispatch('update')
}
torrentWarning (torrentKey, message) {
console.log('warning for torrent %s: %s', torrentKey, message)
}
torrentError (torrentKey, message) {
// TODO: WebTorrent needs semantic errors
if (message.startsWith('Cannot add duplicate torrent')) {
// Remove infohash from the message
message = 'Cannot add duplicate torrent'
}
dispatch('error', message)
var torrentSummary = this.getTorrentSummary(torrentKey)
if (torrentSummary) {
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
torrentSummary.status = 'paused'
dispatch('update')
}
}
torrentMetadata (torrentKey, torrentInfo) {
// Summarize torrent
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.status = 'downloading'
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
torrentSummary.path = torrentInfo.path
torrentSummary.magnetURI = torrentInfo.magnetURI
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
// For now, check whether torrentSummary.files has already been set:
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
if (!hasDetailedFileInfo) {
torrentSummary.files = torrentInfo.files
}
if (!torrentSummary.selections) {
torrentSummary.selections = torrentSummary.files.map((x) => true)
}
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
dispatch('update')
// Save the .torrent file, if it hasn't been saved already
if (!torrentSummary.torrentFileName) ipcRenderer.send('wt-save-torrent-file', torrentKey)
// Auto-generate a poster image, if it hasn't been generated already
if (!torrentSummary.posterFileName) ipcRenderer.send('wt-generate-torrent-poster', torrentKey)
}
torrentDone (torrentKey, torrentInfo) {
// Update the torrent summary
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.status = 'seeding'
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
// Don't notify if we merely finished verifying data files that were already on disk.
if (torrentInfo.bytesReceived > 0) {
if (!this.state.window.isFocused) {
this.state.dock.badge += 1
}
showDoneNotification(torrentSummary)
ipcRenderer.send('downloadFinished', getTorrentPath(torrentSummary))
}
dispatch('update')
}
torrentProgress (progressInfo) {
// Overall progress across all active torrents, 0 to 1
var progress = progressInfo.progress
var hasActiveTorrents = progressInfo.hasActiveTorrents
// Hide progress bar when client has no torrents, or progress is 100%
// TODO: isn't this equivalent to: if (progress === 1) ?
if (!hasActiveTorrents || progress === 1) {
progress = -1
}
// Show progress bar under the WebTorrent taskbar icon, on OSX
this.state.dock.progress = progress
// Update progress for each individual torrent
progressInfo.torrents.forEach((p) => {
var torrentSummary = this.getTorrentSummary(p.torrentKey)
if (!torrentSummary) {
console.log('warning: got progress for missing torrent %s', p.torrentKey)
return
}
torrentSummary.progress = p
})
// 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()
dispatch('update')
}
torrentFileModtimes (torrentKey, fileModtimes) {
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.fileModtimes = fileModtimes
dispatch('saveStateThrottled')
}
torrentFileSaved (torrentKey, torrentFileName) {
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.torrentFileName = torrentFileName
dispatch('saveStateThrottled')
}
torrentPosterSaved (torrentKey, posterFileName) {
var torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.posterFileName = posterFileName
dispatch('saveStateThrottled')
}
torrentAudioMetadata (infoHash, index, info) {
var torrentSummary = this.getTorrentSummary(infoHash)
var fileSummary = torrentSummary.files[index]
fileSummary.audioInfo = info
dispatch('update')
}
torrentServerRunning (serverInfo) {
this.state.server = serverInfo
}
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
// Returns undefined if we don't know that infoHash
getTorrentSummary (torrentKey) {
return TorrentSummary.getByKey(this.state, torrentKey)
}
}
function getTorrentPath (torrentSummary) {
var itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
if (torrentSummary.files.length > 1) {
itemPath = path.dirname(itemPath)
}
return itemPath
}
function showDoneNotification (torrent) {
var notif = new window.Notification('Download Complete', {
body: torrent.name,
silent: true
})
notif.onclick = function () {
ipcRenderer.send('show')
}
sound.play('DONE')
}

View File

@@ -0,0 +1,282 @@
const fs = require('fs')
const path = require('path')
const electron = require('electron')
const {dispatch} = require('../lib/dispatcher')
const State = require('../lib/state')
const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary')
const ipcRenderer = electron.ipcRenderer
const instantIoRegex = /^(https:\/\/)?instant\.io\/#/
// Controls the torrent list: creating, adding, deleting, & manipulating torrents
module.exports = class TorrentListController {
constructor (state) {
this.state = state
}
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
addTorrent (torrentId) {
if (torrentId.path) {
// Use path string instead of W3C File object
torrentId = torrentId.path
}
// 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)) {
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
}
var torrentKey = this.state.nextTorrentKey++
var path = this.state.saved.prefs.downloadPath
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
dispatch('backToList')
}
// Shows the Create Torrent page with options to seed a given file or folder
showCreateTorrent (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') {
this.state.location.go({
url: 'create-torrent',
files: files
})
return
}
// ... or it will be an array of mixed file and folder paths. We have to walk
// through all the folders and find the files
findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles))
}
// Switches between the advanced and simple Create Torrent UI
toggleCreateTorrentAdvanced () {
var info = this.state.location.current()
if (info.url !== 'create-torrent') return
info.showAdvanced = !info.showAdvanced
}
// Creates a new torrent and start seeeding
createTorrent (options) {
var state = this.state
var torrentKey = state.nextTorrentKey++
ipcRenderer.send('wt-create-torrent', torrentKey, options)
state.location.backToFirst(function () {
state.location.clearForward('create-torrent')
})
}
// Starts downloading and/or seeding a given torrentSummary.
startTorrentingSummary (torrentSummary) {
var s = torrentSummary
// Backward compatibility for config files save before we had torrentKey
if (!s.torrentKey) s.torrentKey = this.state.nextTorrentKey++
// Use Downloads folder by default
if (!s.path) s.path = this.state.saved.prefs.downloadPath
ipcRenderer.send('wt-start-torrenting',
s.torrentKey,
TorrentSummary.getTorrentID(s),
s.path,
s.fileModtimes,
s.selections)
}
// TODO: use torrentKey, not infoHash
toggleTorrent (infoHash) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new'
this.startTorrentingSummary(torrentSummary)
sound.play('ENABLE')
} else {
torrentSummary.status = 'paused'
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
sound.play('DISABLE')
}
}
toggleTorrentFile (infoHash, index) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
torrentSummary.selections[index] = !torrentSummary.selections[index]
// Let the WebTorrent process know to start or stop fetching that file
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
}
confirmDeleteTorrent (infoHash, deleteData) {
this.state.modal = {
id: 'remove-torrent-modal',
infoHash,
deleteData
}
}
// TODO: use torrentKey, not infoHash
deleteTorrent (infoHash, deleteData) {
ipcRenderer.send('wt-stop-torrenting', infoHash)
var index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) {
var summary = this.state.saved.torrents[index]
// remove torrent and poster file
deleteFile(TorrentSummary.getTorrentPath(summary))
deleteFile(TorrentSummary.getPosterPath(summary)) // TODO: will the css path hack affect windows?
// optionally delete the torrent data
if (deleteData) moveItemToTrash(summary)
// remove torrent from saved list
this.state.saved.torrents.splice(index, 1)
State.saveThrottled(this.state)
}
this.state.location.clearForward('player') // prevent user from going forward to a deleted torrent
sound.play('DELETE')
}
toggleSelectTorrent (infoHash) {
if (this.state.selectedInfoHash === infoHash) {
this.state.selectedInfoHash = null
} else {
this.state.selectedInfoHash = infoHash
}
}
openTorrentContextMenu (infoHash) {
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
var menu = new electron.remote.Menu()
menu.append(new electron.remote.MenuItem({
label: 'Remove From List',
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
}))
menu.append(new electron.remote.MenuItem({
label: 'Remove Data File',
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
}))
menu.append(new electron.remote.MenuItem({
type: 'separator'
}))
if (torrentSummary.files) {
menu.append(new electron.remote.MenuItem({
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
click: () => showItemInFolder(torrentSummary)
}))
menu.append(new electron.remote.MenuItem({
type: 'separator'
}))
}
menu.append(new electron.remote.MenuItem({
label: 'Copy Magnet Link to Clipboard',
click: () => electron.clipboard.writeText(torrentSummary.magnetURI)
}))
menu.append(new electron.remote.MenuItem({
label: 'Copy Instant.io Link to Clipboard',
click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
}))
menu.append(new electron.remote.MenuItem({
label: 'Save Torrent File As...',
click: () => saveTorrentFileAs(torrentSummary)
}))
menu.popup(electron.remote.getCurrentWindow())
}
}
// Recursively finds {name, path, size} for all files in a folder
// Calls `cb` on success, calls `onError` on failure
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) {
if (err) return dispatch('error', err)
// Files: return name, path, and size
if (!stat.isDirectory()) {
var filePath = fileOrFolder
return cb([{
name: path.basename(filePath),
path: filePath,
size: stat.size
}])
}
// Folders: recurse, make a list of all the files
var folderPath = fileOrFolder
fs.readdir(folderPath, function (err, fileNames) {
if (err) return dispatch('error', err)
var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
findFilesRecursive(paths, cb)
})
})
}
function deleteFile (path) {
if (!path) return
fs.unlink(path, function (err) {
if (err) dispatch('error', err)
})
}
// Delete all files in a torrent
function moveItemToTrash (torrentSummary) {
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
ipcRenderer.send('moveItemToTrash', filePath)
}
function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
}
function saveTorrentFileAs (torrentSummary) {
var downloadPath = this.state.saved.prefs.downloadPath
var newFileName = path.parse(torrentSummary.name).name + '.torrent'
var opts = {
title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
]
}
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return dispatch('error', err)
})
})
})
}

View File

@@ -0,0 +1,26 @@
const State = require('../lib/state')
// Controls the UI checking for new versions of the app, prompting install
module.exports = class UpdateController {
constructor (state) {
this.state = state
}
// Shows a modal saying that we have an update
updateAvailable (version) {
var skipped = this.state.saved.skippedVersions
if (skipped && skipped.includes(version)) {
console.log('new version skipped by user: v' + version)
return
}
this.state.modal = { id: 'update-available-modal', version: version }
}
// Don't show the modal again until the next version
skipVersion (version) {
var skipped = this.state.saved.skippedVersions
if (!skipped) skipped = this.state.saved.skippedVersions = []
skipped.push(version)
State.saveThrottled(this.state)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,17 +3,18 @@
// * 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,
setVolume setVolume,
setRate
} }
var airplay = require('airplay-js') // Lazy load these for a ~300ms improvement in startup time
var chromecasts = require('chromecasts')() var airplayer, chromecasts, dlnacasts
var dlnacasts = require('dlnacasts')()
var config = require('../../config') var config = require('../../config')
@@ -31,25 +32,54 @@ function init (appState, callback) {
state = appState state = appState
update = callback update = callback
// Load modules, scan the network for devices
airplayer = require('airplayer')()
chromecasts = require('chromecasts')()
dlnacasts = require('dlnacasts')()
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(),
@@ -57,7 +87,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()
}) })
@@ -65,7 +96,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) {
@@ -83,19 +114,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
@@ -105,36 +136,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'
@@ -144,55 +198,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(),
@@ -200,7 +266,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()
}) })
@@ -208,7 +275,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
@@ -227,19 +294,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
@@ -249,61 +316,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()
@@ -316,8 +400,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
@@ -331,31 +415,39 @@ 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) {
var player
var result = true
if (state.playing.location === 'chromecast') {
// TODO find how to control playback rate on chromecast
castCallback()
result = false
} else if (state.playing.location === 'airplay') {
player = state.devices.airplay
player.rate(rate, castCallback)
} else {
result = false
} }
return result
} }
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

@@ -4,81 +4,123 @@ function LocationHistory () {
if (!new.target) return new LocationHistory() if (!new.target) return new LocationHistory()
this._history = [] this._history = []
this._forward = [] this._forward = []
this._pending = null this._pending = false
} }
LocationHistory.prototype.go = function (page, cb) { LocationHistory.prototype.url = function () {
console.log('go', page) return this.current() && this.current().url
this.clearForward()
this._go(page, cb)
}
LocationHistory.prototype._go = function (page, cb) {
if (this._pending) return
if (page.onbeforeload) {
this._pending = page
page.onbeforeload((err) => {
if (this._pending !== page) return /* navigation was cancelled */
this._pending = null
if (err) {
if (cb) cb(err)
return
}
this._history.push(page)
if (cb) cb()
})
} else {
this._history.push(page)
if (cb) cb()
}
}
LocationHistory.prototype.back = function (cb) {
if (this._history.length <= 1) return
var page = this._history.pop()
if (page.onbeforeunload) {
// TODO: this is buggy. If the user clicks back twice, then those pages
// may end up in _forward in the wrong order depending on which onbeforeunload
// call finishes first.
page.onbeforeunload(() => {
this._forward.push(page)
if (cb) cb()
})
} else {
this._forward.push(page)
if (cb) cb()
}
}
LocationHistory.prototype.forward = function (cb) {
if (this._forward.length === 0) return
var page = this._forward.pop()
this._go(page, cb)
}
LocationHistory.prototype.clearForward = function () {
this._forward = []
} }
LocationHistory.prototype.current = function () { LocationHistory.prototype.current = function () {
return this._history[this._history.length - 1] return this._history[this._history.length - 1]
} }
LocationHistory.prototype.go = function (page, cb) {
if (!cb) cb = noop
if (this._pending) return cb(null)
console.log('go', page)
this.clearForward()
this._go(page, cb)
}
LocationHistory.prototype.back = function (cb) {
var self = this
if (!cb) cb = noop
if (self._history.length <= 1 || self._pending) return cb(null)
var page = self._history.pop()
self._unload(page, done)
function done (err) {
if (err) return cb(err)
self._forward.push(page)
self._load(self.current(), cb)
}
}
LocationHistory.prototype.hasBack = function () { LocationHistory.prototype.hasBack = function () {
return this._history.length > 1 return this._history.length > 1
} }
LocationHistory.prototype.forward = function (cb) {
if (!cb) cb = noop
if (this._forward.length === 0 || this._pending) return cb(null)
var page = this._forward.pop()
this._go(page, cb)
}
LocationHistory.prototype.hasForward = function () { LocationHistory.prototype.hasForward = function () {
return this._forward.length > 0 return this._forward.length > 0
} }
LocationHistory.prototype.pending = function () { LocationHistory.prototype.clearForward = function (url) {
return this._pending if (url == null) {
this._forward = []
} else {
console.log(this._forward)
console.log(url)
this._forward = this._forward.filter(function (page) {
return page.url !== url
})
}
} }
LocationHistory.prototype.clearPending = function () { LocationHistory.prototype.backToFirst = function (cb) {
this._pending = null var self = this
if (!cb) cb = noop
if (self._history.length <= 1) return cb(null)
self.back(function (err) {
if (err) return cb(err)
self.backToFirst(cb)
})
} }
LocationHistory.prototype._go = function (page, cb) {
var self = this
if (!cb) cb = noop
self._unload(self.current(), done1)
function done1 (err) {
if (err) return cb(err)
self._load(page, done2)
}
function done2 (err) {
if (err) return cb(err)
self._history.push(page)
cb(null)
}
}
LocationHistory.prototype._load = function (page, cb) {
var self = this
self._pending = true
if (page && page.onbeforeload) page.onbeforeload(done)
else done(null)
function done (err) {
self._pending = false
cb(err)
}
}
LocationHistory.prototype._unload = function (page, cb) {
var self = this
self._pending = true
if (page && page.onbeforeunload) page.onbeforeunload(done)
else done(null)
function done (err) {
self._pending = false
cb(err)
}
}
function noop () {}

View File

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

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

@@ -0,0 +1,220 @@
var appConfig = require('application-config')('WebTorrent')
var path = require('path')
var {EventEmitter} = require('events')
var config = require('../../config')
var migrations = require('./migrations')
var State = module.exports = Object.assign(new EventEmitter(), {
getDefaultPlayState,
load,
save,
saveThrottled
})
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)
delete state.saveStateTimeout
// 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, (err) => {
if (err) console.error(err)
else State.emit('savedState')
})
}
// Write, but no more than once a second
function saveThrottled (state) {
if (state.saveStateTimeout) return
state.saveStateTimeout = setTimeout(function () {
if (!state.saveStateTimeout) return
save(state)
}, 1000)
}

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

@@ -0,0 +1,152 @@
// 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)
if (config.IS_PRODUCTION) {
postToServer()
} else {
// Development: telemetry used only for local debugging
// Empty uncaught errors, etc at the start of every run
reset()
}
}
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) {
console.error('uncaught error', procName, err)
// Not initialized yet? Ignore.
// Hopefully uncaught errors immediately on startup are fixed in dev
if (!telemetry) return
var message, stack
if (err instanceof Error) {
message = err.message
stack = err.stack
} else {
message = String(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

@@ -2,20 +2,21 @@ module.exports = {
isPlayable, isPlayable,
isVideo, isVideo,
isAudio, isAudio,
isPlayableTorrent isTorrent,
isPlayableTorrentSummary,
pickFileToPlay
} }
var path = require('path') var path = require('path')
/** // Checks whether a fileSummary or file path is audio/video that we can play,
* Determines whether a file in a torrent is audio/video we can play // based on the file extension
*/
function isPlayable (file) { function isPlayable (file) {
return isVideo(file) || isAudio(file) return isVideo(file) || isAudio(file)
} }
// Checks whether a fileSummary or file path is playable video
function isVideo (file) { function isVideo (file) {
var ext = path.extname(file.name).toLowerCase()
return [ return [
'.avi', '.avi',
'.m4v', '.m4v',
@@ -24,21 +25,59 @@ function isVideo (file) {
'.mp4', '.mp4',
'.mpg', '.mpg',
'.ogv', '.ogv',
'.webm' '.webm',
].includes(ext) '.wmv'
].includes(getFileExtension(file))
} }
// Checks whether a fileSummary or file path is playable audio
function isAudio (file) { function isAudio (file) {
var ext = path.extname(file.name).toLowerCase()
return [ return [
'.aac', '.aac',
'.ac3', '.ac3',
'.mp3', '.mp3',
'.ogg', '.ogg',
'.wav' '.wav'
].includes(ext) ].includes(getFileExtension(file))
} }
function isPlayableTorrent (torrentSummary) { // Checks if the argument is either:
// - a string that's a valid filename ending in .torrent
// - a file object where obj.name is ends in .torrent
// - a string that's a magnet link (magnet://...)
function isTorrent (file) {
var isTorrentFile = getFileExtension(file) === '.torrent'
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
return isTorrentFile || isMagnet
}
function getFileExtension (file) {
var name = typeof file === 'string' ? file : file.name
return path.extname(name).toLowerCase()
}
function isPlayableTorrentSummary (torrentSummary) {
return torrentSummary.files && torrentSummary.files.some(isPlayable) return torrentSummary.files && torrentSummary.files.some(isPlayable)
} }
// Picks the default file to play from a list of torrent or torrentSummary files
// Returns an index or undefined, if no files are playable
function pickFileToPlay (files) {
// first, try to find the biggest video file
var videoFiles = files.filter(isVideo)
if (videoFiles.length > 0) {
var largestVideoFile = videoFiles.reduce(function (a, b) {
return a.length > b.length ? a : b
})
return files.indexOf(largestVideoFile)
}
// if there are no videos, play the first audio file
var audioFiles = files.filter(isAudio)
if (audioFiles.length > 0) {
return files.indexOf(audioFiles[0])
}
// no video or audio means nothing is playable
return undefined
}

View File

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

@@ -1,6 +1,9 @@
module.exports = { module.exports = {
getPosterPath, getPosterPath,
getTorrentPath getTorrentPath,
getByKey,
getTorrentID,
getFileOrFolder
} }
var path = require('path') var path = require('path')
@@ -10,15 +13,44 @@ 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, '/')
} }
// Expects a torrentSummary
// Returns a torrentID: filename, magnet URI, or infohash
function getTorrentID (torrentSummary) {
var s = torrentSummary
if (s.torrentFileName) { // Load torrent file from disk
return getTorrentPath(s)
} else { // Load torrent from DHT
return s.magnetURI || s.infoHash
}
}
// Expects a torrentKey or infoHash
// Returns the corresponding torrentSummary, or undefined
function getByKey (state, torrentKey) {
if (!torrentKey) return undefined
return state.saved.torrents.find((x) =>
x.torrentKey === torrentKey || x.infoHash === torrentKey)
}
// Returns the path to either the file (in a single-file torrent) or the root
// folder (in multi-file torrent)
// WARNING: assumes that multi-file torrents consist of a SINGLE folder.
// TODO: make this assumption explicit, enforce it in the `create-torrent`
// module. Store root folder explicitly to avoid hacky path processing below.
function getFileOrFolder (torrentSummary) {
var ts = torrentSummary
return path.join(ts.path, ts.files[0].path.split('/')[0])
}

View File

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

View File

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

447
renderer/main.js Normal file
View File

@@ -0,0 +1,447 @@
console.time('init')
const crashReporter = require('../crash-reporter')
crashReporter.init()
const dragDrop = require('drag-drop')
const electron = require('electron')
const mainLoop = require('main-loop')
const createElement = require('virtual-dom/create-element')
const diff = require('virtual-dom/diff')
const patch = require('virtual-dom/patch')
const config = require('../config')
const App = require('./views/app')
const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound')
const State = require('./lib/state')
const TorrentPlayer = require('./lib/torrent-player')
const MediaController = require('./controllers/media-controller')
const UpdateController = require('./controllers/update-controller')
const PrefsController = require('./controllers/prefs-controller')
const PlaybackController = require('./controllers/playback-controller')
const SubtitlesController = require('./controllers/subtitles-controller')
const TorrentListController = require('./controllers/torrent-list-controller')
const TorrentController = require('./controllers/torrent-controller')
// Yo-yo pattern: state object lives here and percolates down thru all the views.
// Events come back up from the views via dispatch(...)
require('./lib/dispatcher').setDispatch(dispatch)
// From dispatch(...), events are sent to one of the controllers
var controllers = null
// This dependency is the slowest-loading, so we lazy load it
var Cast = null
// 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
// 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.
var state, vdomLoop
State.load(onState)
// Called once when the application loads. (Not once per window.)
// Connects to the torrent networks, sets up the UI and OS integrations like
// the dock icon and drag+drop.
function onState (err, _state) {
if (err) return onError(err)
state = _state
// Create controllers
controllers = {
media: new MediaController(state),
update: new UpdateController(state),
prefs: new PrefsController(state, config),
playback: new PlaybackController(state, config, update),
subtitles: new SubtitlesController(state),
torrentList: new TorrentListController(state),
torrent: new TorrentController(state)
}
// Add first page to location history
state.location.go({ url: 'home' })
// Restart everything we were torrenting last time the app ran
resumeTorrents()
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
window.setTimeout(delayedInit, config.DELAYED_INIT)
// The UI is built with virtual-dom, a minimalist library extracted from React
// The concepts--one way data flow, a pure function that renders state to a
// virtual DOM tree, and a diff that applies changes in the vdom to the real
// DOM, are all the same. Learn more: https://facebook.github.io/react/
vdomLoop = mainLoop(state, render, {
create: createElement,
diff: diff,
patch: patch
})
document.body.appendChild(vdomLoop.target)
// Listen for messages from the main process
setupIpc()
// Calling update() updates the UI given the current state
// 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
setInterval(update, 1000)
// OS integrations:
// ...drag and drop a torrent or video file to play or seed
dragDrop('body', onOpen)
// ...same thing if you paste a torrent
document.addEventListener('paste', onPaste)
// ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur)
// ...window visibility state.
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
// Log uncaught JS errors
window.addEventListener('error',
(e) => telemetry.logUncaughtError('window', e.error || e.target), true)
// Done! Ideally we want to get here < 500ms after the user clicks the app
sound.play('STARTUP')
console.timeEnd('init')
}
// Runs a few seconds after the app loads, to avoid slowing down startup time
function delayedInit () {
lazyLoadCast()
sound.preload()
telemetry.init(state)
}
// Lazily loads Chromecast and Airplay support
function lazyLoadCast () {
if (!Cast) {
Cast = require('./lib/cast')
Cast.init(state, update) // Search the local network for Chromecast and Airplays
}
return Cast
}
// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
// tree. Any events, such as button clicks, will turn into calls to dispatch()
function render (state) {
try {
return App(state)
} catch (e) {
console.log('rendering error: %s\n\t%s', e.message, e.stack)
}
}
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
function update () {
controllers.playback.showOrHidePlayerControls()
vdomLoop.update(state)
updateElectron()
}
// Some state changes can't be reflected in the DOM, instead we have to
// tell the main process to update the window or OS integrations
function updateElectron () {
if (state.window.title !== state.prev.title) {
state.prev.title = state.window.title
ipcRenderer.send('setTitle', state.window.title)
}
if (state.dock.progress !== state.prev.progress) {
state.prev.progress = state.dock.progress
ipcRenderer.send('setProgress', state.dock.progress)
}
if (state.dock.badge !== state.prev.badge) {
state.prev.badge = state.dock.badge
ipcRenderer.send('setBadge', state.dock.badge || '')
}
}
const dispatchHandlers = {
// Torrent list: creating, deleting, selecting torrents
'openTorrentFile': () => ipcRenderer.send('openTorrentFile'),
'openFiles': () => ipcRenderer.send('openFiles'), /* shows the open file dialog */
'openTorrentAddress': () => { state.modal = { id: 'open-torrent-address-modal' } },
'addTorrent': (torrentId) => controllers.torrentList.addTorrent(torrentId),
'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths),
'toggleCreateTorrentAdvanced': () => controllers.torrentList.toggleCreateTorrentAdvanced(),
'createTorrent': (options) => controllers.torrentList.createTorrent(options),
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index),
'confirmDeleteTorrent': (infoHash, deleteData) => controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData),
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
'startTorrentingSummary': (torrentSummary) =>
controllers.torrentList.startTorrentingSummary(torrentSummary),
// Playback
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
'playPause': () => controllers.playback.playPause(),
'skip': (time) => controllers.playback.skip(time),
'skipTo': (time) => controllers.playback.skipTo(time),
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
'changeVolume': (delta) => controllers.playback.changeVolume(delta),
'setVolume': (vol) => controllers.playback.setVolume(vol),
'openItem': (infoHash, index) => controllers.playback.openItem(infoHash, index),
// Subtitles
'openSubtitles': () => controllers.subtitles.openSubtitles(),
'selectSubtitle': (index) => controllers.subtitles.selectSubtitle(index),
'toggleSubtitlesMenu': () => controllers.subtitles.toggleSubtitlesMenu(),
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect),
// Local media: <video>, <audio>, VLC
'mediaStalled': () => controllers.media.mediaStalled(),
'mediaError': (err) => controllers.media.mediaError(err),
'mediaSuccess': () => controllers.media.mediaSuccess(),
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
'vlcPlay': () => controllers.media.vlcPlay(),
'vlcNotFound': () => controllers.media.vlcNotFound(),
// Remote casting: Chromecast, Airplay, etc
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
'selectCastDevice': (index) => lazyLoadCast().selectDevice(index),
'stopCasting': () => lazyLoadCast().stop(),
// Preferences screen
'preferences': () => controllers.prefs.show(),
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
// Update (check for new versions on Linux, where there's no auto updater)
'updateAvailable': (version) => controllers.update.updateAvailable(version),
'skipVersion': (version) => controllers.update.skipVersion(version),
// Navigation between screens (back, forward, ESC, etc)
'exitModal': () => { state.modal = null },
'backToList': backToList,
'escapeBack': escapeBack,
'back': () => state.location.back(),
'forward': () => state.location.forward(),
// Controlling the window
'setDimensions': setDimensions,
'toggleFullScreen': (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
'setTitle': (title) => { state.window.title = title },
// Everything else
'onOpen': onOpen,
'error': onError,
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
'saveState': () => State.save(state),
'saveStateThrottled': () => State.saveThrottled(state),
'update': () => {} // No-op, just trigger an update
}
// Events from the UI never modify state directly. Instead they call dispatch()
function dispatch (action, ...args) {
// Log dispatch calls, for debugging
if (!['mediaMouseMoved', 'mediaTimeUpdate'].includes(action)) {
console.log('dispatch: %s %o', action, args)
}
var handler = dispatchHandlers[action]
if (handler) handler(...args)
else console.error('Missing dispatch handler: ' + action)
// Update the virtual-dom, unless it's just a mouse move event
if (action !== 'mediaMouseMoved' ||
controllers.playback.showOrHidePlayerControls()) {
update()
}
}
// Listen to events from the main and webtorrent processes
function setupIpc () {
ipcRenderer.on('log', (e, ...args) => console.log(...args))
ipcRenderer.on('error', (e, ...args) => console.error(...args))
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
var tc = controllers.torrent
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
ipcRenderer.on('wt-warning', (e, ...args) => tc.torrentWarning(...args))
ipcRenderer.on('wt-error', (e, ...args) => tc.torrentError(...args))
ipcRenderer.on('wt-progress', (e, ...args) => tc.torrentProgress(...args))
ipcRenderer.on('wt-file-modtimes', (e, ...args) => tc.torrentFileModtimes(...args))
ipcRenderer.on('wt-file-saved', (e, ...args) => tc.torrentFileSaved(...args))
ipcRenderer.on('wt-poster', (e, ...args) => tc.torrentPosterSaved(...args))
ipcRenderer.on('wt-audio-metadata', (e, ...args) => tc.torrentAudioMetadata(...args))
ipcRenderer.on('wt-server-running', (e, ...args) => tc.torrentServerRunning(...args))
ipcRenderer.on('wt-uncaught-error', (e, err) => telemetry.logUncaughtError('webtorrent', err))
ipcRenderer.send('ipcReady')
State.on('savedState', () => ipcRenderer.send('savedState'))
}
// Quits any modal popovers and returns to the torrent list screen
function backToList () {
// Exit any modals and screens with a back button
state.modal = null
state.location.backToFirst(function () {
// If we were already on the torrent list, scroll to the top
var contentTag = document.querySelector('.content')
if (contentTag) contentTag.scrollTop = 0
// Work around virtual-dom issue: it doesn't expose its redraw function,
// and only redraws on requestAnimationFrame(). That means when the user
// closes the window (hide window / minimize to tray) and we want to pause
// the video, we update the vdom but it keeps playing until you reopen!
var mediaTag = document.querySelector('video,audio')
if (mediaTag) mediaTag.pause()
})
}
// Quits modals, full screen, or goes back. Happens when the user hits ESC
function escapeBack () {
if (state.modal) {
dispatch('exitModal')
} else if (state.window.isFullScreen) {
dispatch('toggleFullScreen')
} else {
dispatch('back')
}
}
// Starts all torrents that aren't paused on program startup
function resumeTorrents () {
state.saved.torrents
.filter((torrentSummary) => torrentSummary.status !== 'paused')
.forEach((torrentSummary) => controllers.torrentList.startTorrentingSummary(torrentSummary))
}
// Set window dimensions to match video dimensions or fill the screen
function setDimensions (dimensions) {
// Don't modify the window size if it's already maximized
if (electron.remote.getCurrentWindow().isMaximized()) {
state.window.bounds = null
return
}
// Save the bounds of the window for later. See restoreBounds()
state.window.bounds = {
x: window.screenX,
y: window.screenY,
width: window.outerWidth,
height: window.outerHeight
}
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
// Limit window size to screen size
var screenWidth = window.screen.width
var screenHeight = window.screen.height
var aspectRatio = dimensions.width / dimensions.height
var scaleFactor = Math.min(
Math.min(screenWidth / dimensions.width, 1),
Math.min(screenHeight / dimensions.height, 1)
)
var width = Math.max(
Math.floor(dimensions.width * scaleFactor),
config.WINDOW_MIN_WIDTH
)
var height = Math.max(
Math.floor(dimensions.height * scaleFactor),
config.WINDOW_MIN_HEIGHT
)
ipcRenderer.send('setAspectRatio', aspectRatio)
ipcRenderer.send('setBounds', {x: null, y: null, width, height})
state.playing.aspectRatio = aspectRatio
}
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
// via any method (drag-drop, drag to app icon, command line)
function onOpen (files) {
if (!Array.isArray(files)) files = [ files ]
if (state.modal) {
state.modal = null
}
var subtitles = files.filter(controllers.subtitles.isSubtitle)
if (state.location.url() === 'home' || subtitles.length === 0) {
if (files.every(TorrentPlayer.isTorrent)) {
if (state.location.url() !== 'home') {
backToList()
}
// All .torrent files? Add them.
files.forEach((file) => controllers.torrentList.addTorrent(file))
} else {
// Show the Create Torrent screen. Let's seed those files.
controllers.torrentList.showCreateTorrent(files)
}
} else if (state.location.url() === 'player') {
controllers.subtitles.addSubtitles(subtitles, true)
}
update()
}
function onError (err) {
console.error(err.stack || err)
sound.play('ERROR')
state.errors.push({
time: new Date().getTime(),
message: err.message || err
})
update()
}
function onPaste (e) {
if (e.target.tagName.toLowerCase() === 'input') return
var torrentIds = electron.clipboard.readText().split('\n')
torrentIds.forEach(function (torrentId) {
torrentId = torrentId.trim()
if (torrentId.length === 0) return
controllers.torrentList.addTorrent(torrentId)
})
update()
}
function onFocus (e) {
state.window.isFocused = true
state.dock.badge = 0
update()
}
function onBlur () {
state.window.isFocused = false
update()
}
function onVisibilityChange () {
state.window.isVisible = !document.webkitHidden
}
function onFullscreenChanged (e, isFullScreen) {
state.window.isFullScreen = isFullScreen
if (!isFullScreen) {
// Aspect ratio gets reset in fullscreen mode, so restore it (OS X)
ipcRenderer.send('setAspectRatio', state.playing.aspectRatio)
}
update()
}

View File

@@ -1,265 +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: {}
}
}
/* 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 */
subtitles: {
tracks: [], /* subtitles file (Buffer) */
enabled: false
},
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'
}
]
}
],
downloadPath: config.IS_PORTABLE
? path.join(config.CONFIG_PATH, 'Downloads')
: remote.app.getPath('downloads')
}
}

View File

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

View File

@@ -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]
} }
} }
@@ -47,8 +41,8 @@ function CreateTorrentPage (state) {
basePath = pathPrefix basePath = pathPrefix
} else { } else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a" // Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a"
defaultName = files[0].name defaultName = path.basename(pathPrefix)
basePath = path.basename(pathPrefix) basePath = path.dirname(pathPrefix)
} }
var maxFileElems = 100 var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file) { var fileElems = files.slice(0, maxFileElems).map(function (file) {
@@ -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}
@@ -71,7 +65,8 @@ function CreateTorrentPage (state) {
<label>Path:</label> <label>Path:</label>
<div class='torrent-attribute'>${pathPrefix}</div> <div class='torrent-attribute'>${pathPrefix}</div>
</p> </p>
<div class='expand-collapse ${collapsedClass}' onclick=${handleToggleShowAdvanced}> <div class='expand-collapse ${collapsedClass}'
onclick=${dispatcher('toggleCreateTorrentAdvanced')}>
${info.showAdvanced ? 'Basic' : 'Advanced'} ${info.showAdvanced ? 'Basic' : 'Advanced'}
</div> </div>
<div class="create-torrent-advanced ${collapsedClass}"> <div class="create-torrent-advanced ${collapsedClass}">
@@ -93,7 +88,7 @@ function CreateTorrentPage (state) {
</p> </p>
</div> </div>
<p class="float-right"> <p class="float-right">
<button class='button-flat light' onclick=${handleCancel}>Cancel</button> <button class='button-flat light' onclick=${dispatcher('back')}>Cancel</button>
<button class='button-raised' onclick=${handleOK}>Create Torrent</button> <button class='button-raised' onclick=${handleOK}>Create Torrent</button>
</p> </p>
</div> </div>
@@ -119,19 +114,28 @@ function CreateTorrentPage (state) {
comment: comment comment: comment
} }
dispatch('createTorrent', options) dispatch('createTorrent', options)
dispatch('backToList')
} }
}
function handleCancel () { function CreateTorrentErrorPage () {
dispatch('backToList') return hx`
} <div class='create-torrent'>
<h2>Create torrent</h2>
function handleToggleShowAdvanced () { <p class="torrent-info">
// TODO: what's the clean way to handle this? <p>
// Should every button on every screen have its own dispatch()? Sorry, you must select at least one file that is not a hidden file.
info.showAdvanced = !info.showAdvanced </p>
dispatch('update') <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

View File

@@ -1,16 +1,13 @@
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`
<div class='header'> <div class='header'>
${getTitle()} ${getTitle()}
<div class='nav left'> <div class='nav left float-left'>
<i.icon.back <i.icon.back
class=${state.location.hasBack() ? '' : 'disabled'} class=${state.location.hasBack() ? '' : 'disabled'}
title='Back' title='Back'
@@ -24,7 +21,7 @@ function Header (state) {
chevron_right chevron_right
</i> </i>
</div> </div>
<div class='nav right'> <div class='nav right float-right'>
${getAddButton()} ${getAddButton()}
</div> </div>
</div> </div>
@@ -37,12 +34,12 @@ function Header (state) {
} }
function getAddButton () { function getAddButton () {
if (state.location.current().url !== 'player') { if (state.location.url() === 'home') {
return hx` return hx`
<i <i
class='icon add' class='icon add'
title='Add torrent' title='Add torrent'
onclick=${dispatcher('showOpenTorrentFile')}> onclick=${dispatcher('openFiles')}>
add add
</i> </i>
` `

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,12 +1,10 @@
module.exports = Player module.exports = Player
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var prettyBytes = require('prettier-bytes')
var Bitfield = require('bitfield') var Bitfield = require('bitfield')
var prettyBytes = require('prettier-bytes')
var zeroFill = require('zero-fill')
var 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')
@@ -36,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()
@@ -44,40 +43,57 @@ function renderMedia (state) {
mediaElement.play() mediaElement.play()
} }
// When the user clicks or drags on the progress bar, jump to that position // When the user clicks or drags on the progress bar, jump to that position
if (state.playing.jumpToTime) { if (state.playing.jumpToTime != null) {
mediaElement.currentTime = state.playing.jumpToTime mediaElement.currentTime = state.playing.jumpToTime
state.playing.jumpToTime = null state.playing.jumpToTime = null
} }
if (state.playing.playbackRate !== mediaElement.playbackRate) {
mediaElement.playbackRate = state.playing.playbackRate
}
// 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
state.playing.setVolume = null state.playing.setVolume = null
} }
// fix textTrack cues not been removed <track> rerender // Switch to the newly added subtitle track, if available
if (state.playing.subtitles.change) { var tracks = mediaElement.textTracks || []
var tracks = mediaElement.textTracks for (var j = 0; j < tracks.length; j++) {
for (var j = 0; j < tracks.length; j++) { var isSelectedTrack = j === state.playing.subtitles.selectedIndex
// mode is not an <track> attribute, only available on DOM tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
tracks[j].mode = (tracks[j].label === state.playing.subtitles.change) ? 'showing' : 'hidden' }
}
state.playing.subtitles.change = null // Save video position
var file = state.getPlayingFileSummary()
file.currentTime = state.playing.currentTime = mediaElement.currentTime
file.duration = state.playing.duration = mediaElement.duration
// Save selected subtitle
if (state.playing.subtitles.selectedIndex !== -1) {
var index = state.playing.subtitles.selectedIndex
file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath
} else if (file.selectedSubtitle != null) {
delete file.selectedSubtitle
} }
state.playing.currentTime = mediaElement.currentTime
state.playing.duration = mediaElement.duration
state.playing.volume = mediaElement.volume state.playing.volume = mediaElement.volume
} }
// Add subtitles to the <video> tag // Add subtitles to the <video> tag
var trackTags = [] var trackTags = []
if (state.playing.subtitles.selectedIndex >= 0) {
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) { for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i] var track = state.playing.subtitles.tracks[i]
var isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push(hx` trackTags.push(hx`
<track <track
${track.selected ? 'default' : ''} ${isSelected ? 'default' : ''}
label=${track.label} label=${track.label}
type='subtitles' type='subtitles'
src=${track.buffer}> src=${track.buffer}>
@@ -112,7 +128,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
@@ -129,12 +145,15 @@ function renderMedia (state) {
} }
function onCanPlay (e) { function onCanPlay (e) {
var video = e.target var elem = e.target
if (video.webkitVideoDecodedByteCount > 0 && if (state.playing.type === 'video' &&
video.webkitAudioDecodedByteCount === 0) { elem.webkitVideoDecodedByteCount === 0) {
dispatch('mediaError', 'Video codec unsupported')
} else if (elem.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported') dispatch('mediaError', 'Audio codec unsupported')
} else { } else {
video.play() dispatch('mediaSuccess')
elem.play()
} }
} }
} }
@@ -154,7 +173,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`
@@ -165,8 +185,7 @@ function renderOverlay (state) {
} }
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
var torrentSummary = getPlayingTorrentSummary(state) var fileSummary = state.getPlayingFileSummary()
var fileSummary = torrentSummary.files[state.playing.fileIndex]
if (!fileSummary.audioInfo) return if (!fileSummary.audioInfo) return
var info = fileSummary.audioInfo var info = fileSummary.audioInfo
@@ -185,15 +204,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>`
} }
@@ -204,7 +245,7 @@ function renderLoadingSpinner (state) {
(new Date().getTime() - state.playing.lastTimeUpdate > 2000) (new Date().getTime() - state.playing.lastTimeUpdate > 2000)
if (!isProbablyStalled) return if (!isProbablyStalled) return
var prog = getPlayingTorrentSummary(state).progress || {} var prog = state.getPlayingTorrentSummary().progress || {}
var fileProgress = 0 var fileProgress = 0
if (prog.files) { if (prog.files) {
var file = prog.files[state.playing.fileIndex] var file = prog.files[state.playing.fileIndex]
@@ -248,8 +289,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
@@ -268,24 +311,51 @@ function renderCastScreen (state) {
` `
} }
function renderSubtitlesOptions (state) { function renderCastOptions (state) {
var subtitles = state.playing.subtitles if (!state.devices.castMenu) return
if (!subtitles.tracks.length || !subtitles.show) return
var items = subtitles.tracks.map(function (track) { 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` return hx`
<li onclick=${dispatcher('selectSubtitle', track.label)}> <li onclick=${dispatcher('selectCastDevice', ix)}>
<i.icon>${track.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i> <i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
${track.label} ${name}
</li> </li>
` `
}) })
return hx` return hx`
<ul.subtitles-list> <ul.options-list>
${items} ${items}
<li onclick=${dispatcher('selectSubtitle', '')}> </ul>
<i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i> `
}
function renderSubtitlesOptions (state) {
var subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return
var items = subtitles.tracks.map(function (track, ix) {
var isSelected = state.playing.subtitles.selectedIndex === ix
return hx`
<li onclick=${dispatcher('selectSubtitle', ix)}>
<i.icon>${'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
${track.label}
</li>
`
})
var noneSelected = state.playing.subtitles.selectedIndex === -1
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
return hx`
<ul.options-list>
${items}
<li onclick=${dispatcher('selectSubtitle', -1)}>
<i.icon>${noneClass}</i>
None None
</li> </li>
</ul> </ul>
@@ -294,10 +364,10 @@ function renderSubtitlesOptions (state) {
function renderPlayerControls (state) { function renderPlayerControls (state) {
var positionPercent = 100 * state.playing.currentTime / state.playing.duration var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' } var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0 var captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled' ? 'disabled'
: state.playing.subtitles.enabled : state.playing.subtitles.selectedIndex >= 0
? 'active' ? 'active'
: '' : ''
@@ -305,15 +375,27 @@ function renderPlayerControls (state) {
hx` hx`
<div class='playback-bar'> <div class='playback-bar'>
${renderLoadingBar(state)} ${renderLoadingBar(state)}
<div class='playback-cursor' style=${playbackCursorStyle}></div> <div
<div class='scrub-bar' class='playback-cursor'
style=${playbackCursorStyle}>
</div>
<div
class='scrub-bar'
draggable='true' draggable='true'
ondragstart=${handleDragStart}
onclick=${handleScrub}, onclick=${handleScrub},
ondrag=${handleScrub}></div> ondrag=${handleScrub}>
</div>
</div> </div>
`, `,
hx` hx`
<i class='icon fullscreen' <i class='icon play-pause float-left' onclick=${dispatcher('playPause')}>
${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>
`,
hx`
<i
class='icon fullscreen float-right'
onclick=${dispatcher('toggleFullScreen')}> onclick=${dispatcher('toggleFullScreen')}>
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'} ${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i> </i>
@@ -323,132 +405,131 @@ function renderPlayerControls (state) {
if (state.playing.type === 'video') { if (state.playing.type === 'video') {
// show closed captions icon // show closed captions icon
elements.push(hx` elements.push(hx`
<i.icon.closed-captions <i.icon.closed-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) { // Add the cast buttons. Icons for each cast type, connected/disconnected:
chromecastClass = 'active' var buttonIcons = {
dlnaClass = 'disabled' 'chromecast': {true: 'cast_connected', false: 'cast'},
airplayClass = 'disabled' 'airplay': {true: 'airplay', false: 'airplay'},
chromecastHandler = dispatcher('closeDevice') 'dlna': {true: 'tv', false: 'tv'}
airplayHandler = undefined }
dlnaHandler = undefined castTypes.forEach(function (castType) {
} else if (isOnAirplay) { // Do we show this button (eg. the Chromecast button) at all?
chromecastClass = 'disabled' var isCasting = state.playing.location.startsWith(castType)
dlnaClass = 'disabled' var player = state.devices[castType]
airplayClass = 'active' if ((!player || player.getDevices().length === 0) && !isCasting) return
chromecastHandler = undefined
airplayHandler = dispatcher('closeDevice') // Show the button. Three options for eg the Chromecast button:
dlnaHandler = undefined var buttonClass, buttonHandler
} else if (isOnDlna) { if (isCasting) {
chromecastClass = 'disabled' // Option 1: we are currently connected to Chromecast. Button stops the cast.
dlnaClass = 'active' buttonClass = 'active'
airplayClass = 'disabled' buttonHandler = dispatcher('stopCasting')
chromecastHandler = undefined } else if (isCastingAnywhere) {
airplayHandler = undefined // Option 2: we are currently connected somewhere else. Button disabled.
dlnaHandler = dispatcher('closeDevice') buttonClass = 'disabled'
} else { buttonHandler = undefined
chromecastClass = '' } else {
airplayClass = '' // Option 3: we are not connected anywhere. Button opens Chromecast menu.
dlnaClass = '' buttonClass = ''
chromecastHandler = dispatcher('openDevice', 'chromecast') buttonHandler = dispatcher('toggleCastMenu', castType)
airplayHandler = dispatcher('openDevice', 'airplay') }
dlnaHandler = dispatcher('openDevice', 'dlna') var buttonIcon = buttonIcons[castType][isCasting]
}
if (state.devices.chromecast || isOnChromecast) {
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
elements.push(hx`
<i.icon.device
class=${chromecastClass}
onclick=${chromecastHandler}>
${castIcon}
</i>
`)
}
if (state.devices.airplay || isOnAirplay) {
elements.push(hx`
<i.icon.device
class=${airplayClass}
onclick=${airplayHandler}>
airplay
</i>
`)
}
if (state.devices.dlna || isOnDlna) {
elements.push(hx`
<i.icon.device
class=${dlnaClass}
onclick=${dlnaHandler}>
tv
</i>
`)
}
// On OSX, the back button is in the title bar of the window; see app.js
// On other platforms, we render one over the video on mouseover
if (process.platform !== 'darwin') {
elements.push(hx` elements.push(hx`
<i.icon.back <i.icon.device.float-right
onclick=${dispatcher('back')}> class=${buttonClass}
chevron_left onclick=${buttonHandler}>
${buttonIcon}
</i> </i>
`) `)
} })
// render volume // 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`
<div.volume> <div class='volume float-left'>
<i.icon.volume-icon onmousedown=${handleVolumeMute}> <i
${volumeIcon} class='icon volume-icon float-left'
</i> onmousedown=${handleVolumeMute}>
<input.volume-slider ${volumeIcon}
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume} </i>
onmousedown=${handleVolumeScrub} <input
onmouseup=${handleVolumeScrub} class='volume-slider float-right'
onmousemove=${handleVolumeScrub} type='range' min='0' max='1' step='0.05'
style=${volumeStyle} value=${volumeChanging !== false ? volumeChanging : volume}
/> onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub}
style=${volumeStyle}
/>
</div> </div>
`) `)
// Finally, the big button in the center plays or pauses the video // Show video playback progress
var currentTimeStr = formatTime(state.playing.currentTime)
var durationStr = formatTime(state.playing.duration)
elements.push(hx` elements.push(hx`
<i class='icon play-pause' onclick=${dispatcher('playPause')}> <span class='time float-left'>
${state.playing.isPaused ? 'play_arrow' : 'pause'} ${currentTimeStr} / ${durationStr}
</i> </span>
`) `)
// render playback rate
if (state.playing.playbackRate !== 1) {
elements.push(hx`
<span class='rate float-left'>
${state.playing.playbackRate}x
</span>
`)
}
return hx` return hx`
<div class='player-controls'> <div class='controls'>
${elements} ${elements}
${renderCastOptions(state)}
${renderSubtitlesOptions(state)} ${renderSubtitlesOptions(state)}
</div> </div>
` `
function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) {
var dt = e.dataTransfer
dt.effectAllowed = 'none'
}
}
// Handles a click or drag to scrub (jump to another position in the video) // Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) { function handleScrub (e) {
if (!e.clientX) return
dispatch('mediaMouseMoved') dispatch('mediaMouseMoved')
var windowWidth = document.querySelector('body').clientWidth var windowWidth = document.querySelector('body').clientWidth
var fraction = e.clientX / windowWidth var fraction = e.clientX / windowWidth
var position = fraction * state.playing.duration /* seconds */ var position = fraction * state.playing.duration /* seconds */
dispatch('playbackJump', position) dispatch('skipTo', position)
} }
// Handles volume muting and Unmuting // Handles volume muting and Unmuting
@@ -484,7 +565,7 @@ function renderPlayerControls (state) {
// if no subtitles available select it // if no subtitles available select it
dispatch('openSubtitles') dispatch('openSubtitles')
} else { } else {
dispatch('showSubtitles') dispatch('toggleSubtitlesMenu')
} }
} }
} }
@@ -495,7 +576,7 @@ var volumeChanging = false
// Renders the loading bar. Shows which parts of the torrent are loaded, which // Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be "spongey" / non-contiguous // can be "spongey" / non-contiguous
function renderLoadingBar (state) { function renderLoadingBar (state) {
var torrentSummary = getPlayingTorrentSummary(state) var torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary.progress) { if (!torrentSummary.progress) {
return [] return []
} }
@@ -532,7 +613,7 @@ function renderLoadingBar (state) {
// Returns the CSS background-image string for a poster image + dark vignette // Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) { function cssBackgroundImagePoster (state) {
var torrentSummary = getPlayingTorrentSummary(state) var torrentSummary = state.getPlayingTorrentSummary()
var posterPath = TorrentSummary.getPosterPath(torrentSummary) var posterPath = TorrentSummary.getPosterPath(torrentSummary)
if (!posterPath) return '' if (!posterPath) return ''
return cssBackgroundImageDarkGradient() + `, url(${posterPath})` return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
@@ -543,7 +624,17 @@ function cssBackgroundImageDarkGradient () {
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' 'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
} }
function getPlayingTorrentSummary (state) { function formatTime (time) {
var infoHash = state.playing.infoHash if (typeof time !== 'number' || Number.isNaN(time)) {
return state.saved.torrents.find((x) => x.infoHash === infoHash) return '0:00'
}
var hours = Math.floor(time / 3600)
var minutes = Math.floor(time % 3600 / 60)
if (hours > 0) {
minutes = zeroFill(2, minutes)
}
var seconds = zeroFill(2, Math.floor(time % 60))
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
} }

View File

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

View File

@@ -0,0 +1,26 @@
module.exports = RemoveTorrentModal
var {dispatch, dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function RemoveTorrentModal (state) {
var message = state.modal.deleteData
? 'Are you sure you want to remove this torrent from the list and delete the data file?'
: 'Are you sure you want to remove this torrent from the list?'
var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove'
return hx`
<div>
<p><strong>${message}</strong></p>
<p class='float-right'>
<button class='button button-flat' onclick=${dispatcher('exitModal')}>Cancel</button>
<button class='button button-raised' onclick=${handleRemove}>${buttonText}</button>
</p>
</div>
`
function handleRemove () {
dispatch('deleteTorrent', state.modal.infoHash, state.modal.deleteData)
dispatch('exitModal')
}
}

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
@@ -118,12 +114,7 @@ function TorrentList (state) {
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'unplayable') { if (torrentSummary.playStatus === 'timeout') {
playIcon = 'play_arrow'
playClass = 'disabled'
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
'View details and click on individual files to open them in another program.'
} else if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning' playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.' playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else { } else {
@@ -143,14 +134,26 @@ function TorrentList (state) {
downloadTooltip = 'Click to start torrenting.' downloadTooltip = 'Click to start torrenting.'
} }
// Do we have a saved position? Show it using a radial progress bar on top
// of the play button, unless already showing a spinner there:
var positionElem
var willShowSpinner = torrentSummary.playStatus === 'requested'
var defaultFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
var fraction = defaultFile.currentTime / defaultFile.duration
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position'
}
// Only show the play button for torrents that contain playable media // Only show the play button for torrents that contain playable media
var playButton var playButton
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) { if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
playButton = hx` playButton = hx`
<i.button-round.icon.play <i.button-round.icon.play
title=${playTooltip} title=${playTooltip}
class=${playClass} class=${playClass}
onclick=${dispatcher('play', infoHash)}> onclick=${dispatcher('playFile', infoHash)}>
${playIcon} ${playIcon}
</i> </i>
` `
@@ -158,6 +161,7 @@ function TorrentList (state) {
return hx` return hx`
<div class='buttons'> <div class='buttons'>
${positionElem}
${playButton} ${playButton}
<i.button-round.icon.download <i.button-round.icon.download
class=${torrentSummary.status} class=${torrentSummary.status}
@@ -168,7 +172,7 @@ function TorrentList (state) {
<i <i
class='icon delete' class='icon delete'
title='Remove torrent' title='Remove torrent'
onclick=${dispatcher('deleteTorrent', infoHash)}> onclick=${dispatcher('confirmDeleteTorrent', infoHash, false)}>
close close
</i> </i>
</div> </div>
@@ -186,11 +190,17 @@ function TorrentList (state) {
filesElement = hx`<div class='files warning'>${message}</div>` filesElement = hx`<div class='files warning'>${message}</div>`
} else { } else {
// We do know the files. List them and show download stats for each one // We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files.map( var fileRows = torrentSummary.files
(file, index) => renderFileRow(torrentSummary, file, index)) .map((file, index) => ({ file, index }))
.sort(function (a, b) {
if (a.file.name < b.file.name) return -1
if (b.file.name < a.file.name) return 1
return 0
})
.map((object) => renderFileRow(torrentSummary, object.file, object.index))
filesElement = hx` filesElement = hx`
<div class='files'> <div class='files'>
<strong>Files</strong>
<table> <table>
${fileRows} ${fileRows}
</table> </table>
@@ -208,7 +218,9 @@ function TorrentList (state) {
// Show a single torrentSummary file in the details view for a single torrent // Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrentSummary, file, index) { function renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded // First, find out how much of the file we've downloaded
var isDone = false // Are we even torrenting it?
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
var isDone = false // Are we finished torrenting it?
var progress = '' var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files) { if (torrentSummary.progress && torrentSummary.progress.files) {
var fileProg = torrentSummary.progress.files[index] var fileProg = torrentSummary.progress.files[index]
@@ -216,28 +228,69 @@ function TorrentList (state) {
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%' progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
} }
// Second, render the file as a table row // Second, for media files where we saved our position, show how far we got
var positionElem
if (file.currentTime) {
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
positionElem = renderRadialProgressBar(file.currentTime / file.duration)
}
// Finally, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var icon var icon
var rowClass = ''
var handleClick var handleClick
if (TorrentPlayer.isPlayable(file)) { if (isPlayable) {
icon = 'play_arrow' /* playable? add option to play */ icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index) handleClick = dispatcher('playFile', infoHash, index)
} else { } else {
icon = 'description' /* file icon, opens in OS default app */ icon = 'description' /* file icon, opens in OS default app */
rowClass = isDone ? '' : 'disabled' handleClick = dispatcher('openItem', infoHash, index)
handleClick = dispatcher('openFile', infoHash, index)
} }
var rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return hx` return hx`
<tr onclick=${handleClick} class='${rowClass}'> <tr onclick=${handleClick}>
<td class='col-icon'> <td class='col-icon ${rowClass}'>
${positionElem}
<i class='icon'>${icon}</i> <i class='icon'>${icon}</i>
</td> </td>
<td class='col-name'>${file.name}</td> <td class='col-name ${rowClass}'>
<td class='col-progress'>${progress}</td> ${file.name}
<td class='col-size'>${prettyBytes(file.length)}</td> </td>
<td class='col-progress ${rowClass}'>
${isSelected ? progress : ''}
</td>
<td class='col-size ${rowClass}'>
${prettyBytes(file.length)}
</td>
<td class='col-select'
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
</td>
</tr> </tr>
` `
} }
} }
function renderRadialProgressBar (fraction, cssClass) {
var rotation = 360 * fraction
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return hx`
<div class="radial-progress ${cssClass}">
<div class="circle">
<div class="mask full" style=${transformFill}>
<div class="fill" style=${transformFill}></div>
</div>
<div class="mask half">
<div class="fill" style=${transformFill}></div>
<div class="fill fix" style=${transformFix}></div>
</div>
</div>
<div class="inset"></div>
</div>
`
}

View File

@@ -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()
@@ -49,8 +42,8 @@ function init () {
client.on('warning', (err) => ipc.send('wt-warning', null, err.message)) client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message)) client.on('error', (err) => ipc.send('wt-error', null, err.message))
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) => ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes)) startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
ipc.on('wt-stop-torrenting', (e, infoHash) => ipc.on('wt-stop-torrenting', (e, infoHash) =>
stopTorrenting(infoHash)) stopTorrenting(infoHash))
ipc.on('wt-create-torrent', (e, torrentKey, options) => ipc.on('wt-create-torrent', (e, torrentKey, options) =>
@@ -65,15 +58,21 @@ function init () {
startServer(infoHash, index)) startServer(infoHash, index))
ipc.on('wt-stop-server', (e) => ipc.on('wt-stop-server', (e) =>
stopServer()) stopServer())
ipc.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections))
ipc.send('ipcReadyWebTorrent') ipc.send('ipcReadyWebTorrent')
window.addEventListener('error', (e) =>
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
true)
setInterval(updateTorrentProgress, 1000) setInterval(updateTorrentProgress, 1000)
} }
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object // Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent- // See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
function startTorrenting (torrentKey, torrentID, path, fileModtimes) { function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
console.log('starting torrent %s: %s', torrentKey, torrentID) console.log('starting torrent %s: %s', torrentKey, torrentID)
var torrent = client.add(torrentID, { var torrent = client.add(torrentID, {
@@ -81,8 +80,13 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
fileModtimes: fileModtimes fileModtimes: fileModtimes
}) })
torrent.key = torrentKey torrent.key = torrentKey
// Listen for ready event, progress notifications, etc
addTorrentEvents(torrent) addTorrentEvents(torrent)
// Only download the files the user wants, not necessarily all files
torrent.once('ready', () => selectFiles(torrent, selections))
return torrent return torrent
} }
@@ -157,9 +161,7 @@ function getTorrentFileInfo (file) {
return { return {
name: file.name, name: file.name,
length: file.length, length: file.length,
path: file.path, path: file.path
numPiecesPresent: 0,
numPieces: null
} }
} }
@@ -175,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)
@@ -188,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)
}) })
@@ -201,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
@@ -272,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) {
@@ -310,6 +312,44 @@ function getAudioMetadata (infoHash, index) {
}) })
} }
function selectFiles (torrentOrInfoHash, selections) {
// Get the torrent object
var torrent
if (typeof torrentOrInfoHash === 'string') {
torrent = client.get(torrentOrInfoHash)
} else {
torrent = torrentOrInfoHash
}
// Selections not specified?
// Load all files. We still need to replace the default whole-torrent
// selection with individual selections for each file, so we can
// select/deselect files later on
if (!selections) {
selections = torrent.files.map((x) => true)
}
// Selections specified incorrectly?
if (selections.length !== torrent.files.length) {
throw new Error('got ' + selections.length + ' file selections, ' +
'but the torrent contains ' + torrent.files.length + ' files')
}
// Remove default selection (whole torrent)
torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files)
for (var i = 0; i < selections.length; i++) {
var file = torrent.files[i]
if (selections[i]) {
file.select()
} else {
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
file.deselect()
}
}
}
// Gets a WebTorrent handle by torrentKey // Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key // Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) { function getTorrent (torrentKey) {

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

@@ -13,7 +13,7 @@ Exec=$EXEC_PATH %U
TryExec=$TRY_EXEC_PATH TryExec=$TRY_EXEC_PATH
StartupNotify=false StartupNotify=false
Categories=Network;FileTransfer;P2P; Categories=Network;FileTransfer;P2P;
MimeType=application/x-bittorrent;x-scheme-handler/magnet; MimeType=application/x-bittorrent;x-scheme-handler/magnet;x-scheme-handler/stream-magnet;
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress; Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB