Compare commits

...

357 Commits

Author SHA1 Message Date
DC
2b6c9ffcdb 0.13.1 2016-08-31 18:49:18 -07:00
DC
1d4b8ab67d Fix Create Torrent 2016-08-31 17:32:22 -07:00
DC
6404168bee changelog 2016-08-31 16:23:45 -07:00
DC
6613366cff authors 2016-08-31 16:09:49 -07:00
DC
74349129f4 0.13.0 2016-08-31 16:09:41 -07:00
DC
cdc2c1d718 Fix Create Torrent file list 2016-08-31 15:53:00 -07:00
DC
f8cc155650 Material UI: finish Create Torrent 2016-08-31 03:35:27 -07:00
DC
b6bdeab50b depcheck: ignore generated code 2016-08-31 00:58:00 -07:00
DC
f528f6033f Preferences: fix Make Default 2016-08-31 00:54:31 -07:00
DC
ef1bc13c38 use depcheck (#841)
* use depcheck to replace our own check-dependencies script

* ignore packges used in npm scripts
2016-08-30 20:42:45 -07:00
DC
2f54feac74 Telemetry: handle undefined values (#851) 2016-08-30 20:22:49 -07:00
Mathias Rasmussen
75d30baaa5 Telemetry: handle undefined values 2016-08-29 03:08:54 +02:00
Noam Okman
990fb57839 ignore packges used in npm scripts 2016-08-27 21:40:04 +03:00
Noam Okman
1883341ddb use depcheck 2016-08-27 21:40:02 +03:00
DC
5f7cece6d1 Fix npm run package
A require() had the wrong case, which apparently works for `npm start` build but fails in the packaged app
2016-08-27 09:05:58 -07:00
Feross Aboukhadijeh
89e77d34f4 Merge pull request #838 from feross/wt-init-timeend
fix: add missing console.timeEnd
2016-08-26 20:18:03 +02:00
Nate Goldman
5eeb8fd6fc fix: add missing console.timeEnd 2016-08-26 09:43:40 -07:00
Feross Aboukhadijeh
808fca031a standard 2016-08-25 17:57:40 -07:00
Feross Aboukhadijeh
6b3c1e3802 Merge pull request #836 from feross/dc/fixes
Telemetry: log version in errors
2016-08-25 15:34:21 +02:00
DC
f488ef7597 Telemetry: fix error logging bugs, [object Object] and [object HTMLMediaElement] 2016-08-25 05:56:46 -07:00
DC
2c179c7465 Telemetry: log version in errors 2016-08-25 03:50:13 -07:00
Mathias Rasmussen
f1cf37200e decrease setProgress updates (#833) 2016-08-25 04:16:30 +02:00
Feross Aboukhadijeh
d2da6881d6 Merge pull request #829 from feross/f/hat
Remove 'hat' dependency
2016-08-25 02:57:32 +02:00
Feross Aboukhadijeh
9c25de58de Merge pull request #830 from feross/f/imageoptim
ImageOptim: losslessly compress all images
2016-08-25 02:57:02 +02:00
Feross Aboukhadijeh
f9aeb676b4 ImageOptim: losslessly compress all images 2016-08-24 03:47:47 -07:00
Feross Aboukhadijeh
396d769bc8 Remove 'hat' dependency 2016-08-24 00:40:33 -07:00
Feross Aboukhadijeh
83901eecba Fixes for PR #825
cc @mathiasvr
2016-08-23 17:25:59 -07:00
Feross Aboukhadijeh
019728cff5 Merge pull request #825 from feross/m/path-error-buttons
Don't render radial progress on path-missing error
2016-08-24 02:20:36 +02:00
Feross Aboukhadijeh
a96241d151 Merge pull request #824 from feross/m/open-partial-files
Don't open partially downloaded files in external app
2016-08-24 02:12:47 +02:00
Feross Aboukhadijeh
ebc9771be5 Merge pull request #823 from feross/m/external-controls
Don't render player controls when using external player
2016-08-24 02:08:58 +02:00
Feross Aboukhadijeh
0c75bac364 Merge pull request #822 from feross/m/playback-sound
Prevent notification sound during playback
2016-08-24 02:05:06 +02:00
Feross Aboukhadijeh
3f7e2c1e4a remove dev tools log 2016-08-23 16:24:41 -07:00
Feross Aboukhadijeh
10bf38bdf0 Merge pull request #826 from feross/readme
Readme improvements
2016-08-24 01:22:17 +02:00
Feross Aboukhadijeh
02508d7d9e Merge pull request #821 from feross/m/fix-poster-image
fix player poster image css bug
2016-08-24 01:21:53 +02:00
Feross Aboukhadijeh
5cb295f722 readme 2016-08-23 16:19:16 -07:00
Feross Aboukhadijeh
b08d273996 Fixes for PR #817
See
https://github.com/feross/webtorrent-desktop/pull/817#discussion-diff-75
968122
2016-08-23 16:14:22 -07:00
Feross Aboukhadijeh
1e27d1803a Merge pull request #817 from feross/menu-error
Fix error with menus.
2016-08-24 01:12:45 +02:00
Mathias Rasmussen
9bc018cc02 don't render radial progress on path error 2016-08-23 22:54:01 +02:00
Mathias Rasmussen
73cdfc6d45 don't open partially downloaded files 2016-08-23 22:23:43 +02:00
Mathias Rasmussen
1afd650997 Don't render player controls for external player 2016-08-23 20:57:31 +02:00
Mathias Rasmussen
9c8eabb46c Prevent notification sound during playback 2016-08-23 20:43:47 +02:00
Mathias Rasmussen
b39884e68f fix player poster image css bug 2016-08-23 20:21:10 +02:00
Benjamin Tan
451d457426 Fix error with menus. 2016-08-23 22:42:05 +08:00
Feross Aboukhadijeh
82853aa017 more progress 2016-08-23 04:21:58 -07:00
Feross Aboukhadijeh
157226f75b create torrent page progress 2016-08-23 03:51:05 -07:00
Feross Aboukhadijeh
509691a85a fixes for new path structure 2016-08-23 03:06:03 -07:00
Feross Aboukhadijeh
8b3aee7e2d move pages to renderer/pages/ 2016-08-23 03:06:03 -07:00
Feross Aboukhadijeh
4025e669eb misc changes 2016-08-23 03:06:03 -07:00
Feross Aboukhadijeh
1a01d7ed92 Use Material UI; improve Preferences Page
New principles for our UI:

- Components should use inline styles whenever possible
- Let's shrink the size of main.css to < 100 lines over time so it just
contains typography and basic styles
- Always require just the individual component that is needed from
Material UI so that the whole library doesn't get loaded (important for
startup perf)
2016-08-23 03:06:03 -07:00
Feross Aboukhadijeh
b4976d27f2 update material icons font 2016-08-23 03:05:33 -07:00
Feross Aboukhadijeh
173d8444d7 Preferences page rehaul: use React components, UI improvements 2016-08-23 03:05:33 -07:00
Feross Aboukhadijeh
aa150b76a5 less janky startup 2016-08-23 03:05:33 -07:00
Feross Aboukhadijeh
e2b5e28e07 add npm run watch command 2016-08-23 03:05:33 -07:00
Feross Aboukhadijeh
1ad07d9977 gitignore folders 2016-08-23 03:05:33 -07:00
Feross Aboukhadijeh
8ba4056894 move main.css to css/main.css 2016-08-23 03:04:58 -07:00
Feross Aboukhadijeh
9ad0316dff ensure Segoe is only used on Windows 2016-08-23 02:57:09 -07:00
Feross Aboukhadijeh
854aae7dc5 Merge pull request #813 from avamsi/patch-1
Add 'Segoe UI'
2016-08-23 11:55:24 +02:00
DC
5b021cd42e Audio: support .m4a 2016-08-23 02:39:45 -07:00
Vamsi Krishna Avula
33417d9b7e add 'Segoe UI' 2016-08-23 14:19:07 +05:30
Feross Aboukhadijeh
275184214a Merge pull request #811 from JaKXz/chore/electron
Switch from electron-prebuilt to electron
2016-08-23 10:35:16 +02:00
Jason Kurian
1f9adbd3cf Switch from electron-prebuilt to electron 2016-08-23 04:32:09 -04:00
DC
092c207dce changelog 2016-08-23 00:31:01 -07:00
DC
603c24faed authors 2016-08-23 00:30:56 -07:00
DC
f259b32cce 0.12.0 2016-08-23 00:07:01 -07:00
DC
eba9aa3e17 Telemetry: log app version 2016-08-22 23:59:52 -07:00
Feross Aboukhadijeh
905eb1611e Merge pull request #807 from feross/dc/fixes
Fix playback + download of default torrents
2016-08-23 06:32:58 +02:00
DC
6d4b8c3c26 Fix playback + download of default torrents
There was a terrible bug introduced in 0809e20a6e -- clicking play on any of the default torrents in a fresh install of the app would fail and result in a 'path missing' error.

This fixes the bug, and also adds a migration step to clean up resulting broken config files
2016-08-22 21:21:32 -07:00
Feross Aboukhadijeh
6a46609cca Merge pull request #804 from feross/dc/fixes
Bugfixes
2016-08-23 01:46:05 +02:00
DC
e872282221 Fix Delete Torrent + Data for newly added magnet links
Before, if you added a magnet link and then tried to delete the torrent plus data before the file list was loaded, it would fail and throw an uncaught error

Fixes #803
2016-08-22 00:58:23 -07:00
DC
24ac5af5b4 Fix jumpToTime
Fixes #801
2016-08-22 00:58:23 -07:00
DC
0ee92fb632 Telemetry: redact stacktraces 2016-08-22 00:54:19 -07:00
Feross Aboukhadijeh
7cbc12c6ff Merge pull request #795 from feross/small-fixes
A bunch of small fixes
2016-08-22 02:04:19 +02:00
Feross Aboukhadijeh
60c82c73cd Merge pull request #793 from feross/debian-system-install
Add system-wide menu item for debian and derivates
2016-08-22 02:03:29 +02:00
Feross Aboukhadijeh
78790e73c7 Merge pull request #788 from feross/content-bounds
Only use setContentBounds for player view
2016-08-22 01:56:14 +02:00
Feross Aboukhadijeh
bf464de16f remove extra console.error
This prevents all errors from being logged twice
2016-08-21 16:55:16 -07:00
Feross Aboukhadijeh
0589963eed code style 2016-08-21 16:54:59 -07:00
Feross Aboukhadijeh
b79971eea5 show video title in webtorrent app for all external players 2016-08-21 16:54:45 -07:00
Feross Aboukhadijeh
d1e557f054 Ignore stdout/stderr from spawned player
This prevents stalling in players like mpv/mplayer for some reason.

I think this could be because of the large number of updates that are
being written to stdout that's filling up a buffer and preventing
playback from continuing.
2016-08-21 16:54:27 -07:00
Feross Aboukhadijeh
93ddb8d638 launch VLC fixes
We can show video title on start now, since we're setting it correctly.

Also, escape the title since it could contain spaces.
2016-08-21 16:53:23 -07:00
Feross Aboukhadijeh
06fdd80845 Merge pull request #792 from feross/dc/telemetry
Telemtry: post at least once a day
2016-08-21 05:48:45 +02:00
grunjol
b0b26f8300 add system-wide launcher and icons for debian and derivates 2016-08-20 23:08:51 -03:00
DC
1db890f5e7 Telemtry: post at least once a day
This ensures that people who keep the app running in the background
for days at a time are still counted as active users.
2016-08-20 18:30:22 -07:00
Feross Aboukhadijeh
0f80f96023 Merge branch 'mathiasvr-external-player' 2016-08-20 01:20:37 -07:00
Feross Aboukhadijeh
2d3673ea33 Fixes to PR #682
- Rename 'playInVlc' preference to 'openExternalPlayer' since we
support more than just VLC now.
- Add default pref options to state.js
2016-08-20 01:19:50 -07:00
Feross Aboukhadijeh
c28260611e Merge pull request #787 from feross/redundant-powersaver
If a power saver block already exists, do nothing
2016-08-20 09:45:00 +02:00
Feross Aboukhadijeh
b5dd00007a Merge pull request #789 from feross/ignore-dot
Ignore '.' argument which is annoying in development
2016-08-20 09:44:52 +02:00
Feross Aboukhadijeh
ac39264f3d Ignore '.' argument which is annoying in development 2016-08-19 22:44:26 -07:00
Feross Aboukhadijeh
667a04a41d Merge branch 'external-player' of https://github.com/mathiasvr/webtorrent-desktop into mathiasvr-external-player
Fixed conflicts in the Preferences page, and added back passing the video title to VLC
2016-08-19 22:06:23 -07:00
Feross Aboukhadijeh
51a9b2ea9b Only use setContentBounds for player view
Fixes: https://github.com/feross/webtorrent-desktop/issues/786
2016-08-19 20:05:52 -07:00
Feross Aboukhadijeh
842ee5ca3c If a power saver block already exists, do nothing
Before this change, when opening the player, both 'onPlayerOpen' and
'onPlayerPlay' would fire, which enabled, disabled, and re-enabled the
power save blocker in quick succession.

Instead, we just want to activate it once.
2016-08-19 20:04:18 -07:00
Feross Aboukhadijeh
2cc67dbda7 AUTHORS 2016-08-19 16:24:47 -07:00
Feross Aboukhadijeh
70bc32614b 0.11.0 2016-08-19 16:24:20 -07:00
Feross Aboukhadijeh
9bf44d7d7e Merge pull request #784 from feross/plist-2
plist@2
2016-08-20 01:21:27 +02:00
Feross Aboukhadijeh
f48ecb87b2 plist@2
Looks like there are no important changes. They just deleted some
deprecated methods we don't use.
2016-08-19 16:19:05 -07:00
Feross Aboukhadijeh
1765aba681 CHANGELOG 2016-08-19 16:15:25 -07:00
Feross Aboukhadijeh
c6063c759e Merge pull request #783 from feross/standard-v8
update to standard v8
2016-08-19 10:46:13 +02:00
Feross Aboukhadijeh
bb4db2cede update to standard v8
The only thing we have to change is to self-close tags that don't
contain anything. This wasn't even an explicit change in standard. It
was already a rule, but I think it wasn't getting enforced very well
until a bugfix.
2016-08-19 01:44:28 -07:00
DC
7c36898f78 Merge pull request #674 from codealchemist/open-in-vlc-preferences
Added Playback preferences and Play in VLC
2016-08-19 00:49:37 -07:00
Alberto Miranda
23e8cdf216 using key on rendered checkbox to avoid react errors; dispatching updatePreferences to update playInVlc. 2016-08-19 01:10:55 -03:00
Alberto Miranda
5ffd4123a1 fixed merge conflicts 2016-08-19 01:09:43 -03:00
Alberto Miranda
27e3c14f10 merged 2016-08-19 00:28:07 -03:00
Feross Aboukhadijeh
d57bfb825a Merge pull request #776 from feross/dc/missing-path
Check for missing download path
2016-08-15 01:10:05 +02:00
DC
0809e20a6e Check path for each torrent 2016-08-13 22:37:14 -07:00
DC
1ec305162e Check for missing download path
Fixes #646
2016-08-12 20:57:03 -07:00
Feross Aboukhadijeh
45d46d7ae8 show title on 'create new torrent' page 2016-08-12 15:35:56 -07:00
Feross Aboukhadijeh
adb41736d5 Merge pull request #775 from feross/dc/768
Create Torrent: make trackers editable again
2016-08-13 00:08:38 +02:00
DC
09d6fa550a Handle Save Torrent File As... -> Cancel 2016-08-12 09:31:28 -07:00
DC
75cc7383cb Create Torrent: make trackers editable again
Fixes #768
2016-08-12 09:21:12 -07:00
DC
4d48b9e7c1 Fix screen stacking bug
You can no longer open a whole stack of Prefs windows, or Create Torrent windows

Simplifies and fixes behavior when dropping files onto the app or the dock icon. Before, you could use drag-drop to create stacks of Create Torrent windows. Now, you can only create torrents from the home screen.

Fixes #665
2016-08-12 09:03:32 -07:00
Feross Aboukhadijeh
563e1ca0ba Support for instant.io links does not belong in webtorrent core 2016-08-11 00:20:07 -07:00
Feross Aboukhadijeh
0fa3b678b0 Merge pull request #772 from feross/electron-1.3.3
Update Electron to 1.3.3
2016-08-11 06:54:51 +02:00
Feross Aboukhadijeh
8420c65d25 Merge pull request #771 from feross/dc/file-handler
Pref: default torrent file handler
2016-08-11 02:27:54 +02:00
Feross Aboukhadijeh
3232e96f6e Resize the window's content area
Fixes: https://github.com/feross/webtorrent-desktop/issues/565

This was trivial thanks to a new Electron API in 1.3.3
2016-08-10 16:48:32 -07:00
Feross Aboukhadijeh
110e25af73 electron-prebuilt@1.3.3 2016-08-10 16:47:58 -07:00
DC
8233faf518 Pref: default torrent file handler
Before, the app made itself the default torrent file handler automatically, pissing off some of our users. Now, it's not by default, and you can change it in the prefs.
2016-08-10 02:23:08 -07:00
Feross Aboukhadijeh
11eb603930 0.10.0 2016-08-05 22:53:49 -07:00
Feross Aboukhadijeh
1d55c51a16 Improve AUTHORS.md rendering 2016-08-05 22:41:37 -07:00
Feross Aboukhadijeh
447a7e514e Merge pull request #764 from feross/smoke-tests
Add Smoke Tests to CONTRIBUTING.md
2016-08-05 22:27:37 -07:00
Feross Aboukhadijeh
fd433784bd Add Smoke Tests to CONTRIBUTING.md
Fix #582
2016-08-05 22:26:33 -07:00
Feross Aboukhadijeh
4e2b196b26 CHANGELOG 2016-08-05 22:06:58 -07:00
Feross Aboukhadijeh
14fcbfcced make logs consistent 2016-08-05 21:44:32 -07:00
Feross Aboukhadijeh
4126d15821 Move .babelrc file into src/ 2016-08-05 21:41:22 -07:00
Feross Aboukhadijeh
0d1cc72798 Merge pull request #760 from feross/babel
Replace deprecated `react-tools` with `babel`
2016-08-05 16:47:10 -07:00
Feross Aboukhadijeh
c42eb789df fix typo 2016-08-05 16:37:25 -07:00
Feross Aboukhadijeh
c1dd0b31cf package: Ignore src/ directory since only build/ is used
@dcposch -- does this look reasonable?
2016-08-04 21:59:01 -07:00
Feross Aboukhadijeh
9afed7fb1b Remove --dev flag, Run React in production mode when Electron is in production mode 2016-08-04 21:58:34 -07:00
Feross Aboukhadijeh
a8239895c6 babel: Add --quiet option 2016-08-04 21:41:37 -07:00
Feross Aboukhadijeh
7531ab4623 Simplify babel integration further
The "react" preset is composed of a bunch of plugins.
https://babeljs.io/docs/plugins/preset-react/

Turns out, we only need 2 of them, not all 5.
2016-08-04 21:37:53 -07:00
Feross Aboukhadijeh
9b36f9cb22 Ensure that build folder gets generated before npm publish
So users using `npm install -g webtorrent-desktop` will always get a
working version.
2016-08-04 21:05:03 -07:00
Feross Aboukhadijeh
29f8ef6b72 Replace deprecated react-tools with babel
- Switch to babel, since react-tools has been deprecated since June 12,
2015. See
https://facebook.github.io/react/blog/2015/06/12/deprecating-jstransform
-and-react-tools.html

- Move babel command to "npm run build"

- Move commands for package into "bin/package.js"
2016-08-04 21:04:49 -07:00
Feross Aboukhadijeh
7752e41416 Merge pull request #757 from feross/fix-695
Fix "Cannot read property 'numPiecesPresent' of undefined"
2016-08-04 15:03:16 -07:00
Feross Aboukhadijeh
756ccd1921 Merge pull request #758 from feross/fix-284
Allow dragging magnet links (Fix #284)
2016-08-04 15:02:32 -07:00
Alberto Miranda
39ae0343fc Merge branch 'master' of https://github.com/feross/webtorrent-desktop 2016-08-04 08:10:31 -03:00
Feross Aboukhadijeh
67409214a4 Allow dragging magnet links (Fix #284) 2016-08-03 20:26:46 -07:00
Feross Aboukhadijeh
b3d0edfec1 Merge pull request #756 from feross/electron-1.3.2
electron-prebuilt@1.3.2
2016-08-03 17:06:35 -07:00
Feross Aboukhadijeh
87b9dba568 Fix "Cannot read property 'numPiecesPresent' of undefined"
Fixes #695
2016-08-03 17:06:04 -07:00
Feross Aboukhadijeh
91a4c0cff5 electron-prebuilt@1.3.2
Changelog: https://github.com/electron/electron/releases/tag/v1.3.2

Nothing in the changelog fixes known WebTorrent Desktop issues.
2016-08-02 19:11:06 -07:00
Feross Aboukhadijeh
9670dc7a81 Merge pull request #752 from feross/user-tasks
Add User Tasks for Windows.
2016-08-02 18:57:44 -07:00
Feross Aboukhadijeh
4f2c5b946d Merge pull request #753 from feross/rimraf
Use `rimraf` instead of `rm -rf` for Windows.
2016-08-02 18:52:35 -07:00
Benjamin Tan
fc53c68dd9 Use rimraf instead of rm -rf for Windows. 2016-07-31 16:36:43 +08:00
Benjamin Tan
03bc4cf9b1 Add User Tasks for Windows.
Closes #114.
2016-07-31 16:29:38 +08:00
Alberto Miranda
c637878603 removed logging; restored minimist in package.js ignore list. 2016-07-29 23:01:17 -03:00
Alberto Miranda
91e61f6cd4 using icon as checkbox 2016-07-29 22:07:44 -03:00
Alberto Miranda
9f66418073 merged with latest master; using icon as checkbox. 2016-07-29 22:07:25 -03:00
Alberto Miranda
2c3d667675 Merge remote-tracking branch 'feross/master' 2016-07-29 20:49:29 -03:00
Feross Aboukhadijeh
2ab93f2309 Merge branch 'location-history' 2016-07-28 22:00:58 -07:00
Feross Aboukhadijeh
05ce20303c fix exception 2016-07-28 20:35:36 -07:00
Feross Aboukhadijeh
5e997d1bbf Merge pull request #748 from feross/location-history
Switch to using `location-history` package
2016-07-28 20:29:55 -07:00
Feross Aboukhadijeh
d95e5b02d6 Remove extra 'resetTitle' call 2016-07-28 20:10:48 -07:00
Feross Aboukhadijeh
536f04985f Switch to using location-history package
https://npmjs.com/package/location-history
2016-07-28 20:07:12 -07:00
Feross Aboukhadijeh
43a81f725f Merge pull request #747 from feross/electron-1.3.1
electron-prebuilt@1.3.1
2016-07-27 16:05:34 -07:00
Feross Aboukhadijeh
8373e69d09 Merge pull request #746 from feross/fix-700
Fix 700
2016-07-27 16:04:29 -07:00
Feross Aboukhadijeh
c5ed5fabd8 Fix typo based on @mathiasvr's feedback 2016-07-27 15:51:19 -07:00
Feross Aboukhadijeh
8ba4dadb10 electron-prebuilt@1.3.1 2016-07-27 14:49:59 -07:00
Feross Aboukhadijeh
112600f5c3 Set video title when opening VLC
Fix #700

The title is set with the `--meta-title` flag to VLC.
2016-07-27 14:46:39 -07:00
Feross Aboukhadijeh
c2f869b362 Use dispatch('setTitle') and add dispatch('resetTitle') 2016-07-27 14:39:22 -07:00
Feross Aboukhadijeh
2590e0effc Merge pull request #743 from feross/f/mac
OS X -> Mac
2016-07-27 13:15:48 -07:00
Feross Aboukhadijeh
b417ef5b03 Merge pull request #744 from feross/f/engines
Add "engines" field to package.json
2016-07-27 13:15:12 -07:00
Feross Aboukhadijeh
1733a506c0 Merge pull request #742 from feross/peerid
Set peer ID to start with "-WD-"
2016-07-27 13:14:54 -07:00
Feross Aboukhadijeh
ac05cc4387 Use arrow function
cc @mathiasvr
2016-07-27 12:37:58 -07:00
Feross Aboukhadijeh
904f337713 Add "engines" field to package.json
Fixes #675
2016-07-26 22:58:07 -07:00
Feross Aboukhadijeh
febad56497 OS X -> Mac 2016-07-26 21:55:05 -07:00
Feross Aboukhadijeh
cb71de2313 Set peer ID to start with "-WD-"
To distinguish WebTorrent Desktop (WD) from WebTorrent in the browser
(WW).

See the spec:

http://www.bittorrent.org/beps/bep_0020.html
https://wiki.theory.org/BitTorrentSpecification
2016-07-26 20:20:01 -07:00
Feross Aboukhadijeh
6891ef1a0d fix paths in clean script 2016-07-26 16:13:51 -07:00
Mathias Rasmussen
6c68645b0f Custom external media player 2016-07-26 23:57:33 +02:00
Feross Aboukhadijeh
767ca71f7d Update README.md 2016-07-26 01:33:24 -07:00
Feross Aboukhadijeh
1605d23509 Merge pull request #740 from feross/electron-1.3.0
electron-prebuilt@1.3.0
2016-07-25 16:42:08 -07:00
Feross Aboukhadijeh
90a0201e38 Merge pull request #739 from feross/fixes-738
Electron: Updates for Electron 1.2.8
2016-07-25 16:41:54 -07:00
Feross Aboukhadijeh
80983c2058 electron-prebuilt@1.3.0
Another Electron was just released. Let's bump from 1.2.8 to 1.3.0.

Changelog:

- Upgrade to Chrome 52
- Update to Node.js 6.3.0
2016-07-25 15:42:55 -07:00
Feross Aboukhadijeh
ad62bbd9d3 Linux: Support showing badge count
This was a macOS-only API before, but it's cross-platform now via
`app.setBadgeCount()`
2016-07-25 15:37:29 -07:00
Feross Aboukhadijeh
1b3b6fef10 Electron: Use 'quit' role 2016-07-25 15:30:02 -07:00
Feross Aboukhadijeh
6e1ff18eb9 macOS: add missing Edit menu roles 2016-07-25 15:29:53 -07:00
Feross Aboukhadijeh
5796ba32a6 Electron: Use default labels and accelerators
Less code for us to maintain.

This also gives us free internationalization in a future Electron
version (they'll set the label dynamically based on the 'role')

One slight regression with this change, but it will be fixed in a
future Electron once this PR is merged:
https://github.com/electron/electron/pull/6600
2016-07-25 15:21:46 -07:00
Feross Aboukhadijeh
2eb33e5f0c Merge pull request #738 from mathiasvr/bump
bump versions of electron and fs-extra
2016-07-25 14:30:17 -07:00
Mathias Rasmussen
7c14d8c909 bump versions of electron and fs-extra 2016-07-25 02:18:14 +02:00
Feross Aboukhadijeh
fe4c1b0ee8 Merge pull request #733 from feross/f/application-config
application-config@1
2016-07-23 02:35:43 -07:00
DC
d21396c618 React: make webtorrent process easier to debug
Add window.client = the WebTorrent client
2016-07-22 22:14:51 -07:00
DC
9df51aec49 React: clean up App component 2016-07-22 21:55:31 -07:00
DC
734b0731a1 React: address PR comments 2016-07-22 21:48:38 -07:00
DC
d20786cd69 React: fix package script 2016-07-22 19:57:06 -07:00
DC
793ea79cab Perf: remove DL button animation
Turns out this is huge. For some inexplicable reason, it improves hover and scroll in the torrent detail view, for a torrent with 350 files, from ~10FPS to ~60FPS.
2016-07-22 17:52:22 -07:00
Feross Aboukhadijeh
073a86ecbd application-config@1
This protects against corrupting the configuration file if the
application crashes before the stream finishes writing to the file

Especially important for large configuration files

See https://github.com/LinusU/node-application-config/pull/3
2016-07-22 17:21:25 -07:00
DC
7b4fd57a94 Perf: don't update torrent progres >1x per second 2016-07-22 16:38:42 -07:00
DC
f86ca0a168 Perf: use px, not em for column widths 2016-07-22 16:23:11 -07:00
DC
0d3c18d3bc Don't show padding files 2016-07-22 13:06:58 -07:00
DC
a4a31d0860 React: fix warnings 2016-07-22 13:06:58 -07:00
DC
946bba19a9 React: convert functions to controls 2016-07-22 13:06:58 -07:00
DC
2a1e987d42 Switch from virtualdom to React 2016-07-22 13:06:55 -07:00
DC
fbcf718440 Perf: skip duplicate update()s, measure app render time 2016-07-22 13:05:17 -07:00
DC
18aadf9d23 Merge v0.9.0 (#730)
v0.9.0
2016-07-21 15:52:54 -07:00
Andrea Tupini
cd575d2005 Display torrent ETA in list item (#726)
* Display torrent ETA in list

ETA being the estimated time to download completion calculated using the
current download speed and missing bytes.

* Refactor ETA string construction

* Removed extra 's' in ETA string construction
2016-07-21 13:18:56 -07:00
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
Alberto Miranda
dc7ccb3956 Merge remote-tracking branch 'feross/master' 2016-07-20 09:10:55 -03:00
Alberto Miranda
a420936657 Merge branch 'master' into open-in-vlc-preferences 2016-07-19 02:11:13 -03:00
Alberto Miranda
dcab7f72d4 fixed error with minimist not being loaded on build 2016-07-19 02:10:37 -03:00
DC
d0733d3370 Fix bug in PlaybackController 2016-07-17 17:34:58 -07:00
Alberto Miranda
a695f7c2d7 using this.state 2016-07-17 21:05:24 -03:00
Alberto Miranda
7677bff6d4 merged with latest master 2016-07-17 20:57:06 -03:00
Alberto Miranda
c7626997de merged with latest master 2016-07-17 20:48:25 -03: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
Alberto Miranda
91a1ab4a56 removed unused config property 2016-06-27 08:21:30 -03: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
Alberto Miranda
3e19cdfb0b fixed js style 2016-06-25 17:40:47 -03:00
Alberto Miranda
2043dc2161 added Playback preferences; added Play in VLC preference. 2016-06-25 17:36:50 -03:00
Alberto Miranda
a9e36472c5 fixed js style 2016-06-24 09:38:08 -03:00
Alberto Miranda
4df4f9b2ad fixed js style 2016-06-24 09:34:54 -03:00
Alberto Miranda
4ad55173a5 added missing method to menu.js 2016-06-24 09:34:12 -03:00
Alberto Miranda
b9c82dd6b2 persisting and reloading "Open in VLC" menu item state. 2016-06-24 09:28:28 -03:00
Alberto Miranda
8333f4893f fixed js style 2016-06-24 01:02:33 -03:00
Alberto Miranda
f071965ae8 fixed js style 2016-06-24 00:57:33 -03:00
Alberto Miranda
a4fa9ac666 added open in vlc feature. 2016-06-24 00:46:42 -03:00
Alberto Miranda
939ee555b7 fixed typo in buttonIcons (dnla instead of dlna). 2016-06-23 23:45:54 -03: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
117 changed files with 6284 additions and 4805 deletions

6
.gitignore vendored
View File

@@ -1,2 +1,4 @@
node_modules
dist
node_modules/
build/
dist/
npm-debug.log.*

2
.nodemonignore Normal file
View File

@@ -0,0 +1,2 @@
build/
dist/

View File

@@ -1,4 +1,4 @@
language: node_js
node_js:
- 'node'
install: npm install standard
install: npm install standard depcheck

View File

@@ -2,24 +2,34 @@
#### Ordered by first contribution.
- Feross Aboukhadijeh <feross@feross.org>
- DC <dcposch@dcpos.ch>
- Nate Goldman <nate@ngoldman.me>
- Chris Morris <chris@chrismorris.org>
- Giuseppe Crinò <giuscri@gmail.com>
- Romain Beaumont <romain.rom1@gmail.com>
- Dan Flettre <fletd01@yahoo.com>
- Liam Gray <liam.r.gray@gmail.com>
- grunjol <grunjol@argenteam.net>
- Rémi Jouannet <remijouannet@users.noreply.github.com>
- Evan Miller <miller.evan815@gmail.com>
- Alex <alxmorais8@msn.com>
- Diego Rodríguez Baquero <diegorbaquero@gmail.com>
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
- gabriel <furstenheim@gmail.com>
- Rolando Guedes <rolando.guedes@3gnt.net>
- Benjamin Tan <demoneaux@gmail.com>
- Mathias Rasmussen <mathiasvr@gmail.com>
- Sergey Bargamon <sergey@bargamon.ru>
- Feross Aboukhadijeh (feross@feross.org)
- DC (dcposch@dcpos.ch)
- Nate Goldman (nate@ngoldman.me)
- Chris Morris (chris@chrismorris.org)
- Giuseppe Crinò (giuscri@gmail.com)
- Romain Beaumont (romain.rom1@gmail.com)
- Dan Flettre (fletd01@yahoo.com)
- Liam Gray (liam.r.gray@gmail.com)
- Rémi Jouannet (remijouannet@users.noreply.github.com)
- Evan Miller (miller.evan815@gmail.com)
- Alex (alxmorais8@msn.com)
- Diego Rodríguez Baquero (diegorbaquero@gmail.com)
- Karlo Luis Martinez Martos (karlo.luis.m@gmail.com)
- gabriel (furstenheim@gmail.com)
- 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)
- Alberto Miranda (codealchemist@gmail.com)
- Adam Gotlib (gotlib.adam+dev@gmail.com)
- Rémi Jouannet (remijouannet@gmail.com)
- Andrea Tupini (tupini07@gmail.com)
- grunjol (grunjol@gmail.com)
- Jason Kurian (jasonk92@gmail.com)
- Vamsi Krishna Avula (vamsi_ism@outlook.com)
- Noam Okman (noamokman@gmail.com)
#### Generated by bin/update-authors.sh.

View File

@@ -1,15 +1,209 @@
# WebTorrent Desktop Version History
## v0.13.0 - 2016-08-31
### Added
- Torrent progress bar
- Support .m4a audio
- Better telemetry: log error versions, report more types of errors
### Changed
- New look - Material UI. Rewrote Create Torrent and Preferences pages.
### Fixed
- Fixed telemetry [object Object] and [object HTMLMediaElement] bugs
- Don't render player controls when playing externally, eg in VLC
- Don't play notification sounds during media playback
## v0.12.0 - 2016-08-23
### Added
- Custom external media player
- Linux: add system-wide launcher and icons for Debian, including Ubuntu
### Changed
- Telemetry improvements: redact stacktraces, log app version
### Fixed
- Fix playback and download of default torrents ("missing path" error) (#804)
- Fix Delete Torrent + Data for newly added magnet links
- Fix jumpToTime error (#804)
## v0.11.0 - 2016-08-19
### Added
- New Preference to "Set WebTorrent as default handler for torrents and magnet links" (#771)
- New Preference to "Always play in VLC" (#674)
- Check for missing default download path and torrent folders on start up (#776)
### Changed
- Do not automatically set WebTorrent as the default handler for torrents (#771)
- Torrents can only be created from the home screen (#770)
- Update Electron to 1.3.3 (#772)
### Fixed
- Allow modifying the default tracker list on the Create Torrent page (#775)
- Prevent opening multiple stacked Preference windows or Create Torrent windows (#770)
- Windows: Player window auto-resize does not match video aspect ratio (#565)
- Missing page title on Create Torrent page
## v0.10.0 - 2016-08-05
### Added
- Drag-and-drop magnet links (selected text) is now supported (#284)
- Windows: Add "User Tasks" shortcuts to app icon in Start Menu (#114)
- Linux: Show badge count for completed torrent downloads
### Changed
- Change WebTorrent Desktop peer ID prefix to 'WD' to distinguish from WebTorrent in the browser, 'WW' (#688)
- Switch UI to React to improve UI rendering speed (#729)
- The primary bottleneck was actually `hyperx`, not `virtual-dom`.
- Update Electron to 1.3.2 (#738) (#739) (#740) (#747) (#756)
- Mac 10.9: Fix the fullscreen button showing
- Mac 10.9: Fix window having border
- Mac 10.9: Fix occasional crash
- Mac: Update Squirrel.Mac to 0.2.1 (fixes situations in which updates would not get applied)
- Mac: Fix window not showing in Window menu
- Mac: Fix context menu always choosing first item by default
- Linux: Fix startup crashes (some Linux distros)
- Linux: Fix menubar not hiding after entering fullscreen (some Linux distros)
- Improved location history (back/forward buttons) to fix rare exceptions (#687) (#748)
- Location history abstraction released independently as [`location-history`](https://www.npmjs.com/package/location-history)
### Fixed
- When streaming to VLC, set VLC window title to torrent file name (#746)
- Fix "Cannot read property 'numPiecesPresent' of undefined" exception (#695)
- Fix rare case where config file could not be completely written (#733)
## 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
- 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 a `instant.io` links (#559)
- Add support for pasting `instant.io` links (#559)
- Add announcement feature
### Changed

View File

@@ -73,3 +73,23 @@ By making a contribution to this project, I certify that:
record of the contribution (including all personal information I submit with it,
including my sign-off) is maintained indefinitely and may be redistributed consistent
with this project or the open source license(s) involved.
## Smoke Tests
Before a release, check that the following basic use cases work correctly:
1. Click "Play" to stream a built-in torrent (e.g. Sintel)
- Ensure that seeking to undownloaded region works and plays immediately.
- Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
2. Check that the auto-updater works
- Open the console and check for the line "No update available" to indicate
3. Add a new .torrent file via drag-and-drop.
- Ensure that it gets added to the list and starts downloading
4. Remove a torrent from the client
- Ensure that the file is removed from `~/Downloads`
5. Create and seed a new a torrent via drag-and-drop.
- Ensure that the torrent gets created and seeding begins.

View File

@@ -7,7 +7,7 @@
<br>
</h1>
<h4 align="center">The streaming torrent client. For OS X, Windows, and Linux.</h4>
<h4 align="center">The streaming torrent app. For Mac, Windows, and Linux.</h4>
<p align="center">
<a href="https://gitter.im/feross/webtorrent"><img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg" alt="Gitter"></a>
@@ -17,31 +17,48 @@
## Install
**WebTorrent Desktop** is still under very active development. You can download the latest version from the [releases](https://github.com/feross/webtorrent-desktop/releases) page.
Download the latest version of WebTorrent Desktop from
[the official website](https://webtorrent.io/desktop/) or the
[GitHub releases](https://github.com/feross/webtorrent-desktop/releases) page.
## Screenshot
**WebTorrent Desktop** is under very active development. You can try out the
current (unstable) development version by cloning the Git repo. See the
instructions below in the ["How to Contribute"](#how-to-contribute) section.
## Screenshots
<p align="center">
<img src="https://webtorrent.io/img/screenshot-main.png" width="562" height="630" alt="screenshot" align="center">
<img src="https://webtorrent.io/img/screenshot-player3.png" alt="screenshot" align="center">
<img src="https://webtorrent.io/img/screenshot-main.png" width="612" height="749" alt="screenshot" align="center">
</p>
## How to Contribute
### Install dependencies
### Get the code
```
$ git clone https://github.com/feross/webtorrent-desktop.git
$ cd webtorrent-desktop
$ npm install
```
### Run app
### Run the app
```
$ npm start
```
### Package app
### Watch the code
Builds app binaries for OS X, Linux, and Windows.
Restart the app automatically every time code changes. Useful during development.
```
$ npm run watch
```
### Package the app
Builds app binaries for Mac, Linux, and Windows.
```
$ npm run package
@@ -50,30 +67,34 @@ $ npm run package
To build for one platform:
```
$ npm run package -- [platform]
$ npm run package -- [platform] [options]
```
Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default).
The following optional arguments are available:
- `--sign` - Sign the application (OS X, Windows)
- `--sign` - Sign the application (Mac, Windows)
- `--package=[type]` - Package single output type.
- `deb` - Debian package
- `zip` - Linux zip file
- `dmg` - OS X disk image
- `dmg` - Mac disk image
- `exe` - Windows installer
- `portable` - Windows portable app
- `all` - All platforms (default)
Note: Even with the `--package` option, the auto-update files (.nupkg for Windows, *-darwin.zip for OS X) will always be produced.
Note: Even with the `--package` option, the auto-update files (.nupkg for Windows,
*-darwin.zip for Mac) will always be produced.
#### Windows build notes
To package the Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs
to be installed.
The Windows app can be packaged from **any** platform.
On OS X, first install [XQuartz](http://www.xquartz.org/), then run:
Note: Windows code signing only works from **Windows**, for now.
Note: To package the Windows app from non-Windows platforms,
[Wine](https://www.winehq.org/) needs to be installed. For example on Mac, first
install [XQuartz](http://www.xquartz.org/), then run:
```
brew install wine
@@ -81,6 +102,23 @@ brew install wine
(Requires the [Homebrew](http://brew.sh/) package manager.)
#### Mac build notes
The Mac app can only be packaged from **macOS**.
#### Linux build notes
The Linux app can be packaged from **any** platform.
### 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 any personally identifying information, nor does it track which
torrents you add.
### Code Style
[![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard)

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env node
var fs = require('fs')
var cp = require('child_process')
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path', 'screen']
var EXECUTABLE_DEPS = ['gh-release', 'standard']
main()
// Scans our codebase and package.json for missing or unused dependencies
// Process returns 0 on success, prints a message and returns 1 on failure
function main () {
if (process.platform === 'win32') {
console.log('Sorry, check-deps only works on Mac and Linux')
return
}
var jsDeps = findJSDeps()
var packageDeps = findPackageDeps()
var missingDeps = jsDeps.filter((dep) =>
packageDeps.indexOf(dep) < 0 &&
BUILT_IN_DEPS.indexOf(dep) < 0)
var unusedDeps = packageDeps.filter((dep) =>
jsDeps.indexOf(dep) < 0 &&
EXECUTABLE_DEPS.indexOf(dep) < 0)
if (missingDeps.length > 0) console.log('Missing package dependencies: ' + missingDeps)
if (unusedDeps.length > 0) console.log('Unused package dependencies: ' + unusedDeps)
if (missingDeps.length + unusedDeps.length > 0) process.exit(1)
console.log('Lookin good!')
}
// Finds all dependencies, required, optional, or dev, in package.json
function findPackageDeps () {
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
var requiredDeps = Object.keys(pkg.dependencies)
var devDeps = Object.keys(pkg.devDependencies)
var optionalDeps = Object.keys(pkg.optionalDependencies)
return [].concat(requiredDeps, devDeps, optionalDeps)
}
// Finds all dependencies required() in the code
function findJSDeps () {
var stdout = cp.execSync('./bin/list-deps.sh')
return stdout.toString().trim().split('\n')
}

View File

@@ -10,11 +10,17 @@ var os = require('os')
var path = require('path')
var rimraf = require('rimraf')
var config = require('../config')
var handlers = require('../main/handlers')
var config = require('../src/config')
var handlers = require('../src/main/handlers')
// First, remove generated files
rimraf.sync('build/')
rimraf.sync('dist/')
// Remove any saved configuration
rimraf.sync(config.CONFIG_PATH)
// Remove any temporary files
var tmpPath
try {
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env node
var electron = require('electron-prebuilt')
var electron = require('electron')
var cp = require('child_process')
var path = require('path')
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
child.on('close', function (code) {
process.exit(code)
process.exitCode = code
})

View File

@@ -1,10 +0,0 @@
#!/bin/sh
# This is a truly heinous hack, but it works pretty nicely.
# Find all modules we're requiring---even conditional requires.
grep "require('" *.js bin/ main/ renderer/ -R |
grep '.js:' |
sed "s/.*require('\([^'\/]*\).*/\1/" |
grep -v '^\.' |
sort |
uniq

View File

@@ -1,8 +1,6 @@
#!/usr/bin/env node
var config = require('../config')
var config = require('../src/config')
var open = require('open')
var path = require('path')
var configPath = path.join(config.CONFIG_PATH, 'config.json')
open(configPath)
open(config.CONFIG_PATH)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* Builds app binaries for OS X, Linux, and Windows.
* Builds app binaries for Mac, Linux, and Windows.
*/
var cp = require('child_process')
@@ -15,10 +15,11 @@ var rimraf = require('rimraf')
var series = require('run-series')
var zip = require('cross-zip')
var config = require('../config')
var config = require('../src/config')
var pkg = require('../package.json')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
var BUILD_PATH = path.join(config.ROOT_PATH, 'build')
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
var argv = minimist(process.argv.slice(2), {
@@ -36,6 +37,12 @@ var argv = minimist(process.argv.slice(2), {
function build () {
rimraf.sync(DIST_PATH)
rimraf.sync(BUILD_PATH)
console.log('Babel: Building JSX...')
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
console.log('Babel: Built JSX.')
var platform = argv._[0]
if (platform === 'darwin') {
buildDarwin(printDone)
@@ -56,11 +63,11 @@ function build () {
var all = {
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
// property on Windows, and `NSHumanReadableCopyright` on OS X.
// property on Windows, and `NSHumanReadableCopyright` on Mac.
'app-copyright': config.APP_COPYRIGHT,
// The release version of the application. Maps to the `ProductVersion` metadata
// property on Windows, and `CFBundleShortVersionString` on OS X.
// property on Windows, and `CFBundleShortVersionString` on Mac.
'app-version': pkg.version,
// Package the application's source code into an archive, using Electron's archive
@@ -73,7 +80,7 @@ var all = {
'asar-unpack': 'WebTorrent*',
// The build version of the application. Maps to the FileVersion metadata property on
// Windows, and CFBundleVersion on OS X. Note: Windows requires the build version to
// Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to
// start with a number. We're using the version of the underlying WebTorrent library.
'build-version': require('webtorrent/package.json').version,
@@ -82,7 +89,7 @@ var all = {
// Pattern which specifies which files to ignore when copying files to create the
// package(s).
ignore: /^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
// The application name.
name: config.APP_NAME,
@@ -98,24 +105,24 @@ var all = {
prune: true,
// The Electron version with which the app is built (without the leading 'v')
version: require('electron-prebuilt/package.json').version
version: require('electron/package.json').version
}
var darwin = {
// Build for OS X
// Build for Mac
platform: 'darwin',
// Build 64 bit binaries only.
arch: 'x64',
// The bundle identifier to use in the application's plist (OS X only).
// The bundle identifier to use in the application's plist (Mac only).
'app-bundle-id': 'io.webtorrent.webtorrent',
// The application category type, as shown in the Finder via "View" -> "Arrange by
// Application Category" when viewing the Applications directory (OS X only).
// Application Category" when viewing the Applications directory (Mac only).
'app-category-type': 'public.app-category.utilities',
// The bundle identifier to use in the application helper's plist (OS X only).
// The bundle identifier to use in the application helper's plist (Mac only).
'helper-bundle-id': 'io.webtorrent.webtorrent-helper',
// Application icon.
@@ -171,10 +178,10 @@ build()
function buildDarwin (cb) {
var plist = require('plist')
console.log('OS X: Packaging electron...')
console.log('Mac: Packaging electron...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err)
console.log('OS X: Packaged electron. ' + buildPath)
console.log('Mac: Packaged electron. ' + buildPath)
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
var contentsPath = path.join(appPath, 'Contents')
@@ -206,6 +213,12 @@ function buildDarwin (cb) {
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
CFBundleURLName: 'BitTorrent Magnet URL',
CFBundleURLSchemes: [ 'magnet' ]
},
{
CFBundleTypeRole: 'Editor',
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
CFBundleURLName: 'BitTorrent Stream-Magnet URL',
CFBundleURLSchemes: [ 'stream-magnet' ]
}
]
@@ -255,9 +268,9 @@ function buildDarwin (cb) {
* - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by
* the same author as the current version.
* - So users will not a see a warning about the app coming from an "Unidentified
* Developer" when they open it for the first time (OS X Gatekeeper).
* Developer" when they open it for the first time (Mac Gatekeeper).
*
* To sign an OS X app for distribution outside the App Store, the following are
* To sign an Mac app for distribution outside the App Store, the following are
* required:
* - Xcode
* - Xcode Command Line Tools (xcode-select --install)
@@ -269,10 +282,10 @@ function buildDarwin (cb) {
verbose: true
}
console.log('OS X: Signing app...')
console.log('Mac: Signing app...')
sign(signOpts, function (err) {
if (err) return cb(err)
console.log('OS X: Signed app.')
console.log('Mac: Signed app.')
cb(null)
})
}
@@ -287,24 +300,24 @@ function buildDarwin (cb) {
function packageZip () {
// Create .zip file (used by the auto-updater)
console.log('OS X: Creating zip...')
console.log('Mac: Creating zip...')
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
zip.zipSync(inPath, outPath)
console.log('OS X: Created zip.')
console.log('Mac: Created zip.')
}
function packageDmg (cb) {
console.log('OS X: Creating dmg...')
console.log('Mac: Creating dmg...')
var appDmg = require('appdmg')
var targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
rimraf.sync(targetPath)
// Create a .dmg (OS X disk image) file, for easy user installation.
// Create a .dmg (Mac disk image) file, for easy user installation.
var dmgOpts = {
basepath: config.ROOT_PATH,
target: targetPath,
@@ -327,12 +340,12 @@ function buildDarwin (cb) {
}
var dmg = appDmg(dmgOpts)
dmg.on('error', cb)
dmg.once('error', cb)
dmg.on('progress', function (info) {
if (info.type === 'step-begin') console.log(info.title + '...')
})
dmg.on('finish', function (info) {
console.log('OS X: Created dmg.')
dmg.once('finish', function (info) {
console.log('Mac: Created dmg.')
cb(null)
})
}
@@ -460,7 +473,7 @@ function buildLinux (cb) {
info: {
arch: destArch === 'x64' ? 'amd64' : 'i386',
targetDir: DIST_PATH,
depends: 'libc6 (>= 2.4)',
depends: 'gconf2, libgtk2.0-0, libnss3, libxss1',
scripts: {
postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'),
prerm: path.join(config.STATIC_PATH, 'linux', 'prerm')
@@ -471,6 +484,11 @@ function buildLinux (cb) {
dest: destPath,
expand: true,
cwd: filesPath
}, {
src: ['./**'],
dest: path.join('/usr', 'share'),
expand: true,
cwd: path.join(config.STATIC_PATH, 'linux', 'share')
}], function (err) {
if (err) return cb(err)
console.log(`Linux: Created ${destArch} deb.`)

View File

@@ -1,9 +1,10 @@
#!/bin/sh
set -e
npm run update-authors
git diff --exit-code
npm run package -- --sign
git push
git push --tags
npm publish
./node_modules/.bin/gh-release
npm run gh-release

View File

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

View File

@@ -2,17 +2,17 @@
# Update AUTHORS.md based on git history.
git log --reverse --format='%aN <%aE>' | perl -we '
git log --reverse --format='%aN (%aE)' | perl -we '
BEGIN {
%seen = (), @authors = ();
}
while (<>) {
next if $seen{$_};
next if /<support\@greenkeeper.io>/;
next if /<ungoldman\@gmail.com>/;
next if /<grunjol\@users.noreply.github.com>/;
next if /<dc\@DCs-MacBook.local>/;
next if /<rolandoguedes\@gmail.com>/;
next if /(support\@greenkeeper.io)/;
next if /(ungoldman\@gmail.com)/;
next if /(dc\@DCs-MacBook.local)/;
next if /(rolandoguedes\@gmail.com)/;
next if /(grunjol\@users.noreply.github.com)/;
$seen{$_} = push @authors, "- ", $_;
}
END {

View File

@@ -1 +1 @@
require('./main')
require('./build/main')

View File

@@ -1,38 +0,0 @@
module.exports = {
init
}
var electron = require('electron')
var get = require('simple-get')
var config = require('../config')
var log = require('./log')
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform
function init () {
get.concat(ANNOUNCEMENT_URL, function (err, res, data) {
if (err) return log('failed to retrieve remote message')
if (res.statusCode !== 200) return log('no remote message')
try {
data = JSON.parse(data.toString())
} catch (err) {
data = {
title: 'WebTorrent Desktop Announcement',
message: 'WebTorrent Desktop Announcement',
detail: data.toString()
}
}
electron.dialog.showMessageBox({
type: 'info',
buttons: ['OK'],
title: data.title,
message: data.message,
detail: data.detail
}, function () {})
})
}

View File

@@ -1,249 +0,0 @@
module.exports = {
init
}
var electron = require('electron')
var app = electron.app
var ipcMain = electron.ipcMain
var log = require('./log')
var menu = require('./menu')
var windows = require('./windows')
var shortcuts = require('./shortcuts')
var vlc = require('./vlc')
// has to be a number, not a boolean, and undefined throws an error
var powerSaveBlockerId = 0
// messages from the main process, to be sent once the WebTorrent process starts
var messageQueueMainToWebTorrent = []
// holds a ChildProcess while we're playing a video in VLC, null otherwise
var vlcProcess
function init () {
ipcMain.on('ipcReady', function (e) {
windows.main.show()
app.ipcReady = true
app.emit('ipcReady')
})
ipcMain.on('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window',
messageQueueMainToWebTorrent.length)
messageQueueMainToWebTorrent.forEach(function (message) {
windows.webtorrent.send(message.name, ...message.args)
log('webtorrent: sent queued %s', message.name)
})
})
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
ipcMain.on('setBounds', function (e, bounds, maximize) {
setBounds(bounds, maximize)
})
ipcMain.on('setAspectRatio', function (e, aspectRatio) {
setAspectRatio(aspectRatio)
})
ipcMain.on('setBadge', function (e, text) {
setBadge(text)
})
ipcMain.on('setProgress', function (e, progress) {
setProgress(progress)
})
ipcMain.on('toggleFullScreen', function (e, flag) {
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()
shortcuts.onPlayerOpen()
})
ipcMain.on('onPlayerClose', function () {
menu.onPlayerClose()
shortcuts.onPlayerOpen()
})
ipcMain.on('focusWindow', function (e, windowName) {
windows.focusWindow(windows[windowName])
})
ipcMain.on('downloadFinished', function (e, filePath) {
if (app.dock) {
// Bounces the Downloads stack if the filePath is inside the Downloads folder.
app.dock.downloadFinished(filePath)
}
})
ipcMain.on('checkForVLC', function (e) {
vlc.checkForVLC(function (isInstalled) {
windows.main.send('checkForVLC', isInstalled)
})
})
ipcMain.on('vlcPlay', function (e, url) {
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url]
console.log('Running vlc ' + args.join(' '))
vlc.spawn(args, function (err, proc) {
if (err) return windows.main.send('dispatch', 'vlcNotFound')
vlcProcess = proc
// If it works, close the modal after a second
var closeModalTimeout = setTimeout(() =>
windows.main.send('dispatch', 'exitModal'), 1000)
vlcProcess.on('close', function (code) {
clearTimeout(closeModalTimeout)
if (!vlcProcess) return // Killed
console.log('VLC exited with code ', code)
if (code === 0) {
windows.main.send('dispatch', 'backToList')
} else {
windows.main.send('dispatch', 'vlcNotFound')
}
vlcProcess = null
})
vlcProcess.on('error', function (e) {
console.log('VLC error', e)
})
})
})
ipcMain.on('vlcQuit', function () {
if (!vlcProcess) return
console.log('Killing VLC, pid ' + vlcProcess.pid)
vlcProcess.kill('SIGKILL') // kill -9
vlcProcess = null
})
// Capture all events
var oldEmit = ipcMain.emit
ipcMain.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) {
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
// Send message to main window
windows.main.send(name, ...args)
log('webtorrent: got %s', name)
} else if (app.ipcReadyWebTorrent) {
// Send message to webtorrent window
windows.webtorrent.send(name, ...args)
log('webtorrent: sent %s', name)
} else {
// Queue message for webtorrent window, it hasn't finished loading yet
messageQueueMainToWebTorrent.push({
name: name,
args: args
})
log('webtorrent: queueing %s', name)
}
return
}
// Emit all other events normally
oldEmit.call(ipcMain, 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

@@ -1,553 +0,0 @@
module.exports = {
init,
onPlayerClose,
onPlayerOpen,
onToggleFullScreen,
onWindowHide,
onWindowShow,
// TODO: move these out of menu.js -- they don't belong here
showOpenSeedFiles,
showOpenTorrentAddress,
showOpenTorrentFile,
toggleFullScreen
}
var electron = require('electron')
var app = electron.app
var config = require('../config')
var log = require('./log')
var windows = require('./windows')
var appMenu
function init () {
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
electron.Menu.setApplicationMenu(appMenu)
if (app.dock) {
var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
app.dock.setMenu(dockMenu)
}
}
function toggleFullScreen (flag) {
log('toggleFullScreen %s', flag)
if (windows.main && windows.main.isVisible()) {
flag = flag != null ? flag : !windows.main.isFullScreen()
if (flag) {
// Allows the window to use the full screen in fullscreen mode (OS X).
windows.main.setAspectRatio(0)
}
windows.main.setFullScreen(flag)
}
}
// Sets whether the window should always show on top of other windows
function toggleFloatOnTop (flag) {
log('toggleFloatOnTop %s', flag)
if (windows.main) {
flag = flag != null ? flag : !windows.main.isAlwaysOnTop()
windows.main.setAlwaysOnTop(flag)
getMenuItem('Float on Top').checked = flag
}
}
function toggleDevTools () {
log('toggleDevTools')
if (windows.main) {
windows.main.toggleDevTools()
}
}
function showWebTorrentWindow () {
log('showWebTorrentWindow')
windows.webtorrent.show()
windows.webtorrent.webContents.openDevTools({ detach: true })
}
function playPause () {
if (windows.main) {
windows.main.send('dispatch', 'playPause')
}
}
function increaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', 0.1)
}
}
function decreaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', -0.1)
}
}
function openSubtitles () {
if (windows.main) {
windows.main.send('dispatch', 'openSubtitles')
}
}
function skipForward () {
if (windows.main) {
windows.main.send('dispatch', 'skip', 1)
}
}
function skipBack () {
if (windows.main) {
windows.main.send('dispatch', 'skip', -1)
}
}
function increasePlaybackRate () {
if (windows.main) {
windows.main.send('dispatch', 'changePlaybackRate', 1)
}
}
function decreasePlaybackRate () {
if (windows.main) {
windows.main.send('dispatch', 'changePlaybackRate', -1)
}
}
// Open the preferences window
function showPreferences () {
if (windows.main) {
windows.main.send('dispatch', 'preferences')
}
}
function escapeBack () {
if (windows.main) {
windows.main.send('dispatch', 'escapeBack')
}
}
function onWindowShow () {
log('onWindowShow')
getMenuItem('Full Screen').enabled = true
getMenuItem('Float on Top').enabled = true
}
function onWindowHide () {
log('onWindowHide')
getMenuItem('Full Screen').enabled = false
getMenuItem('Float on Top').enabled = false
}
function onPlayerOpen () {
log('onPlayerOpen')
getMenuItem('Play/Pause').enabled = true
getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true
getMenuItem('Add Subtitles File...').enabled = true
getMenuItem('Step Forward').enabled = true
getMenuItem('Step Backward').enabled = true
getMenuItem('Increase Speed').enabled = true
getMenuItem('Decrease Speed').enabled = true
}
function onPlayerClose () {
log('onPlayerClose')
getMenuItem('Play/Pause').enabled = false
getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false
getMenuItem('Add Subtitles File...').enabled = false
getMenuItem('Step Forward').enabled = false
getMenuItem('Step Backward').enabled = false
getMenuItem('Increase Speed').enabled = false
getMenuItem('Decrease Speed').enabled = false
}
function onToggleFullScreen (isFullScreen) {
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen()
windows.main.setMenuBarVisibility(!isFullScreen)
getMenuItem('Full Screen').checked = isFullScreen
windows.main.send('fullscreenChanged', isFullScreen)
}
function getMenuItem (label) {
for (var i = 0; i < appMenu.items.length; i++) {
var menuItem = appMenu.items[i].submenu.items.find(function (item) {
return item.label === label
})
if (menuItem) return menuItem
}
}
// Prompts the user for a file, then creates a torrent. Only allows a single file
// selection.
function showOpenSeedFile () {
electron.dialog.showOpenDialog({
title: 'Select a file for the torrent file.',
properties: [ 'openFile' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
windows.main.send('dispatch', 'showCreateTorrent', selectedPaths)
})
}
// 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
windows.main.send('dispatch', 'showCreateTorrent', selectedPaths)
})
}
// 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 = [
{
label: 'File',
submenu: [
{
label: process.platform === 'darwin'
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
},
{
type: 'separator'
},
{
label: process.platform === 'win32'
? 'Close'
: 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
},
{
label: 'Edit',
submenu: [
{
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
role: 'cut'
},
{
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
role: 'copy'
},
{
label: 'Paste Torrent Address',
accelerator: 'CmdOrCtrl+V',
role: 'paste'
},
{
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall'
},
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => showPreferences()
}
]
},
{
label: 'View',
submenu: [
{
label: 'Full Screen',
type: 'checkbox',
accelerator: process.platform === 'darwin'
? 'Ctrl+Command+F'
: 'F11',
click: () => toggleFullScreen()
},
{
label: 'Float on Top',
type: 'checkbox',
click: () => toggleFloatOnTop()
},
{
type: 'separator'
},
{
label: 'Developer',
submenu: [
{
label: 'Developer Tools',
accelerator: process.platform === 'darwin'
? 'Alt+Command+I'
: 'Ctrl+Shift+I',
click: toggleDevTools
},
{
label: 'Show WebTorrent Process',
accelerator: process.platform === 'darwin'
? 'Alt+Command+P'
: 'Ctrl+Shift+P',
click: showWebTorrentWindow
}
]
},
{
type: 'separator'
},
{
label: 'Go Back',
accelerator: 'Esc',
click: escapeBack
}
]
},
{
label: 'Playback',
submenu: [
{
label: 'Play/Pause',
accelerator: 'Space',
click: playPause,
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
enabled: false
},
{
type: 'separator'
},
{
label: 'Step Forward',
accelerator: 'CmdOrCtrl+Alt+Right',
click: skipForward,
enabled: false
},
{
label: 'Step Backward',
accelerator: 'CmdOrCtrl+Alt+Left',
click: skipBack,
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Speed',
accelerator: 'CmdOrCtrl+=',
click: increasePlaybackRate,
enabled: false
},
{
label: 'Decrease Speed',
accelerator: 'CmdOrCtrl+-',
click: decreasePlaybackRate,
enabled: false
},
{
type: 'separator'
},
{
label: 'Add Subtitles File...',
click: openSubtitles,
enabled: false
}
]
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Learn more about ' + config.APP_NAME,
click: () => electron.shell.openExternal(config.HOME_PAGE_URL)
},
{
label: 'Contribute on GitHub',
click: () => electron.shell.openExternal(config.GITHUB_URL)
},
{
type: 'separator'
},
{
label: 'Report an Issue...',
click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES)
}
]
}
]
if (process.platform === 'darwin') {
// Add WebTorrent app menu (OS X)
template.unshift({
label: config.APP_NAME,
submenu: [
{
label: 'About ' + config.APP_NAME,
role: 'about'
},
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => showPreferences()
},
{
type: 'separator'
},
{
label: 'Services',
role: 'services',
submenu: []
},
{
type: 'separator'
},
{
label: 'Hide ' + config.APP_NAME,
accelerator: 'Command+H',
role: 'hide'
},
{
label: 'Hide Others',
accelerator: 'Command+Alt+H',
role: 'hideothers'
},
{
label: 'Show All',
role: 'unhide'
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: () => app.quit()
}
]
})
// Add Window menu (OS X)
template.splice(5, 0, {
label: 'Window',
role: 'window',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
},
{
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
]
})
}
// In Linux and Windows it is not possible to open both folders and files
if (process.platform === 'linux' || process.platform === 'win32') {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: showOpenSeedFile
})
// Help menu (Windows, Linux)
template[4].submenu.push(
{
type: 'separator'
},
{
label: 'About ' + config.APP_NAME,
click: windows.createAboutWindow
}
)
}
// Add "File > Quit" menu item so Linux distros where the system tray icon is missing
// will have a way to quit the app.
if (process.platform === 'linux') {
// File menu (Linux)
template[0].submenu.push({
label: 'Quit',
click: () => app.quit()
})
}
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

@@ -1,19 +0,0 @@
module.exports = {
onPlayerClose,
onPlayerOpen
}
var electron = require('electron')
var windows = require('./windows')
function onPlayerOpen () {
// Register special "media key" for play/pause, available on some keyboards
electron.globalShortcut.register(
'MediaPlayPause',
() => windows.main.send('dispatch', 'playPause')
)
}
function onPlayerClose () {
electron.globalShortcut.unregister('MediaPlayPause')
}

View File

@@ -1,81 +0,0 @@
module.exports = {
init,
hasTray
}
var cp = require('child_process')
var path = require('path')
var electron = require('electron')
var app = electron.app
var windows = require('./windows')
var trayIcon
function init () {
// OS X has no tray icon
if (process.platform === 'darwin') return
// On Linux, asynchronously check for libappindicator1
if (process.platform === 'linux') {
checkLinuxTraySupport(function (supportsTray) {
if (supportsTray) createTrayIcon()
})
}
// Windows always supports minimize-to-tray
if (process.platform === 'win32') createTrayIcon()
}
function hasTray () {
return !!trayIcon
}
function createTrayIcon () {
trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
// 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()
windows.main.on('show', updateTrayMenu)
windows.main.on('hide', updateTrayMenu)
}
function checkLinuxTraySupport (cb) {
// 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
// always have it. If it was installed from the zip file, we might not.
cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) {
if (err) return cb(false)
// Unfortunately there's no cleaner way, as far as I can tell, to check
// whether a debian package is installed:
cb(stdout.endsWith('\tinstall\n'))
})
}
function updateTrayMenu () {
var showHideMenuItem
if (windows.main.isVisible()) {
showHideMenuItem = { label: 'Hide to tray', click: hideApp }
} else {
showHideMenuItem = { label: 'Show', click: showApp }
}
var contextMenu = electron.Menu.buildFromTemplate([
showHideMenuItem,
{ label: 'Quit', click: () => app.quit() }
])
trayIcon.setContextMenu(contextMenu)
}
function showApp () {
windows.main.show()
}
function hideApp () {
windows.main.hide()
windows.main.send('dispatch', 'backToList')
}

View File

@@ -1,22 +0,0 @@
module.exports = {
checkForVLC,
spawn
}
var cp = require('child_process')
var vlcCommand = require('vlc-command')
// Finds if VLC is installed on Mac, Windows, or Linux.
// Calls back with true or false: whether VLC was detected
function checkForVLC (cb) {
vlcCommand((err) => cb(!err))
}
// Spawns VLC with child_process.spawn() to return a ChildProcess object
// Calls back with (err, childProcess)
function spawn (args, cb) {
vlcCommand(function (err, vlcPath) {
if (err) return cb(err)
cb(null, cp.spawn(vlcPath, args))
})
}

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
}

View File

@@ -1,10 +1,10 @@
{
"name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.6.1",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.13.1",
"author": {
"name": "WebTorrent, LLC",
"email": "feross@feross.org",
"email": "feross@webtorrent.io",
"url": "https://webtorrent.io"
},
"bin": {
@@ -14,35 +14,45 @@
"url": "https://github.com/feross/webtorrent-desktop/issues"
},
"dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js",
"application-config": "^0.2.1",
"airplayer": "^2.0.0",
"application-config": "^1.0.0",
"bitfield": "^1.0.2",
"chromecasts": "^1.8.0",
"create-torrent": "^3.24.5",
"deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0",
"drag-drop": "^2.11.0",
"electron-prebuilt": "1.1.1",
"fs-extra": "^0.27.0",
"hyperx": "^2.0.2",
"drag-drop": "^2.12.1",
"electron": "1.3.3",
"fs-extra": "^0.30.0",
"iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1",
"main-loop": "^3.2.0",
"location-history": "^1.0.0",
"material-ui": "^0.15.4",
"musicmetadata": "^2.0.2",
"network-address": "^1.1.0",
"parse-torrent": "^5.7.3",
"prettier-bytes": "^1.0.1",
"react": "^15.2.1",
"react-dom": "^15.2.1",
"react-tap-event-plugin": "^1.0.0",
"run-parallel": "^1.1.6",
"semver": "^5.1.0",
"simple-concat": "^1.0.0",
"simple-get": "^2.0.0",
"srt-to-vtt": "^1.1.1",
"virtual-dom": "^2.1.1",
"vlc-command": "^1.0.1",
"webtorrent": "0.x",
"winreg": "^1.2.0",
"zero-fill": "^2.2.3"
},
"devDependencies": {
"babel-cli": "^6.11.4",
"babel-plugin-syntax-jsx": "^6.13.0",
"babel-plugin-transform-es2015-destructuring": "^6.9.0",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-plugin-transform-react-jsx": "^6.8.0",
"cross-zip": "^2.0.1",
"depcheck": "^0.6.4",
"electron-osx-sign": "^0.3.0",
"electron-packager": "^7.0.0",
"electron-winstaller": "^2.3.0",
@@ -50,11 +60,15 @@
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"nobin-debian-installer": "^0.0.10",
"nodemon": "^1.10.2",
"open": "0.0.5",
"plist": "^1.2.0",
"plist": "^2.0.1",
"rimraf": "^2.5.2",
"run-series": "^1.1.4",
"standard": "^7.0.0"
"standard": "*"
},
"engines": {
"node": ">=4.0.0"
},
"homepage": "https://webtorrent.io",
"keywords": [
@@ -63,8 +77,8 @@
"electron-app",
"hybrid webtorrent client",
"mad science",
"torrent client",
"torrent",
"torrent client",
"webtorrent"
],
"license": "MIT",
@@ -78,11 +92,15 @@
"url": "git://github.com/feross/webtorrent-desktop.git"
},
"scripts": {
"build": "babel --quiet src --out-dir build",
"clean": "node ./bin/clean.js",
"open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js",
"start": "electron .",
"test": "standard && node ./bin/check-deps.js",
"update-authors": "./bin/update-authors.sh"
"prepublish": "npm run build",
"start": "npm run build && electron .",
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist",
"gh-release": "gh-release",
"update-authors": "./bin/update-authors.sh",
"watch": "nodemon --exec 'npm run start' --ext js,pug,css"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
module.exports = {
setDispatch,
dispatch,
dispatcher
}
// Memoize most of our event handlers, which are functions in the form
// () => dispatch(<args>)
// ... this prevents virtual-dom from updating every listener on every update()
var _dispatchers = {}
var _dispatch = () => {}
function setDispatch (dispatch) {
_dispatch = dispatch
}
// Get a _memoized event handler that calls dispatch()
// All args must be JSON-able
function dispatcher (...args) {
var json = JSON.stringify(args)
var handler = _dispatchers[json]
if (!handler) {
handler = _dispatchers[json] = (e) => {
// Don't click on whatever is below the button
e.stopPropagation()
// Don't regisiter clicks on disabled buttons
if (e.currentTarget.classList.contains('disabled')) return
_dispatch.apply(null, args)
}
}
return handler
}
function dispatch (...args) {
_dispatch.apply(null, args)
}

View File

@@ -1,126 +0,0 @@
module.exports = LocationHistory
function LocationHistory () {
if (!new.target) return new LocationHistory()
this._history = []
this._forward = []
this._pending = false
}
LocationHistory.prototype.url = function () {
return this.current() && this.current().url
}
LocationHistory.prototype.current = function () {
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 () {
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 () {
return this._forward.length > 0
}
LocationHistory.prototype.clearForward = function (url) {
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.backToFirst = function (cb) {
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

@@ -1,44 +0,0 @@
module.exports = {
isPlayable,
isVideo,
isAudio,
isPlayableTorrent
}
var path = require('path')
/**
* Determines whether a file in a torrent is audio/video we can play
*/
function isPlayable (file) {
return isVideo(file) || isAudio(file)
}
function isVideo (file) {
var ext = path.extname(file.name).toLowerCase()
return [
'.avi',
'.m4v',
'.mkv',
'.mov',
'.mp4',
'.mpg',
'.ogv',
'.webm'
].includes(ext)
}
function isAudio (file) {
var ext = path.extname(file.name).toLowerCase()
return [
'.aac',
'.ac3',
'.mp3',
'.ogg',
'.wav'
].includes(ext)
}
function isPlayableTorrent (torrentSummary) {
return torrentSummary.files && torrentSummary.files.some(isPlayable)
}

View File

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

View File

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

View File

@@ -1,83 +0,0 @@
module.exports = App
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var Header = require('./header')
var Views = {
'home': require('./torrent-list'),
'player': require('./player'),
'create-torrent': require('./create-torrent-page'),
'preferences': require('./preferences')
}
var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal'),
'update-available-modal': require('./update-available-modal'),
'unsupported-media-modal': require('./unsupported-media-modal')
}
function App (state) {
// 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
var hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local' &&
state.playing.playbackRate === 1
var cls = [
'view-' + state.location.url(), /* e.g. view-home, view-player */
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
]
if (state.window.isFullScreen) cls.push('is-fullscreen')
if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls')
return hx`
<div class='app ${cls.join(' ')}'>
${Header(state)}
${getErrorPopover(state)}
<div class='content'>${getView(state)}</div>
${getModal(state)}
</div>
`
}
function getErrorPopover (state) {
var now = new Date().getTime()
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
var hasErrors = recentErrors.length > 0
var errorElems = recentErrors.map(function (error) {
return hx`<div class='error'>${error.message}</div>`
})
return hx`
<div class='error-popover ${hasErrors ? 'visible' : 'hidden'}'>
<div class='title'>Error</div>
${errorElems}
</div>
`
}
function getModal (state) {
if (!state.modal) return
var contents = Modals[state.modal.id](state)
return hx`
<div class='modal'>
<div class='modal-background'></div>
<div class='modal-content'>
${contents}
</div>
</div>
`
}
function getView (state) {
var url = state.location.url()
return Views[url](state)
}

View File

@@ -1,162 +0,0 @@
module.exports = CreateTorrentPage
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var createTorrent = require('create-torrent')
var path = require('path')
var prettyBytes = require('prettier-bytes')
var {dispatch, dispatcher} = require('../lib/dispatcher')
function CreateTorrentPage (state) {
var info = state.location.current()
// Preprocess: exclude .DS_Store and other dotfiles
var files = info.files
.filter((f) => !f.name.startsWith('.'))
.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
var pathPrefix = info.folderPath
if (!pathPrefix) {
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
pathPrefix = path.dirname(pathPrefix)
}
}
// Sanity check: show the number of files and total size
var numFiles = files.length
var totalBytes = files
.map((f) => f.size)
.reduce((a, b) => a + b, 0)
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
// Then, use the name of the base folder (or sole file, for a single file torrent)
// as the default name. Show all files relative to the base folder.
var defaultName, basePath
if (files.length === 1) {
// Single file torrent: /a/b/foo.jpg -> torrent name "foo.jpg", path "/a/b"
defaultName = files[0].name
basePath = pathPrefix
} else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a"
defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file) {
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
return hx`<div>${relativePath}</div>`
})
if (files.length > maxFileElems) {
fileElems.push(hx`<div>+ ${maxFileElems - files.length} more</div>`)
}
var trackers = createTorrent.announceList.join('\n')
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
return hx`
<div class='create-torrent-page'>
<h2>Create torrent ${defaultName}</h2>
<p class="torrent-info">
${torrentInfo}
</p>
<p class='torrent-attribute'>
<label>Path:</label>
<div class='torrent-attribute'>${pathPrefix}</div>
</p>
<div class='expand-collapse ${collapsedClass}' onclick=${handleToggleShowAdvanced}>
${info.showAdvanced ? 'Basic' : 'Advanced'}
</div>
<div class="create-torrent-advanced ${collapsedClass}">
<p class='torrent-attribute'>
<label>Comment:</label>
<textarea class='torrent-attribute torrent-comment'></textarea>
</p>
<p class='torrent-attribute'>
<label>Trackers:</label>
<textarea class='torrent-attribute torrent-trackers'>${trackers}</textarea>
</p>
<p class='torrent-attribute'>
<label>Private:</label>
<input type='checkbox' class='torrent-is-private' value='torrent-is-private'>
</p>
<p class='torrent-attribute'>
<label>Files:</label>
<div>${fileElems}</div>
</p>
</div>
<p class="float-right">
<button class='button-flat light' onclick=${handleCancel}>Cancel</button>
<button class='button-raised' onclick=${handleOK}>Create Torrent</button>
</p>
</div>
`
function handleOK () {
var announceList = document.querySelector('.torrent-trackers').value
.split('\n')
.map((s) => s.trim())
.filter((s) => s !== '')
var isPrivate = document.querySelector('.torrent-is-private').checked
var comment = document.querySelector('.torrent-comment').value.trim()
var options = {
// We can't let the user choose their own name if we want WebTorrent
// to use the files in place rather than creating a new folder.
// If we ever want to add support for that:
// name: document.querySelector('.torrent-name').value
name: defaultName,
path: basePath,
files: files,
announce: announceList,
private: isPrivate,
comment: comment
}
dispatch('createTorrent', options)
}
function handleCancel () {
dispatch('back')
}
function handleToggleShowAdvanced () {
// TODO: what's the clean way to handle this?
// Should every button on every screen have its own dispatch()?
info.showAdvanced = !info.showAdvanced
dispatch('update')
}
}
function CreateTorrentErrorPage () {
return hx`
<div class='create-torrent-page'>
<h2>Create torrent</h2>
<p class="torrent-info">
<p>
Sorry, you must select at least one file that is not a hidden file.
</p>
<p>
Hidden files, starting with a . character, are not included.
</p>
</p>
<p class="float-right">
<button class='button-flat light' onclick=${dispatcher('back')}>
Cancel
</button>
</p>
</div>
`
}
// Finds the longest common prefix
function findCommonPrefix (a, b) {
for (var i = 0; i < a.length && i < b.length; i++) {
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
}
if (i === a.length) return a
if (i === b.length) return b
return a.substring(0, i)
}

View File

@@ -1,51 +0,0 @@
module.exports = Header
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatcher} = require('../lib/dispatcher')
function Header (state) {
return hx`
<div class='header'>
${getTitle()}
<div class='nav left float-left'>
<i.icon.back
class=${state.location.hasBack() ? '' : 'disabled'}
title='Back'
onclick=${dispatcher('back')}>
chevron_left
</i>
<i.icon.forward
class=${state.location.hasForward() ? '' : 'disabled'}
title='Forward'
onclick=${dispatcher('forward')}>
chevron_right
</i>
</div>
<div class='nav right float-right'>
${getAddButton()}
</div>
</div>
`
function getTitle () {
if (process.platform === 'darwin') {
return hx`<div class='title ellipsis'>${state.window.title}</div>`
}
}
function getAddButton () {
if (state.location.url() === 'home') {
return hx`
<i
class='icon add'
title='Add torrent'
onclick=${dispatcher('showOpenTorrentFile')}>
add
</i>
`
}
}
}

View File

@@ -1,36 +0,0 @@
module.exports = OpenTorrentAddressModal
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatch} = require('../lib/dispatcher')
function OpenTorrentAddressModal (state) {
return hx`
<div class='open-torrent-address-modal'>
<p><label>Enter torrent address or magnet link</label></p>
<p>
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
</p>
<p class='float-right'>
<button class='button button-flat' onclick=${handleCancel}>CANCEL</button>
<button class='button button-raised' onclick=${handleOK}>OK</button>
</p>
<script>document.querySelector('#add-torrent-url').focus()</script>
</div>
`
}
function handleKeyPress (e) {
if (e.which === 13) handleOK() /* hit Enter to submit */
}
function handleOK () {
dispatch('exitModal')
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
}
function handleCancel () {
dispatch('exitModal')
}

View File

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

View File

@@ -1,298 +0,0 @@
module.exports = TorrentList
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var prettyBytes = require('prettier-bytes')
var TorrentSummary = require('../lib/torrent-summary')
var TorrentPlayer = require('../lib/torrent-player')
var {dispatcher} = require('../lib/dispatcher')
function TorrentList (state) {
var torrentRows = state.saved.torrents.map(
(torrentSummary) => renderTorrent(torrentSummary))
return hx`
<div class='torrent-list'>
${torrentRows}
<div class='torrent-placeholder'>
<span class='ellipsis'>Drop a torrent file here or paste a magnet link</span>
</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) {
// Get ephemeral data (like progress %) directly from the WebTorrent handle
var infoHash = torrentSummary.infoHash
var isSelected = infoHash && state.selectedInfoHash === infoHash
// Background image: show some nice visuals, like a frame from the movie, if possible
var style = {}
if (torrentSummary.posterFileName) {
var gradient = isSelected
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
style.backgroundImage = gradient + `, url('${posterPath}')`
}
// Foreground: name of the torrent, basic info like size, play button,
// cast buttons if available, and delete
var classes = ['torrent']
// playStatus turns the play button into a loading spinner or error icon
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled')
classes = classes.join(' ')
return hx`
<div style=${style} class=${classes}
oncontextmenu=${infoHash && dispatcher('openTorrentContextMenu', infoHash)}
onclick=${infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
${renderTorrentMetadata(torrentSummary)}
${infoHash ? renderTorrentButtons(torrentSummary) : ''}
${isSelected ? renderTorrentDetails(torrentSummary) : ''}
</div>
`
}
// Show name, download status, % complete
function renderTorrentMetadata (torrentSummary) {
var name = torrentSummary.name || 'Loading torrent...'
var elements = [hx`
<div class='name ellipsis'>${name}</div>
`]
// If it's downloading/seeding then show progress info
var prog = torrentSummary.progress
if (torrentSummary.status !== 'paused' && prog) {
elements.push(hx`
<div class='ellipsis'>
${renderPercentProgress()}
${renderTotalProgress()}
${renderPeers()}
${renderDownloadSpeed()}
${renderUploadSpeed()}
</div>
`)
}
return hx`<div class='metadata'>${elements}</div>`
function renderPercentProgress () {
var progress = Math.floor(100 * prog.progress)
return hx`<span>${progress}%</span>`
}
function renderTotalProgress () {
var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0)
if (downloaded === total) {
return hx`<span>${downloaded}</span>`
} else {
return hx`<span>${downloaded} / ${total}</span>`
}
}
function renderPeers () {
if (prog.numPeers === 0) return
var count = prog.numPeers === 1 ? 'peer' : 'peers'
return hx`<span>${prog.numPeers} ${count}</span>`
}
function renderDownloadSpeed () {
if (prog.downloadSpeed === 0) return
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>`
}
function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>`
}
}
// Download button toggles between torrenting (DL/seed) and paused
// Play button starts streaming the torrent immediately, unpausing if needed
function renderTorrentButtons (torrentSummary) {
var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else {
playIcon = 'play_arrow'
playTooltip = 'Start streaming'
}
var downloadIcon, downloadTooltip
if (torrentSummary.status === 'seeding') {
downloadIcon = 'file_upload'
downloadTooltip = 'Seeding. Click to stop.'
} else if (torrentSummary.status === 'downloading') {
downloadIcon = 'file_download'
downloadTooltip = 'Torrenting. Click to stop.'
} else {
downloadIcon = 'file_download'
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
var playButton
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
playButton = hx`
<i.button-round.icon.play
title=${playTooltip}
class=${playClass}
onclick=${dispatcher('play', infoHash)}>
${playIcon}
</i>
`
}
return hx`
<div class='buttons'>
${positionElem}
${playButton}
<i.button-round.icon.download
class=${torrentSummary.status}
title=${downloadTooltip}
onclick=${dispatcher('toggleTorrent', infoHash)}>
${downloadIcon}
</i>
<i
class='icon delete'
title='Remove torrent'
onclick=${dispatcher('deleteTorrent', infoHash)}>
close
</i>
</div>
`
}
// Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrentSummary) {
var filesElement
if (!torrentSummary.files) {
// We don't know what files this torrent contains
var message = torrentSummary.status === 'paused'
? 'Failed to load torrent info. Click the download button to try again...'
: 'Downloading torrent info...'
filesElement = hx`<div class='files warning'>${message}</div>`
} else {
// We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files
.sort(function (a, b) {
if (a.name < b.name) return -1
if (b.name < a.name) return 1
return 0
})
.map((file, index) => renderFileRow(torrentSummary, file, index))
filesElement = hx`
<div class='files'>
<table>
${fileRows}
</table>
</div>
`
}
return hx`
<div class='torrent-details'>
${filesElement}
</div>
`
}
// Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded
var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
var isDone = false // Are we finished torrenting it?
var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files) {
var fileProg = torrentSummary.progress.files[index]
isDone = fileProg.numPiecesPresent === fileProg.numPieces
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
}
// 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 icon
var handleClick
if (isPlayable) {
icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index)
} else {
icon = 'description' /* file icon, opens in OS default app */
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`
<tr onclick=${handleClick}>
<td class='col-icon ${rowClass}'>
${positionElem}
<i class='icon'>${icon}</i>
</td>
<td class='col-name ${rowClass}'>
${file.name}
</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>
`
}
}
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,42 +0,0 @@
module.exports = UnsupportedMediaModal
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var electron = require('electron')
var {dispatch, dispatcher} = require('../lib/dispatcher')
function UnsupportedMediaModal (state) {
var err = state.modal.error
var message = (err && err.getMessage)
? err.getMessage()
: err
var actionButton = state.modal.vlcInstalled
? hx`<button class="button-raised" onclick=${onPlay}>Play in VLC</button>`
: hx`<button class="button-raised" onclick=${onInstall}>Install VLC</button>`
var vlcMessage = state.modal.vlcNotFound
? 'Couldn\'t run VLC. Please make sure it\'s installed.'
: ''
return hx`
<div>
<p><strong>Sorry, we can't play that file.</strong></p>
<p>${message}</p>
<p class='float-right'>
<button class="button-flat" onclick=${dispatcher('backToList')}>Cancel</button>
${actionButton}
</p>
<p class='error-text'>${vlcMessage}</p>
</div>
`
function onInstall () {
electron.shell.openExternal('http://www.videolan.org/vlc/')
state.modal.vlcInstalled = true // Assume they'll install it successfully
}
function onPlay () {
dispatch('vlcPlay')
}
}

View File

@@ -1,32 +0,0 @@
module.exports = UpdateAvailableModal
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var electron = require('electron')
var {dispatch} = require('../lib/dispatcher')
function UpdateAvailableModal (state) {
return hx`
<div class='update-available-modal'>
<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>
<button class='primary' onclick=${handleOK}>Show Download Page</button>
<button class='cancel' onclick=${handleCancel}>Skip This Release</button>
</p>
</div>
`
function handleOK () {
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
dispatch('exitModal')
}
function handleCancel () {
dispatch('skipVersion', state.modal.version)
dispatch('exitModal')
}
}

8
src/.babelrc Normal file
View File

@@ -0,0 +1,8 @@
{
"plugins": [
"syntax-jsx",
"transform-es2015-destructuring",
"transform-object-rest-spread",
"transform-react-jsx"
]
}

View File

@@ -4,31 +4,58 @@ var path = require('path')
var APP_NAME = 'WebTorrent'
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')
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_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
APP_NAME: APP_NAME,
APP_TEAM: APP_TEAM,
APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
CONFIG_PATH: getConfigPath(),
CONFIG_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 */,
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
@@ -38,12 +65,14 @@ module.exports = {
IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(),
ROOT_PATH: __dirname,
STATIC_PATH: path.join(__dirname, 'static'),
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: path.join(__dirname, '..'),
STATIC_PATH: path.join(__dirname, '..', 'static'),
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, 'renderer', 'webtorrent.html'),
WINDOW_ABOUT: 'file://' + path.join(__dirname, '..', 'static', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents
WINDOW_MIN_WIDTH: 425
@@ -57,6 +86,22 @@ function getConfigPath () {
}
}
function getDefaultDownloadPath () {
if (!process || !process.type) {
return ''
}
if (isPortable()) {
return path.join(getConfigPath(), 'Downloads')
}
var electron = require('electron')
return process.type === 'renderer'
? electron.remote.app.getPath('downloads')
: electron.app.getPath('downloads')
}
function isPortable () {
try {
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)

57
src/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 available')
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 () {}

121
src/main/dialog.js Normal file
View File

@@ -0,0 +1,121 @@
module.exports = {
openSeedFile,
openSeedDirectory,
openTorrentFile,
openTorrentAddress,
openFiles
}
var electron = require('electron')
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 Mac, so the window title is used instead.
*/
function setTitle (title) {
if (process.platform === 'darwin') {
windows.main.dispatch('setTitle', title)
}
}
function resetTitle () {
windows.main.dispatch('resetTitle')
}

58
src/main/dock.js Normal file
View File

@@ -0,0 +1,58 @@
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. (Mac)
*/
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. (Mac)
*/
function downloadFinished (path) {
if (!app.dock) return
log(`downloadFinished: ${path}`)
app.dock.downloadFinished(path)
}
/**
* Display a counter badge for the app. (Mac, Linux)
*/
function setBadge (count) {
log(`setBadge: ${count}`)
app.setBadgeCount(Number(count))
}
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

@@ -0,0 +1,65 @@
module.exports = {
spawn,
kill,
checkInstall
}
var cp = require('child_process')
var vlcCommand = require('vlc-command')
var log = require('./log')
var windows = require('./windows')
// holds a ChildProcess while we're playing a video in an external player, null otherwise
var proc
function checkInstall (path, cb) {
// check for VLC if external player has not been specified by the user
// otherwise assume the player is installed
if (path == null) return vlcCommand((err) => cb(!err))
process.nextTick(() => cb(true))
}
function spawn (path, url, title) {
if (path != null) return spawnExternal(path, [url])
// Try to find and use VLC if external player is not specified
vlcCommand(function (err, vlcPath) {
if (err) return windows.main.dispatch('externalPlayerNotFound')
var args = ['--play-and-exit', '--video-on-top', '--quiet', `--meta-title=${JSON.stringify(title)}`, url]
spawnExternal(vlcPath, args)
})
}
function kill () {
if (!proc) return
log('Killing external player, pid ' + proc.pid)
proc.kill('SIGKILL') // kill -9
proc = null
}
function spawnExternal (path, args) {
log('Running external media player:', path + ' ' + args.join(' '))
proc = cp.spawn(path, args, {stdio: 'ignore'})
// If it works, close the modal after a second
var closeModalTimeout = setTimeout(() =>
windows.main.dispatch('exitModal'), 1000)
proc.on('close', function (code) {
clearTimeout(closeModalTimeout)
if (!proc) return // Killed
log('External player exited with code ', code)
if (code === 0) {
windows.main.dispatch('backToList')
} else {
windows.main.dispatch('externalPlayerNotFound')
}
proc = null
})
proc.on('error', function (e) {
log('External player error', e)
})
}

View File

@@ -3,9 +3,8 @@ module.exports = {
uninstall
}
var path = require('path')
var config = require('../config')
var path = require('path')
function install () {
if (process.platform === 'darwin') {
@@ -35,11 +34,12 @@ function installDarwin () {
var electron = require('electron')
var app = electron.app
// On OS X, only protocols that are listed in Info.plist can be set as the default
// handler at runtime.
// On Mac, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime.
app.setAsDefaultProtocolClient('magnet')
app.setAsDefaultProtocolClient('stream-magnet')
// File handlers are registered in the Info.plist.
// File handlers are defined in `Info.plist`.
}
function uninstallDarwin () {}
@@ -55,10 +55,28 @@ function installWin32 () {
var log = require('./log')
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)
var iconPath = path.join(
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
)
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:
@@ -190,6 +208,7 @@ function uninstallWin32 () {
var Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
function unregisterProtocolHandlerWin32 (protocol, command) {
@@ -261,11 +280,16 @@ function installLinux () {
var config = require('../config')
var log = require('./log')
// Do not install in user dir if running on system
if (/^\/opt/.test(process.execPath)) return
installDesktopFile()
installIconFile()
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)
}

View File

@@ -8,26 +8,34 @@ var ipcMain = electron.ipcMain
var announcement = require('./announcement')
var config = require('../config')
var crashReporter = require('../crash-reporter')
var handlers = require('./handlers')
var dialog = require('./dialog')
var dock = require('./dock')
var ipc = require('./ipc')
var log = require('./log')
var menu = require('./menu')
var squirrelWin32 = require('./squirrel-win32')
var tray = require('./tray')
var updater = require('./updater')
var userTasks = require('./user-tasks')
var windows = require('./windows')
var shouldQuit = false
var argv = sliceArgv(process.argv)
if (config.IS_PRODUCTION) {
// When Electron is running in production mode (packaged app), then run React
// in production mode too.
process.env.NODE_ENV = 'production'
}
if (process.platform === 'win32') {
shouldQuit = squirrelWin32.handleEvent(argv[0])
argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1)
argv = argv.filter((arg) => !arg.includes('--squirrel'))
}
if (!shouldQuit) {
// Prevent multiple instances of app from running at same time. New instances signal
// this instance and quit.
// Prevent multiple instances of app from running at same time. New instances
// signal this instance and quit.
shouldQuit = app.makeSingleInstance(onAppOpen)
if (shouldQuit) {
app.quit()
@@ -53,22 +61,29 @@ function init () {
ipc.init()
app.on('will-finish-launching', function () {
app.once('will-finish-launching', function () {
crashReporter.init()
})
app.on('ready', function () {
isReady = true
windows.createMainWindow()
windows.createWebTorrentHiddenWindow()
windows.main.init()
windows.webtorrent.init()
menu.init()
// To keep app startup fast, some code is delayed.
setTimeout(delayedInit, config.DELAYED_INIT)
// Report uncaught exceptions
process.on('uncaughtException', (err) => {
console.error(err)
var error = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', error)
})
})
app.on('ipcReady', function () {
app.once('ipcReady', function () {
log('Command line args:', argv)
processArgv(argv)
console.timeEnd('init')
@@ -79,34 +94,37 @@ function init () {
app.isQuitting = true
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())
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 () {
if (isReady) windows.createMainWindow()
if (isReady) windows.main.show()
})
}
function delayedInit () {
announcement.init()
dock.init()
tray.init()
handlers.install()
updater.init()
userTasks.init()
}
function onOpen (e, torrentId) {
e.preventDefault()
if (app.ipcReady) {
windows.main.send('dispatch', 'onOpen', torrentId)
// Magnet links opened from Chrome won't focus the app without a setTimeout. The
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
// Magnet links opened from Chrome won't focus the app without a setTimeout.
// The confirmation dialog Chrome shows causes Chrome to steal back the focus.
// Electron issue: https://github.com/atom/electron/issues/4338
setTimeout(function () {
windows.focusWindow(windows.main)
}, 100)
setTimeout(() => windows.main.show(), 100)
processArgv([ torrentId ])
} else {
argv.push(torrentId)
}
@@ -117,7 +135,7 @@ function onAppOpen (newArgv) {
if (app.ipcReady) {
log('Second app instance opened, but was prevented:', newArgv)
windows.focusWindow(windows.main)
windows.main.show()
processArgv(newArgv)
} else {
@@ -130,27 +148,25 @@ function sliceArgv (argv) {
}
function processArgv (argv) {
var pathsToOpen = []
var torrentIds = []
argv.forEach(function (arg) {
if (arg === '-n') {
menu.showOpenSeedFiles()
dialog.openSeedDirectory()
} else if (arg === '-o') {
menu.showOpenTorrentFile()
dialog.openTorrentFile()
} else if (arg === '-u') {
menu.showOpenTorrentAddress()
dialog.openTorrentAddress()
} else if (arg.startsWith('-psn')) {
// Ignore OS X launchd "process serial number" argument
// More: https://github.com/feross/webtorrent-desktop/issues/214
} else {
pathsToOpen.push(arg)
// Ignore Mac launchd "process serial number" argument
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
} else if (arg !== '.') {
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
// development copy of WebTorrent is started while a production version is
// running.
torrentIds.push(arg)
}
})
if (pathsToOpen.length > 0) openFilePaths(pathsToOpen)
}
// Send files to the renderer process
// Opening files means either adding torrents, creating and seeding a torrent
// from files, or adding subtitles
function openFilePaths (paths) {
windows.main.send('dispatch', 'onOpen', paths)
if (torrentIds.length > 0) {
windows.main.dispatch('onOpen', torrentIds)
}
}

154
src/main/ipc.js Normal file
View File

@@ -0,0 +1,154 @@
module.exports = {
init
}
var electron = require('electron')
var app = electron.app
var dialog = require('./dialog')
var dock = require('./dock')
var handlers = require('./handlers')
var log = require('./log')
var menu = require('./menu')
var powerSaveBlocker = require('./power-save-blocker')
var shell = require('./shell')
var shortcuts = require('./shortcuts')
var externalPlayer = require('./external-player')
var windows = require('./windows')
var thumbar = require('./thumbar')
// Messages from the main process, to be sent once the WebTorrent process starts
var messageQueueMainToWebTorrent = []
function init () {
var ipc = electron.ipcMain
ipc.once('ipcReady', function (e) {
app.ipcReady = true
app.emit('ipcReady')
})
ipc.once('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window',
messageQueueMainToWebTorrent.length)
messageQueueMainToWebTorrent.forEach(function (message) {
windows.webtorrent.send(message.name, ...message.args)
log('webtorrent: sent queued %s', message.name)
})
})
/**
* Dialog
*/
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
ipc.on('openFiles', () => dialog.openFiles())
/**
* Dock
*/
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
/**
* Events
*/
ipc.on('onPlayerOpen', function () {
menu.setPlayerOpen(true)
powerSaveBlocker.enable()
shortcuts.enable()
thumbar.enable()
})
ipc.on('onPlayerClose', function () {
menu.setPlayerOpen(false)
powerSaveBlocker.disable()
shortcuts.disable()
thumbar.disable()
})
ipc.on('onPlayerPlay', function () {
powerSaveBlocker.enable()
thumbar.onPlayerPlay()
})
ipc.on('onPlayerPause', function () {
powerSaveBlocker.disable()
thumbar.onPlayerPause()
})
/**
* 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))
/**
* File handlers
*/
ipc.on('setDefaultFileHandler', (e, flag) => {
if (flag) handlers.install()
else handlers.uninstall()
})
/**
* 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))
ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
/**
* External Media Player
*/
ipc.on('checkForExternalPlayer', function (e, path) {
externalPlayer.checkInstall(path, function (isInstalled) {
windows.main.send('checkForExternalPlayer', isInstalled)
})
})
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
// Capture all events
var oldEmit = ipc.emit
ipc.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) {
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
// Send message to main window
windows.main.send(name, ...args)
log('webtorrent: got %s', name)
} else if (app.ipcReadyWebTorrent) {
// Send message to webtorrent window
windows.webtorrent.send(name, ...args)
log('webtorrent: sent %s', name)
} else {
// Queue message for webtorrent window, it hasn't finished loading yet
messageQueueMainToWebTorrent.push({
name: name,
args: args
})
log('webtorrent: queueing %s', name)
}
return
}
// Emit all other events normally
oldEmit.call(ipc, name, e, ...args)
}
}

View File

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

372
src/main/menu.js Normal file
View File

@@ -0,0 +1,372 @@
module.exports = {
init,
setPlayerOpen,
setWindowFocus,
setAllowNav,
onToggleAlwaysOnTop,
onToggleFullScreen
}
var electron = require('electron')
var app = electron.app
var config = require('../config')
var dialog = require('./dialog')
var shell = require('./shell')
var windows = require('./windows')
var menu
function init () {
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(menu)
}
function setPlayerOpen (flag) {
getMenuItem('Play/Pause').enabled = flag
getMenuItem('Increase Volume').enabled = flag
getMenuItem('Decrease Volume').enabled = flag
getMenuItem('Step Forward').enabled = flag
getMenuItem('Step Backward').enabled = flag
getMenuItem('Increase Speed').enabled = flag
getMenuItem('Decrease Speed').enabled = flag
getMenuItem('Add Subtitles File...').enabled = flag
}
function setWindowFocus (flag) {
getMenuItem('Full Screen').enabled = flag
getMenuItem('Float on Top').enabled = flag
}
// Disallow opening more screens on top of the current one.
function setAllowNav (flag) {
getMenuItem('Preferences').enabled = flag
if (process.platform === 'darwin') {
getMenuItem('Create New Torrent...').enabled = flag
} else {
getMenuItem('Create New Torrent from Folder...').enabled = flag
getMenuItem('Create New Torrent from File...').enabled = flag
}
}
function onToggleAlwaysOnTop (flag) {
getMenuItem('Float on Top').checked = flag
}
function onToggleFullScreen (flag) {
getMenuItem('Full Screen').checked = flag
}
function getMenuItem (label) {
for (var i = 0; i < menu.items.length; i++) {
var menuItem = menu.items[i].submenu.items.find(function (item) {
return item.label === label
})
if (menuItem) return menuItem
}
}
function getMenuTemplate () {
var template = [
{
label: 'File',
submenu: [
{
label: process.platform === 'darwin'
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
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()
},
{
type: 'separator'
},
{
role: 'close'
}
]
},
{
label: 'Edit',
submenu: [
{
role: 'undo'
},
{
role: 'redo'
},
{
type: 'separator'
},
{
role: 'cut'
},
{
role: 'copy'
},
{
label: 'Paste Torrent Address',
role: 'paste'
},
{
role: 'delete'
},
{
role: 'selectall'
}
]
},
{
label: 'View',
submenu: [
{
label: 'Full Screen',
type: 'checkbox',
accelerator: process.platform === 'darwin'
? 'Ctrl+Command+F'
: 'F11',
click: () => windows.main.toggleFullScreen()
},
{
label: 'Float on Top',
type: 'checkbox',
click: () => windows.main.toggleAlwaysOnTop()
},
{
type: 'separator'
},
{
label: 'Go Back',
accelerator: 'Esc',
click: () => windows.main.dispatch('escapeBack')
},
{
type: 'separator'
},
{
label: 'Developer',
submenu: [
{
label: 'Developer Tools',
accelerator: process.platform === 'darwin'
? 'Alt+Command+I'
: 'Ctrl+Shift+I',
click: () => windows.main.toggleDevTools()
},
{
label: 'Show WebTorrent Process',
accelerator: process.platform === 'darwin'
? 'Alt+Command+P'
: 'Ctrl+Shift+P',
click: () => windows.webtorrent.toggleDevTools()
}
]
}
]
},
{
label: 'Playback',
submenu: [
{
label: 'Play/Pause',
accelerator: 'Space',
click: () => windows.main.dispatch('playPause'),
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: () => windows.main.dispatch('changeVolume', 0.1),
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
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
}
]
},
{
label: 'Help',
role: 'help',
submenu: [
{
label: 'Learn more about ' + config.APP_NAME,
click: () => shell.openExternal(config.HOME_PAGE_URL)
},
{
label: 'Contribute on GitHub',
click: () => shell.openExternal(config.GITHUB_URL)
},
{
type: 'separator'
},
{
label: 'Report an Issue...',
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
}
]
}
]
if (process.platform === 'darwin') {
// Add WebTorrent app menu (Mac)
template.unshift({
label: config.APP_NAME,
submenu: [
{
role: 'about'
},
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => windows.main.dispatch('preferences')
},
{
type: 'separator'
},
{
role: 'services',
submenu: []
},
{
type: 'separator'
},
{
role: 'hide'
},
{
role: 'hideothers'
},
{
role: 'unhide'
},
{
type: 'separator'
},
{
role: 'quit'
}
]
})
// Add Window menu (Mac)
template.splice(5, 0, {
role: 'window',
submenu: [
{
role: 'minimize'
},
{
type: 'separator'
},
{
role: 'front'
}
]
})
}
// On Windows and Linux, open dialogs do not support selecting both files and
// folders and files, so add an extra menu item so there is one for each type.
if (process.platform === 'linux' || process.platform === 'win32') {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: () => dialog.openSeedFile()
})
// Edit menu (Windows, Linux)
template[1].submenu.push(
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => windows.main.dispatch('preferences')
})
// Help menu (Windows, Linux)
template[4].submenu.push(
{
type: 'separator'
},
{
label: 'About ' + config.APP_NAME,
click: () => windows.about.init()
}
)
}
// Add "File > Quit" menu item so Linux distros where the system tray icon is
// missing will have a way to quit the app.
if (process.platform === 'linux') {
// File menu (Linux)
template[0].submenu.push({
label: 'Quit',
click: () => app.quit()
})
}
return template
}

View File

@@ -0,0 +1,34 @@
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 () {
if (electron.powerSaveBlocker.isStarted(blockId)) {
// If a power saver block already exists, do nothing.
return
}
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)) {
// If a power saver block does not exist, do nothing.
return
}
electron.powerSaveBlocker.stop(blockId)
log(`powerSaveBlocker.disable: ${blockId}`)
}

41
src/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)
}

20
src/main/shortcuts.js Normal file
View File

@@ -0,0 +1,20 @@
module.exports = {
disable,
enable
}
var electron = require('electron')
var windows = require('./windows')
function enable () {
// Register play/pause media key, available on some keyboards.
electron.globalShortcut.register(
'MediaPlayPause',
() => windows.main.dispatch('playPause')
)
}
function disable () {
// Return the media key to the OS, so other apps can use it.
electron.globalShortcut.unregister('MediaPlayPause')
}

View File

@@ -12,8 +12,8 @@ var app = electron.app
var handlers = require('./handlers')
var exeName = path.basename(process.execPath)
var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe')
var EXE_NAME = path.basename(process.execPath)
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
function handleEvent (cmd) {
if (cmd === '--squirrel-install') {
@@ -61,15 +61,17 @@ function handleEvent (cmd) {
}
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
}
// 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) {
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) {
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) {
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) {
var homeDir = os.homedir()
if (homeDir) {
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
// Check if the desktop shortcut has been previously deleted and and keep it deleted
// if it was
// If the desktop shortcut was deleted by the user, then keep it deleted.
fs.access(desktopShortcutPath, function (err) {
var desktopShortcutExists = !err
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) {
spawnUpdate(['--removeShortcut', exeName], cb)
spawnUpdate(['--removeShortcut', EXE_NAME], cb)
}

54
src/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)
}

109
src/main/tray.js Normal file
View File

@@ -0,0 +1,109 @@
module.exports = {
hasTray,
init,
setWindowFocus
}
var electron = require('electron')
var app = electron.app
var config = require('../config')
var windows = require('./windows')
var tray
function init () {
if (process.platform === 'linux') {
initLinux()
}
if (process.platform === 'win32') {
initWin32()
}
// Mac apps generally do not have menu bar icons
}
/**
* Returns true if there a tray icon is active.
*/
function hasTray () {
return !!tray
}
function setWindowFocus (flag) {
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) {
var cp = require('child_process')
// 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
// always have it. If it was installed from the zip file, we might not.
cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) {
if (err) return cb(false)
// Unfortunately there's no cleaner way, as far as I can tell, to check
// whether a debian package is installed:
cb(stdout.endsWith('\tinstall\n'))
})
}
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 () {
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
tray.setContextMenu(contextMenu)
}
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()
}
}
}
}
function getIconPath () {
return process.platform === 'win32'
? config.APP_ICON + '.ico'
: config.APP_ICON + '.png'
}

View File

@@ -21,27 +21,27 @@ function init () {
}
}
// The Electron auto-updater does not support Linux yet, so manually check for updates and
// `show the user a modal notification.
// The Electron auto-updater does not support Linux yet, so manually check for
// updates and show the user a modal notification.
function initLinux () {
get.concat(AUTO_UPDATE_URL, onResponse)
}
function onResponse (err, res, data) {
if (err) return log(`Update error: ${err.message}`)
if (res.statusCode === 200) {
// Update available
try {
data = JSON.parse(data)
} catch (err) {
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}`)
function onResponse (err, res, data) {
if (err) return log(`Update error: ${err.message}`)
if (res.statusCode === 200) {
// Update available
try {
data = JSON.parse(data)
} catch (err) {
return log(`Update error: Invalid JSON response: ${err.message}`)
}
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}`)
}
}
@@ -63,7 +63,7 @@ function initDarwinWin32 () {
electron.autoUpdater.on(
'update-not-available',
() => log('Update not available')
() => log('No update available')
)
electron.autoUpdater.on(

43
src/main/user-tasks.js Normal file
View File

@@ -0,0 +1,43 @@
module.exports = {
init
}
var electron = require('electron')
var app = electron.app
/**
* Add a user task menu to the app icon on right-click. (Windows)
*/
function init () {
if (process.platform !== 'win32') return
app.setUserTasks(getUserTasks())
}
function getUserTasks () {
return [
{
arguments: '-n',
title: 'Create New Torrent...',
description: 'Create a new torrent'
},
{
arguments: '-o',
title: 'Open Torrent File...',
description: 'Open a .torrent file'
},
{
arguments: '-u',
title: 'Open Torrent Address...',
description: 'Open a torrent from a URL'
}
].map(getUserTasksItem)
}
function getUserTasksItem (item) {
return Object.assign(item, {
program: process.execPath,
iconPath: process.execPath,
iconIndex: 0
})
}

48
src/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'
}

View File

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

226
src/main/windows/main.js Normal file
View File

@@ -0,0 +1,226 @@
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 = 38
var TORRENT_HEIGHT = 100
function init () {
if (main.win) {
return main.win.show()
}
var win = main.win = new electron.BrowserWindow({
backgroundColor: '#282828',
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 (Mac)
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. (Mac)
*/
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))
}
// Resize the window's content area (so window border doesn't need to be taken
// into account)
if (bounds.contentBounds) {
main.win.setContentBounds(bounds, true)
} else {
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. (Mac)
main.win.setAspectRatio(0)
}
main.win.setFullScreen(flag)
}
function onWindowBlur () {
menu.setWindowFocus(false)
tray.setWindowFocus(false)
}
function onWindowFocus () {
menu.setWindowFocus(true)
tray.setWindowFocus(true)
}
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

@@ -0,0 +1,35 @@
const React = require('react')
const colors = require('material-ui/styles/colors')
class Heading extends React.Component {
static get propTypes () {
return {
level: React.PropTypes.number
}
}
static get defaultProps () {
return {
level: 1
}
}
render () {
const HeadingTag = 'h' + this.props.level
return (
<HeadingTag
style={{
color: colors.grey100,
fontSize: 20,
marginBottom: 15,
marginTop: 30
}}
>
{this.props.children}
</HeadingTag>
)
}
}
module.exports = Heading

View File

@@ -0,0 +1,93 @@
const colors = require('material-ui/styles/colors')
const electron = require('electron')
const React = require('react')
const remote = electron.remote
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
class PathSelector extends React.Component {
static get propTypes () {
return {
className: React.PropTypes.string,
dialog: React.PropTypes.object,
displayValue: React.PropTypes.string,
id: React.PropTypes.string,
onChange: React.PropTypes.func,
title: React.PropTypes.string.isRequired,
value: React.PropTypes.string
}
}
constructor (props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
handleClick () {
var opts = Object.assign({
defaultPath: this.props.value,
properties: [ 'openFile', 'openDirectory' ]
}, this.props.dialog)
remote.dialog.showOpenDialog(
remote.getCurrentWindow(),
opts,
(filenames) => {
if (!Array.isArray(filenames)) return
this.props.onChange && this.props.onChange(filenames[0])
}
)
}
render () {
const id = this.props.title.replace(' ', '-').toLowerCase()
return (
<div
className={this.props.className}
style={{
alignItems: 'center',
display: 'flex',
width: '100%'
}}
>
<div
className='label'
style={{
flex: '0 auto',
marginRight: 10,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
>
{this.props.title}:
</div>
<TextField
className='control'
disabled
id={id}
inputStyle={{
color: colors.grey50
}}
style={{
flex: '1',
fontSize: 14
}}
value={this.props.displayValue || this.props.value}
/>
<RaisedButton
className='control'
label='Change'
onClick={this.handleClick}
style={{
marginLeft: 10
}}
/>
</div>
)
}
}
module.exports = PathSelector

View File

@@ -0,0 +1,52 @@
const React = require('react')
const FlatButton = require('material-ui/FlatButton').default
class ShowMore extends React.Component {
static get propTypes () {
return {
defaultExpanded: React.PropTypes.bool,
hideLabel: React.PropTypes.string,
showLabel: React.PropTypes.string
}
}
static get defaultProps () {
return {
hideLabel: 'Hide more...',
showLabel: 'Show more...'
}
}
constructor (props) {
super(props)
this.state = {
expanded: !!this.props.defaultExpanded
}
this.handleClick = this.handleClick.bind(this)
}
handleClick () {
this.setState({
expanded: !this.state.expanded
})
}
render () {
const label = this.state.expanded
? this.props.hideLabel
: this.props.showLabel
return (
<div style={this.props.style}>
{this.state.expanded ? this.props.children : null}
<FlatButton
onClick={this.handleClick}
label={label} />
</div>
)
}
}
module.exports = ShowMore

View File

@@ -0,0 +1,26 @@
const React = require('react')
const {dispatcher} = require('../lib/dispatcher')
module.exports = class CreateTorrentErrorPage extends React.Component {
render () {
return (
<div className='create-torrent'>
<h2>Create torrent</h2>
<p className='torrent-info'>
<p>
Sorry, you must select at least one file that is not a hidden file.
</p>
<p>
Hidden files, starting with a . character, are not included.
</p>
</p>
<p className='float-right'>
<button className='button-flat light' onClick={dispatcher('cancel')}>
Cancel
</button>
</p>
</div>
)
}
}

View File

@@ -0,0 +1,52 @@
const React = require('react')
const {dispatcher} = require('../lib/dispatcher')
class Header extends React.Component {
render () {
var loc = this.props.state.location
return (
<div className='header'>
{this.getTitle()}
<div className='nav left float-left'>
<i
className={'icon back ' + (loc.hasBack() ? '' : 'disabled')}
title='Back'
onClick={dispatcher('back')}>
chevron_left
</i>
<i
className={'icon forward ' + (loc.hasForward() ? '' : 'disabled')}
title='Forward'
onClick={dispatcher('forward')}>
chevron_right
</i>
</div>
<div className='nav right float-right'>
{this.getAddButton()}
</div>
</div>
)
}
getTitle () {
if (process.platform !== 'darwin') return null
var state = this.props.state
return (<div className='title ellipsis'>{state.window.title}</div>)
}
getAddButton () {
var state = this.props.state
if (state.location.url() !== 'home') return null
return (
<i
className='icon add'
title='Add torrent'
onClick={dispatcher('openFiles')}>
add
</i>
)
}
}
module.exports = Header

View File

@@ -0,0 +1,32 @@
const React = require('react')
const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class OpenTorrentAddressModal extends React.Component {
render () {
// TODO: dcposch remove janky inline <script>
return (
<div className='open-torrent-address-modal'>
<p><label>Enter torrent address or magnet link</label></p>
<p>
<input id='add-torrent-url' type='text' onKeyPress={handleKeyPress} />
</p>
<p className='float-right'>
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
<button className='button button-raised' onClick={handleOK}>OK</button>
</p>
<script>document.querySelector('#add-torrent-url').focus()</script>
</div>
)
}
}
function handleKeyPress (e) {
if (e.which === 13) handleOK() /* hit Enter to submit */
}
function handleOK () {
dispatch('exitModal')
// TODO: dcposch use React refs instead
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
}

View File

@@ -0,0 +1,28 @@
const React = require('react')
const {dispatch, dispatcher} = require('../lib/dispatcher')
module.exports = class RemoveTorrentModal extends React.Component {
render () {
var state = this.props.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 (
<div>
<p><strong>{message}</strong></p>
<p className='float-right'>
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
<button className='button button-raised' onClick={handleRemove}>{buttonText}</button>
</p>
</div>
)
function handleRemove () {
dispatch('deleteTorrent', state.modal.infoHash, state.modal.deleteData)
dispatch('exitModal')
}
}
}

View File

@@ -0,0 +1,44 @@
const React = require('react')
const electron = require('electron')
const path = require('path')
const {dispatcher} = require('../lib/dispatcher')
module.exports = class UnsupportedMediaModal extends React.Component {
render () {
var state = this.props.state
var err = state.modal.error
var message = (err && err.getMessage)
? err.getMessage()
: err
var playerPath = state.saved.prefs.externalPlayerPath
var playerName = playerPath
? path.basename(playerPath).split('.')[0]
: 'VLC'
var actionButton = state.modal.externalPlayerInstalled
? (<button className='button-raised' onClick={dispatcher('openExternalPlayer')}>Play in {playerName}</button>)
: (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>)
var playerMessage = state.modal.externalPlayerNotFound
? 'Couldn\'t run external player. Please make sure it\'s installed.'
: ''
return (
<div>
<p><strong>Sorry, we can't play that file.</strong></p>
<p>{message}</p>
<p className='float-right'>
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
{actionButton}
</p>
<p className='error-text'>{playerMessage}</p>
</div>
)
}
onInstall () {
electron.shell.openExternal('http://www.videolan.org/vlc/')
// TODO: dcposch send a dispatch rather than modifying state directly
var state = this.props.state
state.modal.externalPlayerInstalled = true // Assume they'll install it successfully
}
}

View File

@@ -0,0 +1,30 @@
const React = require('react')
const electron = require('electron')
const {dispatch} = require('../lib/dispatcher')
module.exports = class UpdateAvailableModal extends React.Component {
render () {
var state = this.props.state
return (
<div className='update-available-modal'>
<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 className='float-right'>
<button className='button button-flat' onClick={handleSkip}>Skip This Release</button>
<button className='button button-raised' onClick={handleShow}>Show Download Page</button>
</p>
</div>
)
function handleShow () {
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
dispatch('exitModal')
}
function handleSkip () {
dispatch('skipVersion', state.modal.version)
dispatch('exitModal')
}
}
}

View File

@@ -0,0 +1,57 @@
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('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) {
state.modal = {
id: 'unsupported-media-modal',
error: error,
externalPlayerInstalled: isInstalled
}
})
}
}
mediaTimeUpdate () {
this.state.playing.lastTimeUpdate = new Date().getTime()
this.state.playing.isStalled = false
}
mediaMouseMoved () {
this.state.playing.mouseStationarySince = new Date().getTime()
}
openExternalPlayer () {
var state = this.state
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title)
state.playing.location = 'external'
}
externalPlayerNotFound () {
var modal = this.state.modal
if (modal && modal.id === 'unsupported-media-modal') {
modal.externalPlayerNotFound = true
}
}
}

View File

@@ -0,0 +1,321 @@
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>,external player) 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',
setup: (cb) => {
this.play()
this.openPlayer(infoHash, index, cb)
},
destroy: () => this.closePlayer()
}, (err) => {
if (err) dispatch('error', err)
})
}
// Open a file in OS default app.
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 (!Number.isFinite(time)) {
console.error('Tried to skip to a non-finite time ' + time)
return console.trace()
}
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
openPlayer (infoHash, index, cb) {
var torrentSummary = TorrentSummary.getByKey(this.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.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
} else {
this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
}
}
openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
var fileSummary = torrentSummary.files[index]
// update state
var state = this.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()
}
state.window.title = torrentSummary.files[state.playing.fileIndex].name
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer')
this.update()
cb()
return
}
// otherwise, play the video
this.update()
ipcRenderer.send('onPlayerOpen')
cb()
})
}
closePlayer () {
console.log('closePlayer')
// Quit any external players, like Chromecast/Airplay/etc or VLC
var state = this.state
if (isCasting(state)) {
Cast.stop()
}
if (state.playing.location === 'external') {
ipcRenderer.send('quitExternalPlayer')
}
// 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.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()
}
}
// 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,56 @@
const State = require('../lib/state')
const {dispatch} = require('../lib/dispatcher')
const ipcRenderer = require('electron').ipcRenderer
// 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',
setup: function (cb) {
// initialize preferences
state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
ipcRenderer.send('setAllowNav', false)
cb()
},
destroy: () => {
ipcRenderer.send('setAllowNav', true)
this.save()
}
})
}
// Updates a single property in the UNSAVED prefs
// For example: updatePreferences('foo.bar', 'baz')
// Call save() 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
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
}
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
State.save(state)
dispatch('checkDownloadPath')
}
}

View File

@@ -0,0 +1,139 @@
const electron = require('electron')
const fs = require('fs-extra')
const path = require('path')
const parallel = require('run-parallel')
const remote = electron.remote
const {dispatch} = require('../lib/dispatcher')
module.exports = class SubtitlesController {
constructor (state) {
this.state = state
}
openSubtitles () {
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,191 @@
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()
}
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')
}
// Only play notification sound if player is inactive
if (this.state.playing.isPaused) sound.play('DONE')
}

View File

@@ -0,0 +1,296 @@
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
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) {
// You can only create torrents from the home screen.
if (this.state.location.url() !== 'home') {
return dispatch('error', 'Please go back to the torrent list before creating a new torrent.')
}
// 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,
setup: (cb) => {
this.state.window.title = 'Create New Torrent'
cb(null)
}
})
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))
}
// 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.cancel()
}
// Starts downloading and/or seeding a given torrentSummary.
startTorrentingSummary (torrentKey) {
var s = TorrentSummary.getByKey(this.state, torrentKey)
if (!s) throw new Error('Missing key: ' + torrentKey)
// New torrent: give it a path
if (!s.path) {
// Use Downloads folder by default
s.path = this.state.saved.prefs.downloadPath
return start()
}
// Existing torrent: check that the path is still there
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
if (err) {
s.error = 'path-missing'
return
}
start()
})
function start () {
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.torrentKey)
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)
if (filePath) 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) {
if (!savePath) return // They clicked Cancel
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)
}
}

View File

@@ -3,8 +3,9 @@
// * Starts and stops casting, provides remote video controls
module.exports = {
init,
open,
close,
toggleMenu,
selectDevice,
stop,
play,
pause,
seek,
@@ -12,9 +13,8 @@ module.exports = {
setRate
}
var airplay = require('airplay-js')
var chromecasts = require('chromecasts')()
var dlnacasts = require('dlnacasts')()
// Lazy load these for a ~300ms improvement in startup time
var airplayer, chromecasts, dlnacasts
var config = require('../../config')
@@ -32,25 +32,54 @@ function init (appState, callback) {
state = appState
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
chromecasts.on('update', function (player) {
state.devices.chromecast = chromecastPlayer(player)
chromecasts.on('update', function (device) {
// 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) {
state.devices.dlna = dlnaPlayer(player)
dlnacasts.on('update', function (device) {
state.devices.dlna.addDevice(device)
})
var browser = airplay.createBrowser()
browser.on('deviceOn', function (player) {
state.devices.airplay = airplayPlayer(player)
}).start()
airplayer.on('update', function (device) {
state.devices.airplay.addDevice(device)
})
}
// chromecast player implementation
function chromecastPlayer (player) {
function addEvents () {
player.on('error', function (err) {
function chromecastPlayer () {
var ret = {
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.errors.push({
time: new Date().getTime(),
@@ -58,7 +87,8 @@ function chromecastPlayer (player) {
})
update()
})
player.on('disconnect', function () {
device.on('disconnect', function () {
if (device !== ret.device) return
state.playing.location = 'local'
update()
})
@@ -66,7 +96,7 @@ function chromecastPlayer (player) {
function open () {
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',
title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) {
@@ -84,19 +114,19 @@ function chromecastPlayer (player) {
}
function play (callback) {
player.play(null, null, callback)
ret.device.play(null, null, callback)
}
function pause (callback) {
player.pause(callback)
ret.device.pause(callback)
}
function stop (callback) {
player.stop(callback)
ret.device.stop(callback)
}
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)
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
@@ -106,36 +136,59 @@ function chromecastPlayer (player) {
}
function seek (time, callback) {
player.seek(time, callback)
ret.device.seek(time, callback)
}
function volume (volume, callback) {
player.volume(volume, callback)
}
addEvents()
return {
player: player,
open: open,
play: play,
pause: pause,
stop: stop,
status: status,
seek: seek,
volume: volume
ret.device.volume(volume, callback)
}
}
// 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 () {
player.play(state.server.networkURL, 0, function (res) {
if (res.statusCode !== 200) {
ret.device.play(state.server.networkURL, function (err, res) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to AirPlay.'
message: 'Could not connect to AirPlay. ' + err.message
})
} else {
state.playing.location = 'airplay'
@@ -145,55 +198,67 @@ function airplayPlayer (player) {
}
function play (callback) {
player.rate(1, callback)
ret.device.resume(callback)
}
function pause (callback) {
player.rate(0, callback)
ret.device.pause(callback)
}
function stop (callback) {
player.stop(callback)
ret.device.stop(callback)
}
function status () {
player.status(function (status) {
state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume
// According to docs is in [-30 - 0] (db) range
// should be converted to [0 - 1] using (val / 30 + 1)
update()
ret.device.playbackInfo(function (err, res, status) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to AirPlay. ' + err.message
})
} else {
state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position
update()
}
})
}
function seek (time, callback) {
player.scrub(time, callback)
ret.device.scrub(time, 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
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
function dlnaPlayer (player) {
function addEvents () {
player.on('error', function (err) {
var ret = {
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.errors.push({
time: new Date().getTime(),
@@ -201,7 +266,8 @@ function dlnaPlayer (player) {
})
update()
})
player.on('disconnect', function () {
device.on('disconnect', function () {
if (device !== ret.device) return
state.playing.location = 'local'
update()
})
@@ -209,7 +275,7 @@ function dlnaPlayer (player) {
function open () {
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',
title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
@@ -228,19 +294,19 @@ function dlnaPlayer (player) {
}
function play (callback) {
player.play(null, null, callback)
ret.device.play(null, null, callback)
}
function pause (callback) {
player.pause(callback)
ret.device.pause(callback)
}
function stop (callback) {
player.stop(callback)
ret.device.stop(callback)
}
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)
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
@@ -250,61 +316,78 @@ function dlnaPlayer (player) {
}
function seek (time, callback) {
player.seek(time, callback)
ret.device.seek(time, callback)
}
function volume (volume, callback) {
player.volume(volume, function (err) {
ret.device.volume(volume, function (err) {
// quick volume update
state.playing.volume = volume
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
function startStatusInterval () {
statusInterval = setInterval(function () {
var device = getDevice()
if (device) {
device.status()
}
var player = getPlayer()
if (player) player.status()
}, 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') {
throw new Error('You can\'t connect to ' + location + ' when already connected to another device')
}
state.playing.location = location + '-pending'
var device = getDevice(location)
if (device) {
getDevice(location).open()
startStatusInterval()
}
// Find all cast devices of the given type
var player = getPlayer(location)
var devices = player ? player.getDevices() : []
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
// 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()
}
// Stops casting, move video back to local screen
function close () {
var device = getDevice()
if (device) {
device.stop(stoppedCasting)
function stop () {
var player = getPlayer()
if (player) {
player.stop(function () {
player.device = null
stoppedCasting()
})
clearInterval(statusInterval)
} else {
stoppedCasting()
@@ -313,12 +396,14 @@ function close () {
function stoppedCasting () {
state.playing.location = 'local'
state.playing.jumpToTime = state.playing.currentTime
state.playing.jumpToTime = Number.isFinite(state.playing.currentTime)
? state.playing.currentTime
: 0
update()
}
function getDevice (location) {
if (location && state.devices[location]) {
function getPlayer (location) {
if (location) {
return state.devices[location]
} else if (state.playing.location === 'chromecast') {
return state.devices.chromecast
@@ -332,29 +417,25 @@ function getDevice (location) {
}
function play () {
var device = getDevice()
if (device) {
device.play(castCallback)
}
var player = getPlayer()
if (player) player.play(castCallback)
}
function pause () {
var device = getDevice()
if (device) {
device.pause(castCallback)
}
var player = getPlayer()
if (player) player.pause(castCallback)
}
function setRate (rate) {
var device
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') {
device = state.devices.airplay
device.rate(rate, castCallback)
player = state.devices.airplay
player.rate(rate, castCallback)
} else {
result = false
}
@@ -362,17 +443,13 @@ function setRate (rate) {
}
function seek (time) {
var device = getDevice()
if (device) {
device.seek(time, castCallback)
}
var player = getPlayer()
if (player) player.seek(time, castCallback)
}
function setVolume (volume) {
var device = getDevice()
if (device) {
device.volume(volume, castCallback)
}
var player = getPlayer()
if (player) player.volume(volume, castCallback)
}
function castCallback () {

View File

@@ -0,0 +1,39 @@
module.exports = {
dispatch,
dispatcher,
setDispatch
}
var dispatchers = {}
var _dispatch = function () {}
function setDispatch (dispatch) {
_dispatch = dispatch
}
function dispatch (...args) {
_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 React from updating the listener functions on
// each update().
function dispatcher (...args) {
var str = JSON.stringify(args)
var handler = dispatchers[str]
if (!handler) {
handler = dispatchers[str] = function (e) {
// Do not propagate click to elements below the button
e.stopPropagation()
if (e.currentTarget.classList.contains('disabled')) {
// Ignore clicks on disabled elements
return
}
dispatch(...args)
}
}
return handler
}

View File

@@ -0,0 +1,137 @@
/* eslint-disable camelcase */
module.exports = {
run
}
const semver = require('semver')
const config = require('../../config')
const TorrentSummary = require('./torrent-summary')
const fs = require('fs')
// 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)
}
if (semver.lt(version, '0.11.0')) {
migrate_0_11_0(state.saved)
}
if (semver.lt(version, '0.12.0')) {
migrate_0_12_0(state.saved)
}
// Config is now on the new version
state.saved.version = config.APP_VERSION
}
function migrate_0_7_0 (saved) {
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) {
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) {
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 == null) {
saved.prefs = {
downloadPath: config.DEFAULT_DOWNLOAD_PATH
}
}
}
function migrate_0_11_0 (saved) {
if (saved.prefs.isFileHandler == null) {
// The app used to make itself the default torrent file handler automatically
saved.prefs.isFileHandler = true
}
}
function migrate_0_12_0 (saved) {
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
}
delete saved.prefs.playInVlc
// Undo a terrible bug where clicking Play on a default torrent on a fresh
// install results in a "path missing" error
// See https://github.com/feross/webtorrent-desktop/pull/806
var defaultTorrentFiles = [
'6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent',
'88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent',
'6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent',
'02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent',
'3ba219a8634bf7bae3d848192b2da75ae995589d.torrent'
]
saved.torrents.forEach(function (torrentSummary) {
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
var fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
if (!fileOrFolder) return
try {
fs.statSync(fileOrFolder)
} catch (e) {
// Default torrent with "missing path" error. Clear path.
delete torrentSummary.path
}
})
}

231
src/renderer/lib/state.js Normal file
View File

@@ -0,0 +1,231 @@
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() */
title: null,
progress: -1,
badge: null
},
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:
*
* Mac ~/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,
volume: 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,
isFileHandler: false,
openExternalPlayer: false,
externalPlayerPath: null
},
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
}
if (key === 'error') {
continue // Don't save error states
}
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)
}

View File

@@ -0,0 +1,192 @@
// 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.version = config.APP_VERSION
telemetry.timestamp = now.toISOString()
telemetry.localTime = now.toTimeString()
telemetry.screens = getScreenInfo()
telemetry.system = getSystemInfo()
telemetry.approxNumTorrents = getApproxNumTorrents(state)
if (config.IS_PRODUCTION) {
postToServer()
// If the user keeps WebTorrent running for a long time, post every 12h
setInterval(postToServer, 12 * 3600 * 1000)
} 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 = {
minVersion: config.APP_VERSION,
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, e) {
// Not initialized yet? Ignore.
// Hopefully uncaught errors immediately on startup are fixed in dev
if (!telemetry) return
var message
var stack = ''
if (e == null) {
message = 'Unexpected undefined error'
} else if (e.message) {
// err is either an Error or a plain object {message, stack}
message = e.message
stack = e.stack
} else if (e.error) {
// Uncaught Javascript errors (window.onerror), err is an ErrorEvent
if (!e.error.message) {
message = 'Unexpected ErrorEvent.error: ' + Object.keys(e.error).join(' ')
} else {
message = e.error.message
stack = e.error.stack
}
} else {
// Resource errors (captured element.onerror), err is an Event
if (!e.target) {
message = 'Unexpected unknown error'
} else if (!e.target.error) {
message = 'Unexpected resource loading error: ' + getElemString(e.target)
} else {
message = 'Resource error ' + getElemString(e.target) + ': ' + e.target.error.code
}
}
if (typeof stack !== 'string') stack = 'Unexpected stack: ' + stack
if (typeof message !== 'string') message = 'Unexpected message: ' + message
// Remove the first part of each file path in the stack trace.
// - Privacy: remove personal info like C:\Users\<full name>
// - Aggregation: this lets us find which stacktraces occur often
stack = stack.replace(/\(.*app.asar/g, '(...')
// 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)
// Log the app version *at the time of the error*
var version = config.APP_VERSION
telemetry.uncaughtErrors.push({process: procName, message, stack, version})
}
// Turns a DOM element into a string, eg "DIV.my-class.visible"
function getElemString (elem) {
var ret = elem.tagName
try {
ret += '.' + Array.from(elem.classList).join('.')
} catch (e) {}
return ret
}
// 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

@@ -0,0 +1,84 @@
module.exports = {
isPlayable,
isVideo,
isAudio,
isTorrent,
isPlayableTorrentSummary,
pickFileToPlay
}
var path = require('path')
// Checks whether a fileSummary or file path is audio/video that we can play,
// based on the file extension
function isPlayable (file) {
return isVideo(file) || isAudio(file)
}
// Checks whether a fileSummary or file path is playable video
function isVideo (file) {
return [
'.avi',
'.m4v',
'.mkv',
'.mov',
'.mp4',
'.mpg',
'.ogv',
'.webm',
'.wmv'
].includes(getFileExtension(file))
}
// Checks whether a fileSummary or file path is playable audio
function isAudio (file) {
return [
'.aac',
'.ac3',
'.mp3',
'.ogg',
'.wav',
'.m4a'
].includes(getFileExtension(file))
}
// 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)
}
// 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

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

View File

@@ -0,0 +1,57 @@
module.exports = {
getPosterPath,
getTorrentPath,
getByKey,
getTorrentID,
getFileOrFolder
}
var path = require('path')
var config = require('../../config')
// Expects a torrentSummary
// Returns an absolute path to the torrent file, or null if unavailable
function getTorrentPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.torrentFileName) return null
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
}
// Expects a torrentSummary
// Returns an absolute path to the poster image, or null if unavailable
function getPosterPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.posterFileName) return null
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
// Backslashes in URLS in CSS cause bizarre string encoding issues
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
if (!ts.path || !ts.files || ts.files.length === 0) return null
return path.join(ts.path, ts.files[0].path.split('/')[0])
}

460
src/renderer/main.js Normal file
View File

@@ -0,0 +1,460 @@
console.time('init')
const crashReporter = require('../crash-reporter')
crashReporter.init()
const dragDrop = require('drag-drop')
const electron = require('electron')
const fs = require('fs')
const React = require('react')
const ReactDOM = require('react-dom')
const config = require('../config')
const telemetry = require('./lib/telemetry')
const sound = require('./lib/sound')
const State = require('./lib/state')
const TorrentPlayer = require('./lib/torrent-player')
// Required by Material UI -- adds `onTouchTap` event
require('react-tap-event-plugin')()
const App = require('./pages/App')
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
// Root React component
var app
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 = window.state = _state // Make available for easier debugging
// 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',
setup: (cb) => {
state.window.title = config.APP_WINDOW_TITLE
cb(null)
}
})
// Restart everything we were torrenting last time the app ran
resumeTorrents()
// 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)
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
window.setTimeout(delayedInit, config.DELAYED_INIT)
// Listen for messages from the main process
setupIpc()
// Warn if the download dir is gone, eg b/c an external drive is unplugged
checkDownloadPath()
// OS integrations:
// ...drag and drop files/text to start torrenting or seeding
dragDrop('body', {
onDrop: onOpen,
onDropText: 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),
true /* capture */)
// 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
}
// React loop:
// 1. update() - recompute the virtual DOM, diff, apply to the real DOM
// 2. event - might be a click or other DOM event, or something external
// 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller
// 4. controller - the controller handles the event, changing the state object
function update () {
controllers.playback.showOrHidePlayerControls()
app.setState(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.toFixed(2) !== state.prev.progress.toFixed(2)) {
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 || 0)
}
}
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),
'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': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
// 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>, external players
'mediaStalled': () => controllers.media.mediaStalled(),
'mediaError': (err) => controllers.media.mediaError(err),
'mediaSuccess': () => controllers.media.mediaSuccess(),
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
// 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),
'checkDownloadPath': checkDownloadPath,
// 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(),
'cancel': () => state.location.cancel(),
// Controlling the window
'setDimensions': setDimensions,
'toggleFullScreen': (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
'setTitle': (title) => { state.window.title = title },
'resetTitle': () => { state.window.title = config.APP_WINDOW_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, but don't spam
if (!['mediaMouseMoved', 'mediaTimeUpdate', 'update'].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
})
}
// 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
.map((torrentSummary) => {
// Torrent keys are ephemeral, reassigned each time the app runs.
// On startup, give all torrents a key, even the ones that are paused.
torrentSummary.torrentKey = state.nextTorrentKey++
return torrentSummary
})
.filter((s) => s.status !== 'paused')
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey))
}
// 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', {contentBounds: true, 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 ]
var url = state.location.url()
var allTorrents = files.every(TorrentPlayer.isTorrent)
var allSubtitles = files.every(controllers.subtitles.isSubtitle)
if (allTorrents) {
// Drop torrents onto the app: go to home screen, add torrents, no matter what
dispatch('backToList')
// All .torrent files? Add them.
files.forEach((file) => controllers.torrentList.addTorrent(file))
} else if (url === 'player' && allSubtitles) {
// Drop subtitles onto a playing video: add subtitles
controllers.subtitles.addSubtitles(files, true)
} else if (url === 'home') {
// Drop files onto home screen: show Create Torrent
state.modal = null
controllers.torrentList.showCreateTorrent(files)
} else {
// Drop files onto any other screen: show error
return onError('Please go back to the torrent list before creating a new torrent.')
}
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 (Mac)
ipcRenderer.send('setAspectRatio', state.playing.aspectRatio)
}
update()
}
function checkDownloadPath () {
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
if (err) {
state.downloadPathStatus = 'missing'
return console.error(err)
}
if (stat.isDirectory()) state.downloadPathStatus = 'ok'
else state.downloadPathStatus = 'missing'
})
}

111
src/renderer/pages/App.js Normal file
View File

@@ -0,0 +1,111 @@
const colors = require('material-ui/styles/colors')
const React = require('react')
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
const Header = require('../components/header')
const Views = {
'home': require('./TorrentListPage'),
'player': require('./PlayerPage'),
'create-torrent': require('./CreateTorrentPage'),
'preferences': require('./PreferencesPage')
}
const Modals = {
'open-torrent-address-modal': require('../components/open-torrent-address-modal'),
'remove-torrent-modal': require('../components/remove-torrent-modal'),
'update-available-modal': require('../components/update-available-modal'),
'unsupported-media-modal': require('../components/unsupported-media-modal')
}
darkBaseTheme.fontFamily = process.platform === 'win32'
? '"Segoe UI", sans-serif'
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
darkBaseTheme.palette.primary1Color = colors.cyan500
darkBaseTheme.palette.primary2Color = colors.cyan500
darkBaseTheme.palette.primary3Color = colors.grey600
darkBaseTheme.palette.accent1Color = colors.redA200
darkBaseTheme.palette.accent2Color = colors.redA400
darkBaseTheme.palette.accent3Color = colors.redA100
class App extends React.Component {
render () {
var state = this.props.state
// 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
var hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local' &&
state.playing.playbackRate === 1
var cls = [
'view-' + state.location.url(), /* e.g. view-home, view-player */
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
]
if (state.window.isFullScreen) cls.push('is-fullscreen')
if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls')
var vdom = (
<MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}>
<div className={'app ' + cls.join(' ')}>
<Header state={state} />
{this.getErrorPopover()}
<div key='content' className='content'>{this.getView()}</div>
{this.getModal()}
</div>
</MuiThemeProvider>
)
return vdom
}
getErrorPopover () {
var state = this.props.state
var now = new Date().getTime()
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
var hasErrors = recentErrors.length > 0
var errorElems = recentErrors.map(function (error, i) {
return (<div key={i} className='error'>{error.message}</div>)
})
return (
<div key='errors'
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
<div key='title' className='title'>Error</div>
{errorElems}
</div>
)
}
getModal () {
var state = this.props.state
if (!state.modal) return
var ModalContents = Modals[state.modal.id]
return (
<div key='modal' className='modal'>
<div key='modal-background' className='modal-background' />
<div key='modal-content' className='modal-content'>
<ModalContents state={state} />
</div>
</div>
)
}
getView () {
var state = this.props.state
var View = Views[state.location.url()]
return (<View state={state} />)
}
}
module.exports = App

View File

@@ -0,0 +1,209 @@
const createTorrent = require('create-torrent')
const path = require('path')
const prettyBytes = require('prettier-bytes')
const React = require('react')
const {dispatch, dispatcher} = require('../lib/dispatcher')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
const Checkbox = require('material-ui/Checkbox').default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/Heading')
const ShowMore = require('../components/ShowMore')
class CreateTorrentPage extends React.Component {
constructor (props) {
super(props)
var state = this.props.state
var info = state.location.current()
// First, extract the base folder that the files are all in
var pathPrefix = info.folderPath
if (!pathPrefix) {
pathPrefix = info.files.map((x) => x.path).reduce(findCommonPrefix)
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
pathPrefix = path.dirname(pathPrefix)
}
}
// Then, exclude .DS_Store and other dotfiles
var files = info.files
.filter((f) => !containsDots(f.path, pathPrefix))
.map((f) => ({name: f.name, path: f.path, size: f.size}))
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
// Then, use the name of the base folder (or sole file, for a single file torrent)
// as the default name. Show all files relative to the base folder.
var defaultName, basePath
if (files.length === 1) {
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
defaultName = files[0].name
basePath = pathPrefix
} else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a'
defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
// Default trackers
var trackers = createTorrent.announceList.join('\n')
this.state = {
comment: '',
isPrivate: false,
pathPrefix,
basePath,
defaultName,
files,
trackers
}
// Create React event handlers only once
this.setIsPrivate = (_, isPrivate) => this.setState({isPrivate})
this.setComment = (_, comment) => this.setState({comment})
this.setTrackers = (_, trackers) => this.setState({trackers})
this.handleSubmit = handleSubmit.bind(this)
}
render () {
var files = this.state.files
// Sanity check: show the number of files and total size
var numFiles = files.length
var totalBytes = files
.map((f) => f.size)
.reduce((a, b) => a + b, 0)
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
return (
<div className='create-torrent'>
<Heading level={1}>Create torrent {this.state.defaultName}</Heading>
<div className='torrent-info'>{torrentInfo}</div>
<div className='torrent-attribute'>
<label>Path:</label>
<div>{this.state.pathPrefix}</div>
</div>
<ShowMore
style={{
marginBottom: 10
}}
hideLabel='Hide advanced settings...'
showLabel='Show advanced settings...' >
{this.renderAdvanced()}
</ShowMore>
<div className='float-right'>
<FlatButton
label='Cancel'
style={{
marginRight: 10
}}
onClick={dispatcher('cancel')}
/>
<RaisedButton
label='Create Torrent'
primary
onClick={this.handleSubmit}
/>
</div>
</div>
)
}
renderAdvanced () {
// Create file list
var maxFileElems = 100
var files = this.state.files
var fileElems = files.slice(0, maxFileElems).map((file, i) => {
var relativePath = path.relative(this.state.pathPrefix, file.path)
return (<div key={i}>{relativePath}</div>)
})
if (files.length > maxFileElems) {
fileElems.push(<div key='more'>+ {files.length - maxFileElems} more</div>)
}
// Align the text fields
var textFieldStyle = { width: '' }
var textareaStyle = { margin: 0 }
return (
<div key='advanced' className='create-torrent-advanced'>
<div key='private' className='torrent-attribute'>
<label>Private:</label>
<Checkbox
className='torrent-is-private'
style={{display: ''}}
checked={this.state.isPrivate}
onCheck={this.setIsPrivate} />
</div>
<div key='trackers' className='torrent-attribute'>
<label>Trackers:</label>
<TextField
className='torrent-trackers'
style={textFieldStyle}
textareaStyle={textareaStyle}
multiLine
rows={2}
rowsMax={10}
value={this.state.trackers}
onChange={this.setTrackers} />
</div>
<div key='comment' className='torrent-attribute'>
<label>Comment:</label>
<TextField
className='torrent-comment'
style={textFieldStyle}
textareaStyle={textareaStyle}
hintText='Optionally describe your torrent...'
multiLine
rows={2}
rowsMax={10}
value={this.state.comment}
onChange={this.setComment} />
</div>
<div key='files' className='torrent-attribute'>
<label>Files:</label>
<div>{fileElems}</div>
</div>
</div>
)
}
}
function handleSubmit () {
var announceList = this.state.trackers
.split('\n')
.map((s) => s.trim())
.filter((s) => s !== '')
var options = {
// We can't let the user choose their own name if we want WebTorrent
// to use the files in place rather than creating a new folder.
name: this.state.defaultName,
path: this.state.basePath,
files: this.state.files,
announce: announceList,
private: this.state.isPrivate,
comment: this.state.comment.trim()
}
dispatch('createTorrent', options)
}
// Finds the longest common prefix
function findCommonPrefix (a, b) {
for (var i = 0; i < a.length && i < b.length; i++) {
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
}
if (i === a.length) return a
if (i === b.length) return b
return a.substring(0, i)
}
function containsDots (path, pathPrefix) {
var suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
return ('/' + suffix).includes('/.')
}
module.exports = CreateTorrentPage

View File

@@ -1,30 +1,30 @@
module.exports = Player
const React = require('react')
const Bitfield = require('bitfield')
const prettyBytes = require('prettier-bytes')
const zeroFill = require('zero-fill')
const path = require('path')
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var Bitfield = require('bitfield')
var prettyBytes = require('prettier-bytes')
var zeroFill = require('zero-fill')
var TorrentSummary = require('../lib/torrent-summary')
var {dispatch, dispatcher} = require('../lib/dispatcher')
const TorrentSummary = require('../lib/torrent-summary')
const {dispatch, dispatcher} = require('../lib/dispatcher')
// Shows a streaming video player. Standard features + Chromecast + Airplay
function Player (state) {
// Show the video as large as will fit in the window, play immediately
// If the video is on Chromecast or Airplay, show a title screen instead
var showVideo = state.playing.location === 'local'
return hx`
<div
class='player'
onwheel=${handleVolumeWheel}
onmousemove=${dispatcher('mediaMouseMoved')}>
${showVideo ? renderMedia(state) : renderCastScreen(state)}
${renderPlayerControls(state)}
module.exports = class Player extends React.Component {
render () {
// Show the video as large as will fit in the window, play immediately
// If the video is on Chromecast or Airplay, show a title screen instead
var state = this.props.state
var showVideo = state.playing.location === 'local'
var showControls = state.playing.location !== 'external'
return (
<div
className='player'
onWheel={handleVolumeWheel}
onMouseMove={dispatcher('mediaMouseMoved')}>
{showVideo ? renderMedia(state) : renderCastScreen(state)}
{showControls ? renderPlayerControls(state) : null}
</div>
`
)
}
}
// Handles volume change by wheel
@@ -46,13 +46,19 @@ function renderMedia (state) {
mediaElement.play()
}
// 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
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
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
mediaElement.volume = state.playing.setVolume
@@ -60,7 +66,7 @@ function renderMedia (state) {
}
// Switch to the newly added subtitle track, if available
var tracks = mediaElement.textTracks
var tracks = mediaElement.textTracks || []
for (var j = 0; j < tracks.length; j++) {
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
@@ -70,6 +76,15 @@ function renderMedia (state) {
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.volume = mediaElement.volume
}
@@ -79,42 +94,44 @@ function renderMedia (state) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i]
var isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push(hx`
trackTags.push(
<track
${isSelected ? 'default' : ''}
label=${track.label}
key={i}
default={isSelected ? 'default' : ''}
label={track.label}
type='subtitles'
src=${track.buffer}>
`)
src={track.buffer} />
)
}
}
// Create the <audio> or <video> tag
var mediaTag = hx`
<div
src='${state.server.localURL}'
ondblclick=${dispatcher('toggleFullScreen')}
onloadedmetadata=${onLoadedMetadata}
onended=${onEnded}
onstalling=${dispatcher('mediaStalled')}
onerror=${dispatcher('mediaError')}
ontimeupdate=${dispatcher('mediaTimeUpdate')}
onencrypted=${dispatcher('mediaEncrypted')}
oncanplay=${onCanPlay}>
${trackTags}
</div>
`
mediaTag.tagName = state.playing.type // conditional tag name
var MediaTagName = state.playing.type
var mediaTag = (
<MediaTagName
src={state.server.localURL}
onDoubleClick={dispatcher('toggleFullScreen')}
onLoadedMetadata={onLoadedMetadata}
onEnded={onEnded}
onStalled={dispatcher('mediaStalled')}
onError={dispatcher('mediaError')}
onTimeUpdate={dispatcher('mediaTimeUpdate')}
onEncrypted={dispatcher('mediaEncrypted')}
onCanPlay={onCanPlay}>
{trackTags}
</MediaTagName>
)
// Show the media.
return hx`
return (
<div
class='letterbox'
onmousemove=${dispatcher('mediaMouseMoved')}>
${mediaTag}
${renderOverlay(state)}
key='letterbox'
className='letterbox'
onMouseMove={dispatcher('mediaMouseMoved')}>
{mediaTag}
{renderOverlay(state)}
</div>
`
)
// As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) {
@@ -140,6 +157,7 @@ function renderMedia (state) {
} else if (elem.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported')
} else {
dispatch('mediaSuccess')
elem.play()
}
}
@@ -164,11 +182,11 @@ function renderOverlay (state) {
return
}
return hx`
<div class='media-overlay-background' style=${style}>
<div class='media-overlay'>${elems}</div>
return (
<div key='overlay' className='media-overlay-background' style={style}>
<div className='media-overlay'>{elems}</div>
</div>
`
)
}
function renderAudioMetadata (state) {
@@ -194,36 +212,36 @@ function renderAudioMetadata (state) {
// Show a small info box in the middle of the screen with title/album/etc
var elems = []
if (artist) {
elems.push(hx`
<div class='audio-artist'>
<label>Artist</label>${artist}
elems.push((
<div key='artist' className='audio-artist'>
<label>Artist</label>{artist}
</div>
`)
))
}
if (album) {
elems.push(hx`
<div class='audio-album'>
<label>Album</label>${album}
elems.push((
<div key='album' className='audio-album'>
<label>Album</label>{album}
</div>
`)
))
}
if (track) {
elems.push(hx`
<div class='audio-track'>
<label>Track</label>${track}
elems.push((
<div key='track' className='audio-track'>
<label>Track</label>{track}
</div>
`)
))
}
// Align the title with the other info, if available. Otherwise, center title
var emptyLabel = hx`<label></label>`
elems.unshift(hx`
<div class='audio-title'>
${elems.length ? emptyLabel : undefined}${title}
var emptyLabel = (<label />)
elems.unshift((
<div key='title' className='audio-title'>
{elems.length ? emptyLabel : undefined}{title}
</div>
`)
))
return hx`<div class='audio-metadata'>${elems}</div>`
return (<div key='audio-metadata' className='audio-metadata'>{elems}</div>)
}
function renderLoadingSpinner (state) {
@@ -239,16 +257,16 @@ function renderLoadingSpinner (state) {
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
}
return hx`
<div class='media-stalled'>
<div class='loading-spinner'>&nbsp;</div>
<div class='loading-status ellipsis'>
<span class='progress'>${fileProgress}%</span> downloaded,
<span> ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
<span> ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
return (
<div key='loading' className='media-stalled'>
<div key='loading-spinner' className='loading-spinner'>&nbsp;</div>
<div key='loading-progress' className='loading-status ellipsis'>
<span className='progress'>{fileProgress}%</span> downloaded,
<span> {prettyBytes(prog.downloadSpeed || 0)}/s</span>
<span> {prettyBytes(prog.uploadSpeed || 0)}/s</span>
</div>
</div>
`
)
}
function renderCastScreen (state) {
@@ -265,9 +283,12 @@ function renderCastScreen (state) {
castIcon = 'tv'
castType = 'DLNA'
isCast = true
} else if (state.playing.location === 'vlc') {
} else if (state.playing.location === 'external') {
// TODO: get the player name in a more reliable way
var playerPath = state.saved.prefs.externalPlayerPath
var playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
castIcon = 'tv'
castType = 'VLC'
castType = playerName
isCast = false
} else if (state.playing.location === 'error') {
castIcon = 'error_outline'
@@ -276,8 +297,10 @@ function renderCastScreen (state) {
}
var isStarting = state.playing.location.endsWith('-pending')
var castName = state.playing.castName
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 = ''
// Show a nice title image, if possible
@@ -285,42 +308,66 @@ function renderCastScreen (state) {
backgroundImage: cssBackgroundImagePoster(state)
}
return hx`
<div class='letterbox' style=${style}>
<div class='cast-screen'>
<i class='icon'>${castIcon}</i>
<div class='cast-type'>${castType}</div>
<div class='cast-status'>${castStatus}</div>
return (
<div key='cast' className='letterbox' style={style}>
<div className='cast-screen'>
<i className='icon'>{castIcon}</i>
<div key='type' className='cast-type'>{castType}</div>
<div key='status' className='cast-status'>{castStatus}</div>
</div>
</div>
`
)
}
function renderSubtitlesOptions (state) {
function renderCastOptions (state) {
if (!state.devices.castMenu) return
var {location, devices} = state.devices.castMenu
var player = state.devices[location]
var items = devices.map(function (device, ix) {
var isSelected = player.device === device
var name = device.name
return (
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
{name}
</li>
)
})
return (
<ul key='cast-options' className='options-list'>
{items}
</ul>
)
}
function renderSubtitleOptions (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}
return (
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
<i className='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.subtitles-list>
${items}
<li onclick=${dispatcher('selectSubtitle', -1)}>
<i.icon>${noneClass}</i>
return (
<ul key='subtitle-options' className='options-list'>
{items}
<li onClick={dispatcher('selectSubtitle', -1)}>
<i className='icon'>{noneClass}</i>
None
</li>
</ul>
`
)
}
function renderPlayerControls (state) {
@@ -333,114 +380,95 @@ function renderPlayerControls (state) {
: ''
var elements = [
hx`
<div class='playback-bar'>
${renderLoadingBar(state)}
<div
class='playback-cursor'
style=${playbackCursorStyle}>
</div>
<div
class='scrub-bar'
draggable='true'
ondragstart=${handleDragStart}
onclick=${handleScrub},
ondrag=${handleScrub}>
</div>
</div>
`,
hx`
<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')}>
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i>
`
<div key='playback-bar' className='playback-bar'>
{renderLoadingBar(state)}
<div
key='cursor'
className='playback-cursor'
style={playbackCursorStyle}
/>
<div
key='scrub-bar'
className='scrub-bar'
draggable='true'
onDragStart={handleDragStart}
onClick={handleScrub}
onDrag={handleScrub}
/>
</div>,
<i
key='play'
className='icon play-pause float-left'
onClick={dispatcher('playPause')}>
{state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>,
<i
key='fullscreen'
className='icon fullscreen float-right'
onClick={dispatcher('toggleFullScreen')}>
{state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i>
]
if (state.playing.type === 'video') {
// show closed captions icon
elements.push(hx`
<i.icon.closed-captions.float-right
class=${captionsClass}
onclick=${handleSubtitles}>
closed_captions
elements.push((
<i
key='subtitles'
className={'icon closed-caption float-right ' + captionsClass}
onClick={handleSubtitles}>
closed_caption
</i>
`)
))
}
// If we've detected a Chromecast or AppleTV, the user can play video there
var isOnChromecast = state.playing.location.startsWith('chromecast')
var isOnAirplay = state.playing.location.startsWith('airplay')
var isOnDlna = state.playing.location.startsWith('dlna')
var chromecastClass, chromecastHandler
var airplayClass, airplayHandler
var dlnaClass, dlnaHandler
if (isOnChromecast) {
chromecastClass = 'active'
dlnaClass = 'disabled'
airplayClass = 'disabled'
chromecastHandler = dispatcher('closeDevice')
airplayHandler = undefined
dlnaHandler = undefined
} else if (isOnAirplay) {
chromecastClass = 'disabled'
dlnaClass = 'disabled'
airplayClass = 'active'
chromecastHandler = undefined
airplayHandler = dispatcher('closeDevice')
dlnaHandler = undefined
} else if (isOnDlna) {
chromecastClass = 'disabled'
dlnaClass = 'active'
airplayClass = 'disabled'
chromecastHandler = undefined
airplayHandler = undefined
dlnaHandler = dispatcher('closeDevice')
} else {
chromecastClass = ''
airplayClass = ''
dlnaClass = ''
chromecastHandler = dispatcher('openDevice', 'chromecast')
airplayHandler = dispatcher('openDevice', 'airplay')
dlnaHandler = dispatcher('openDevice', 'dlna')
}
if (state.devices.chromecast || isOnChromecast) {
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
elements.push(hx`
<i.icon.device.float-right
class=${chromecastClass}
onclick=${chromecastHandler}>
${castIcon}
</i>
`)
}
if (state.devices.airplay || isOnAirplay) {
elements.push(hx`
<i.icon.device.float-right
class=${airplayClass}
onclick=${airplayHandler}>
airplay
</i>
`)
}
if (state.devices.dlna || isOnDlna) {
elements.push(hx`
<i
class='icon device float-right'
class=${dlnaClass}
onclick=${dlnaHandler}>
tv
</i>
`)
}
var castTypes = ['chromecast', 'airplay', 'dlna']
var isCastingAnywhere = castTypes.some(
(castType) => state.playing.location.startsWith(castType))
// render volume
// Add the cast buttons. Icons for each cast type, connected/disconnected:
var buttonIcons = {
'chromecast': {true: 'cast_connected', false: 'cast'},
'airplay': {true: 'airplay', false: 'airplay'},
'dlna': {true: 'tv', false: 'tv'}
}
castTypes.forEach(function (castType) {
// Do we show this button (eg. the Chromecast button) at all?
var isCasting = state.playing.location.startsWith(castType)
var player = state.devices[castType]
if ((!player || player.getDevices().length === 0) && !isCasting) return
// Show the button. Three options for eg the Chromecast button:
var buttonClass, buttonHandler
if (isCasting) {
// Option 1: we are currently connected to Chromecast. Button stops the cast.
buttonClass = 'active'
buttonHandler = dispatcher('stopCasting')
} else if (isCastingAnywhere) {
// Option 2: we are currently connected somewhere else. Button disabled.
buttonClass = 'disabled'
buttonHandler = undefined
} else {
// Option 3: we are not connected anywhere. Button opens Chromecast menu.
buttonClass = ''
buttonHandler = dispatcher('toggleCastMenu', castType)
}
var buttonIcon = buttonIcons[castType][isCasting]
elements.push((
<i
key={castType}
className={'icon device float-right ' + buttonClass}
onClick={buttonHandler}>
{buttonIcon}
</i>
))
})
// Render volume slider
var volume = state.playing.volume
var volumeIcon = 'volume_' + (
volume === 0 ? 'off'
@@ -453,49 +481,50 @@ function renderPlayerControls (state) {
'color-stop(' + (volume * 100) + '%, #727272))'
}
elements.push(hx`
<div class='volume float-left'>
// TODO: dcposch change the range input to use value / onChanged instead of
// "readonly" / onMouse[Down,Move,Up]
elements.push((
<div key='volume' className='volume float-left'>
<i
class='icon volume-icon float-left'
onmousedown=${handleVolumeMute}>
${volumeIcon}
className='icon volume-icon float-left'
onMouseDown={handleVolumeMute}>
{volumeIcon}
</i>
<input
class='volume-slider float-right'
className='volume-slider float-right'
type='range' min='0' max='1' step='0.05'
value=${volumeChanging !== false ? volumeChanging : volume}
onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub}
style=${volumeStyle}
value={volume}
onChange={handleVolumeScrub}
style={volumeStyle}
/>
</div>
`)
))
// Show video playback progress
var currentTimeStr = formatTime(state.playing.currentTime)
var durationStr = formatTime(state.playing.duration)
elements.push(hx`
<span class='time float-left'>
${currentTimeStr} / ${durationStr}
elements.push((
<span key='time' className='time float-left'>
{currentTimeStr} / {durationStr}
</span>
`)
))
// render playback rate
if (state.playing.playbackRate !== 1) {
elements.push(hx`
<span class='rate float-left'>
${state.playing.playbackRate}x
elements.push((
<span key='rate' className='rate float-left'>
{state.playing.playbackRate}x
</span>
`)
))
}
return hx`
<div class='controls'>
${elements}
${renderSubtitlesOptions(state)}
return (
<div key='controls' className='controls'>
{elements}
{renderCastOptions(state)}
{renderSubtitleOptions(state)}
</div>
`
)
function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
@@ -507,11 +536,12 @@ function renderPlayerControls (state) {
// Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) {
if (!e.clientX) return
dispatch('mediaMouseMoved')
var windowWidth = document.querySelector('body').clientWidth
var fraction = e.clientX / windowWidth
var position = fraction * state.playing.duration /* seconds */
dispatch('playbackJump', position)
dispatch('skipTo', position)
}
// Handles volume muting and Unmuting
@@ -525,21 +555,7 @@ function renderPlayerControls (state) {
// Handles volume slider scrub
function handleVolumeScrub (e) {
switch (e.type) {
case 'mouseup':
volumeChanging = false
dispatch('setVolume', e.offsetX / 50)
break
case 'mousedown':
volumeChanging = this.value
break
case 'mousemove':
// only change if move was started by click
if (volumeChanging !== false) {
volumeChanging = this.value
}
break
}
dispatch('setVolume', e.target.value)
}
function handleSubtitles (e) {
@@ -552,11 +568,8 @@ function renderPlayerControls (state) {
}
}
// lets scrub without sending to volume backend
var volumeChanging = false
// 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) {
var torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary.progress) {
@@ -579,18 +592,15 @@ function renderLoadingBar (state) {
}
// Output some bars to show which parts of the file are loaded
return hx`
<div class='loading-bar'>
${parts.map(function (part) {
var style = {
left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / fileProg.numPieces) + '%'
}
var loadingBarElems = parts.map(function (part, i) {
var style = {
left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / fileProg.numPieces) + '%'
}
return hx`<div class='loading-bar-part' style=${style}></div>`
})}
</div>
`
return (<div key={i} className='loading-bar-part' style={style} />)
})
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
}
// Returns the CSS background-image string for a poster image + dark vignette
@@ -598,7 +608,7 @@ function cssBackgroundImagePoster (state) {
var torrentSummary = state.getPlayingTorrentSummary()
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
if (!posterPath) return ''
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
return cssBackgroundImageDarkGradient() + `, url('${posterPath}')`
}
function cssBackgroundImageDarkGradient () {

View File

@@ -0,0 +1,174 @@
const colors = require('material-ui/styles/colors')
const path = require('path')
const React = require('react')
const Checkbox = require('material-ui/Checkbox').default
const Heading = require('../components/Heading')
const PathSelector = require('../components/PathSelector')
const RaisedButton = require('material-ui/RaisedButton').default
const {dispatch} = require('../lib/dispatcher')
class PreferencesPage extends React.Component {
constructor (props) {
super(props)
this.handleDownloadPathChange =
this.handleDownloadPathChange.bind(this)
this.handleOpenExternalPlayerChange =
this.handleOpenExternalPlayerChange.bind(this)
this.handleExternalPlayerPathChange =
this.handleExternalPlayerPathChange.bind(this)
}
downloadPathSelector () {
return (
<Preference>
<PathSelector
dialog={{
title: 'Select download directory',
properties: [ 'openDirectory' ]
}}
onChange={this.handleDownloadPathChange}
title='Download location'
value={this.props.state.unsaved.prefs.downloadPath}
/>
</Preference>
)
}
handleDownloadPathChange (filePath) {
dispatch('updatePreferences', 'downloadPath', filePath)
}
openExternalPlayerCheckbox () {
return (
<Preference>
<Checkbox
className='control'
checked={!this.props.state.unsaved.prefs.openExternalPlayer}
label={'Play torrent media files using WebTorrent'}
onCheck={this.handleOpenExternalPlayerChange}
/>
</Preference>
)
}
handleOpenExternalPlayerChange (e, isChecked) {
dispatch('updatePreferences', 'openExternalPlayer', !isChecked)
}
externalPlayerPathSelector () {
const playerName = path.basename(
this.props.state.unsaved.prefs.externalPlayerPath || 'VLC'
)
const description = this.props.state.unsaved.prefs.openExternalPlayer
? `Torrent media files will always play in ${playerName}.`
: `Torrent media files will play in ${playerName} if WebTorrent cannot ` +
'play them.'
return (
<Preference>
<p>{description}</p>
<PathSelector
dialog={{
title: 'Select media player app',
properties: [ 'openFile' ]
}}
displayValue={playerName}
onChange={this.handleExternalPlayerPathChange}
title='External player'
value={this.props.state.unsaved.prefs.externalPlayerPath}
/>
</Preference>
)
}
handleExternalPlayerPathChange (filePath) {
if (path.extname(filePath) === '.app') {
// Mac: Use executable in packaged .app bundle
filePath += '/Contents/MacOS/' + path.basename(filePath, '.app')
}
dispatch('updatePreferences', 'externalPlayerPath', filePath)
}
setDefaultAppButton () {
var isFileHandler = this.props.state.unsaved.prefs.isFileHandler
if (isFileHandler) {
return (
<Preference>
<p>WebTorrent is your default torrent app. Hooray!</p>
</Preference>
)
}
return (
<Preference>
<p>WebTorrent is not currently the default torrent app.</p>
<RaisedButton
className='control'
onClick={this.handleSetDefaultApp}
label='Make WebTorrent the default'
/>
</Preference>
)
}
handleSetDefaultApp () {
dispatch('updatePreferences', 'isFileHandler', true)
}
render () {
var style = {
color: colors.grey400,
marginLeft: 25,
marginRight: 25
}
return (
<div style={style} >
<PreferencesSection title='Downloads'>
{this.downloadPathSelector()}
</PreferencesSection>
<PreferencesSection title='Playback'>
{this.openExternalPlayerCheckbox()}
{this.externalPlayerPathSelector()}
</PreferencesSection>
<PreferencesSection title='Default torrent app'>
{this.setDefaultAppButton()}
</PreferencesSection>
</div>
)
}
}
class PreferencesSection extends React.Component {
static get propTypes () {
return {
title: React.PropTypes.string
}
}
render () {
var style = {
marginBottom: 25,
marginTop: 25
}
return (
<div style={style}>
<Heading level={2}>{this.props.title}</Heading>
{this.props.children}
</div>
)
}
}
class Preference extends React.Component {
render () {
var style = { marginBottom: 10 }
return (<div style={style}>{this.props.children}</div>)
}
}
module.exports = PreferencesPage

View File

@@ -0,0 +1,390 @@
const React = require('react')
const prettyBytes = require('prettier-bytes')
const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player')
const {dispatcher} = require('../lib/dispatcher')
module.exports = class TorrentList extends React.Component {
render () {
var state = this.props.state
var contents = []
if (state.downloadPathStatus === 'missing') {
contents.push(
<div key='torrent-missing-path'>
<p>Download path missing: {state.saved.prefs.downloadPath}</p>
<p>Check that all drives are connected?</p>
<p>Alternatively, choose a new download path
in <a href='#' onClick={dispatcher('preferences')}>Preferences</a>
</p>
</div>
)
}
var torrentElems = state.saved.torrents.map(
(torrentSummary) => this.renderTorrent(torrentSummary)
)
contents.push(...torrentElems)
contents.push(
<div key='torrent-placeholder' className='torrent-placeholder'>
<span className='ellipsis'>Drop a torrent file here or paste a magnet link</span>
</div>
)
return (
<div key='torrent-list' className='torrent-list'>
{contents}
</div>
)
}
renderTorrent (torrentSummary) {
var state = this.props.state
var infoHash = torrentSummary.infoHash
var isSelected = infoHash && state.selectedInfoHash === infoHash
// Background image: show some nice visuals, like a frame from the movie, if possible
var style = {}
if (torrentSummary.posterFileName) {
var gradient = isSelected
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
style.backgroundImage = gradient + `, url('${posterPath}')`
}
// Foreground: name of the torrent, basic info like size, play button,
// cast buttons if available, and delete
var classes = ['torrent']
// playStatus turns the play button into a loading spinner or error icon
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled')
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
return (
<div
key={torrentSummary.torrentKey}
style={style}
className={classes.join(' ')}
onContextMenu={infoHash && dispatcher('openTorrentContextMenu', infoHash)}
onClick={infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
{this.renderTorrentMetadata(torrentSummary)}
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
</div>
)
}
// Show name, download status, % complete
renderTorrentMetadata (torrentSummary) {
var name = torrentSummary.name || 'Loading torrent...'
var elements = [(
<div key='name' className='name ellipsis'>{name}</div>
)]
// If it's downloading/seeding then show progress info
var prog = torrentSummary.progress
if (torrentSummary.error) {
elements.push(
<div key='progress-info' className='ellipsis'>
{getErrorMessage(torrentSummary)}
</div>
)
} else if (torrentSummary.status !== 'paused' && prog) {
elements.push(
<div key='progress-info' className='ellipsis'>
{renderPercentProgress()}
{renderTotalProgress()}
{renderPeers()}
{renderDownloadSpeed()}
{renderUploadSpeed()}
{renderEta()}
</div>
)
}
return (<div key='metadata' className='metadata'>{elements}</div>)
function renderPercentProgress () {
var progress = Math.floor(100 * prog.progress)
return (<span key='percent-progress'>{progress}%</span>)
}
function renderTotalProgress () {
var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0)
if (downloaded === total) {
return (<span key='total-progress'>{downloaded}</span>)
} else {
return (<span key='total-progress'>{downloaded} / {total}</span>)
}
}
function renderPeers () {
if (prog.numPeers === 0) return
var count = prog.numPeers === 1 ? 'peer' : 'peers'
return (<span key='peers'>{prog.numPeers} {count}</span>)
}
function renderDownloadSpeed () {
if (prog.downloadSpeed === 0) return
return (<span key='download'> {prettyBytes(prog.downloadSpeed)}/s</span>)
}
function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return
return (<span key='upload'> {prettyBytes(prog.uploadSpeed)}/s</span>)
}
function renderEta () {
var downloaded = prog.downloaded
var total = prog.length || 0
var missing = total - downloaded
var downloadSpeed = prog.downloadSpeed
if (downloadSpeed === 0 || missing === 0) return
var rawEta = missing / downloadSpeed
var hours = Math.floor(rawEta / 3600) % 24
var minutes = Math.floor(rawEta / 60) % 60
var seconds = Math.floor(rawEta % 60)
// Only display hours and minutes if they are greater than 0 but always
// display minutes if hours is being displayed
var hoursStr = hours ? hours + 'h' : ''
var minutesStr = (hours || minutes) ? minutes + 'm' : ''
var secondsStr = seconds + 's'
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
}
}
// Download button toggles between torrenting (DL/seed) and paused
// Play button starts streaming the torrent immediately, unpausing if needed
renderTorrentButtons (torrentSummary) {
var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else {
playIcon = 'play_arrow'
playTooltip = 'Start streaming'
}
var downloadIcon, downloadTooltip
if (torrentSummary.status === 'seeding') {
downloadIcon = 'file_upload'
downloadTooltip = 'Seeding. Click to stop.'
} else if (torrentSummary.status === 'downloading') {
downloadIcon = 'file_download'
downloadTooltip = 'Torrenting. Click to stop.'
} else {
downloadIcon = 'file_download'
downloadTooltip = 'Click to start torrenting.'
}
// Only show the play/dowload buttons for torrents that contain playable media
var playButton, downloadButton, positionElem
if (!torrentSummary.error) {
downloadButton = (
<i
key='download-button'
className={'button-round icon download ' + torrentSummary.status}
title={downloadTooltip}
onClick={dispatcher('toggleTorrent', infoHash)}
>
{downloadIcon}
</i>
)
// 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 willShowSpinner = torrentSummary.playStatus === 'requested'
var defaultFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
var fraction = defaultFile.currentTime / defaultFile.duration
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position'
}
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
playButton = (
<i
key='play-button'
title={playTooltip}
className={'button-round icon play ' + playClass}
onClick={dispatcher('playFile', infoHash)}
>
{playIcon}
</i>
)
}
}
return (
<div key='buttons' className='buttons'>
{positionElem}
{playButton}
{downloadButton}
<i
key='delete-button'
className='icon delete'
title='Remove torrent'
onClick={dispatcher('confirmDeleteTorrent', infoHash, false)}>
close
</i>
</div>
)
}
// Show files, per-file download status and play buttons, and so on
renderTorrentDetails (torrentSummary) {
var filesElement
if (torrentSummary.error || !torrentSummary.files) {
var message = ''
if (torrentSummary.error === 'path-missing') {
// Special case error: this torrent's download dir or file is missing
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
} else if (torrentSummary.error) {
// General error for this torrent: just show the message
message = torrentSummary.error.message || torrentSummary.error
} else if (torrentSummary.status === 'paused') {
// No file info, no infohash, and we're not trying to download from the DHT
message = 'Failed to load torrent info. Click the download button to try again...'
} else {
// No file info, no infohash, trying to load from the DHT
message = 'Downloading torrent info...'
}
filesElement = (
<div key='files' className='files warning'>
{message}
</div>
)
} else {
// We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files
.filter((file) => !file.path.includes('/.____padding_file/'))
.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) => this.renderFileRow(torrentSummary, object.file, object.index))
filesElement = (
<div key='files' className='files'>
<table>
<tbody>
{fileRows}
</tbody>
</table>
</div>
)
}
return (
<div key='details' className='torrent-details'>
{filesElement}
</div>
)
}
// Show a single torrentSummary file in the details view for a single torrent
renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded
// Are we even torrenting it?
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
var isDone = false // Are we finished torrenting it?
var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files &&
torrentSummary.progress.files[index]) {
var fileProg = torrentSummary.progress.files[index]
isDone = fileProg.numPiecesPresent === fileProg.numPieces
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
}
// 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 = this.renderRadialProgressBar(file.currentTime / file.duration)
}
// Finally, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash
var icon
var handleClick
if (isPlayable) {
icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('playFile', infoHash, index)
} else {
icon = 'description' /* file icon, opens in OS default app */
handleClick = isDone
? dispatcher('openItem', infoHash, index)
: (e) => e.stopPropagation() // noop if file is not ready
}
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed
var rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return (
<tr key={index} onClick={handleClick}>
<td className={'col-icon ' + rowClass}>
{positionElem}
<i className='icon'>{icon}</i>
</td>
<td className={'col-name ' + rowClass}>
{file.name}
</td>
<td className={'col-progress ' + rowClass}>
{isSelected ? progress : ''}
</td>
<td className={'col-size ' + rowClass}>
{prettyBytes(file.length)}
</td>
<td className='col-select'
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
</td>
</tr>
)
}
renderRadialProgressBar (fraction, cssClass) {
var rotation = 360 * fraction
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return (
<div key='radial-progress' className={'radial-progress ' + cssClass}>
<div key='circle' className='circle'>
<div key='mask-full' className='mask full' style={transformFill}>
<div key='fill' className='fill' style={transformFill} />
</div>
<div key='mask-half' className='mask half'>
<div key='fill' className='fill' style={transformFill} />
<div key='fill-fix' className='fill fix' style={transformFix} />
</div>
</div>
<div key='inset' className='inset' />
</div>
)
}
}
function getErrorMessage (torrentSummary) {
var err = torrentSummary.error
if (err === 'path-missing') {
return (
<span>
Path missing.<br />
Fix and restart the app, or delete the torrent.
</span>
)
}
return 'Error'
}

View File

@@ -2,14 +2,16 @@
// process from the main window.
console.time('init')
var WebTorrent = require('webtorrent')
var defaultAnnounceList = require('create-torrent').announceList
var crypto = require('crypto')
var deepEqual = require('deep-equal')
var defaultAnnounceList = require('create-torrent').announceList
var electron = require('electron')
var fs = require('fs-extra')
var musicmetadata = require('musicmetadata')
var networkAddress = require('network-address')
var path = require('path')
var WebTorrent = require('webtorrent')
var zeroFill = require('zero-fill')
var crashReporter = require('../crash-reporter')
var config = require('../config')
@@ -26,24 +28,42 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
.map((arr) => arr[0])
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
/**
* WebTorrent version.
*/
var VERSION = require('../../package.json').version
/**
* Version number in Azureus-style. Generated from major and minor semver version.
* For example:
* '0.16.1' -> '0016'
* '1.2.5' -> '0102'
*/
var VERSION_STR = VERSION.match(/([0-9]+)/g)
.slice(0, 2)
.map((v) => zeroFill(2, v))
.join('')
/**
* Version prefix string (used in peer ID). WebTorrent uses the Azureus-style
* encoding: '-', two characters for client id ('WW'), four ascii digits for version
* number, '-', followed by random numbers.
* For example:
* '-WW0102-'...
*/
var VERSION_PREFIX = '-WD' + VERSION_STR + '-'
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq
var client = window.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
// HACK #2: Windows: Disable WebRTC to fix Chrome 50 / Electron 1.1.[1-3] crash.
// Issue: https://github.com/electron/electron/issues/5629
wrtc: process.platform === 'linux'
}
peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
})
// WebTorrent-to-HTTP streaming sever
var server = window.server = null
var server = null
// Used for diffing, so we only send progress updates when necessary
var prevProgress = window.prevProgress = null
var prevProgress = null
init()
@@ -72,7 +92,12 @@ function init () {
ipc.send('ipcReadyWebTorrent')
window.addEventListener('error', (e) =>
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
true)
setInterval(updateTorrentProgress, 1000)
console.timeEnd('init')
}
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
@@ -182,7 +207,7 @@ function saveTorrentFile (torrentKey) {
}
// 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) {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath)
@@ -195,7 +220,7 @@ function saveTorrentFile (torrentKey) {
// 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
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) {
cb(torrentPath, exists)
})
@@ -208,10 +233,10 @@ function generateTorrentPoster (torrentKey) {
torrentPoster(torrent, function (err, buf, extension) {
if (err) return console.log('error generating poster: %o', err)
// 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)
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) {
if (err) return console.log('error saving poster: %o', err)
// show the poster
@@ -279,7 +304,7 @@ function getTorrentProgress () {
function startServer (infoHash, index) {
var torrent = client.get(infoHash)
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) {

0
static/MaterialIcons-Regular.woff2 Normal file → Executable file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Some files were not shown because too many files have changed in this diff Show More