Compare commits

...

201 Commits

Author SHA1 Message Date
DC
229143ffb2 VLC support 2016-04-27 03:27:45 -07:00
DC
3d4d1c8650 Create Torrent: exclude .DS_Store, fix drag-drop 2016-04-27 03:21:14 -07:00
DC
1479369db1 Convert Create Torrent modal to page, clean up App 2016-04-27 02:51:45 -07:00
DC
31ef283e7b Create Torrent dialog 2016-04-27 02:51:45 -07:00
DC
6b70554e63 Center video on current screen (#427)
Fixes #404
2016-04-22 19:59:17 -07:00
grunjol
9a1c329434 detect files with uppercase extensions as playable (#434) 2016-04-21 18:00:15 -03:00
Feross Aboukhadijeh
4aaf6dee05 comment 2016-04-19 23:23:16 -07:00
Feross Aboukhadijeh
86f08ee891 add changelog placeholder 2016-04-19 23:23:13 -07:00
DC
0b85ba9f32 Show an error when adding a dupe torrent
This works around a WebTorrent bug where calling client.add(torrentFilePath) to add a duplicate torrent -- in other words, one whose infoHash we're already torrenting -- creates a new torrent object and later throws an error. Inconsistently, calling client.add(magnetURI) or client.add(infoHash) to add a duplicate torrent returns the existing torrent object that we're already torrenting and doesn't throw an error.

This also fixes a prety nasty bug where pasting a dupe magnet link changed the torrentKey of an existing torrent, breaking the communication between the main and WebTorrent windows

Fixes #364
2016-04-19 20:31:13 -07:00
DC
812ce8724d Show an error when adding an invalid magnet link (#428)
Fixes #386
2016-04-19 20:09:28 -07:00
DC
06f81ff759 Remove extra filesystem dependencies 2016-04-19 06:59:11 -07:00
DC
2693075f9f Keep all torrent files and poster images in app config folder
Fixes #402
2016-04-19 06:59:10 -07:00
DC
c1713810b9 Clean up init 2016-04-19 06:35:28 -07:00
Greenkeeper
e08e5d14a2 chore(package): update electron-packager to version 7.0.0 (#421)
http://greenkeeper.io/
2016-04-18 17:56:05 -07:00
Feross Aboukhadijeh
a3d685e132 OS X: Don't stop music when tabbing to another program (#423) 2016-04-18 17:17:32 -07:00
grunjol
5471760278 srt-to-vtt@1.1.1 (#419) 2016-04-16 09:42:00 -03:00
Feross Aboukhadijeh
969c784df4 Windows Portable App (#417)
* packager: call callbacks consistently

Before this, the callbacks would not being called, which would lead to
an incomplete build on non-OS X platforms when trying to build all for
all platforms.

* packager: Always produce OS X update file regardless of --package option

This makes it consistent with how the windows build always produces the
.nupkg autoupdate files

* packager: fix duplicate npm install

Move "npm prune && npm dedupe" into the release script. Remove an extra
"npm install"

* Make Windows portable app

When a folder named "Portable Settings" exists in same folder as
WebTorrent.exe, then use it instead of the default application config
path.

Closes #358

* packager: remove redundant signing warning

* cross platform zip function

* Set config file path to match config.CONFIG_PATH

* portable app: make electron settings portable

* portable: fix poster/torrent paths

* use cross-zip

* portable app: default download folder inside 'Portable Settings'
2016-04-16 04:18:21 -07:00
DC
85e49dea6d Button styles (#414) 2016-04-15 19:02:38 -07:00
Greenkeeper
a497afe5cf chore(package): update electron-prebuilt to version 0.37.6 (#415)
http://greenkeeper.io/
2016-04-15 17:44:29 -07:00
Feross Aboukhadijeh
2333171de7 Many packager improvements; Windows signing! (#413)
* Many packager improvements; Windows signing!

* Windows signing works now! (Certs are on an external USB stick that
must be plugged into the build machine during build. We can't do the
same for OS X because certs need to exist in the login Keychain to be
found.)

Fixes #219

* Signing is now optional (so OS X and Windows contributors can run
`npm run package` without errors)

* zip, dmg, and deb arguments are now passed in as e.g. "--package=dmg"

* Print a huge warning when signing is disabled so we're less likely to
ship unsigned binaries to users.

* Make console.logs during packaging consistent and parallel
("creating..." followed by "created.")

* More aggressive signing warnings

* Warn when building OS X app on non-OS X platform (because signing
will never work on non-OS X platforms)
* Warn when building Windows app on non-Windows platform (because
signing doesn't work yet on non-Windows platforms)
2016-04-14 22:32:36 -07:00
grunjol
04318d7580 Add multiple subtitles support (#406)
* add multiple subtitles support

* cleanup and remove log
2016-04-14 21:47:50 -07:00
Feross Aboukhadijeh
5e6e5fce1e Remove "Add Fake Airplay/Chromecast" menu items (#411) 2016-04-14 19:42:25 -07:00
Feross Aboukhadijeh
af2ad46958 Only show CC icon for video (#412) 2016-04-14 19:42:13 -07:00
Feross Aboukhadijeh
432d7d4a56 Simplify play/pause handling (#410)
I found it awkward to listen to the video tags 'playing' and 'paused'
events, when we're controlling the state that defines what state it's
in in the first place.

This commit removes those listeners, in favor of just setting things to
the right state immediately when play(), pause(), or playPause() is
called.

Added play(), pause() methods for clarity.
2016-04-14 16:16:54 -07:00
Feross Aboukhadijeh
f93685811a handle case where cb is undefined 2016-04-14 16:06:24 -07:00
Feross Aboukhadijeh
914d07df03 Show error when media format is unsupported (#409)
* fix error about pop

* location-history: add optional callbacks

* set handler on first tick

discovered by @dcposch

* Show error when media format is unsupported

Before this change, the player would just get stuck on the loading
screen forever without notifying the user.
2016-04-14 15:30:26 -07:00
Feross Aboukhadijeh
9c60f104c8 Use winreg 1.1.1 instead of feross fork (#408) 2016-04-14 14:29:44 -07:00
DC
ee7e630177 Block power save (suspend) while casting (#403)
Fixes #397
2016-04-13 11:51:37 -07:00
Feross Aboukhadijeh
ae168ae885 add default torrent: The WIRED CD (#401)
* add default torrent: The WIRED CD

* remove additional unneeded files
2016-04-13 00:24:16 -07:00
DC
ad0fcaed46 Fix two tray icon bugs (#395)
* Stop media on Tray Icon > Hide

* Linux tray support: check for libappindicator1

Fixes #383
2016-04-13 00:23:18 -07:00
Karlo Luis Martinez Martos
304b81908d Windows Volume Mixer fix (#387)
Made a smaller version (32x32) of the .png icon
2016-04-13 00:15:10 -07:00
Feross Aboukhadijeh
b10f8c5bed Fix app.getPath API 2016-04-10 23:10:42 -07:00
Feross Aboukhadijeh
f6b9dbbbc4 Use Electron API to get 'Downloads' folder (#382)
Fixes #359 and #349.
2016-04-10 21:46:24 -07:00
Feross Aboukhadijeh
59cc912378 electron-packager@6 2016-04-10 21:33:12 -07:00
Feross Aboukhadijeh
33663bef3e Linux build: Fix incorrect log output (#381)
Now we use a function closure to capture the `destArch` variable so the
for loop can't change it.
2016-04-10 21:22:34 -07:00
grunjol
e75cd45ec0 packge all linux versions (#379) 2016-04-10 19:54:52 -07:00
Feross Aboukhadijeh
c98f3cd040 Fix JS error on app quit (#377)
This was a rare race condition during app shutdown where a 'wt-'
message would be sent from the hidden webtorrent window to the main
window after the main window was already closed.

Fixes #373
2016-04-10 18:50:00 -07:00
Feross Aboukhadijeh
4c4caba002 Fix text field focus after repeated open (#376)
For #333
2016-04-10 18:34:11 -07:00
Feross Aboukhadijeh
45f6cc5247 Preload sound files for instant playback (#374)
* rm dist at start of build

* renderer style

* preload sound files for instant playback

The first time a sound file is played, the Audio object is cached.

5s after startup, all sound files are automatically preloaded.
2016-04-10 16:46:46 -07:00
DC
69460db294 Exit media when user closes window (#348) 2016-04-10 16:46:34 -07:00
Diego Rodríguez Baquero
f8095fcdbf Use latest webtorrent (#366)
While we have 0.x versions :)
2016-04-10 16:44:11 -07:00
Feross Aboukhadijeh
1a0a2b3658 Add subtitle support (via drag-n-drop) (#361)
* issue template

* cleanup closePlayer() and stopServer()

* Add subtitle support (via drag-n-drop)

Drag and drop a subtitles file (.srt or .vtt) onto the player (or the
app icon on OS X) to add subtitles to the currently playing video.

For #281

* add multiple subtitles structure

* add open subtitle dialog from cc player controls
2016-04-10 16:42:18 -07:00
Alex
f9141dd39c 32 bit build for Linux (#369)
* Add 32 bit arch for Linux

* Fix trailing spaces
2016-04-10 16:38:35 -07:00
Feross Aboukhadijeh
8c2d49f029 Enforce minimimum window size when resizing player (#342)
For audio-only .mov files, which are 0x0.

Closes #340
2016-04-07 21:27:25 -07:00
Evan Miller
da1e120de9 Create error on zero-byte poster (#352)
* Error on zero-byte poster

* return cb to stop execution
2016-04-07 21:25:00 -07:00
Rémi Jouannet
457aca25ee add mute/unmute with the volume icon (#355) 2016-04-07 21:06:28 -07:00
grunjol
ae73ae29c4 add volume icon and slider (#330) 2016-04-07 14:24:23 -03:00
DC
5abf421f11 Auto updater: tell server which platform we're on 2016-04-07 04:35:23 -07:00
Feross Aboukhadijeh
e792532051 CHANGELOG 2016-04-07 03:15:08 -07:00
Feross Aboukhadijeh
5c39665b6a 0.3.3 2016-04-07 03:07:56 -07:00
Feross Aboukhadijeh
d1c4579398 Depend on master electron-packager to fix OS X icon 2016-04-07 03:06:25 -07:00
Feross Aboukhadijeh
d80d8ef1f5 0.3.2 2016-04-07 01:02:07 -07:00
Feross Aboukhadijeh
d49a8e772f Faster startup time (50ms) 2016-04-07 00:58:37 -07:00
Feross Aboukhadijeh
1947a03e94 Changelog 2016-04-07 00:25:19 -07:00
Feross Aboukhadijeh
bc6ae4523f Revert "TEMPORARY: Comment out code that requires Electron 0.37.4"
This reverts commit 9c550997c9.
2016-04-06 21:57:11 -07:00
Greenkeeper
442ac9184f chore(package): update electron-prebuilt to version 0.37.5
http://greenkeeper.io/
2016-04-06 21:52:25 -07:00
Feross Aboukhadijeh
824f4ce3cf CHANGELOG 2016-04-06 21:29:34 -07:00
Feross Aboukhadijeh
cc324024ba Add ISSUE_TEMPLATE 2016-04-06 21:29:25 -07:00
grunjol
0921f89eb7 Linux .deb file: update symlink on package update
* overwrite symlink on update

* fix size and list definition
2016-04-07 00:48:00 -03:00
DC
628c93bc1e Pause audio reliably when closing the window
Before it only paused video...
2016-04-06 05:47:39 -07:00
Feross Aboukhadijeh
25109a7ebb 0.3.1 2016-04-06 05:10:37 -07:00
Feross Aboukhadijeh
e6963d0307 CHANGELOG 2016-04-06 05:09:14 -07:00
DC
9a2f16b29a Add crash reporting 2016-04-06 05:05:26 -07:00
DC
6a17aa7c76 Cast screen background: cover, don't tile 2016-04-06 04:48:56 -07:00
Feross Aboukhadijeh
80c549ae77 0.3.0 2016-04-06 03:42:36 -07:00
DC
9d4c65d1b1 Queue messages for WebTorrent window until it's ready 2016-04-06 03:28:56 -07:00
DC
2e08eea43d Remove Developer > Reload
It will leave the app in a bad state, now that we hae a separate WebTorrent process
2016-04-06 03:28:56 -07:00
Feross Aboukhadijeh
27914ef13b Merge branch 'f/ux' 2016-04-06 03:27:57 -07:00
Feross Aboukhadijeh
10d9678946 Shortcut tweak 2016-04-06 03:27:43 -07:00
Feross Aboukhadijeh
d8a150a60d Torrent list title: Show more of the title 2016-04-06 02:57:20 -07:00
DC
5abc319ecf Show torrent metadata on 'metadata' event, don't wait for 'ready' 2016-04-06 02:55:41 -07:00
DC
e0f02ace9e Open Torrent File should only let you select torrent files 2016-04-06 02:55:41 -07:00
DC
28c386d916 Fix file list enabled/disabled logic
* Files that are either 100% downloaded OR streamble should be enabled
* Disabled files should not be clickable

This fixes both bugs.
2016-04-06 02:55:41 -07:00
DC
5022877b04 Stop casting if you hit ESC or Back while casting 2016-04-06 02:55:40 -07:00
DC
aac862cddf Only show play button for torrents that are playable 2016-04-06 02:55:40 -07:00
DC
08a806a643 Add setBounds logging 2016-04-06 02:55:40 -07:00
DC
5e5d8799a7 Fix playback bug: ensure openPlayer() always calls its callback 2016-04-06 02:55:40 -07:00
Feross Aboukhadijeh
3e079a2fb4 Torrent list title: Show more of the title 2016-04-06 02:54:45 -07:00
Feross Aboukhadijeh
464b41a435 CHANGELOG 2016-04-06 02:43:12 -07:00
DC
3ad815cec4 Add File > Quit for Linux users with broken system trays
Works around #303
2016-04-06 01:33:40 -07:00
DC
85b6ca0639 Fix player flakiness
* No more pause icon in the file list
* Reset state.playng completely after each play
* Fixes #318: false "cannot add dupe torrent" error
2016-04-06 00:58:34 -07:00
Feross Aboukhadijeh
8629fc956d clear cast interval when casting stops; naming
* fix ipc logs

* minWidth 425

So title "WebTorrent (BETA)" doesn't get cut off

* clear cast interval when casting stops; naming

Fix #300
2016-04-05 23:12:23 -07:00
Feross Aboukhadijeh
292898de9e Merge pull request #317 from feross/f/psn
Ignore OS X -psn_xxxx command line argument
2016-04-05 22:24:46 -07:00
Feross Aboukhadijeh
c26b6713de Ignore OS X -psn_xxxx command line argument
Fix #214
2016-04-05 22:21:22 -07:00
Feross Aboukhadijeh
cec7556f9a Merge pull request #316 from feross/f/prevent
Prevent killing the WebTorrent process
2016-04-05 19:47:24 -07:00
Feross Aboukhadijeh
d5340bf644 Fix copy magnet URI to clipboard
Fox #304
2016-04-05 19:45:16 -07:00
Feross Aboukhadijeh
08948e8258 Prevent killing the WebTorrent process
Also, add shortcut for opening webtorrent inspector
2016-04-05 19:31:23 -07:00
DC
6447966e91 Hide DL status on paused torrents
Fixes #309
2016-04-05 19:27:39 -07:00
DC
3080001d3d Make WebTorrent process easier to debug 2016-04-05 19:17:57 -07:00
Feross Aboukhadijeh
b8d9d29e90 Merge pull request #314 from feross/cast-connected
Use Chromecast connected icon when applicable
2016-04-05 19:15:17 -07:00
Feross Aboukhadijeh
ef275b8888 Use Chromecast connected icon when applicable 2016-04-05 19:13:00 -07:00
Feross Aboukhadijeh
ce05ae202c Merge pull request #312 from feross/cast-errors
Consistent error handling on all cast devices
2016-04-05 19:12:04 -07:00
Feross Aboukhadijeh
ee7205bb84 Consistent error handling on all cast devices 2016-04-05 19:01:12 -07:00
DC
46a6ded095 Developers menu button to show dev tools 2016-04-05 19:01:00 -07:00
Feross Aboukhadijeh
074c5824e8 Merge pull request #307 from feross/dc/ux
Show video controls immediately on mouse move
2016-04-05 18:46:03 -07:00
Feross Aboukhadijeh
6d8f70ac35 dlnacasts@0.0.3 2016-04-05 18:42:17 -07:00
Feross Aboukhadijeh
6329afc6a2 Merge pull request #310 from feross/notif
Notification Fixes
2016-04-05 18:39:55 -07:00
DC
b31281c35e Don't load cast module when seeking 2016-04-05 18:35:32 -07:00
Feross Aboukhadijeh
b3da0fc05c show notifications even when window is focus 2016-04-05 18:34:30 -07:00
Feross Aboukhadijeh
2549307a7e style 2016-04-05 18:34:09 -07:00
Feross Aboukhadijeh
cf3b319fc7 Notification: Focus/unminimize/activate window on click
Before, we would only focus, but not unminimize or show, etc.
2016-04-05 18:33:48 -07:00
Feross Aboukhadijeh
11f78b2881 Fix torrent name missing in done notification
Fix #305
2016-04-05 18:26:27 -07:00
Feross Aboukhadijeh
32e391e21e use feross/dlnacasts
to fix missing mime dep until
https://github.com/grunjol/dlnacasts/pull/1 is merged
2016-04-05 18:20:17 -07:00
DC
d2b95163fb Show video controls immediately on mouse move 2016-04-05 17:58:38 -07:00
Feross Aboukhadijeh
c83f345977 Fixes for PR #299 2016-04-05 16:35:55 -07:00
DC
db9e3e90c5 WebTorrent process
* Separate hidden window, with its own renderer process, for WebTorrent
  (Must be a window. You cannot run WebRTC at all in a Web Worker, and you can't
   run it well in a node process like the electron main process.)

* Disabled the create-torrent-modal for now. That gives us a consistent UX
  regardless of whether the user dragged files or folders onto the app or opened
  the Create New Torrent menu item.

* Main process routes all messages between the main and webtorrent windows.

* The renderer index.js is smaller now (but still too big), with the WebTorrent
  interface moved to webtorrent.js / it's own process.

* The UI should be faster now, and should not lag under load.
2016-04-05 15:36:26 -07:00
Feross Aboukhadijeh
38ce25592f run npm publish as part of release process 2016-04-05 15:19:20 -07:00
Feross Aboukhadijeh
3ac502e1c3 Merge pull request #302 from feross/media-queries
Remove media queries
2016-04-05 15:17:55 -07:00
Feross Aboukhadijeh
0bf9eba043 Remove media queries
They make the app feel too much like a webpage. I don't like the UI
jumping around as I resize the window.
2016-04-05 14:07:05 -07:00
Feross Aboukhadijeh
d6633d94bf Merge pull request #287 from grunjol/feature-dlna-support
Add DLNA support
2016-04-05 13:56:01 -07:00
grunjol
ac07023ca5 inform user about connection/unsupported errors 2016-04-05 13:36:26 -03:00
grunjol
57e5eed47f fix DLNA icon, calls, remove unneeded depencendies in dlnacasts 2016-04-05 12:23:41 -03:00
grunjol
6aa7058184 add DLNA support 2016-04-05 12:23:40 -03:00
Feross Aboukhadijeh
2509c0c951 Windows: Fix iconUrl path 2016-04-05 04:00:45 -07:00
Feross Aboukhadijeh
b17043230f Merge pull request #298 from feross/win
Add crash reporting
2016-04-04 23:52:28 -07:00
Feross Aboukhadijeh
76487326f1 Move crash report URL to config.js 2016-04-04 23:44:51 -07:00
Feross Aboukhadijeh
997aa7c922 TEMPORARY: Comment out code that requires Electron 0.37.4 2016-04-04 23:27:12 -07:00
Feross Aboukhadijeh
22421e365c Revert "chore(package): update electron-prebuilt to version 0.37.4"
This reverts commit 509e2804e0.
2016-04-04 23:12:11 -07:00
Feross Aboukhadijeh
7c38f374f3 Setup crash reporter 2016-04-04 23:10:47 -07:00
Feross Aboukhadijeh
c422151ef8 HACK: add setTimeout to uninstaller 2016-04-04 20:55:19 -07:00
Feross Aboukhadijeh
3b805f6cc3 try-catch console.timeEnd()
If user refreshes after renderer crashes, the this line throws an
exception
2016-04-04 20:40:39 -07:00
Feross Aboukhadijeh
65a3443e7d Fix clean script 2016-04-04 20:36:54 -07:00
Feross Aboukhadijeh
0d17b69c3a Merge pull request #297 from feross/win
Uninstall handlers on Windows uninstall/npm run clean
2016-04-04 20:34:44 -07:00
Feross Aboukhadijeh
7970066ccd Uninstall handlers on Windows uninstall/npm run clean 2016-04-04 20:27:48 -07:00
Feross Aboukhadijeh
eddd81e8d7 Merge pull request #295 from feross/win
Windows Installer improvements
2016-04-04 19:58:29 -07:00
Feross Aboukhadijeh
60daeb9225 fixpack 2016-04-04 19:36:21 -07:00
Feross Aboukhadijeh
361e811e93 Windows: Final .exe name: WebTorrentSetup-vX.X.X.exe 2016-04-04 19:36:06 -07:00
Feross Aboukhadijeh
38022bb3df Windows: use feross fork for setupExe option 2016-04-04 19:23:10 -07:00
Feross Aboukhadijeh
31a9133eba Merge pull request #294 from feross/darwin
OS X: Name update file with "-darwin.zip"
2016-04-04 18:45:12 -07:00
Feross Aboukhadijeh
663e607797 Windows: Add valid iconUrl – for building on non-Windows platforms 2016-04-04 18:42:46 -07:00
Feross Aboukhadijeh
82c9e8f8ab Windows packager: use Github URL for remoteRelease 2016-04-04 18:23:27 -07:00
Feross Aboukhadijeh
3d8429fb12 New update URL: /desktop/update 2016-04-04 18:22:58 -07:00
Feross Aboukhadijeh
5d2b5c1c81 OS X: Name update file with "-darwin.zip" 2016-04-04 17:58:59 -07:00
Feross Aboukhadijeh
c20a809014 Windows: use remote releases URL for updates 2016-04-04 17:55:08 -07:00
Feross Aboukhadijeh
b7858a03b5 Windows: don't generate .msi 2016-04-04 17:54:58 -07:00
Feross Aboukhadijeh
27bf803539 Windows: name Setup.exe with version number 2016-04-04 17:54:50 -07:00
Feross Aboukhadijeh
0b64dcf4ec Windows: Fix protocol registration 2016-04-04 17:52:19 -07:00
Feross Aboukhadijeh
30631fb879 Merge pull request #292 from grunjol/fix-follow-debian-convention
Follow debian package name convention
2016-04-04 16:59:23 -07:00
grunjol
6c275a33ea follow debian package name convention 2016-04-04 20:25:23 -03:00
Feross Aboukhadijeh
fe00d162e1 Merge pull request #290 from feross/deb
Name the .deb file consistently
2016-04-04 15:32:54 -07:00
Feross Aboukhadijeh
3599ec077b Name the .deb file consistently
Instead of "webtorrent-desktop_0.2.0-1_amd64.deb", name it
"WebTorrent-v0.2.0.deb"
2016-04-04 15:31:24 -07:00
Feross Aboukhadijeh
a65ee51358 Merge pull request #264 from grunjol/feature-debian-package
Linux: Create .deb package
2016-04-04 15:09:41 -07:00
Feross Aboukhadijeh
50b8f42312 Merge pull request #289 from feross/fix-tray
Windows/Linux: Fix broken Quit option in Tray icon
2016-04-04 13:50:32 -07:00
Feross Aboukhadijeh
ccaf0de63d Windows/Linux: Fix broken Quit option in Tray icon
This also cleans up the code by handling quit in the same way for all
platforms, removing the special case in tray.js for darwin.

We already have a 'before-quit' handler in main/index.js, so this is
now handled there :)
2016-04-04 13:40:27 -07:00
Feross Aboukhadijeh
9d8e79fb35 Windows: Shorter install splash screen 2016-04-04 13:18:59 -07:00
Feross Aboukhadijeh
189ec60f4e Merge pull request #286 from feross/protocol-handler
OS X: Use app.setAsDefaultProtocolClient
2016-04-04 13:17:43 -07:00
Feross Aboukhadijeh
6fcc9c23b8 OS X: Use app.setAsDefaultProtocolClient
- OS X: Register as default handler for "magnet" on startup

Also:

- Log errors in the renderer process, like the rest of errors in the
main process
- Windows: Less chance of registry write race condition

Fix #285
2016-04-04 04:57:23 -07:00
Feross Aboukhadijeh
d0515bb2a2 Windows: register protocol handlers on startup
Just like other OSes. There's no reason for there to be a difference.
2016-04-04 04:43:27 -07:00
grunjol
4f79fbfc41 refactor cast module
* refactor cast module

* fix standard

* remove debug dependency
2016-04-04 01:22:02 -07:00
Feross Aboukhadijeh
5ccabe756a fix: OS X has no tray icon 2016-04-04 01:08:18 -07:00
DC
fb42b84245 Save state when quitting on OSX 2016-04-04 01:08:18 -07:00
DC
ee5b6ea472 PR #278 fixes 2016-04-04 01:08:18 -07:00
DC
609df9eb1e Torrent warning & error events
Fixes #253
2016-04-04 01:08:18 -07:00
DC
20157f39ee Seed in place, don't copy to /tmp
Fixes https://github.com/feross/webtorrent-desktop/issues/254
2016-04-04 01:08:18 -07:00
Feross Aboukhadijeh
c975f2f2d0 Merge pull request #283 from feross/greenkeeper-electron-prebuilt-0.37.4
Update electron-prebuilt to version 0.37.4 🚀
2016-04-03 21:22:54 -07:00
greenkeeperio-bot
c9559419cd chore(package): update electron-prebuilt to version 0.37.4
http://greenkeeper.io/
2016-04-03 04:08:48 -07:00
grunjol
a064794c87 set error handling in pre and post init script 2016-04-02 09:20:00 -03:00
grunjol
faee840073 update installer version 2016-04-02 09:20:00 -03:00
grunjol
dd8ed77153 add package invocation change for deb/zip package 2016-04-02 09:20:00 -03:00
grunjol
27727a7a97 Linux: create Debian package 2016-04-02 09:20:00 -03:00
Feross Aboukhadijeh
f3771dd645 chore(package): update webtorrent to version 0.90.0
Update webtorrent to version 0.90.0 🚀
2016-04-02 02:03:01 -07:00
greenkeeperio-bot
52d909c374 chore(package): update webtorrent to version 0.90.0
http://greenkeeper.io/
2016-04-02 01:22:56 -07:00
Feross Aboukhadijeh
ae69d186f6 Fixes for PR #279 2016-04-02 00:39:37 -07:00
DC
04b7cdb24f Don't re-verify unchanged files
Keep track of the most recent file modtimes at which we verified that the torrent piece hashes all match.
2016-04-02 00:38:29 -07:00
Feross Aboukhadijeh
8868ec6d68 Update CONTRIBUTING.md 2016-04-01 21:27:47 -07:00
DC
15242666ce UI: Pending torrent is not necessarily from magnet link 2016-04-01 03:54:17 -07:00
Feross Aboukhadijeh
a5255a3621 webtorrent@0.89 2016-04-01 01:13:39 -07:00
Feross Aboukhadijeh
76072b1be1 fix badge link issue
the spaces between badges were linked
2016-04-01 00:31:52 -07:00
Feross Aboukhadijeh
7485750b41 electron-prebuilt@0.37.3 2016-04-01 00:12:58 -07:00
Feross Aboukhadijeh
f7482fce5b remove debug dependency 2016-04-01 00:09:51 -07:00
Feross Aboukhadijeh
e402fb93fb webtorrent-app -> webtorrent-desktop 2016-04-01 00:09:45 -07:00
DC
520ab99b21 Toggle show/hide in tray icon
Fix state saving on app exit
2016-03-31 08:38:35 -07:00
DC
742061183b Show new files immediately when seeding
Fixes #208
2016-03-31 08:07:55 -07:00
DC
8a16ddb3d0 Don't log mediaTimeUpdate, less noise 2016-03-30 20:48:25 -07:00
DC
af783e0532 Save audio metadata, after extraction
Fixes #260
2016-03-30 20:47:04 -07:00
Feross Aboukhadijeh
d1806d9503 0.2.0 2016-03-29 03:58:38 -07:00
Feross Aboukhadijeh
1ce894c134 changelog 2016-03-29 03:57:29 -07:00
Feross Aboukhadijeh
8b1d7e5394 Changelog v0.2.0 2016-03-29 03:52:19 -07:00
DC
39a6832631 Minimize to tray
Fixes #150
2016-03-29 03:51:15 -07:00
Feross Aboukhadijeh
9694a9f5fd CHANGELOG 2016-03-29 03:16:23 -07:00
Feross Aboukhadijeh
0683255281 webtorrent@0.88.1 2016-03-29 02:53:08 -07:00
DC
3a76629f09 UX polish: highlight drag-drop even when placeholder isn't visible
Before, if you scrolled so that the bottom placeholder wasn't visible, there was no indication that the app is still a drag target.
2016-03-29 02:37:25 -07:00
DC
630e8611ba Add Cosmos Laundromat as a default torrent 2016-03-29 00:41:48 -07:00
DC
cc273e7312 Loading spinner for videos
Only worked for audio before
2016-03-29 00:41:42 -07:00
DC
c8da083526 Make npm run package work on Mac and Linux 2016-03-28 23:09:07 -07:00
DC
840966c7f0 Remove global shortcuts when player isn't active 2016-03-28 22:57:40 -07:00
Feross Aboukhadijeh
8ce7235c2b webtorrent@0.88
Lots of perf fixes for #256
2016-03-28 22:55:23 -07:00
DC
f70cef2cee Loading spinner: center, text-overflow ellipsis 2016-03-28 22:36:45 -07:00
DC
dc2e2a82e7 Loading spinner: show download speed 2016-03-28 21:23:52 -07:00
DC
c70fef3feb Show spinner when audio/video is stalled
Fixes #243
2016-03-28 21:16:06 -07:00
DC
1afedac12f Fix version, my bad 2016-03-28 20:10:02 -07:00
DC
b8ff4b378b Linux updater: better message 2016-03-28 18:52:09 -07:00
DC
86069a7173 Linux update notifications
Fixes #257
2016-03-28 16:16:43 -07:00
Feross Aboukhadijeh
25db4eec9d Update package.js for electron-packager v6 2016-03-28 13:55:18 -07:00
Feross Aboukhadijeh
9080a69e3c Merge pull request #258 from feross/greenkeeper-electron-packager-6.0.0
Update electron-packager to version 6.0.0 🚀
2016-03-28 13:53:28 -07:00
Nate Goldman
986fbf5418 app -> desktop 2016-03-28 13:50:27 -07:00
Feross Aboukhadijeh
df04363f7c WebTorrent Desktop 2016-03-28 13:36:51 -07:00
greenkeeperio-bot
57117e9043 chore(package): update electron-packager to version 6.0.0
http://greenkeeper.io/
2016-03-28 07:59:53 -07:00
Feross Aboukhadijeh
5dd104a588 update 0.1.1 changelog 2016-03-28 01:18:13 -07:00
Feross Aboukhadijeh
849365f839 package .zip files for Linux 2016-03-28 01:17:57 -07:00
Feross Aboukhadijeh
e3e32f154c Fixes for PR #250 2016-03-28 00:48:14 -07:00
53 changed files with 3759 additions and 1339 deletions

9
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,9 @@
**What version of WebTorrent Desktop?** (See the 'About WebTorrent' menu)
**What operating system and version?**
**What did you do?**
**What did you expect to happen?**
**What actually happened?**

View File

@@ -1,6 +1,112 @@
# WebTorrent.app Version History # WebTorrent Desktop Version History
## v0.1.1 ## UNRELEASED
### Added
### Changed
- Use Squirrel.Windows 1.3.0
- Fix installing when the app is already installed
- Don't kill unrelated processes on uninstall
### Fixed
## v0.3.3 - 2016-04-07
### Fixed
- App icon was incorrect (OS X)
## v0.3.2 - 2016-04-07
### Added
- Register WebTorrent as default handler for magnet links (OS X)
### Changed
- Faster startup time (50ms)
- Update Electron to 0.37.5
- Remove the white flash when loading pages and resizing the window
- Fix crash when sending IPC messages
### Fixed
- Fix installation bugs with .deb file (Linux)
- Pause audio reliably when closing the window
- Enforce minimimum window size when resizing player (for audio-only .mov files, which are 0x0)
## v0.3.1 - 2016-04-06
### Added
- Add crash reporter to torrent engine process
### Fixed
- Fix cast screen background: cover, don't tile
## v0.3.0 - 2016-04-06
### Added
- **Ubuntu/Debian support!** (.deb installer)
- **DLNA streaming support**
- Add "File > Quit" menu item (Linux)
- App uninstaller (Windows)
- Crash reporting
### Changed
- On startup, do not re-verify files when timestamps are unchanged
- Moved torrent engine to an independent process, for better UI performance
- Removed media queries (UI resizing based on window width)
- Improved Chromecast icon, when connected
### Fixed
- "Download Complete" notification shows consistently
- Create new torrents and seed them without copying to temporary folder
- Clicking the "Download Complete" notification will always activate app
- Fixed harmless "-psn_###" error on first app startup
- Hide play buttons on unplayable torrents
- Better error handling when Chromecast/Airplay cannot connect
- Show player controls immediately on mouse move
- When creating a torrent, show it in UI immediately
- Stop casting to TV when player is closed
- Torrent engine: Fixed memory leaks in `torrent-discovery` and `bittorrent-tracker`
- Torrent engine: Fixed sub-optimal tcp/webrtc connection timeouts
- Torrent engine: Throttle web seed connections to maximum of 4
Thanks to @dcposch, @grunjol, and @feross for contributing to this release.
## v0.2.0 - 2016-03-29
### Added
- Minimise to tray (Windows, Linux)
- Show spinner and download speed when player is stalled waiting for data
- Highlight window on drag-and-drop
- Show notification to update to new app version (Linux)
- We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so
Linux users need to download new versions manually.
### Changed
- Renamed WebTorrent.app to WebTorrent Desktop
- Add Cosmos Laundromat as a default torrent
### Fixed
- Only capture media keys when player is active
- Update WebTorrent to 0.88.1 for performance improvements
- When seeding, do not proactively connect to new peers
- When seeding, do not accept new peers from peer exchange (ut_pex)
- Fixed leaks, and other improvements that result in less garbage collection
Thanks to @dcposch, @ungoldman, and @feross for contributing to this release.
## v0.1.1 - 2016-03-28
- Performance improvements - Performance improvements
- Improve app startup time by over 100% - Improve app startup time by over 100%
@@ -17,8 +123,13 @@
- Fixed - Fixed
- Crash when ".local/share/{applications,icons}" path did not exist (Linux) - Crash when ".local/share/{applications,icons}" path did not exist (Linux)
- WebTorrent executable can be moved without breaking torrents in the client - WebTorrent executable can be moved without breaking torrents in the client
- Video progress bar shows progress for current file, not full torrent
- Video player window shows file title instead of torrent title
## v0.1.0 Thanks to @dcposch, @ungoldman, @rom1504, @grunjol, @Flet, and @feross for contributing to
this release.
## v0.1.0 - 2016-03-25
- **Windows support!** - **Windows support!**
- Includes auto-updater, just like the OS X version. - Includes auto-updater, just like the OS X version.
@@ -33,20 +144,20 @@
**NOTE:** OS X users must install v0.1.0 manually because the app bundle ID was changed in this release, and the auto-updater cannot handle this condition. **NOTE:** OS X users must install v0.1.0 manually because the app bundle ID was changed in this release, and the auto-updater cannot handle this condition.
Thanks to @dcposch, @ngoldman, and @feross for contributing to this release. Thanks to @dcposch, @ungoldman, and @feross for contributing to this release.
## v0.0.1 ## v0.0.1 - 2016-03-21
- Wait 10 seconds (instead of 60 seconds) after app launch before checking for updates. - Wait 10 seconds (instead of 60 seconds) after app launch before checking for updates.
## v0.0.0 ## v0.0.0 - 2016-03-21
The first official release of WebTorrent.app, the streaming torrent client for OS X, The first official release of WebTorrent Desktop, the streaming torrent client for OS X,
Windows, and Linux. For now, we're only releasing binaries for OS X. Windows, and Linux. For now, we're only releasing binaries for OS X.
WebTorrent.app is in ALPHA and under very active development  expect lots more polish in WebTorrent Desktop is in ALPHA and under very active development  expect lots more polish in
the coming weeks! If you know JavaScript and want to help us out, there's the coming weeks! If you know JavaScript and want to help us out, there's
[lots to do](https://github.com/feross/webtorrent-app/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+contribution%22)! [lots to do](https://github.com/feross/webtorrent-desktop/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+contribution%22)!
### Features ### Features

View File

@@ -4,44 +4,72 @@ Contributions welcome!
**Before spending lots of time on something, ask for feedback on your idea first!** **Before spending lots of time on something, ask for feedback on your idea first!**
Please search issues and pull requests before adding something new to avoid duplicating efforts and conversations. Please search issues and pull requests before adding something new to avoid duplicating
efforts and conversations.
This project welcomes non-code contributions, too! The following types of contributions are welcome: This project welcomes non-code contributions, too! The following types of contributions
are welcome:
- **Ideas**: participate in an issue thread or start your own to have your voice heard. - **Ideas**: participate in an issue thread or start your own to have your voice heard.
- **Writing**: contribute your expertise in an area by helping expand the included content. - **Writing**: contribute your expertise in an area by helping expand the included docs.
- **Copy editing**: fix typos, clarify language, and generally improve the quality of the content. - **Copy editing**: fix typos, clarify language, and improve the quality of the docs.
- **Formatting**: help keep content easy to read with consistent formatting. - **Formatting**: help keep docs easy to read with consistent formatting.
## Code Style ## Code Style
[![standard][standard-image]][standard-url] [![standard][standard-image]][standard-url]
This repository uses [`standard`][standard-url] to maintain code style and consistency, and to avoid style arguments. `npm test` runs `standard` automatically, so you don't have to! This repository uses [`standard`][standard-url] to maintain code style and consistency,
and to avoid style arguments. `npm test` runs `standard` automatically, so you don't have
to!
[standard-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg [standard-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg
[standard-url]: https://github.com/feross/standard [standard-url]: https://github.com/feross/standard
# Project Governance ## Project Governance
**This is an [OPEN Open Source Project](http://openopensource.org/).** Individuals making significant and valuable contributions are given commit-access to the
project to contribute as they see fit. This project is more like an open wiki than a
standard guarded open source project.
Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. ### Rules
## Rules
There are a few basic ground-rules for contributors: There are a few basic ground-rules for contributors:
1. **No `--force` pushes** or modifying the Git history in any way. 1. **No `--force` pushes** or modifying the Git history in any way.
1. **Non-master branches** ought to be used for ongoing work. 2. **Non-master branches** should be used for ongoing work.
1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 3. **Significant modifications** like API changes should be subject to a **pull request**
1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. to solicit feedback from other contributors.
1. Contributors should attempt to adhere to the prevailing code style. 4. **Pull requests** are *encouraged* for all contributions to solicit feedback, but left to
the discretion of the contributor.
## Releases ### Releases
Declaring formal releases remains the prerogative of the project maintainer. Declaring formal releases remains the prerogative of the project maintainer.
## Changes to this arrangement ### Changes to this arrangement
This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. This is an experiment and feedback is welcome! This document may also be subject to pull-
requests or changes by contributors where you believe you have something valuable to add
or change.
## Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
- (a) The contribution was created in whole or in part by me and I have the right to
submit it under the open source license indicated in the file; or
- (b) The contribution is based upon previous work that, to the best of my knowledge, is
covered under an appropriate open source license and I have the right under that license
to submit that work with modifications, whether created in whole or in part by me, under
the same open source license (unless I am permitted to submit under a different
license), as indicated in the file; or
- (c) The contribution was provided directly to me by some other person who certified
(a), (b) or (c) and I have not modified it.
- (d) I understand and agree that this project and the contribution are public and that a
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.

View File

@@ -2,7 +2,7 @@
<br> <br>
<a href="https://webtorrent.io"><img src="https://webtorrent.io/img/WebTorrent.png" alt="WebTorrent" width="200"></a> <a href="https://webtorrent.io"><img src="https://webtorrent.io/img/WebTorrent.png" alt="WebTorrent" width="200"></a>
<br> <br>
WebTorrent.app WebTorrent Desktop
<br> <br>
<br> <br>
</h1> </h1>
@@ -10,28 +10,19 @@
<h4 align="center">The streaming torrent client. For OS X, Windows, and Linux.</h4> <h4 align="center">The streaming torrent client. For OS X, Windows, and Linux.</h4>
<p align="center"> <p align="center">
<a href="https://gitter.im/feross/webtorrent"> <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>
<img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg" <a href="https://travis-ci.org/feross/webtorrent-desktop"><img src="https://img.shields.io/travis/feross/webtorrent-desktop/master.svg" alt="Travis"></a>
alt="Gitter"> <a href="https://github.com/feross/webtorrent-desktop/releases"><img src="https://img.shields.io/github/release/feross/webtorrent-desktop.svg" alt="Release"></a>
</a>
<a href="https://travis-ci.org/feross/webtorrent-app">
<img src="https://img.shields.io/travis/feross/webtorrent-app/master.svg"
alt="Travis Build">
</a>
<a href="https://github.com/feross/webtorrent-app/releases">
<img src="https://img.shields.io/github/release/feross/webtorrent-app.svg"
alt="Latest Release Version">
</a>
</p> </p>
## Install ## Install
**WebTorrent.app** is still under very active development. You can download the latest version from the [releases](https://github.com/feross/webtorrent-app/releases) page. **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.
## Screenshot ## Screenshot
<p align="center"> <p align="center">
<img src="./static/screenshot.png" width="562" height="630" alt="screenshot" align="center"> <img src="https://webtorrent.io/img/screenshot-main.png" width="562" height="630" alt="screenshot" align="center">
</p> </p>
## How to Contribute ## How to Contribute
@@ -62,7 +53,20 @@ To build for one platform:
$ npm run package -- [platform] $ npm run package -- [platform]
``` ```
Where `[platform]` is `darwin`, `linux`, or `win32`. Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default).
The following optional arguments are available:
- `--sign` - Sign the application (OS X, Windows)
- `--package=[type]` - Package single output type.
- `deb` - Debian package
- `zip` - Linux zip file
- `dmg` - OS X 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.
#### Windows build notes #### Windows build notes
@@ -84,4 +88,3 @@ brew install wine
## License ## License
MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org). MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org).

View File

@@ -1,17 +1,22 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* Remove all traces of WebTorrent.app from the system (config and temp files). * Remove all traces of WebTorrent Desktop from the system (config and temp files).
* Useful for developers. * Useful for developers.
*/ */
var config = require('../config')
var os = require('os') var os = require('os')
var path = require('path') var path = require('path')
var pathExists = require('path-exists') var pathExists = require('path-exists')
var rimraf = require('rimraf') var rimraf = require('rimraf')
var config = require('../config')
var handlers = require('../main/handlers')
rimraf.sync(config.CONFIG_PATH) rimraf.sync(config.CONFIG_PATH)
var tmpPath = path.join(pathExists.sync('/tmp') ? '/tmp' : os.tmpDir(), 'webtorrent') var tmpPath = path.join(pathExists.sync('/tmp') ? '/tmp' : os.tmpDir(), 'webtorrent')
rimraf.sync(tmpPath) rimraf.sync(tmpPath)
// Uninstall .torrent file and magnet link handlers
handlers.uninstall()

View File

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

View File

@@ -4,18 +4,48 @@
* Builds app binaries for OS X, Linux, and Windows. * Builds app binaries for OS X, Linux, and Windows.
*/ */
var config = require('../config')
var cp = require('child_process') var cp = require('child_process')
var electronPackager = require('electron-packager') var electronPackager = require('electron-packager')
var fs = require('fs') var fs = require('fs')
var minimist = require('minimist')
var mkdirp = require('mkdirp')
var path = require('path') var path = require('path')
var pkg = require('../package.json')
var rimraf = require('rimraf') var rimraf = require('rimraf')
var series = require('run-series')
var zip = require('cross-zip')
var config = require('../config')
var pkg = require('../package.json')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
/*
* Path to folder with the following files:
* - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt)
*/
var CERT_PATH = process.platform === 'win32'
? 'D:'
: '/Volumes/Certs'
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
var argv = minimist(process.argv.slice(2), {
boolean: [
'sign'
],
default: {
package: 'all',
sign: false
},
string: [
'package'
]
})
function build () { function build () {
var platform = process.argv[2] rimraf.sync(DIST_PATH)
var platform = argv._[0]
if (platform === 'darwin') { if (platform === 'darwin') {
buildDarwin(printDone) buildDarwin(printDone)
} else if (platform === 'win32') { } else if (platform === 'win32') {
@@ -23,10 +53,10 @@ function build () {
} else if (platform === 'linux') { } else if (platform === 'linux') {
buildLinux(printDone) buildLinux(printDone)
} else { } else {
buildDarwin(function (err, buildPath) { buildDarwin(function (err) {
printDone(err, buildPath) printDone(err)
buildWin32(function (err, buildPath) { buildWin32(function (err) {
printDone(err, buildPath) printDone(err)
buildLinux(printDone) buildLinux(printDone)
}) })
}) })
@@ -37,8 +67,9 @@ var all = {
// Build 64 bit binaries only. // Build 64 bit binaries only.
arch: 'x64', arch: 'x64',
// The application source directory. // The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
dir: config.ROOT_PATH, // property on Windows, and `NSHumanReadableCopyright` on OS X.
'app-copyright': config.APP_COPYRIGHT,
// The release version of the application. Maps to the `ProductVersion` metadata // 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 OS X.
@@ -58,15 +89,18 @@ var all = {
// Windows requires the build version to start with a number :/ so we stick on a prefix // Windows requires the build version to start with a number :/ so we stick on a prefix
'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''), 'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''),
// The application source directory.
dir: config.ROOT_PATH,
// Pattern which specifies which files to ignore when copying files to create the // Pattern which specifies which files to ignore when copying files to create the
// package(s). // package(s).
ignore: /^\/dist|\/(appveyor.yml|.appveyor.yml|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: /^\/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. // The application name.
name: config.APP_NAME, name: config.APP_NAME,
// The base directory where the finished package(s) are created. // The base directory where the finished package(s) are created.
out: path.join(config.ROOT_PATH, 'dist'), out: DIST_PATH,
// Replace an already existing output directory. // Replace an already existing output directory.
overwrite: true, overwrite: true,
@@ -105,16 +139,13 @@ var win32 = {
// Company that produced the file. // Company that produced the file.
CompanyName: config.APP_NAME, CompanyName: config.APP_NAME,
// Copyright notices that apply to the file.
LegalCopyright: config.APP_COPYRIGHT,
// Name of the program, displayed to users // Name of the program, displayed to users
FileDescription: config.APP_NAME, FileDescription: config.APP_NAME,
// Original name of the file, not including a path. This information enables an // Original name of the file, not including a path. This information enables an
// application to determine whether a file has been renamed by a user. The format of // application to determine whether a file has been renamed by a user. The format of
// the name depends on the file system for which the file was created. // the name depends on the file system for which the file was created.
OriginalFilename: 'WebTorrent.exe', OriginalFilename: config.APP_NAME + '.exe',
// Name of the product with which the file is distributed. // Name of the product with which the file is distributed.
ProductName: config.APP_NAME, ProductName: config.APP_NAME,
@@ -130,7 +161,10 @@ var win32 = {
} }
var linux = { var linux = {
platform: 'linux' platform: 'linux',
// Build 32/64 bit binaries.
arch: 'all'
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option. // Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
} }
@@ -140,8 +174,10 @@ build()
function buildDarwin (cb) { function buildDarwin (cb) {
var plist = require('plist') var plist = require('plist')
console.log('OS X: Packaging electron...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('OS X: Packaged electron. ' + buildPath[0])
var appPath = path.join(buildPath[0], config.APP_NAME + '.app') var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
var contentsPath = path.join(appPath, 'Contents') var contentsPath = path.join(appPath, 'Contents')
@@ -149,6 +185,8 @@ function buildDarwin (cb) {
var infoPlistPath = path.join(contentsPath, 'Info.plist') var infoPlistPath = path.join(contentsPath, 'Info.plist')
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
// TODO: Use new `extend-info` and `extra-resource` opts to electron-packager,
// available as of v6.
infoPlist.CFBundleDocumentTypes = [ infoPlist.CFBundleDocumentTypes = [
{ {
CFBundleTypeExtensions: [ 'torrent' ], CFBundleTypeExtensions: [ 'torrent' ],
@@ -176,19 +214,30 @@ function buildDarwin (cb) {
} }
] ]
infoPlist.NSHumanReadableCopyright = config.APP_COPYRIGHT
fs.writeFileSync(infoPlistPath, plist.build(infoPlist)) fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
// Copy torrent file icon into app bundle // Copy torrent file icon into app bundle
cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`) cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`)
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
var appDmg = require('appdmg') if (argv.sign) {
signApp(function (err) {
if (err) return cb(err)
pack(cb)
})
} else {
printWarning()
pack(cb)
}
} else {
printWarning()
}
function signApp (cb) {
var sign = require('electron-osx-sign') var sign = require('electron-osx-sign')
/* /*
* Sign the app with Apple Developer ID certificate. We sign the app for 2 reasons: * Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
* - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by * - So the auto-updater (Squirrrel.Mac) can check that app updates are signed by
* the same author as the current version. * the same author as the current version.
* - So users will not a see a warning about the app coming from an "Unidentified * - So users will not a see a warning about the app coming from an "Unidentified
@@ -206,48 +255,71 @@ function buildDarwin (cb) {
verbose: true verbose: true
} }
console.log('OS X: Signing app...')
sign(signOpts, function (err) { sign(signOpts, function (err) {
if (err) return cb(err) if (err) return cb(err)
console.log('OS X: Signed app.')
cb(null)
})
}
// Create .zip file (used by the auto-updater) function pack (cb) {
var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.zip') packageZip() // always produce .zip file, used for automatic updates
cp.execSync(`pushd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'} && popd`)
console.log('Created OS X .zip file.')
var targetPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg') if (argv.package === 'dmg' || argv.package === 'all') {
rimraf.sync(targetPath) packageDmg(cb)
}
}
// Create a .dmg (OS X disk image) file, for easy user installation. function packageZip () {
var dmgOpts = { // Create .zip file (used by the auto-updater)
basepath: config.ROOT_PATH, console.log('OS X: Creating zip...')
target: targetPath,
specification: { var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
title: config.APP_NAME, var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
icon: config.APP_ICON + '.icns', zip(inPath, outPath)
background: path.join(config.STATIC_PATH, 'appdmg.png'),
'icon-size': 128, console.log('OS X: Created zip.')
contents: [ }
{ x: 122, y: 240, type: 'file', path: appPath },
{ x: 380, y: 240, type: 'link', path: '/Applications' }, function packageDmg (cb) {
// Hide hidden icons out of view, for users who have hidden files shown. console.log('OS X: Creating dmg...')
// https://github.com/LinusU/node-appdmg/issues/45#issuecomment-153924954
{ x: 50, y: 500, type: 'position', path: '.background' }, var appDmg = require('appdmg')
{ x: 100, y: 500, type: 'position', path: '.DS_Store' },
{ x: 150, y: 500, type: 'position', path: '.Trashes' }, var targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
{ x: 200, y: 500, type: 'position', path: '.VolumeIcon.icns' } rimraf.sync(targetPath)
]
} // Create a .dmg (OS X disk image) file, for easy user installation.
var dmgOpts = {
basepath: config.ROOT_PATH,
target: targetPath,
specification: {
title: config.APP_NAME,
icon: config.APP_ICON + '.icns',
background: path.join(config.STATIC_PATH, 'appdmg.png'),
'icon-size': 128,
contents: [
{ x: 122, y: 240, type: 'file', path: appPath },
{ x: 380, y: 240, type: 'link', path: '/Applications' },
// Hide hidden icons out of view, for users who have hidden files shown.
// https://github.com/LinusU/node-appdmg/issues/45#issuecomment-153924954
{ x: 50, y: 500, type: 'position', path: '.background' },
{ x: 100, y: 500, type: 'position', path: '.DS_Store' },
{ x: 150, y: 500, type: 'position', path: '.Trashes' },
{ x: 200, y: 500, type: 'position', path: '.VolumeIcon.icns' }
]
} }
}
var dmg = appDmg(dmgOpts) var dmg = appDmg(dmgOpts)
dmg.on('error', cb) dmg.on('error', cb)
dmg.on('progress', function (info) { dmg.on('progress', function (info) {
if (info.type === 'step-begin') console.log(info.title + '...') if (info.type === 'step-begin') console.log(info.title + '...')
}) })
dmg.on('finish', function (info) { dmg.on('finish', function (info) {
console.log('Created OS X disk image (.dmg) file.') console.log('OS X: Created dmg.')
cb(null, buildPath) cb(null)
})
}) })
} }
}) })
@@ -256,38 +328,148 @@ function buildDarwin (cb) {
function buildWin32 (cb) { function buildWin32 (cb) {
var installer = require('electron-winstaller') var installer = require('electron-winstaller')
console.log('Windows: Packaging electron...')
electronPackager(Object.assign({}, all, win32), function (err, buildPath) { electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('Windows: Packaged electron. ' + buildPath[0])
console.log('Creating Windows installer...') var signWithParams
installer.createWindowsInstaller({ if (process.platform === 'win32') {
name: config.APP_NAME, if (argv.sign) {
productName: config.APP_NAME, var certificateFile = path.join(CERT_PATH, 'authenticode.p12')
title: config.APP_NAME, var certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
exe: config.APP_NAME + '.exe', var timestampServer = 'http://timestamp.comodoca.com'
signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256`
} else {
printWarning()
}
} else {
printWarning()
}
appDirectory: buildPath[0], var tasks = []
outputDirectory: path.join(config.ROOT_PATH, 'dist'), if (argv.package === 'exe' || argv.package === 'all') {
version: pkg.version, tasks.push((cb) => packageInstaller(cb))
description: config.APP_NAME, }
authors: config.APP_TEAM, if (argv.package === 'portable' || argv.package === 'all') {
iconUrl: config.APP_ICON + '.ico', tasks.push((cb) => packagePortable(cb))
setupIcon: config.APP_ICON + '.ico', }
// certificateFile: '', // TODO series(tasks, cb)
usePackageJson: false,
loadingGif: path.join(config.STATIC_PATH, 'loading.gif') function packageInstaller (cb) {
}).then(function () { console.log('Windows: Creating installer...')
console.log('Created Windows installer.') installer.createWindowsInstaller({
cb(null, buildPath) appDirectory: buildPath[0],
}).catch(cb) authors: config.APP_TEAM,
description: config.APP_NAME,
exe: config.APP_NAME + '.exe',
iconUrl: config.GITHUB_URL_RAW + '/static/' + config.APP_NAME + '.ico',
loadingGif: path.join(config.STATIC_PATH, 'loading.gif'),
name: config.APP_NAME,
noMsi: true,
outputDirectory: DIST_PATH,
productName: config.APP_NAME,
remoteReleases: config.GITHUB_URL,
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
setupIcon: config.APP_ICON + '.ico',
signWithParams: signWithParams,
title: config.APP_NAME,
usePackageJson: false,
version: pkg.version
}).then(function () {
console.log('Windows: Created installer.')
cb(null)
}).catch(cb)
}
function packagePortable (cb) {
// Create Windows portable app
console.log('Windows: Creating portable app...')
var portablePath = path.join(buildPath[0], 'Portable Settings')
mkdirp.sync(portablePath)
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
zip(inPath, outPath)
console.log('Windows: Created portable app.')
cb(null)
}
}) })
} }
function buildLinux (cb) { function buildLinux (cb) {
electronPackager(Object.assign({}, all, linux), cb) console.log('Linux: Packaging electron...')
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
if (err) return cb(err)
console.log('Linux: Packaged electron. ' + buildPath[0])
var tasks = []
buildPath.forEach(function (filesPath) {
var destArch = filesPath.split('-').pop()
if (argv.package === 'deb' || argv.package === 'all') {
tasks.push((cb) => packageDeb(filesPath, destArch, cb))
}
if (argv.package === 'zip' || argv.package === 'all') {
tasks.push((cb) => packageZip(filesPath, destArch, cb))
}
})
series(tasks, cb)
})
function packageDeb (filesPath, destArch, cb) {
// Create .deb file for Debian-based platforms
console.log(`Linux: Creating ${destArch} deb...`)
var deb = require('nobin-debian-installer')()
var destPath = path.join('/opt', pkg.name)
deb.pack({
package: pkg,
info: {
arch: destArch === 'x64' ? 'amd64' : 'i386',
targetDir: DIST_PATH,
depends: 'libc6 (>= 2.4)',
scripts: {
postinst: path.join(config.STATIC_PATH, 'linux', 'postinst'),
prerm: path.join(config.STATIC_PATH, 'linux', 'prerm')
}
}
}, [{
src: ['./**'],
dest: destPath,
expand: true,
cwd: filesPath
}], function (err) {
if (err) return cb(err)
console.log(`Linux: Created ${destArch} deb.`)
cb(null)
})
}
function packageZip (filesPath, destArch, cb) {
// Create .zip file for Linux
console.log(`Linux: Creating ${destArch} zip...`)
var inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
zip(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`)
cb(null)
}
} }
function printDone (err, buildPath) { function printDone (err) {
if (err) console.error(err.message || err) if (err) console.error(err.message || err)
else console.log('Built ' + buildPath[0]) }
/*
* Print a large warning when signing is disabled so we are less likely to accidentally
* ship unsigned binaries to users.
*/
function printWarning () {
console.log(fs.readFileSync(path.join(__dirname, 'warning.txt'), 'utf8'))
} }

View File

@@ -2,7 +2,8 @@
set -e set -e
git diff --exit-code git diff --exit-code
npm run package npm run package -- --sign
git push git push
git push --tags git push --tags
npm publish
gh-release gh-release

View File

@@ -6,4 +6,6 @@ npm run update-authors
git diff --exit-code git diff --exit-code
rm -rf node_modules/ rm -rf node_modules/
npm install npm install
npm prune
npm dedupe
npm test npm test

12
bin/warning.txt Normal file
View File

@@ -0,0 +1,12 @@
*********************************************************
_ _ ___ ______ _ _ _____ _ _ _____
| | | |/ _ \ | ___ \ \ | |_ _| \ | | __ \
| | | / /_\ \| |_/ / \| | | | | \| | | \/
| |/\| | _ || /| . ` | | | | . ` | | __
\ /\ / | | || |\ \| |\ |_| |_| |\ | |_\ \
\/ \/\_| |_/\_| \_\_| \_/\___/\_| \_/\____/
Application is NOT signed. Do not ship this to users!
*********************************************************

View File

@@ -1,10 +1,13 @@
var applicationConfigPath = require('application-config-path') var appConfig = require('application-config')('WebTorrent')
var path = require('path') var path = require('path')
var pathExists = require('path-exists')
var APP_NAME = 'WebTorrent' var APP_NAME = 'WebTorrent'
var APP_TEAM = 'The WebTorrent Project' var APP_TEAM = 'The WebTorrent Project'
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 = { module.exports = {
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM, APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
@@ -14,53 +17,43 @@ module.exports = {
APP_VERSION: APP_VERSION, APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)', APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
AUTO_UPDATE_URL: 'https://webtorrent.io/app/update?version=' + APP_VERSION,
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */, AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' +
'?version=' + APP_VERSION + '&platform=' + process.platform,
CONFIG_PATH: applicationConfigPath(APP_NAME), CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'),
CONFIG_TORRENT_PATH: path.join(applicationConfigPath(APP_NAME), 'Torrents'),
CONFIG_PATH: getConfigPath(),
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(), IS_PRODUCTION: isProduction(),
ROOT_PATH: __dirname, ROOT_PATH: __dirname,
STATIC_PATH: path.join(__dirname, 'static'), STATIC_PATH: path.join(__dirname, 'static'),
SOUND_ADD: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'),
volume: 0.2
},
SOUND_DELETE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'delete.wav'),
volume: 0.1
},
SOUND_DISABLE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'disable.wav'),
volume: 0.2
},
SOUND_DONE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'done.wav'),
volume: 0.2
},
SOUND_ENABLE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'enable.wav'),
volume: 0.2
},
SOUND_ERROR: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'error.wav'),
volume: 0.2
},
SOUND_PLAY: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'play.wav'),
volume: 0.2
},
SOUND_STARTUP: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'startup.wav'),
volume: 0.4
},
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'), WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html') WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html'),
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, 'renderer', 'webtorrent.html'),
WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents
WINDOW_MIN_WIDTH: 425
}
function getConfigPath () {
if (isPortable()) {
return PORTABLE_PATH
} else {
return path.dirname(appConfig.filePath)
}
}
function isPortable () {
return process.platform === 'win32' && isProduction() && pathExists(PORTABLE_PATH)
} }
function isProduction () { function isProduction () {

15
crash-reporter.js Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
init
}
var config = require('./config')
var electron = require('electron')
function init () {
electron.crashReporter.start({
companyName: config.APP_NAME,
productName: config.APP_NAME,
submitURL: config.CRASH_REPORT_URL
})
console.log('crash reporter started')
}

View File

@@ -3,9 +3,11 @@ module.exports = {
} }
var electron = require('electron') var electron = require('electron')
var get = require('simple-get')
var config = require('../config') var config = require('../config')
var log = require('./log') var log = require('./log')
var windows = require('./windows')
var autoUpdater = electron.autoUpdater var autoUpdater = electron.autoUpdater
@@ -20,7 +22,7 @@ function init () {
* We always check for updates on app startup. To keep app startup fast, we delay this * We always check for updates on app startup. To keep app startup fast, we delay this
* first check so it happens when there is less going on. * first check so it happens when there is less going on.
*/ */
setTimeout(() => autoUpdater.checkForUpdates(), config.AUTO_UPDATE_CHECK_STARTUP_DELAY) setTimeout(checkForUpdates, config.AUTO_UPDATE_CHECK_STARTUP_DELAY)
autoUpdater.on('checking-for-update', () => log('Checking for app update')) autoUpdater.on('checking-for-update', () => log('Checking for app update'))
autoUpdater.on('update-available', () => log('App update available')) autoUpdater.on('update-available', () => log('App update available'))
@@ -29,3 +31,20 @@ function init () {
log('App update downloaded: ', releaseName, updateURL) log('App update downloaded: ', releaseName, updateURL)
}) })
} }
function checkForUpdates () {
// Electron's built-in auto updater only supports Mac and Windows, for now
if (process.platform !== 'linux') {
return autoUpdater.checkForUpdates()
}
// If we're on Linux, we have to do it ourselves
get.concat(config.AUTO_UPDATE_URL, function (err, res, data) {
if (err) return log('Error checking for app update: ' + err.message)
if (![200, 204].includes(res.statusCode)) return log('Error checking for app update, got HTTP ' + res.statusCode)
if (res.statusCode !== 200) return
var obj = JSON.parse(data)
windows.main.send('dispatch', 'updateAvailable', obj.version)
})
}

View File

@@ -1,43 +1,264 @@
module.exports = { module.exports = {
init install,
uninstall
} }
var path = require('path') var path = require('path')
var log = require('./log') function install () {
if (process.platform === 'darwin') {
function init () { installDarwin()
}
if (process.platform === 'win32') { if (process.platform === 'win32') {
initWindows() installWin32()
} }
if (process.platform === 'linux') { if (process.platform === 'linux') {
initLinux() installLinux()
} }
} }
function initWindows () { function uninstall () {
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico') if (process.platform === 'darwin') {
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, process.execPath) uninstallDarwin()
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, process.execPath) }
if (process.platform === 'win32') {
uninstallWin32()
}
if (process.platform === 'linux') {
uninstallLinux()
}
} }
function initLinux () { function installDarwin () {
var config = require('../config') var electron = require('electron')
var fs = require('fs') var app = electron.app
var mkdirp = require('mkdirp')
// On OS X, only protocols that are listed in Info.plist can be set as the default
// handler at runtime.
app.setAsDefaultProtocolClient('magnet')
// File handlers are registered in the Info.plist.
}
function uninstallDarwin () {}
function installWin32 () {
var Registry = require('winreg')
var log = require('./log')
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, process.execPath)
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, process.execPath)
/**
* To add a protocol handler, the following keys must be added to the Windows registry:
*
* HKEY_CLASSES_ROOT
* $PROTOCOL
* (Default) = "$NAME"
* URL Protocol = ""
* DefaultIcon
* (Default) = "$ICON"
* shell
* open
* command
* (Default) = "$COMMAND" "%1"
*
* Source: https://msdn.microsoft.com/en-us/library/aa767914.aspx
*
* However, the "HKEY_CLASSES_ROOT" key can only be written by the Administrator user.
* So, we instead write to "HKEY_CURRENT_USER\Software\Classes", which is inherited by
* "HKEY_CLASSES_ROOT" anyway, and can be written by unprivileged users.
*/
function registerProtocolHandlerWin32 (protocol, name, icon, command) {
var protocolKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + protocol
})
setProtocol()
function setProtocol (err) {
if (err) log.error(err.message)
protocolKey.set('', Registry.REG_SZ, name, setURLProtocol)
}
function setURLProtocol (err) {
if (err) log.error(err.message)
protocolKey.set('URL Protocol', Registry.REG_SZ, '', setIcon)
}
function setIcon (err) {
if (err) log.error(err.message)
var iconKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
})
iconKey.set('', Registry.REG_SZ, icon, setCommand)
}
function setCommand (err) {
if (err) log.error(err.message)
var commandKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
})
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done)
}
function done (err) {
if (err) log.error(err.message)
}
}
/**
* To add a file handler, the following keys must be added to the Windows registry:
*
* HKEY_CLASSES_ROOT
* $EXTENSION
* (Default) = "$EXTENSION_ID"
* $EXTENSION_ID
* (Default) = "$NAME"
* DefaultIcon
* (Default) = "$ICON"
* shell
* open
* command
* (Default) = "$COMMAND" "%1"
*/
function registerFileHandlerWin32 (ext, id, name, icon, command) {
setExt()
function setExt () {
var extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + ext
})
extKey.set('', Registry.REG_SZ, id, setId)
}
function setId (err) {
if (err) log.error(err.message)
var idKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id
})
idKey.set('', Registry.REG_SZ, name, setIcon)
}
function setIcon (err) {
if (err) log.error(err.message)
var iconKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
})
iconKey.set('', Registry.REG_SZ, icon, setCommand)
}
function setCommand (err) {
if (err) log.error(err.message)
var commandKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
})
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done)
}
function done (err) {
if (err) log.error(err.message)
}
}
}
function uninstallWin32 () {
var Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', process.execPath)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', process.execPath)
function unregisterProtocolHandlerWin32 (protocol, command) {
getCommand()
function getCommand () {
var commandKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
})
commandKey.get('', function (err, item) {
if (!err && item.value.indexOf(command) >= 0) {
destroyProtocol()
}
})
}
function destroyProtocol () {
var protocolKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol
})
protocolKey.destroy(function () {})
}
}
function unregisterFileHandlerWin32 (ext, id, command) {
eraseId()
function eraseId () {
var idKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + id
})
idKey.destroy(getExt)
}
function getExt () {
var extKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + ext
})
extKey.get('', function (err, item) {
if (!err && item.value === id) {
destroyExt()
}
})
}
function destroyExt () {
var extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + ext
})
extKey.destroy(function () {})
}
}
}
function installLinux () {
var fs = require('fs-extra')
var os = require('os') var os = require('os')
var path = require('path') var path = require('path')
var config = require('../config')
var log = require('./log')
installDesktopFile() installDesktopFile()
installIconFile() installIconFile()
function installDesktopFile () { function installDesktopFile () {
var templatePath = path.join(config.STATIC_PATH, 'webtorrent.desktop') var templatePath = path.join(config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop')
fs.readFile(templatePath, 'utf8', writeDesktopFile) fs.readFile(templatePath, 'utf8', writeDesktopFile)
} }
function writeDesktopFile (err, desktopFile) { function writeDesktopFile (err, desktopFile) {
if (err) return console.error(err.message) if (err) return log.error(err.message)
var appPath = config.IS_PRODUCTION ? path.dirname(process.execPath) : config.ROOT_PATH var appPath = config.IS_PRODUCTION ? path.dirname(process.execPath) : config.ROOT_PATH
var execPath = process.execPath + (config.IS_PRODUCTION ? '' : ' \.') var execPath = process.execPath + (config.IS_PRODUCTION ? '' : ' \.')
@@ -53,11 +274,11 @@ function initLinux () {
'.local', '.local',
'share', 'share',
'applications', 'applications',
'webtorrent.desktop' 'webtorrent-desktop.desktop'
) )
mkdirp(path.dirname(desktopFilePath)) fs.mkdirp(path.dirname(desktopFilePath))
fs.writeFile(desktopFilePath, desktopFile, function (err) { fs.writeFile(desktopFilePath, desktopFile, function (err) {
if (err) return console.error(err.message) if (err) return log.error(err.message)
}) })
} }
@@ -67,99 +288,42 @@ function initLinux () {
} }
function writeIconFile (err, iconFile) { function writeIconFile (err, iconFile) {
if (err) return console.error(err.message) if (err) return log.error(err.message)
var iconFilePath = path.join( var iconFilePath = path.join(
os.homedir(), os.homedir(),
'.local', '.local',
'share', 'share',
'icons', 'icons',
'webtorrent.png' 'webtorrent-desktop.png'
) )
mkdirp(path.dirname(iconFilePath)) fs.mkdirp(path.dirname(iconFilePath))
fs.writeFile(iconFilePath, iconFile, function (err) { fs.writeFile(iconFilePath, iconFile, function (err) {
if (err) return console.error(err.message) if (err) return log.error(err.message)
}) })
} }
} }
/** function uninstallLinux () {
* To add a protocol handler on Windows, the following keys must be added to the Windows var os = require('os')
* registry: var path = require('path')
* var fs = require('fs-extra')
* HKEY_CLASSES_ROOT
* $PROTOCOL
* (Default) = "$NAME"
* URL Protocol = ""
* DefaultIcon
* (Default) = "$ICON"
* shell
* open
* command
* (Default) = "$COMMAND" "%1"
*
* Source: https://msdn.microsoft.com/en-us/library/aa767914.aspx
*
* However, the "HKEY_CLASSES_ROOT" key can only be written by the Administrator user.
* So, we instead write to "HKEY_CURRENT_USER\Software\Classes", which is inherited by
* "HKEY_CLASSES_ROOT" anyway, and can be written by unprivileged users.
*/
function registerProtocolHandlerWin32 (protocol, name, icon, command) { var desktopFilePath = path.join(
var Registry = require('winreg') os.homedir(),
'.local',
'share',
'applications',
'webtorrent-desktop.desktop'
)
fs.removeSync(desktopFilePath)
var protocolKey = new Registry({ var iconFilePath = path.join(
hive: Registry.HKCU, // HKEY_CURRENT_USER os.homedir(),
key: '\\Software\\Classes\\' + protocol '.local',
}) 'share',
protocolKey.set('', Registry.REG_SZ, name, callback) 'icons',
protocolKey.set('URL Protocol', Registry.REG_SZ, '', callback) 'webtorrent-desktop.png'
)
var iconKey = new Registry({ fs.removeSync(iconFilePath)
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
})
iconKey.set('', Registry.REG_SZ, icon, callback)
var commandKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
})
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', callback)
function callback (err) {
if (err) log.error(err.message || err)
}
}
function registerFileHandlerWin32 (ext, id, name, icon, command) {
var Registry = require('winreg')
var extKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + ext
})
extKey.set('', Registry.REG_SZ, id, callback)
var idKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id
})
idKey.set('', Registry.REG_SZ, name, callback)
var iconKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
})
iconKey.set('', Registry.REG_SZ, icon, callback)
var commandKey = new Registry({
hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
})
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', callback)
function callback (err) {
if (err) log.error(err.message || err)
}
} }

View File

@@ -1,9 +1,11 @@
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
var ipcMain = electron.ipcMain
var autoUpdater = require('./auto-updater') var autoUpdater = require('./auto-updater')
var config = require('../config') var config = require('../config')
var crashReporter = require('../crash-reporter')
var handlers = require('./handlers') var handlers = require('./handlers')
var ipc = require('./ipc') var ipc = require('./ipc')
var log = require('./log') var log = require('./log')
@@ -11,6 +13,7 @@ var menu = require('./menu')
var shortcuts = require('./shortcuts') var shortcuts = require('./shortcuts')
var squirrelWin32 = require('./squirrel-win32') var squirrelWin32 = require('./squirrel-win32')
var windows = require('./windows') var windows = require('./windows')
var tray = require('./tray')
var shouldQuit = false var shouldQuit = false
var argv = sliceArgv(process.argv) var argv = sliceArgv(process.argv)
@@ -34,6 +37,10 @@ if (!shouldQuit) {
} }
function init () { function init () {
if (config.IS_PORTABLE) {
app.setPath('userData', config.CONFIG_PATH)
}
app.ipcReady = false // main window has finished loading and IPC is ready app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false app.isQuitting = false
@@ -44,15 +51,17 @@ function init () {
ipc.init() ipc.init()
app.on('will-finish-launching', function () { app.on('will-finish-launching', function () {
crashReporter.init()
autoUpdater.init() autoUpdater.init()
setupCrashReporter()
}) })
app.on('ready', function () { app.on('ready', function () {
menu.init() menu.init()
windows.createMainWindow() windows.createMainWindow()
windows.createWebTorrentHiddenWindow()
shortcuts.init() shortcuts.init()
if (process.platform !== 'win32') handlers.init() tray.init()
handlers.install()
}) })
app.on('ipcReady', function () { app.on('ipcReady', function () {
@@ -60,19 +69,19 @@ function init () {
processArgv(argv) processArgv(argv)
}) })
app.on('before-quit', function () { app.on('before-quit', function (e) {
if (app.isQuitting) return
app.isQuitting = true app.isQuitting = true
e.preventDefault()
windows.main.send('dispatch', 'saveState') /* try to save state on exit */
ipcMain.once('savedState', () => app.quit())
setTimeout(() => app.quit(), 2000) /* quit after 2 secs, at most */
}) })
app.on('activate', function () { app.on('activate', function () {
windows.createMainWindow() windows.createMainWindow()
}) })
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
} }
function onOpen (e, torrentId) { function onOpen (e, torrentId) {
@@ -109,28 +118,18 @@ function sliceArgv (argv) {
} }
function processArgv (argv) { function processArgv (argv) {
argv.forEach(function (argvi) { argv.forEach(function (arg) {
switch (argvi) { if (arg === '-n') {
case '-n': windows.main.send('dispatch', 'showOpenSeedFiles')
windows.main.send('dispatch', 'showCreateTorrent') } else if (arg === '-o') {
break windows.main.send('dispatch', 'showOpenTorrentFile')
case '-o': } else if (arg === '-u') {
windows.main.send('dispatch', 'showOpenTorrentFile') windows.main.send('showOpenTorrentAddress')
break } else if (arg.startsWith('-psn')) {
case '-u': // Ignore OS X launchd "process serial number" argument
windows.main.send('showOpenTorrentAddress') // More: https://github.com/feross/webtorrent-desktop/issues/214
break } else {
default: windows.main.send('dispatch', 'onOpen', arg)
windows.main.send('dispatch', 'onOpen', argvi)
} }
}) })
} }
function setupCrashReporter () {
// require('crash-reporter').start({
// productName: 'WebTorrent',
// companyName: 'WebTorrent',
// submitURL: 'https://webtorrent.io/crash-report',
// autoSubmit: true
// })
}

View File

@@ -2,7 +2,6 @@ module.exports = {
init init
} }
var debug = require('debug')('webtorrent-app:ipcMain')
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
@@ -12,6 +11,7 @@ var powerSaveBlocker = electron.powerSaveBlocker
var log = require('./log') var log = require('./log')
var menu = require('./menu') var menu = require('./menu')
var windows = require('./windows') var windows = require('./windows')
var shortcuts = require('./shortcuts')
// has to be a number, not a boolean, and undefined throws an error // has to be a number, not a boolean, and undefined throws an error
var powerSaveBlockID = 0 var powerSaveBlockID = 0
@@ -20,14 +20,23 @@ function init () {
ipcMain.on('ipcReady', function (e) { ipcMain.on('ipcReady', function (e) {
app.ipcReady = true app.ipcReady = true
app.emit('ipcReady') app.emit('ipcReady')
setTimeout(function () { windows.main.show()
windows.main.show() console.timeEnd('init')
console.timeEnd('init') })
}, 50)
var messageQueueMainToWebTorrent = []
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('showOpenTorrentFile', menu.showOpenTorrentFile)
ipcMain.on('showCreateTorrent', menu.showCreateTorrent) ipcMain.on('showOpenSeedFiles', menu.showOpenSeedFiles)
ipcMain.on('setBounds', function (e, bounds, maximize) { ipcMain.on('setBounds', function (e, bounds, maximize) {
setBounds(bounds, maximize) setBounds(bounds, maximize)
@@ -61,21 +70,68 @@ function init () {
ipcMain.on('blockPowerSave', blockPowerSave) ipcMain.on('blockPowerSave', blockPowerSave)
ipcMain.on('unblockPowerSave', unblockPowerSave) ipcMain.on('unblockPowerSave', unblockPowerSave)
ipcMain.on('onPlayerOpen', menu.onPlayerOpen) ipcMain.on('onPlayerOpen', function () {
ipcMain.on('onPlayerClose', menu.onPlayerClose) menu.onPlayerOpen()
shortcuts.registerPlayerShortcuts()
})
ipcMain.on('onPlayerClose', function () {
menu.onPlayerClose()
shortcuts.unregisterPlayerShortcuts()
})
ipcMain.on('focusWindow', function (e, windowName) {
windows.focusWindow(windows[windowName])
})
// 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) { function setBounds (bounds, maximize) {
// Do nothing in fullscreen // Do nothing in fullscreen
if (!windows.main || windows.main.isFullScreen()) return 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 // Maximize or minimize, if the second argument is present
var willBeMaximized var willBeMaximized
if (maximize === true) { if (maximize === true) {
if (!windows.main.isMaximized()) windows.main.maximize() if (!windows.main.isMaximized()) {
log('setBounds: maximizing')
windows.main.maximize()
}
willBeMaximized = true willBeMaximized = true
} else if (maximize === false) { } else if (maximize === false) {
if (windows.main.isMaximized()) windows.main.unmaximize() if (windows.main.isMaximized()) {
log('setBounds: unmaximizing')
windows.main.unmaximize()
}
willBeMaximized = false willBeMaximized = false
} else { } else {
willBeMaximized = windows.main.isMaximized() willBeMaximized = windows.main.isMaximized()
@@ -83,12 +139,23 @@ function setBounds (bounds, maximize) {
// Assuming we're not maximized or maximizing, set the window size // Assuming we're not maximized or maximizing, set the window size
if (!willBeMaximized) { 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 screen = require('screen')
var scr = 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) windows.main.setBounds(bounds, true)
} else {
log('setBounds: not setting bounds because of window maximization')
} }
} }
function setAspectRatio (aspectRatio, extraSize) { function setAspectRatio (aspectRatio, extraSize) {
debug('setAspectRatio %o %o', aspectRatio, extraSize) log('setAspectRatio %o %o', aspectRatio, extraSize)
if (windows.main) { if (windows.main) {
windows.main.setAspectRatio(aspectRatio, extraSize) windows.main.setAspectRatio(aspectRatio, extraSize)
} }
@@ -96,13 +163,13 @@ function setAspectRatio (aspectRatio, extraSize) {
// Display string in dock badging area (OS X) // Display string in dock badging area (OS X)
function setBadge (text) { function setBadge (text) {
debug('setBadge %s', text) log('setBadge %s', text)
if (app.dock) app.dock.setBadge(String(text)) if (app.dock) app.dock.setBadge(String(text))
} }
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1. // Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
function setProgress (progress) { function setProgress (progress) {
debug('setProgress %s', progress) log('setProgress %s', progress)
if (windows.main) { if (windows.main) {
windows.main.setProgressBar(progress) windows.main.setProgressBar(progress)
} }
@@ -110,12 +177,12 @@ function setProgress (progress) {
function blockPowerSave () { function blockPowerSave () {
powerSaveBlockID = powerSaveBlocker.start('prevent-display-sleep') powerSaveBlockID = powerSaveBlocker.start('prevent-display-sleep')
debug('blockPowerSave %d', powerSaveBlockID) log('blockPowerSave %d', powerSaveBlockID)
} }
function unblockPowerSave () { function unblockPowerSave () {
if (powerSaveBlocker.isStarted(powerSaveBlockID)) { if (powerSaveBlocker.isStarted(powerSaveBlockID)) {
powerSaveBlocker.stop(powerSaveBlockID) powerSaveBlocker.stop(powerSaveBlockID)
debug('unblockPowerSave %d', powerSaveBlockID) log('unblockPowerSave %d', powerSaveBlockID)
} }
} }

View File

@@ -5,17 +5,17 @@ module.exports = {
onWindowShow, onWindowShow,
onPlayerOpen, onPlayerOpen,
onPlayerClose, onPlayerClose,
showCreateTorrent, showOpenSeedFiles,
showOpenTorrentFile, showOpenTorrentFile,
toggleFullScreen toggleFullScreen
} }
var debug = require('debug')('webtorrent-app:menu')
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
var config = require('../config') var config = require('../config')
var log = require('./log')
var windows = require('./windows') var windows = require('./windows')
var appMenu, dockMenu var appMenu, dockMenu
@@ -29,7 +29,7 @@ function init () {
} }
function toggleFullScreen (flag) { function toggleFullScreen (flag) {
debug('toggleFullScreen %s', flag) log('toggleFullScreen %s', flag)
if (windows.main && windows.main.isVisible()) { if (windows.main && windows.main.isVisible()) {
flag = flag != null ? flag : !windows.main.isFullScreen() flag = flag != null ? flag : !windows.main.isFullScreen()
windows.main.setFullScreen(flag) windows.main.setFullScreen(flag)
@@ -38,7 +38,7 @@ function toggleFullScreen (flag) {
// Sets whether the window should always show on top of other windows // Sets whether the window should always show on top of other windows
function toggleFloatOnTop (flag) { function toggleFloatOnTop (flag) {
debug('toggleFloatOnTop %s', flag) log('toggleFloatOnTop %s', flag)
if (windows.main) { if (windows.main) {
flag = flag != null ? flag : !windows.main.isAlwaysOnTop() flag = flag != null ? flag : !windows.main.isAlwaysOnTop()
windows.main.setAlwaysOnTop(flag) windows.main.setAlwaysOnTop(flag)
@@ -59,32 +59,25 @@ function decreaseVolume () {
} }
function toggleDevTools () { function toggleDevTools () {
debug('toggleDevTools') log('toggleDevTools')
if (windows.main) { if (windows.main) {
windows.main.toggleDevTools() windows.main.toggleDevTools()
} }
} }
function reloadWindow () { function showWebTorrentWindow () {
debug('reloadWindow') windows.webtorrent.show()
if (windows.main) { windows.webtorrent.webContents.openDevTools({ detach: true })
windows.main.webContents.reloadIgnoringCache()
}
}
function addFakeDevice (device) {
debug('addFakeDevice %s', device)
windows.main.send('addFakeDevice', device)
} }
function onWindowShow () { function onWindowShow () {
debug('onWindowShow') log('onWindowShow')
getMenuItem('Full Screen').enabled = true getMenuItem('Full Screen').enabled = true
getMenuItem('Float on Top').enabled = true getMenuItem('Float on Top').enabled = true
} }
function onWindowHide () { function onWindowHide () {
debug('onWindowHide') log('onWindowHide')
getMenuItem('Full Screen').enabled = false getMenuItem('Full Screen').enabled = false
getMenuItem('Float on Top').enabled = false getMenuItem('Float on Top').enabled = false
} }
@@ -116,13 +109,16 @@ function getMenuItem (label) {
} }
// Prompts the user for a file or folder, then makes a torrent out of the data // Prompts the user for a file or folder, then makes a torrent out of the data
function showCreateTorrent () { function showOpenSeedFiles () {
// Allow only a single selection
// To create a multi-file torrent, the user must select a folder
electron.dialog.showOpenDialog({ electron.dialog.showOpenDialog({
title: 'Select a file or folder for the torrent file.', title: 'Select a file or folder for the torrent file.',
properties: [ 'openFile', 'openDirectory', 'multiSelections' ] properties: [ 'openFile', 'openDirectory' ]
}, function (filenames) { }, function (filenames) {
if (!Array.isArray(filenames)) return if (!Array.isArray(filenames)) return
windows.main.send('dispatch', 'seed', filenames) var fileOrFolder = filenames[0]
windows.main.send('dispatch', 'showCreateTorrent', fileOrFolder)
}) })
} }
@@ -130,6 +126,7 @@ function showCreateTorrent () {
function showOpenTorrentFile () { function showOpenTorrentFile () {
electron.dialog.showOpenDialog(windows.main, { electron.dialog.showOpenDialog(windows.main, {
title: 'Select a .torrent file to open.', title: 'Select a .torrent file to open.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ] properties: [ 'openFile', 'multiSelections' ]
}, function (filenames) { }, function (filenames) {
if (!Array.isArray(filenames)) return if (!Array.isArray(filenames)) return
@@ -145,36 +142,44 @@ function showOpenTorrentAddress () {
} }
function getAppMenuTemplate () { function getAppMenuTemplate () {
var fileMenu = [
{
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
},
{
type: 'separator'
},
{
label: process.platform === 'windows' ? 'Close' : 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
// File > Quit for Linux users with distros where the system tray is broken
if (process.platform === 'linux') {
fileMenu.push({
label: 'Quit',
click: () => app.quit()
})
}
var template = [ var template = [
{ {
label: 'File', label: 'File',
submenu: [ submenu: fileMenu
{
label: 'Create New Torrent...',
accelerator: 'CmdOrCtrl+N',
click: showCreateTorrent
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
},
{
type: 'separator'
},
{
label: process.platform === 'darwin'
? 'Close Window'
: 'Close',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
}, },
{ {
label: 'Edit', label: 'Edit',
@@ -238,11 +243,6 @@ function getAppMenuTemplate () {
{ {
label: 'Developer', label: 'Developer',
submenu: [ submenu: [
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click: reloadWindow
},
{ {
label: 'Developer Tools', label: 'Developer Tools',
accelerator: process.platform === 'darwin' accelerator: process.platform === 'darwin'
@@ -251,15 +251,11 @@ function getAppMenuTemplate () {
click: toggleDevTools click: toggleDevTools
}, },
{ {
type: 'separator' label: 'Show WebTorrent Process',
}, accelerator: process.platform === 'darwin'
{ ? 'Alt+Command+P'
label: 'Add Fake Airplay', : 'Ctrl+Shift+P',
click: () => addFakeDevice('airplay') click: showWebTorrentWindow
},
{
label: 'Add Fake Chromecast',
click: () => addFakeDevice('chromecast')
} }
] ]
} }
@@ -286,14 +282,14 @@ function getAppMenuTemplate () {
}, },
{ {
label: 'Contribute on GitHub', label: 'Contribute on GitHub',
click: () => electron.shell.openExternal('https://github.com/feross/webtorrent-app') click: () => electron.shell.openExternal(config.GITHUB_URL)
}, },
{ {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Report an Issue...', label: 'Report an Issue...',
click: () => electron.shell.openExternal('https://github.com/feross/webtorrent-app/issues') click: () => electron.shell.openExternal(config.GITHUB_URL + '/issues')
} }
] ]
} }
@@ -375,7 +371,7 @@ function getDockMenuTemplate () {
{ {
label: 'Create New Torrent...', label: 'Create New Torrent...',
accelerator: 'CmdOrCtrl+N', accelerator: 'CmdOrCtrl+N',
click: showCreateTorrent click: showOpenSeedFiles
}, },
{ {
label: 'Open Torrent File...', label: 'Open Torrent File...',

View File

@@ -1,5 +1,7 @@
module.exports = { module.exports = {
init init,
registerPlayerShortcuts,
unregisterPlayerShortcuts
} }
var electron = require('electron') var electron = require('electron')
@@ -11,11 +13,17 @@ var menu = require('./menu')
var windows = require('./windows') var windows = require('./windows')
function init () { function init () {
// Special "media key" for play/pause, available on some keyboards
globalShortcut.register('MediaPlayPause', () => windows.main.send('dispatch', 'playPause'))
// ⌘+Shift+F is an alternative fullscreen shortcut to the ones defined in menu.js. // ⌘+Shift+F is an alternative fullscreen shortcut to the ones defined in menu.js.
// Electron does not support multiple accelerators for a single menu item, so this // Electron does not support multiple accelerators for a single menu item, so this
// is registered separately here. // is registered separately here.
localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen) localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen)
} }
function registerPlayerShortcuts () {
// Special "media key" for play/pause, available on some keyboards
globalShortcut.register('MediaPlayPause', () => windows.main.send('dispatch', 'playPause'))
}
function unregisterPlayerShortcuts () {
globalShortcut.unregister('MediaPlayPause')
}

View File

@@ -7,7 +7,6 @@ var electron = require('electron')
var fs = require('fs') var fs = require('fs')
var os = require('os') var os = require('os')
var path = require('path') var path = require('path')
var pathExists = require('path-exists')
var app = electron.app var app = electron.app
@@ -18,17 +17,13 @@ var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe')
function handleEvent (cmd) { function handleEvent (cmd) {
if (cmd === '--squirrel-install') { if (cmd === '--squirrel-install') {
// App was installed. // App was installed. Install desktop/start menu shortcuts.
// Install protocol/file handlers, desktop/start menu shortcuts.
handlers.init()
createShortcuts(function () { createShortcuts(function () {
// Ensure user sees install splash screen so they realize that Setup.exe actually // Ensure user sees install splash screen so they realize that Setup.exe actually
// installed an application and isn't the application itself. // installed an application and isn't the application itself.
setTimeout(function () { setTimeout(function () {
app.quit() app.quit()
}, 5000) }, 3000)
}) })
return true return true
} }
@@ -44,9 +39,18 @@ function handleEvent (cmd) {
if (cmd === '--squirrel-uninstall') { if (cmd === '--squirrel-uninstall') {
// App was just uninstalled. Undo anything we did in the --squirrel-install and // App was just uninstalled. Undo anything we did in the --squirrel-install and
// --squirrel-updated handlers // --squirrel-updated handlers
removeShortcuts(function () {
app.quit() // Uninstall .torrent file and magnet link handlers
}) handlers.uninstall()
// Remove desktop/start menu shortcuts.
// HACK: add a callback to handlers.uninstall() so we can remove this setTimeout
setTimeout(function () {
removeShortcuts(function () {
app.quit()
})
}, 1000)
return true return true
} }
@@ -113,7 +117,8 @@ function updateShortcuts (cb) {
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk') var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
// Check if the desktop shortcut has been previously deleted and and keep it deleted // Check if the desktop shortcut has been previously deleted and and keep it deleted
// if it was // if it was
pathExists(desktopShortcutPath).then(function (desktopShortcutExists) { fs.access(desktopShortcutPath, function (err) {
var desktopShortcutExists = !err
createShortcuts(function () { createShortcuts(function () {
if (desktopShortcutExists) { if (desktopShortcutExists) {
cb() cb()

83
main/tray.js Normal file
View File

@@ -0,0 +1,83 @@
module.exports = {
init,
hasTray
}
var cp = require('child_process')
var path = require('path')
var electron = require('electron')
var app = electron.app
var Menu = electron.Menu
var Tray = electron.Tray
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 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 = 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,17 +1,17 @@
var windows = module.exports = { var windows = module.exports = {
about: null, about: null,
main: null, main: null,
createAboutWindow: createAboutWindow, createAboutWindow,
createMainWindow: createMainWindow, createWebTorrentHiddenWindow,
focusWindow: focusWindow createMainWindow,
focusWindow
} }
var electron = require('electron') var electron = require('electron')
var app = electron.app
var config = require('../config') var config = require('../config')
var menu = require('./menu') var menu = require('./menu')
var tray = require('./tray')
function createAboutWindow () { function createAboutWindow () {
if (windows.about) { if (windows.about) {
@@ -48,22 +48,53 @@ function createAboutWindow () {
}) })
} }
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 (!electron.app.isQuitting) {
e.preventDefault()
win.hide()
}
})
win.once('closed', function () {
windows.webtorrent = null
})
}
function createMainWindow () { function createMainWindow () {
if (windows.main) { if (windows.main) {
return focusWindow(windows.main) return focusWindow(windows.main)
} }
var win = windows.main = new electron.BrowserWindow({ var win = windows.main = new electron.BrowserWindow({
backgroundColor: '#282828', backgroundColor: '#1E1E1E',
darkTheme: true, // Forces dark theme (GTK+3) darkTheme: true, // Forces dark theme (GTK+3)
icon: config.APP_ICON + '.png', icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
minWidth: 375, minWidth: config.WINDOW_MIN_WIDTH,
minHeight: 38 + (120 * 2), // header height + 2 torrents minHeight: config.WINDOW_MIN_HEIGHT,
show: false, // Hide window until DOM finishes loading show: false, // Hide window until DOM finishes loading
title: config.APP_WINDOW_TITLE, title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X) titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
useContentSize: true, // Specify web page size without OS chrome useContentSize: true, // Specify web page size without OS chrome
width: 450, width: 500,
height: 38 + (120 * 4) // header height + 4 torrents height: 38 + (120 * 5) // header height + 4 torrents
}) })
win.loadURL(config.WINDOW_MAIN) win.loadURL(config.WINDOW_MAIN)
@@ -78,10 +109,12 @@ function createMainWindow () {
win.on('leave-full-screen', () => menu.onToggleFullScreen(false)) win.on('leave-full-screen', () => menu.onToggleFullScreen(false))
win.on('close', function (e) { win.on('close', function (e) {
if (process.platform === 'darwin' && !app.isQuitting) { if (process.platform !== 'darwin' && !tray.hasTray()) {
electron.app.quit()
} else if (!electron.app.isQuitting) {
e.preventDefault() e.preventDefault()
win.send('dispatch', 'pause')
win.hide() win.hide()
win.send('dispatch', 'backToList')
} }
}) })

View File

@@ -1,71 +1,84 @@
{ {
"name": "webtorrent-app", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.1.1", "version": "0.3.3",
"author": { "author": {
"name": "Feross Aboukhadijeh", "name": "Feross Aboukhadijeh",
"email": "feross@feross.org", "email": "feross@feross.org",
"url": "http://feross.org" "url": "http://feross.org"
}, },
"bin": {
"webtorrent-desktop": "./bin/cmd.js"
},
"bugs": { "bugs": {
"url": "https://github.com/feross/webtorrent-app/issues" "url": "https://github.com/feross/webtorrent-desktop/issues"
}, },
"dependencies": { "dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js", "airplay-js": "guerrerocarlos/node-airplay-js",
"application-config": "^0.2.0", "application-config": "feross/node-application-config",
"application-config-path": "^0.1.0", "bitfield": "^1.0.2",
"chromecasts": "^1.8.0", "chromecasts": "^1.8.0",
"create-torrent": "^3.22.1", "concat-stream": "^1.5.1",
"debug": "^2.2.0", "create-torrent": "^3.24.5",
"deep-equal": "^1.0.1",
"dlnacasts": "^0.0.3",
"drag-drop": "^2.11.0", "drag-drop": "^2.11.0",
"electron-localshortcut": "^0.6.0", "electron-localshortcut": "^0.6.0",
"electron-prebuilt": "0.37.2", "electron-prebuilt": "0.37.6",
"fs-extra": "^0.27.0",
"hyperx": "^2.0.2", "hyperx": "^2.0.2",
"languagedetect": "^1.1.1",
"main-loop": "^3.2.0", "main-loop": "^3.2.0",
"mkdirp": "^0.5.1",
"musicmetadata": "^2.0.2", "musicmetadata": "^2.0.2",
"network-address": "^1.1.0", "network-address": "^1.1.0",
"path-exists": "^2.1.0",
"prettier-bytes": "^1.0.1", "prettier-bytes": "^1.0.1",
"simple-get": "^2.0.0",
"srt-to-vtt": "^1.1.1",
"upload-element": "^1.0.1", "upload-element": "^1.0.1",
"virtual-dom": "^2.1.1", "virtual-dom": "^2.1.1",
"webtorrent": "^0.87.1", "wcjs-player": "^0.5.7",
"winreg": "^1.0.1" "webchimera.js": "^0.2.3",
"webtorrent": "0.x",
"winreg": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"cross-zip": "^1.0.0",
"electron-osx-sign": "^0.3.0", "electron-osx-sign": "^0.3.0",
"electron-packager": "^5.0.0", "electron-packager": "^7.0.0",
"electron-winstaller": "^2.0.5", "electron-winstaller": "feross/windows-installer#build",
"gh-release": "^2.0.3", "gh-release": "^2.0.3",
"minimist": "^1.2.0",
"nobin-debian-installer": "^0.0.9",
"plist": "^1.2.0", "plist": "^1.2.0",
"rimraf": "^2.5.2", "run-series": "^1.1.4",
"standard": "^6.0.5" "standard": "^6.0.5"
}, },
"optionalDependencies": {
"appdmg": "^0.3.6"
},
"homepage": "https://webtorrent.io", "homepage": "https://webtorrent.io",
"keywords": [ "keywords": [
"desktop",
"electron", "electron",
"electron-app" "electron-app",
"webtorrent"
], ],
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"optionalDependencies": {
"appdmg": "^0.3.6"
},
"productName": "WebTorrent", "productName": "WebTorrent",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/feross/webtorrent-app.git" "url": "git://github.com/feross/webtorrent-desktop.git"
}, },
"scripts": { "scripts": {
"clean": "node ./bin/clean.js", "clean": "node ./bin/clean.js",
"debug": "DEBUG=* electron .", "package": "node ./bin/package.js",
"package": "npm install && npm prune && npm dedupe && node ./bin/package.js",
"size": "npm run package -- darwin && du -ch dist/WebTorrent-darwin-x64 | grep total",
"start": "electron .", "start": "electron .",
"test": "standard", "test": "standard",
"update-authors": "./bin/update-authors.sh" "update-authors": "./bin/update-authors.sh"
}, },
"bin": { "cmake-js": {
"webtorrent-app": "./bin/cmd.js" "runtime": "electron",
"runtimeVersion": "0.37.5"
} }
} }

View File

@@ -113,28 +113,32 @@ table {
opacity: 0.3; opacity: 0.3;
} }
/* .float-right {
* BUTTONS float: right;
*/
a,
i {
cursor: default;
-webkit-app-region: no-drag;
} }
a:not(.disabled):hover, .expand-collapse {
i:not(.disabled):hover { cursor: pointer;
-webkit-filter: brightness(1.3);
} }
.btn { .expand-collapse.expanded::before {
width: 40px; content: '▲'
height: 40px; }
border-radius: 20px;
font-size: 22px; .expand-collapse.collapsed::before {
transition: all 0.1s ease-out; content: '▼'
text-align: center; }
.expand-collapse::before {
margin-right: 5px;
}
.expand-collapse.collapsed {
display: block;
}
.collapsed {
display: none;
} }
/* /*
@@ -242,6 +246,10 @@ i:not(.disabled):hover {
* MODAL POPOVERS * MODAL POPOVERS
*/ */
.modal {
z-index: 2;
}
.modal .modal-background { .modal .modal-background {
content: ' '; content: ' ';
position: fixed; position: fixed;
@@ -262,42 +270,139 @@ i:not(.disabled):hover {
width: calc(100% - 20px); width: calc(100% - 20px);
max-width: 600px; max-width: 600px;
box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.4); box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.4);
background-color: white; background-color: #eee;
color: #222; color: #222;
padding: 20px; padding: 20px;
} }
.modal label {
font-size: 16px;
font-weight: bold;
}
.open-torrent-address-modal input { .open-torrent-address-modal input {
width: calc(100% - 100px) width: 100%;
}
.create-torrent-page {
padding: 10px 25px;
overflow: hidden;
}
.create-torrent-page .torrent-attribute {
white-space: nowrap;
}
.create-torrent-page .torrent-attribute>* {
display: inline-block;
}
.create-torrent-page .torrent-attribute label {
width: 60px;
margin-right: 10px;
vertical-align: top;
}
.create-torrent-page .torrent-attribute>div {
width: calc(100% - 90px);
}
.create-torrent-page .torrent-attribute div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.create-torrent-page .torrent-attribute textarea {
width: calc(100% - 80px);
height: 80px;
color: #eee;
background-color: transparent;
line-height: 1.5;
font-size: 14px;
font-family: inherit;
border-radius: 2px;
padding: 4px 6px;
}
.create-torrent-page textarea.torrent-trackers {
height: 200px;
}
.create-torrent-page input.torrent-is-private {
width: initial;
margin: 0;
} }
/* /*
* BUTTONS * BUTTONS
* See https://www.google.com/design/spec/components/buttons.html
*/ */
button { a,
i { /* Links and icons */
cursor: default;
-webkit-app-region: no-drag;
}
a:not(.disabled):hover,
i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
-webkit-filter: brightness(1.3);
}
.button-round { /* Circular icon buttons, used on <i> tags */
width: 40px;
height: 40px;
border-radius: 20px;
font-size: 22px;
transition: all 0.1s ease-out;
text-align: center;
}
button { /* Rectangular text buttons */
background: transparent; background: transparent;
margin-left: 10px; margin-left: 10px;
padding: 0; padding: 0;
border: none; border: none;
border-radius: 3px;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
cursor: pointer; cursor: pointer;
color: #aaa; color: #aaa;
outline: none;
} }
button.primary { button.button-flat {
color: #0cf; color: #222;
padding: 7px 18px;
} }
button:hover { button.button-flat.light {
-webkit-filter: brightness(1.1); color: #eee;
} }
button:active { button.button-flat:hover,
-webkit-filter: brightness(1.1); button.button-flat:focus { /* Material design: focused */
text-shadow: none; background-color: rgba(153, 153, 153, 0.2);
}
button.button-flat:active { /* Material design: pressed */
background-color: rgba(153, 153, 153, 0.4);
}
button.button-raised {
background-color: #2196f3;
color: #eee;
padding: 7px 18px;
}
button.button-raised:hover,
button.button-raised:focus {
background-color: #38a0f5;
}
button.button-raised:active {
background-color: #51abf6;
} }
/* /*
@@ -310,31 +415,7 @@ input {
padding: 6px; padding: 6px;
border: 1px solid #bbb; border: 1px solid #bbb;
border-radius: 3px; border-radius: 3px;
box-shadow: 1px 1px 1px 0px rgba(0,0,0,0.1); outline: none;
}
/*
* PLAYER
*/
.player {
position: absolute;
width: 100%;
height: 100%;
background-color: #000;
}
.player .letterbox {
width: 100%;
height: 100%;
display: flex;
background-size: cover;
background-position: center center;
}
.player video {
display: block;
width: 100%;
} }
/* /*
@@ -345,7 +426,7 @@ input {
background: linear-gradient(to bottom right, #4B79A1, #283E51); background: linear-gradient(to bottom right, #4B79A1, #283E51);
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
background-position: 0 50%; background-position: center;
transition: -webkit-filter 0.1s ease-out; transition: -webkit-filter 0.1s ease-out;
position: relative; position: relative;
animation: fadein .4s; animation: fadein .4s;
@@ -368,10 +449,15 @@ input {
position: absolute; position: absolute;
top: 20px; top: 20px;
left: 20px; left: 20px;
width: calc(100% - 150px); right: 20px;
width: calc(100% - 40px);
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px; text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
} }
.torrent:hover .metadata {
width: calc(100% - 150px);
}
.torrent .metadata span:not(:last-child)::after { .torrent .metadata span:not(:last-child)::after {
content: ' — '; content: ' — ';
} }
@@ -432,7 +518,8 @@ input {
padding-top: 8px; padding-top: 8px;
} }
.torrent.requested .play { .torrent.requested .play,
.loading-spinner {
border-top: 6px solid rgba(255, 255, 255, 0.2); border-top: 6px solid rgba(255, 255, 255, 0.2);
border-right: 6px solid rgba(255, 255, 255, 0.2); border-right: 6px solid rgba(255, 255, 255, 0.2);
border-bottom: 6px solid rgba(255, 255, 255, 0.2); border-bottom: 6px solid rgba(255, 255, 255, 0.2);
@@ -495,6 +582,17 @@ body.drag .torrent-placeholder span {
color: #def; color: #def;
} }
body.drag .app::after {
content: ' ';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 3;
}
/* /*
* TORRENT LIST: EXPANDED TORRENT DETAILS * TORRENT LIST: EXPANDED TORRENT DETAILS
*/ */
@@ -557,6 +655,32 @@ body.drag .torrent-placeholder span {
text-align: right; text-align: right;
} }
/*
* PLAYER
*/
.player {
position: absolute;
width: 100%;
height: 100%;
background-color: #000;
}
.player .letterbox {
width: 100%;
height: 100%;
display: flex;
background-size: cover;
background-position: center;
}
.player .video-player {
display: block;
width: 100%;
height: 100%;
position: relative;
}
/* /*
* PLAYER CONTROLS * PLAYER CONTROLS
*/ */
@@ -632,9 +756,10 @@ body.drag .torrent-placeholder span {
margin: 0 auto; margin: 0 auto;
} }
.player-controls .chromecast, .player-controls .device,
.player-controls .airplay,
.player-controls .fullscreen, .player-controls .fullscreen,
.player-controls .closed-captions,
.player-controls .volume-icon,
.player-controls .back { .player-controls .back {
display: block; display: block;
width: 20px; width: 20px;
@@ -642,12 +767,13 @@ body.drag .torrent-placeholder span {
margin: 5px; margin: 5px;
} }
.player-controls .volume,
.player-controls .back { .player-controls .back {
float: left; float: left;
} }
.player-controls .chromecast, .player-controls .device,
.player-controls .airplay, .player-controls .closed-captions,
.player-controls .fullscreen { .player-controls .fullscreen {
float: right; float: right;
} }
@@ -656,17 +782,50 @@ body.drag .torrent-placeholder span {
margin-right: 15px; margin-right: 15px;
} }
.player-controls .chromecast, .player-controls .volume-icon,
.player-controls .airplay { .player-controls .device {
font-size: 18px; /* make the cast icons less huge */ font-size: 18px; /* make the cast icons less huge */
margin-top: 8px !important; margin-top: 8px !important;
} }
.player-controls .chromecast.active, .player-controls .closed-captions.active,
.player-controls .airplay.active { .player-controls .device.active {
color: #9af; color: #9af;
} }
.player-controls .volume {
display: block;
width: 90px;
}
.player-controls .volume-icon {
float: left;
margin-right: 0px;
}
.player-controls .volume-slider {
-webkit-appearance: none;
width: 50px;
height: 3px;
border: none;
padding: 0;
vertical-align: sub;
}
.player-controls .volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: #fff;
opacity: 1.0;
width: 10px;
height: 10px;
border: 1px solid #303233;
border-radius: 50%;
}
.player-controls .volume-slider:focus {
outline: none;
}
.player .playback-bar:hover .loading-bar { .player .playback-bar:hover .loading-bar {
height: 5px; height: 5px;
} }
@@ -677,6 +836,15 @@ body.drag .torrent-placeholder span {
height: 14px; height: 14px;
} }
::cue {
background: none;
color: #FFF;
font: 24px;
line-height: 1.3em;
text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0;
}
/* /*
* CHROMECAST / AIRPLAY CONTROLS * CHROMECAST / AIRPLAY CONTROLS
*/ */
@@ -704,12 +872,45 @@ body.drag .torrent-placeholder span {
} }
/* /*
* AUDIO DETAILS * Subtitles list
*/ */
.audio-metadata { .subtitles-list {
width: 500px; position: fixed;
max-width: 100%; background: rgba(40, 40, 40, 0.8);
min-width: 100px;
bottom: 45px;
right: 3px;
transition: opacity 0.15s ease-out;
padding: 5px 10px;
border-radius: 3px;
margin: 0;
list-style-type: none;
color: rgba(255, 255, 255, 0.8);
}
.subtitles-list i {
font-size: 11px; /* make the cast icons less huge */
margin-right: 4px !important;
}
/*
* MEDIA OVERLAY / AUDIO DETAILS
*/
.media-overlay-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
background-size: cover;
background-position: center center;
}
.media-overlay {
max-width: calc(100% - 80px);
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
align-self: center; align-self: center;
@@ -719,6 +920,23 @@ body.drag .torrent-placeholder span {
line-height: 2; line-height: 2;
} }
.media-stalled .loading-spinner {
width: 40px;
height: 40px;
margin: 40px auto;
}
.media-stalled .loading-status {
font-size: 18px;
font-weight: normal;
text-align: center;
}
.audio-metadata div {
overflow: hidden;
text-overflow: ellipsis;
}
.audio-metadata .audio-title { .audio-metadata .audio-title {
font-size: 32px; font-size: 32px;
} }
@@ -771,29 +989,3 @@ body.drag .torrent-placeholder span {
.error-popover .error:last-child { .error-popover .error:last-child {
border-bottom: none; border-bottom: none;
} }
/*
* MEDIA QUERIES
*/
@media only screen and (min-width: 700px) {
body {
font-size: 16px;
line-height: 1.5em;
}
.torrent,
.torrent-placeholder {
height: 150px;
}
}
@media only screen and (min-width: 900px) {
body {
font-size: 18px;
line-height: 1.5em;
}
.torrent,
.torrent-placeholder {
height: 180px;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,70 +1,162 @@
var chromecasts = require('chromecasts')()
var airplay = require('airplay-js')
var config = require('../../config')
var state = require('../state')
// The Cast module talks to Airplay and Chromecast // The Cast module talks to Airplay and Chromecast
// * Modifies state when things change // * Modifies state when things change
// * Starts and stops casting, provides remote video controls // * Starts and stops casting, provides remote video controls
module.exports = { module.exports = {
init, init,
openChromecast, open,
openAirplay, close,
stopCasting, play,
playPause, pause,
seek, seek,
setVolume, setVolume
isCasting
} }
var airplay = require('airplay-js')
var chromecasts = require('chromecasts')()
var dlnacasts = require('dlnacasts')()
var config = require('../../config')
// App state. Cast modifies state.playing and state.errors in response to events
var state
// Callback to notify module users when state has changed // Callback to notify module users when state has changed
var update var update
function init (callback) { // setInterval() for updating cast status
var statusInterval = null
// Start looking for cast devices on the local network
function init (appState, callback) {
state = appState
update = callback update = callback
// Start polling Chromecast or Airplay, whenever we're connected // Listen for devices: Chromecast, DLNA and Airplay
setInterval(() => pollCastStatus(state), 1000)
// Listen for devices: Chromecast and Airplay
chromecasts.on('update', function (player) { chromecasts.on('update', function (player) {
state.devices.chromecast = player state.devices.chromecast = chromecastPlayer(player)
addChromecastEvents() })
dlnacasts.on('update', function (player) {
state.devices.dlna = dlnaPlayer(player)
}) })
var browser = airplay.createBrowser() var browser = airplay.createBrowser()
browser.on('deviceOn', function (player) { browser.on('deviceOn', function (player) {
state.devices.airplay = player state.devices.airplay = airplayPlayer(player)
addAirplayEvents()
}).start() }).start()
} }
function addChromecastEvents () { // chromecast player implementation
state.devices.chromecast.on('error', function (err) { function chromecastPlayer (player) {
state.devices.chromecast.errorMessage = err.message function addEvents () {
update() player.on('error', function (err) {
}) state.playing.location = 'local'
state.devices.chromecast.on('disconnect', function () { state.errors.push({
state.playing.location = 'local' time: new Date().getTime(),
update() message: 'Could not connect to Chromecast. ' + err.message
}) })
} update()
})
player.on('disconnect', function () {
state.playing.location = 'local'
update()
})
}
function addAirplayEvents () {} function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
player.play(state.server.networkURL, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name
}, function (err) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to Chromecast. ' + err.message
})
} else {
state.playing.location = 'chromecast'
}
update()
})
}
// Update our state from the remote TV function play (callback) {
function pollCastStatus (state) { player.play(null, null, callback)
if (state.playing.location === 'chromecast') { }
state.devices.chromecast.status(function (err, status) {
function pause (callback) {
player.pause(callback)
}
function stop (callback) {
player.stop(callback)
}
function status () {
player.status(function (err, status) {
if (err) return console.log('error getting %s status: %o', state.playing.location, err) if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED' state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level state.playing.volume = status.volume.muted ? 0 : status.volume.level
update() update()
}) })
} else if (state.playing.location === 'airplay') { }
state.devices.airplay.status(function (status) {
function seek (time, callback) {
player.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
}
}
// airplay player implementation
function airplayPlayer (player) {
function open () {
player.play(state.server.networkURL, 0, function (res) {
if (res.statusCode !== 200) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to AirPlay.'
})
} else {
state.playing.location = 'airplay'
}
update()
})
}
function play (callback) {
player.rate(1, callback)
}
function pause (callback) {
player.rate(0, callback)
}
function stop (callback) {
player.stop(callback)
}
function status () {
player.status(function (status) {
state.playing.isPaused = status.rate === 0 state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position state.playing.currentTime = status.position
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume // TODO: get airplay volume, implementation needed. meanwhile set value in setVolume
@@ -73,54 +165,147 @@ function pollCastStatus (state) {
update() update()
}) })
} }
}
function openChromecast () { function seek (time, callback) {
if (state.playing.location !== 'local') { player.scrub(time, callback)
throw new Error('You can\'t connect to Chromecast when already connected to another device')
} }
state.playing.location = 'chromecast-pending' function volume (volume, callback) {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) // TODO remove line below once we can fetch the information in status update
state.devices.chromecast.play(state.server.networkURL, { state.playing.volume = volume
type: 'video/mp4', volume = (volume - 1) * 30
title: config.APP_NAME + ' — ' + torrentSummary.name player.volume(volume, callback)
}, function (err) {
state.playing.location = err ? 'local' : 'chromecast'
update()
})
update()
}
function openAirplay () {
if (state.playing.location !== 'local') {
throw new Error('You can\'t connect to Airplay when already connected to another device')
} }
state.playing.location = 'airplay-pending' return {
state.devices.airplay.play(state.server.networkURL, 0, function (res) { player: player,
if (res.statusCode !== 200) { 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) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
time: new Date().getTime(), time: new Date().getTime(),
message: 'Couldn\'t connect to Airplay' message: 'Could not connect to DLNA. ' + err.message
}) })
} else { update()
state.playing.location = 'airplay' })
player.on('disconnect', function () {
state.playing.location = 'local'
update()
})
}
function open () {
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
player.play(state.server.networkURL, {
type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
}, function (err) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to DLNA. ' + err.message
})
} else {
state.playing.location = 'dlna'
}
update()
})
}
function play (callback) {
player.play(null, null, callback)
}
function pause (callback) {
player.pause(callback)
}
function stop (callback) {
player.stop(callback)
}
function status () {
player.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
state.playing.volume = status.volume.level
update()
})
}
function seek (time, callback) {
player.seek(time, callback)
}
function volume (volume, callback) {
player.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()
} }
update() }, 1000)
}) }
function open (location) {
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()
}
update() update()
} }
// Stops Chromecast or Airplay, move video back to local screen // Stops casting, move video back to local screen
function stopCasting () { function close () {
if (state.playing.location === 'chromecast') { var device = getDevice()
state.devices.chromecast.stop(stoppedCasting) if (device) {
} else if (state.playing.location === 'airplay') { device.stop(stoppedCasting)
state.devices.airplay.stop(stoppedCasting) clearInterval(statusInterval)
} else if (state.playing.location.endsWith('-pending')) { } else {
// Connecting to Chromecast took too long or errored out. Let the user cancel
stoppedCasting() stoppedCasting()
} }
} }
@@ -131,45 +316,48 @@ function stoppedCasting () {
update() update()
} }
// Checks whether we are connected and already casting function getDevice (location) {
// Returns false if we not casting (state.playing.location === 'local') if (location && state.devices[location]) {
// or if we're trying to connect but haven't yet ('chromecast-pending', etc) return state.devices[location]
function isCasting () { } else if (state.playing.location === 'chromecast') {
return state.playing.location === 'chromecast' || state.playing.location === 'airplay' return state.devices.chromecast
} else if (state.playing.location === 'airplay') {
return state.devices.airplay
} else if (state.playing.location === 'dlna') {
return state.devices.dlna
} else {
return null
}
} }
function playPause () { function play () {
var device var device = getDevice()
if (state.playing.location === 'chromecast') { if (device) {
device = state.devices.chromecast device.play(castCallback)
if (!state.playing.isPaused) device.pause(castCallback) }
else device.play(null, null, castCallback) }
} else if (state.playing.location === 'airplay') {
device = state.devices.airplay function pause () {
if (!state.playing.isPaused) device.rate(0, castCallback) var device = getDevice()
else device.rate(1, castCallback) if (device) {
device.pause(castCallback)
} }
} }
function seek (time) { function seek (time) {
if (state.playing.location === 'chromecast') { var device = getDevice()
state.devices.chromecast.seek(time, castCallback) if (device) {
} else if (state.playing.location === 'airplay') { device.seek(time, castCallback)
state.devices.airplay.scrub(time, castCallback)
} }
} }
function setVolume (volume) { function setVolume (volume) {
if (state.playing.location === 'chromecast') { var device = getDevice()
state.devices.chromecast.volume(volume, castCallback) if (device) {
} else if (state.playing.location === 'airplay') { device.volume(volume, castCallback)
// TODO remove line below once we can fetch the information in status update
state.playing.volume = volume
volume = (volume - 1) * 30
state.devices.airplay.volume(volume, castCallback)
} }
} }
function castCallback () { function castCallback () {
console.log(state.playing.location + ' callback: %o', arguments) console.log('%s callback: %o', state.playing.location, arguments)
} }

View File

@@ -20,11 +20,13 @@ function dispatcher (...args) {
var json = JSON.stringify(args) var json = JSON.stringify(args)
var handler = _dispatchers[json] var handler = _dispatchers[json]
if (!handler) { if (!handler) {
_dispatchers[json] = (e) => { handler = _dispatchers[json] = (e) => {
// Don't click on whatever is below the button if (e && e.stopPropagation && e.currentTarget) {
e.stopPropagation() // Don't click on whatever is below the button
// Don't regisiter clicks on disabled buttons e.stopPropagation()
if (e.target.classList.contains('disabled')) return // Don't register clicks on disabled buttons
if (e.currentTarget.classList.contains('disabled')) return
}
_dispatch.apply(null, args) _dispatch.apply(null, args)
} }
} }

View File

@@ -4,44 +4,59 @@ function LocationHistory () {
if (!new.target) return new LocationHistory() if (!new.target) return new LocationHistory()
this._history = [] this._history = []
this._forward = [] this._forward = []
this._pending = null
} }
LocationHistory.prototype.go = function (page) { LocationHistory.prototype.go = function (page, cb) {
console.log('go', page) console.log('go', page)
this.clearForward() this.clearForward()
this._go(page) this._go(page, cb)
} }
LocationHistory.prototype._go = function (page) { LocationHistory.prototype._go = function (page, cb) {
if (this._pending) return
if (page.onbeforeload) { if (page.onbeforeload) {
this._pending = page
page.onbeforeload((err) => { page.onbeforeload((err) => {
if (err) return if (this._pending !== page) return /* navigation was cancelled */
this._pending = null
if (err) {
if (cb) cb(err)
return
}
this._history.push(page) this._history.push(page)
if (cb) cb()
}) })
} else { } else {
this._history.push(page) this._history.push(page)
if (cb) cb()
} }
} }
LocationHistory.prototype.back = function () { LocationHistory.prototype.back = function (cb) {
if (this._history.length <= 1) return if (this._history.length <= 1) return
var page = this._history.pop() var page = this._history.pop()
if (page.onbeforeunload) { if (page.onbeforeunload) {
// TODO: this is buggy. If the user clicks back twice, then those pages
// may end up in _forward in the wrong order depending on which onbeforeunload
// call finishes first.
page.onbeforeunload(() => { page.onbeforeunload(() => {
this._forward.push(page) this._forward.push(page)
if (cb) cb()
}) })
} else { } else {
this._forward.push(page) this._forward.push(page)
if (cb) cb()
} }
} }
LocationHistory.prototype.forward = function () { LocationHistory.prototype.forward = function (cb) {
if (this._forward.length === 0) return if (this._forward.length === 0) return
var page = this._forward.pop() var page = this._forward.pop()
this._go(page) this._go(page, cb)
} }
LocationHistory.prototype.clearForward = function () { LocationHistory.prototype.clearForward = function () {
@@ -59,3 +74,11 @@ LocationHistory.prototype.hasBack = function () {
LocationHistory.prototype.hasForward = function () { LocationHistory.prototype.hasForward = function () {
return this._forward.length > 0 return this._forward.length > 0
} }
LocationHistory.prototype.pending = function () {
return this._pending
}
LocationHistory.prototype.clearPending = function () {
this._pending = null
}

71
renderer/lib/sound.js Normal file
View File

@@ -0,0 +1,71 @@
module.exports = {
preload,
play
}
var config = require('../../config')
var path = require('path')
/* Cache of Audio elements, for instant playback */
var cache = {}
var sounds = {
ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
volume: 0.2
},
DELETE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
volume: 0.1
},
DISABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
volume: 0.2
},
DONE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
volume: 0.2
},
ENABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
volume: 0.2
},
ERROR: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
volume: 0.2
},
PLAY: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
volume: 0.2
},
STARTUP: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
volume: 0.4
}
}
function preload () {
for (var name in sounds) {
if (!cache[name]) {
var sound = sounds[name]
var audio = cache[name] = new window.Audio()
audio.volume = sound.volume
audio.src = sound.url
}
}
}
function play (name) {
var audio = cache[name]
if (!audio) {
var sound = sounds[name]
if (!sound) {
throw new Error('Invalid sound name')
}
audio = cache[name] = new window.Audio()
audio.volume = sound.volume
audio.src = sound.url
}
audio.currentTime = 0
audio.play()
}

View File

@@ -1,7 +1,8 @@
module.exports = { module.exports = {
isPlayable, isPlayable,
isVideo, isVideo,
isAudio isAudio,
isPlayableTorrent
} }
var path = require('path') var path = require('path')
@@ -14,11 +15,15 @@ function isPlayable (file) {
} }
function isVideo (file) { function isVideo (file) {
var ext = path.extname(file.name) var ext = path.extname(file.name).toLowerCase()
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1 return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
} }
function isAudio (file) { function isAudio (file) {
var ext = path.extname(file.name) var ext = path.extname(file.name).toLowerCase()
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1 return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
} }
function isPlayableTorrent (torrentSummary) {
return torrentSummary.files && torrentSummary.files.some(isPlayable)
}

View File

@@ -20,7 +20,7 @@ function torrentPoster (torrent, cb) {
function getLargestFileByExtension (torrent, extensions) { function getLargestFileByExtension (torrent, extensions) {
var files = torrent.files.filter(function (file) { var files = torrent.files.filter(function (file) {
var extname = path.extname(file.name) var extname = path.extname(file.name).toLowerCase()
return extensions.indexOf(extname) !== -1 return extensions.indexOf(extname) !== -1
}) })
if (files.length === 0) return undefined if (files.length === 0) return undefined
@@ -64,6 +64,8 @@ function torrentPosterFromVideo (file, torrent, cb) {
server.destroy() server.destroy()
if (buf.length === 0) return cb(new Error('Generated poster contains no data'))
cb(null, buf, '.jpg') cb(null, buf, '.jpg')
} }
} }

View File

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

View File

@@ -1,9 +0,0 @@
var path = require('path')
var config = require('../config')
exports.getAbsoluteStaticPath = function (filePath) {
return path.isAbsolute(filePath)
? filePath
: path.join(config.STATIC_PATH, filePath)
}

View File

@@ -5,13 +5,17 @@ var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var Header = require('./header') var Header = require('./header')
var Player = require('./player') var Views = {
var TorrentList = require('./torrent-list') 'home': require('./torrent-list'),
'player': require('./player'),
'create-torrent': require('./create-torrent-page')
}
var Modals = { var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal') 'open-torrent-address-modal': require('./open-torrent-address-modal'),
'update-available-modal': require('./update-available-modal')
} }
function App (state, dispatch) { function App (state) {
// Hide player controls while playing video, if the mouse stays still for a while // Hide player controls while playing video, if the mouse stays still for a while
// Never hide the controls when: // Never hide the controls when:
// * The mouse is over the controls or we're scrubbing (see CSS) // * The mouse is over the controls or we're scrubbing (see CSS)
@@ -38,47 +42,43 @@ function App (state, dispatch) {
return hx` return hx`
<div class='app ${cls.join(' ')}'> <div class='app ${cls.join(' ')}'>
${Header(state, dispatch)} ${Header(state)}
${getErrorPopover()} ${getErrorPopover(state)}
<div class='content'>${getView()}</div> <div class='content'>${getView(state)}</div>
${getModal()} ${getModal(state)}
</div> </div>
` `
}
function getErrorPopover () {
var now = new Date().getTime() function getErrorPopover (state) {
var recentErrors = state.errors.filter((x) => now - x.time < 5000) var now = new Date().getTime()
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
var errorElems = recentErrors.map(function (error) {
return hx`<div class='error'>${error.message}</div>` var errorElems = recentErrors.map(function (error) {
}) return hx`<div class='error'>${error.message}</div>`
return hx` })
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'> return hx`
<div class='title'>Error</div> <div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
${errorElems} <div class='title'>Error</div>
</div> ${errorElems}
` </div>
} `
}
function getModal () {
if (state.modal) { function getModal (state) {
var contents = Modals[state.modal](state, dispatch) if (!state.modal) return
return hx` var contents = Modals[state.modal.id](state)
<div class='modal'> return hx`
<div class='modal-background'></div> <div class='modal'>
<div class='modal-content add-file-modal'> <div class='modal-background'></div>
${contents} <div class='modal-content'>
</div> ${contents}
</div> </div>
` </div>
} `
} }
function getView () { function getView (state) {
if (state.location.current().url === 'home') { var url = state.location.current().url
return TorrentList(state, dispatch) return Views[url](state)
} else if (state.location.current().url === 'player') {
return Player(state, dispatch)
}
}
} }

View File

@@ -0,0 +1,138 @@
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} = 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}))
// First, extract the base folder that the files are all in
var pathPrefix = info.folderPath
if (!pathPrefix) {
if (files.length > 0) {
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
pathPrefix = path.dirname(pathPrefix)
}
} else {
pathPrefix = files[0]
}
}
// Sanity check: show the number of files and total size
var numFiles = files.length
console.log('FILES', files)
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 = path.basename(pathPrefix)
var 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)
dispatch('backToList')
}
function handleCancel () {
dispatch('backToList')
}
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')
}
}
// 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

@@ -9,12 +9,15 @@ var {dispatch} = require('../lib/dispatcher')
function OpenTorrentAddressModal (state) { function OpenTorrentAddressModal (state) {
return hx` return hx`
<div class='open-torrent-address-modal'> <div class='open-torrent-address-modal'>
<p><strong>Enter torrent address or magnet link</strong></p> <p><label>Enter torrent address or magnet link</label></p>
<p> <p>
<input id='add-torrent-url' type='text' autofocus onkeypress=${handleKeyPress} /> <input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
<button class='primary' onclick=${handleOK}>OK</button>
<button class='cancel' onclick=${handleCancel}>Cancel</button>
</p> </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> </div>
` `
} }

View File

@@ -4,7 +4,11 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx') var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var util = require('../util') var WebChimeraPlayer = require('wcjs-player')
var prettyBytes = require('prettier-bytes')
var Bitfield = require('bitfield')
var TorrentSummary = require('../lib/torrent-summary')
var {dispatch, dispatcher} = require('../lib/dispatcher') var {dispatch, dispatcher} = require('../lib/dispatcher')
// Shows a streaming video player. Standard features + Chromecast + Airplay // Shows a streaming video player. Standard features + Chromecast + Airplay
@@ -18,17 +22,23 @@ function Player (state) {
onmousemove=${dispatcher('mediaMouseMoved')}> onmousemove=${dispatcher('mediaMouseMoved')}>
${showVideo ? renderMedia(state) : renderCastScreen(state)} ${showVideo ? renderMedia(state) : renderCastScreen(state)}
${renderPlayerControls(state)} ${renderPlayerControls(state)}
</div> </div>
` `
} }
function renderMedia (state) { function renderMedia (state) {
if (!state.server) return if (!state.server) return
if (false) return renderMediaTag(state)
else return renderMediaVLC(state)
}
// Renders using a <video> or <audio> tag
// Handles only a subset of codecs, but it's cleaner and more efficient
// See renderMediaVLC()
function renderMediaTag (state) {
// Unfortunately, play/pause can't be done just by modifying HTML. // Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary // Instead, grab the DOM node and play/pause it if necessary
var mediaType = state.playing.type /* 'audio' or 'video' */ var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
var mediaElement = document.querySelector(mediaType) /* get the <video> or <audio> tag */
if (mediaElement !== null) { if (mediaElement !== null) {
if (state.playing.isPaused && !mediaElement.paused) { if (state.playing.isPaused && !mediaElement.paused) {
mediaElement.pause() mediaElement.pause()
@@ -40,17 +50,42 @@ function renderMedia (state) {
mediaElement.currentTime = state.playing.jumpToTime mediaElement.currentTime = state.playing.jumpToTime
state.playing.jumpToTime = null state.playing.jumpToTime = null
} }
// set volume // Set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) { if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
mediaElement.volume = state.playing.setVolume mediaElement.volume = state.playing.setVolume
state.playing.setVolume = null state.playing.setVolume = null
} }
// fix textTrack cues not been removed <track> rerender
if (state.playing.subtitles.change) {
var tracks = mediaElement.textTracks
for (var j = 0; j < tracks.length; j++) {
// mode is not an <track> attribute, only available on DOM
tracks[j].mode = (tracks[j].label === state.playing.subtitles.change) ? 'showing' : 'hidden'
}
state.playing.subtitles.change = null
}
state.playing.currentTime = mediaElement.currentTime state.playing.currentTime = mediaElement.currentTime
state.playing.duration = mediaElement.duration state.playing.duration = mediaElement.duration
state.playing.volume = mediaElement.volume state.playing.volume = mediaElement.volume
} }
// Add subtitles to the <video> tag
var trackTags = []
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i]
trackTags.push(hx`
<track
${track.selected ? 'default' : ''}
label=${track.label}
type='subtitles'
src=${track.buffer}>
`)
}
}
// Create the <audio> or <video> tag // Create the <audio> or <video> tag
var mediaTag = hx` var mediaTag = hx`
<div <div
@@ -58,33 +93,28 @@ function renderMedia (state) {
ondblclick=${dispatcher('toggleFullScreen')} ondblclick=${dispatcher('toggleFullScreen')}
onloadedmetadata=${onLoadedMetadata} onloadedmetadata=${onLoadedMetadata}
onended=${onEnded} onended=${onEnded}
onplay=${dispatcher('mediaPlaying')} onstalling=${dispatcher('mediaStalled')}
onpause=${dispatcher('mediaPaused')} onerror=${dispatcher('mediaError')}
ontimeupdate=${dispatcher('mediaTimeUpdate')}
autoplay> autoplay>
${trackTags}
</div> </div>
` `
mediaTag.tagName = mediaType mediaTag.tagName = state.playing.type // conditional tag name
// Show the media. // Show the media.
// Video fills the window, centered with black bars if necessary
// Audio gets a static poster image and a summary of the file metadata.
var isAudio = mediaType === 'audio'
var style = {
backgroundImage: isAudio ? cssBackgroundImagePoster(state) : ''
}
return hx` return hx`
<div <div
class='letterbox' class='letterbox'
style=${style}
onmousemove=${dispatcher('mediaMouseMoved')}> onmousemove=${dispatcher('mediaMouseMoved')}>
${mediaTag} ${mediaTag}
${renderAudioMetadata(state)} ${renderOverlay(state)}
</div> </div>
` `
// As soon as the video loads enough to know the video dimensions, resize the window // As soon as the video loads enough to know the video dimensions, resize the window
function onLoadedMetadata (e) { function onLoadedMetadata (e) {
if (mediaType !== 'video') return if (state.playing.type !== 'video') return
var video = e.target var video = e.target
var dimensions = { var dimensions = {
width: video.videoWidth, width: video.videoWidth,
@@ -99,15 +129,137 @@ function renderMedia (state) {
} }
} }
// Renders using WebChimera.js to render using VLC
// That lets us play media that the <video> tag can't play
function renderMediaVLC (state) {
// Unfortunately, WebChimera can't be done just by modifying HTML.
// Instead, grab the DOM node
if (document.querySelector('#media-player')) {
if (!state.playing.chimera) {
state.playing.chimera = new WebChimeraPlayer('#media-player')
.addPlayer({
autoplay: true,
vlcArgs: ['-vvv'],
wcjsRendererOptions: {'fallbackRenderer': true}
})
.onPlaying(dispatcher('mediaPlaying'))
.onPaused(dispatcher('mediaPaused'))
.onBuffering(dispatcher('mediaStalled'))
.onTime(dispatcher('mediaTimeUpdate'))
.onEnded(onEnded)
.onFrameSetup(onLoadedMetadata)
.addPlaylist(state.server.localURL)
state.playing.chimera.ui(false)
} else {
var player = state.playing.chimera
if (state.playing.isPaused && player.playing()) {
player.pause()
} else if (!state.playing.isPaused && !player.playing()) {
player.play()
}
// When the user clicks or drags on the progress bar, jump to that position
if (state.playing.jumpToTime) {
player.time(state.playing.jumpToTime * 1000) // WebChimera expects milliseconds
state.playing.jumpToTime = null
}
// Set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
player.volume(Math.round(state.playing.setVolume * 100)) // WebChimera expects integer percent
state.playing.setVolume = null
}
state.playing.currentTime = player.time() / 1000
state.playing.duration = player.length() / 1000
state.playing.volume = player.volume() / 100
}
} else {
state.playing.chimera = null
}
// Add subtitles to the <video> tag
var trackTags = []
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i]
trackTags.push(hx`
<track
default=${track.selected ? 'default' : ''}
label=${track.language}
type='subtitles'
src=${track.buffer}>
`)
}
}
// Create the <audio> or <video> tag
var mediaType = state.playing.type /* 'video' or 'audio' */
var mediaTag = hx`
<div id='media-player' class='${mediaType}-player'>
${trackTags}
</div>
`
// Show the media.
return hx`
<div
class='letterbox'
onmousemove=${dispatcher('mediaMouseMoved')}>
${mediaTag}
${renderOverlay(state)}
</div>
`
// As soon as the video loads enough to know the video dimensions, resize the window
function onLoadedMetadata (e) {
if (mediaType !== 'video') return
var dimensions = {
width: player.width(),
height: player.height()
}
dispatch('setDimensions', dimensions)
}
// When the video completes, pause the video instead of looping
function onEnded (e) {
state.playing.isPaused = true
}
}
function renderOverlay (state) {
var elems = []
var audioMetadataElem = renderAudioMetadata(state)
var spinnerElem = renderLoadingSpinner(state)
if (audioMetadataElem) elems.push(audioMetadataElem)
if (spinnerElem) elems.push(spinnerElem)
// Video fills the window, centered with black bars if necessary
// Audio gets a static poster image and a summary of the file metadata.
var style
if (state.playing.type === 'audio') {
style = { backgroundImage: cssBackgroundImagePoster(state) }
} else if (elems.length !== 0) {
style = { backgroundImage: cssBackgroundImageDarkGradient() }
} else {
return /* Video, not audio, and it isn't stalled, so no spinner. No overlay needed. */
}
return hx`
<div class='media-overlay-background' style=${style}>
<div class='media-overlay'>${elems}</div>
</div>
`
}
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
if (!state.playing.audioInfo) return var torrentSummary = getPlayingTorrentSummary(state)
var info = state.playing.audioInfo var fileSummary = torrentSummary.files[state.playing.fileIndex]
if (!fileSummary.audioInfo) return
var info = fileSummary.audioInfo
// Get audio track info // Get audio track info
var title = info.title var title = info.title
if (!title) { if (!title) {
var torrentSummary = getPlayingTorrentSummary(state) title = fileSummary.name
title = torrentSummary.files[state.playing.fileIndex].name
} }
var artist = info.artist && info.artist[0] var artist = info.artist && info.artist[0]
var album = info.album var album = info.album
@@ -119,57 +271,97 @@ function renderAudioMetadata (state) {
track = info.track.no + ' of ' + info.track.of track = info.track.no + ' of ' + info.track.of
} }
// Show a small info box in the middle of the screen // Show a small info box in the middle of the screen with title/album/artist/etc
var elems = [hx`<div class='audio-title'><label></label>${title}</div>`] var elems = []
if (artist) elems.push(hx`<div class='audio-artist'><label>Artist</label>${artist}</div>`) if (artist) elems.push(hx`<div class='audio-artist'><label>Artist</label>${artist}</div>`)
if (album) elems.push(hx`<div class='audio-album'><label>Album</label>${album}</div>`) if (album) elems.push(hx`<div class='audio-album'><label>Album</label>${album}</div>`)
if (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`) if (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`)
// Align the title with the artist/etc info, if available. Otherwise, center the title
var emptyLabel = hx`<label></label>`
elems.unshift(hx`<div class='audio-title'>${elems.length ? emptyLabel : undefined}${title}</div>`)
return hx`<div class='audio-metadata'>${elems}</div>` return hx`<div class='audio-metadata'>${elems}</div>`
} }
function renderLoadingSpinner (state) {
if (state.playing.isPaused) return
var isProbablyStalled = state.playing.isStalled ||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
if (!isProbablyStalled) return
var prog = getPlayingTorrentSummary(state).progress || {}
var fileProgress = 0
if (prog.files) {
var file = prog.files[state.playing.fileIndex]
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>
</div>
</div>
`
}
function renderCastScreen (state) { function renderCastScreen (state) {
var isChromecast = state.playing.location.startsWith('chromecast') var castIcon, castType
var isAirplay = state.playing.location.startsWith('airplay') if (state.playing.location.startsWith('chromecast')) {
castIcon = 'cast_connected'
castType = 'Chromecast'
} else if (state.playing.location.startsWith('airplay')) {
castIcon = 'airplay'
castType = 'AirPlay'
} else if (state.playing.location.startsWith('dlna')) {
castIcon = 'tv'
castType = 'DLNA'
}
var isStarting = state.playing.location.endsWith('-pending') var isStarting = state.playing.location.endsWith('-pending')
if (!isChromecast && !isAirplay) throw new Error('Unimplemented cast type') var castStatus = isStarting ? 'Connecting...' : 'Connected'
// Show a nice title image, if possible // Show a nice title image, if possible
var style = { var style = {
backgroundImage: cssBackgroundImagePoster(state) backgroundImage: cssBackgroundImagePoster(state)
} }
// Show whether we're connected to Chromecast / Airplay
var castStatus = isStarting ? 'Connecting...' : 'Connected'
return hx` return hx`
<div class='letterbox' style=${style}> <div class='letterbox' style=${style}>
<div class='cast-screen'> <div class='cast-screen'>
<i class='icon'>${isAirplay ? 'airplay' : 'cast'}</i> <i class='icon'>${castIcon}</i>
<div class='cast-type'>${isAirplay ? 'AirPlay' : 'Chromecast'}</div> <div class='cast-type'>${castType}</div>
<div class='cast-status'>${castStatus}</div> <div class='cast-status'>${castStatus}</div>
</div> </div>
</div> </div>
` `
} }
// Returns the CSS background-image string for a poster image + dark vignette function renderSubtitlesOptions (state) {
function cssBackgroundImagePoster (state) { var subtitles = state.playing.subtitles
var torrentSummary = getPlayingTorrentSummary(state) if (subtitles.tracks.length && subtitles.show) {
if (!torrentSummary || !torrentSummary.posterURL) return '' return hx`<ul.subtitles-list>
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL) ${subtitles.tracks.map(function (w, i) {
var cleanURL = posterURL.replace(/\\/g, '/') return hx`<li onclick=${dispatcher('selectSubtitle', w.label)}><i.icon>${w.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>${w.label}</li>`
return 'radial-gradient(circle at center, ' + })}
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' + <li onclick=${dispatcher('selectSubtitle', '')}><i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>None</li>
`, url(${cleanURL})` </ul>
} `
}
function getPlayingTorrentSummary (state) {
var infoHash = state.playing.infoHash
return state.saved.torrents.find((x) => x.infoHash === infoHash)
} }
function renderPlayerControls (state) { function renderPlayerControls (state) {
var positionPercent = 100 * state.playing.currentTime / state.playing.duration var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' } var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled'
: state.playing.subtitles.enabled
? 'active'
: ''
var elements = [ var elements = [
hx` hx`
@@ -190,44 +382,79 @@ function renderPlayerControls (state) {
` `
] ]
if (state.playing.type === 'video') {
// show closed captions icon
elements.push(hx`
<i.icon.closed-captions
class=${captionsClass}
onclick=${handleSubtitles}>
closed_captions
</i>
`)
}
// If we've detected a Chromecast or AppleTV, the user can play video there // If we've detected a Chromecast or AppleTV, the user can play video there
var isOnChromecast = state.playing.location.startsWith('chromecast') var isOnChromecast = state.playing.location.startsWith('chromecast')
var isOnAirplay = state.playing.location.startsWith('airplay') var isOnAirplay = state.playing.location.startsWith('airplay')
var chromecastClass, chromecastHandler, airplayClass, airplayHandler var isOnDlna = state.playing.location.startsWith('dlna')
var chromecastClass, chromecastHandler, airplayClass, airplayHandler, dlnaClass, dlnaHandler
if (isOnChromecast) { if (isOnChromecast) {
chromecastClass = 'active' chromecastClass = 'active'
dlnaClass = 'disabled'
airplayClass = 'disabled' airplayClass = 'disabled'
chromecastHandler = dispatcher('stopCasting') chromecastHandler = dispatcher('closeDevice')
airplayHandler = undefined airplayHandler = undefined
dlnaHandler = undefined
} else if (isOnAirplay) { } else if (isOnAirplay) {
chromecastClass = 'disabled' chromecastClass = 'disabled'
dlnaClass = 'disabled'
airplayClass = 'active' airplayClass = 'active'
chromecastHandler = undefined chromecastHandler = undefined
airplayHandler = dispatcher('stopCasting') airplayHandler = dispatcher('closeDevice')
dlnaHandler = undefined
} else if (isOnDlna) {
chromecastClass = 'disabled'
dlnaClass = 'active'
airplayClass = 'disabled'
chromecastHandler = undefined
airplayHandler = undefined
dlnaHandler = dispatcher('closeDevice')
} else { } else {
chromecastClass = '' chromecastClass = ''
airplayClass = '' airplayClass = ''
chromecastHandler = dispatcher('openChromecast') dlnaClass = ''
airplayHandler = dispatcher('openAirplay') chromecastHandler = dispatcher('openDevice', 'chromecast')
airplayHandler = dispatcher('openDevice', 'airplay')
dlnaHandler = dispatcher('openDevice', 'dlna')
} }
if (state.devices.chromecast || isOnChromecast) { if (state.devices.chromecast || isOnChromecast) {
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
elements.push(hx` elements.push(hx`
<i.icon.chromecast <i.icon.device
class=${chromecastClass} class=${chromecastClass}
onclick=${chromecastHandler}> onclick=${chromecastHandler}>
cast ${castIcon}
</i> </i>
`) `)
} }
if (state.devices.airplay || isOnAirplay) { if (state.devices.airplay || isOnAirplay) {
elements.push(hx` elements.push(hx`
<i.icon.airplay <i.icon.device
class=${airplayClass} class=${airplayClass}
onclick=${airplayHandler}> onclick=${airplayHandler}>
airplay airplay
</i> </i>
`) `)
} }
if (state.devices.dlna || isOnDlna) {
elements.push(hx`
<i.icon.device
class=${dlnaClass}
onclick=${dlnaHandler}>
tv
</i>
`)
}
// On OSX, the back button is in the title bar of the window; see app.js // On OSX, the back button is in the title bar of the window; see app.js
// On other platforms, we render one over the video on mouseover // On other platforms, we render one over the video on mouseover
@@ -240,6 +467,31 @@ function renderPlayerControls (state) {
`) `)
} }
// render volume
var volume = state.playing.volume
var volumeIcon = 'volume_' + (volume === 0 ? 'off' : volume < 0.3 ? 'mute' : volume < 0.6 ? 'down' : 'up')
var volumeStyle = { background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' +
'color-stop(' + (volume * 100) + '%, #727272))'
}
elements.push(hx`
<div.volume
onwheel=${handleVolumeWheel}>
<i.icon.volume-icon onmousedown=${handleVolumeMute}>
${volumeIcon}
</i>
<input.volume-slider
type='range' min='0' max='1' step='0.05' value=${volumeChanging !== false ? volumeChanging : volume}
onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub}
onwheel=${handleVolumeWheel}
style=${volumeStyle}
/>
</div>
`)
// Finally, the big button in the center plays or pauses the video // Finally, the big button in the center plays or pauses the video
elements.push(hx` elements.push(hx`
<i class='icon play-pause' onclick=${dispatcher('playPause')}> <i class='icon play-pause' onclick=${dispatcher('playPause')}>
@@ -247,7 +499,12 @@ function renderPlayerControls (state) {
</i> </i>
`) `)
return hx`<div class='player-controls'>${elements}</div>` return hx`
<div class='player-controls'>
${elements}
${renderSubtitlesOptions(state)}
</div>
`
// Handles a click or drag to scrub (jump to another position in the video) // Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) { function handleScrub (e) {
@@ -257,38 +514,83 @@ function renderPlayerControls (state) {
var position = fraction * state.playing.duration /* seconds */ var position = fraction * state.playing.duration /* seconds */
dispatch('playbackJump', position) dispatch('playbackJump', position)
} }
// Handles volume change by wheel
function handleVolumeWheel (e) {
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
}
// Handles volume muting and Unmuting
function handleVolumeMute (e) {
if (state.playing.volume === 0.0) {
dispatch('setVolume', 1.0)
} else {
dispatch('setVolume', 0.0)
}
}
// 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
}
}
function handleSubtitles (e) {
if (!state.playing.subtitles.tracks.length || e.ctrlKey || e.metaKey) {
// if no subtitles available select it
dispatch('openSubtitles')
} else {
dispatch('showSubtitles')
}
}
} }
// lets scrub without sending to volume backend
var volumeChanging = false
// Renders the loading bar. Shows which parts of the torrent are loaded, which // Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be "spongey" / non-contiguous // can be "spongey" / non-contiguous
function renderLoadingBar (state) { function renderLoadingBar (state) {
var torrent = state.client.get(state.playing.infoHash) var torrentSummary = getPlayingTorrentSummary(state)
if (torrent === null) { if (!torrentSummary.progress) {
return [] return []
} }
var file = torrent.files[state.playing.fileIndex]
// Find all contiguous parts of the torrent which are loaded // Find all contiguous parts of the torrent which are loaded
var prog = torrentSummary.progress
var fileProg = prog.files[state.playing.fileIndex]
var parts = [] var parts = []
var lastPartPresent = false var lastPiecePresent = false
var numParts = file._endPiece - file._startPiece + 1 for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
for (var i = file._startPiece; i <= file._endPiece; i++) { var partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
var partPresent = torrent.bitfield.get(i) if (partPresent && !lastPiecePresent) {
if (partPresent && !lastPartPresent) { parts.push({start: i - fileProg.startPiece, count: 1})
parts.push({start: i - file._startPiece, count: 1})
} else if (partPresent) { } else if (partPresent) {
parts[parts.length - 1].count++ parts[parts.length - 1].count++
} }
lastPartPresent = partPresent lastPiecePresent = partPresent
} }
// Output an list of rectangles to show loading progress // Output some bars to show which parts of the file are loaded
return hx` return hx`
<div class='loading-bar'> <div class='loading-bar'>
${parts.map(function (part) { ${parts.map(function (part) {
var style = { var style = {
left: (100 * part.start / numParts) + '%', left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / numParts) + '%' width: (100 * part.count / fileProg.numPieces) + '%'
} }
return hx`<div class='loading-bar-part' style=${style}></div>` return hx`<div class='loading-bar-part' style=${style}></div>`
@@ -296,3 +598,21 @@ function renderLoadingBar (state) {
</div> </div>
` `
} }
// Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) {
var torrentSummary = getPlayingTorrentSummary(state)
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
if (!posterPath) return ''
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
}
function cssBackgroundImageDarkGradient () {
return 'radial-gradient(circle at center, ' +
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
}
function getPlayingTorrentSummary (state) {
var infoHash = state.playing.infoHash
return state.saved.torrents.find((x) => x.infoHash === infoHash)
}

View File

@@ -5,8 +5,7 @@ var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var util = require('../util') var TorrentSummary = require('../lib/torrent-summary')
var TorrentPlayer = require('../lib/torrent-player') var TorrentPlayer = require('../lib/torrent-player')
var {dispatcher} = require('../lib/dispatcher') var {dispatcher} = require('../lib/dispatcher')
@@ -27,22 +26,16 @@ function TorrentList (state) {
function renderTorrent (torrentSummary) { function renderTorrent (torrentSummary) {
// Get ephemeral data (like progress %) directly from the WebTorrent handle // Get ephemeral data (like progress %) directly from the WebTorrent handle
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var torrent = state.client var isSelected = infoHash && state.selectedInfoHash === infoHash
? state.client.torrents.find((x) => x.infoHash === infoHash)
: null
var isSelected = state.selectedInfoHash === infoHash
// Background image: show some nice visuals, like a frame from the movie, if possible // Background image: show some nice visuals, like a frame from the movie, if possible
var style = {} var style = {}
if (torrentSummary.posterURL) { if (torrentSummary.posterFileName) {
var gradient = isSelected 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.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%)' : 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL) var posterPath = TorrentSummary.getPosterPath(torrentSummary)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron): style.backgroundImage = gradient + `, url('${posterPath}')`
// Backslashes in URLS in CSS cause bizarre string encoding issues
var cleanURL = posterURL.replace(/\\/g, '/')
style.backgroundImage = gradient + `, url('${cleanURL}')`
} }
// Foreground: name of the torrent, basic info like size, play button, // Foreground: name of the torrent, basic info like size, play button,
@@ -51,39 +44,40 @@ function TorrentList (state) {
// playStatus turns the play button into a loading spinner or error icon // playStatus turns the play button into a loading spinner or error icon
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus) if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected') if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled')
classes = classes.join(' ') classes = classes.join(' ')
return hx` return hx`
<div style=${style} class=${classes} <div style=${style} class=${classes}
oncontextmenu=${dispatcher('openTorrentContextMenu', infoHash)} oncontextmenu=${infoHash && dispatcher('openTorrentContextMenu', infoHash)}
onclick=${dispatcher('toggleSelectTorrent', infoHash)}> onclick=${infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
${renderTorrentMetadata(torrent, torrentSummary)} ${renderTorrentMetadata(torrentSummary)}
${renderTorrentButtons(torrentSummary)} ${infoHash ? renderTorrentButtons(torrentSummary) : ''}
${isSelected ? renderTorrentDetails(torrent, torrentSummary) : ''} ${isSelected ? renderTorrentDetails(torrentSummary) : ''}
</div> </div>
` `
} }
// Show name, download status, % complete // Show name, download status, % complete
function renderTorrentMetadata (torrent, torrentSummary) { function renderTorrentMetadata (torrentSummary) {
var name = torrentSummary.name || 'Loading torrent...' var name = torrentSummary.name || 'Loading torrent...'
var elements = [hx` var elements = [hx`
<div class='name ellipsis'>${name}</div> <div class='name ellipsis'>${name}</div>
`] `]
// If a torrent is paused and we only get the torrentSummary // If it's downloading/seeding then show progress info
// If it's downloading/seeding then we have more information var prog = torrentSummary.progress
if (torrent) { if (torrentSummary.status !== 'paused' && prog) {
var progress = Math.floor(100 * torrent.progress) var progress = Math.floor(100 * prog.progress)
var downloaded = prettyBytes(torrent.downloaded) var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(torrent.length || 0) var total = prettyBytes(prog.length || 0)
if (downloaded !== total) downloaded += ` / ${total}` if (downloaded !== total) downloaded += ` / ${total}`
elements.push(hx` elements.push(hx`
<div class='status ellipsis'> <div class='status ellipsis'>
${getFilesLength()} ${getFilesLength()}
<span>${getPeers()}</span> <span>${getPeers()}</span>
<span>↓ ${prettyBytes(torrent.downloadSpeed || 0)}/s</span> <span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span>
<span>↑ ${prettyBytes(torrent.uploadSpeed || 0)}/s</span> <span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span>
</div> </div>
`) `)
elements.push(hx` elements.push(hx`
@@ -97,13 +91,13 @@ function TorrentList (state) {
return hx`<div class='metadata'>${elements}</div>` return hx`<div class='metadata'>${elements}</div>`
function getPeers () { function getPeers () {
var count = torrent.numPeers === 1 ? 'peer' : 'peers' var count = prog.numPeers === 1 ? 'peer' : 'peers'
return `${torrent.numPeers} ${count}` return `${prog.numPeers} ${count}`
} }
function getFilesLength () { function getFilesLength () {
if (torrent.ready && torrent.files.length > 1) { if (torrentSummary.files && torrentSummary.files.length > 1) {
return hx`<span class='files'>${torrent.files.length} files</span>` return hx`<span class='files'>${torrentSummary.files.length} files</span>`
} }
} }
} }
@@ -139,15 +133,23 @@ function TorrentList (state) {
downloadTooltip = 'Click to start torrenting.' downloadTooltip = 'Click to start torrenting.'
} }
return hx` // Only show the play button for torrents that contain playable media
<div class='buttons'> var playButton
<i.btn.icon.play if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
playButton = hx`
<i.button-round.icon.play
title=${playTooltip} title=${playTooltip}
class=${playClass} class=${playClass}
onclick=${dispatcher('play', infoHash)}> onclick=${dispatcher('play', infoHash)}>
${playIcon} ${playIcon}
</i> </i>
<i.btn.icon.download `
}
return hx`
<div class='buttons'>
${playButton}
<i.button-round.icon.download
class=${torrentSummary.status} class=${torrentSummary.status}
title=${downloadTooltip} title=${downloadTooltip}
onclick=${dispatcher('toggleTorrent', infoHash)}> onclick=${dispatcher('toggleTorrent', infoHash)}>
@@ -164,19 +166,19 @@ function TorrentList (state) {
} }
// Show files, per-file download status and play buttons, and so on // Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrent, torrentSummary) { function renderTorrentDetails (torrentSummary) {
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var filesElement var filesElement
if (!torrentSummary.files) { if (!torrentSummary.files) {
// We don't know what files this torrent contains // We don't know what files this torrent contains
var message = torrent var message = torrentSummary.status === 'paused'
? 'Downloading torrent data using magnet link...' ? 'Failed to load torrent info. Click the download button to try again...'
: 'Failed to download torrent data from magnet link. Click the download button to try again...' : 'Downloading torrent info...'
filesElement = hx`<div class='files warning'>${message}</div>` filesElement = hx`<div class='files warning'>${message}</div>`
} else { } else {
// We do know the files. List them and show download stats for each one // We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files.map( var fileRows = torrentSummary.files.map(
(file, index) => renderFileRow(torrent, torrentSummary, file, index)) (file, index) => renderFileRow(torrentSummary, file, index))
filesElement = hx` filesElement = hx`
<div class='files'> <div class='files'>
<strong>Files</strong> <strong>Files</strong>
@@ -199,20 +201,22 @@ function TorrentList (state) {
} }
// Show a single torrentSummary file in the details view for a single torrent // Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrent, torrentSummary, file, index) { function renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded // First, find out how much of the file we've downloaded
var isDone = file.numPiecesPresent === file.numPieces var isDone = false
var progress = Math.round(100 * file.numPiecesPresent / (file.numPieces || 0)) + '%' 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, render the file as a table row // Second, render the file as a table row
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var icon var icon
var rowClass = '' var rowClass = ''
var handleClick var handleClick
if (state.playing.infoHash === infoHash && state.playing.fileIndex === index) { if (TorrentPlayer.isPlayable(file)) {
icon = 'pause_arrow' /* playing? add option to pause */
handleClick = undefined // TODO: pause audio
} else if (TorrentPlayer.isPlayable(file)) {
icon = 'play_arrow' /* playable? add option to play */ icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index) handleClick = dispatcher('play', infoHash, index)
} else { } else {

View File

@@ -0,0 +1,32 @@
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')
}
}

22
renderer/webtorrent.html Normal file
View File

@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
background-color: #282828;
margin: 5px;
overflow: hidden;
}
img {
width: 140px;
height: 140px;
}
</style>
</head>
<body>
<script async src="webtorrent.js"></script>
<img src="../static/WebTorrent.png">
</body>
</html>

325
renderer/webtorrent.js Normal file
View File

@@ -0,0 +1,325 @@
// To keep the UI snappy, we run WebTorrent in its own hidden window, a separate
// process from the main window.
console.time('init')
var WebTorrent = require('webtorrent')
var defaultAnnounceList = require('create-torrent').announceList
var deepEqual = require('deep-equal')
var electron = require('electron')
var fs = require('fs-extra')
var musicmetadata = require('musicmetadata')
var networkAddress = require('network-address')
var path = require('path')
var crashReporter = require('../crash-reporter')
var config = require('../config')
var torrentPoster = require('./lib/torrent-poster')
// Report when the process crashes
crashReporter.init()
// Send & receive messages from the main window
var ipc = electron.ipcRenderer
// Force use of webtorrent trackers on all torrents
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
.map((arr) => arr[0])
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
// 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()
// WebTorrent-to-HTTP streaming sever
var server = window.server = null
// Used for diffing, so we only send progress updates when necessary
var prevProgress = window.prevProgress = null
init()
function init () {
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message))
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes))
ipc.on('wt-stop-torrenting', (e, infoHash) =>
stopTorrenting(infoHash))
ipc.on('wt-create-torrent', (e, torrentKey, options) =>
createTorrent(torrentKey, options))
ipc.on('wt-save-torrent-file', (e, torrentKey) =>
saveTorrentFile(torrentKey))
ipc.on('wt-generate-torrent-poster', (e, torrentKey) =>
generateTorrentPoster(torrentKey))
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
getAudioMetadata(infoHash, index))
ipc.on('wt-start-server', (e, infoHash, index) =>
startServer(infoHash, index))
ipc.on('wt-stop-server', (e) =>
stopServer())
ipc.send('ipcReadyWebTorrent')
setInterval(updateTorrentProgress, 1000)
}
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
function startTorrenting (torrentKey, torrentID, path, fileModtimes) {
console.log('starting torrent %s: %s', torrentKey, torrentID)
var torrent
try {
torrent = client.add(torrentID, {
path: path,
fileModtimes: fileModtimes
})
} catch (err) {
return ipc.send('wt-error', torrentKey, err.message)
}
// If we add a duplicate magnet URI or infohash, WebTorrent returns the
// existing torrent object! (If we add a duplicate torrent file, it creates a
// new torrent object and raises an error later.) Workaround:
if (torrent.key) {
return ipc.send('wt-error', torrentKey, 'Can\'t add duplicate torrent')
}
torrent.key = torrentKey
addTorrentEvents(torrent)
return torrent
}
function stopTorrenting (infoHash) {
var torrent = client.get(infoHash)
torrent.destroy()
}
// Create a new torrent, start seeding
function createTorrent (torrentKey, options) {
console.log('creating torrent', torrentKey, options)
var paths = options.files.map((f) => f.path)
var torrent = client.seed(paths, options)
torrent.key = torrentKey
addTorrentEvents(torrent)
ipc.send('wt-new-torrent')
}
function addTorrentEvents (torrent) {
torrent.on('warning', (err) =>
ipc.send('wt-warning', torrent.key, err.message))
torrent.on('error', (err) =>
ipc.send('wt-error', torrent.key, err.message))
torrent.on('infoHash', () =>
ipc.send('wt-infohash', torrent.key, torrent.infoHash))
torrent.on('metadata', torrentMetadata)
torrent.on('ready', torrentReady)
torrent.on('done', torrentDone)
function torrentMetadata () {
var info = getTorrentInfo(torrent)
ipc.send('wt-metadata', torrent.key, info)
updateTorrentProgress()
}
function torrentReady () {
var info = getTorrentInfo(torrent)
ipc.send('wt-ready', torrent.key, info)
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
updateTorrentProgress()
}
function torrentDone () {
var info = getTorrentInfo(torrent)
ipc.send('wt-done', torrent.key, info)
updateTorrentProgress()
torrent.getFileModtimes(function (err, fileModtimes) {
if (err) return onError(err)
ipc.send('wt-file-modtimes', torrent.key, fileModtimes)
})
}
}
// Produces a JSON saveable summary of a torrent
function getTorrentInfo (torrent) {
return {
infoHash: torrent.infoHash,
magnetURI: torrent.magnetURI,
name: torrent.name,
path: torrent.path,
files: torrent.files.map(getTorrentFileInfo),
bytesReceived: torrent.received
}
}
// Produces a JSON saveable summary of a file in a torrent
function getTorrentFileInfo (file) {
return {
name: file.name,
length: file.length,
path: file.path,
numPiecesPresent: 0,
numPieces: null
}
}
// Every time we resolve a magnet URI, save the torrent file so that we never
// have to download it again. Never ask the DHT the same question twice.
function saveTorrentFile (torrentKey) {
var torrent = getTorrent(torrentKey)
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
var fileName = torrent.infoHash + '.torrent'
if (exists) {
// We've already saved the file
return ipc.send('wt-file-saved', torrentKey, fileName)
}
// Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.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)
return ipc.send('wt-file-saved', torrentKey, fileName)
})
})
})
}
// 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')
fs.exists(torrentPath, function (exists) {
cb(torrentPath, exists)
})
}
// Save a JPG that represents a torrent.
// Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) {
var torrent = getTorrent(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) {
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)
fs.writeFile(posterFilePath, buf, function (err) {
if (err) return console.log('error saving poster: %o', err)
// show the poster
ipc.send('wt-poster', torrentKey, posterFileName)
})
})
})
}
function updateTorrentProgress () {
var progress = getTorrentProgress()
// TODO: diff torrent-by-torrent, not once for the whole update
if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
return /* don't send heavy object if it hasn't changed */
}
ipc.send('wt-progress', progress)
prevProgress = progress
}
function getTorrentProgress () {
// First, track overall progress
var progress = client.progress
var hasActiveTorrents = client.torrents.some(function (torrent) {
return torrent.progress !== 1
})
// Track progress for every file in each torrent
// TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield
var torrentProg = client.torrents.map(function (torrent) {
var fileProg = torrent.files && torrent.files.map(function (file, index) {
var numPieces = file._endPiece - file._startPiece + 1
var numPiecesPresent = 0
for (var piece = file._startPiece; piece <= file._endPiece; piece++) {
if (torrent.bitfield.get(piece)) numPiecesPresent++
}
return {
startPiece: file._startPiece,
endPiece: file._endPiece,
numPieces,
numPiecesPresent
}
})
return {
torrentKey: torrent.key,
ready: torrent.ready,
progress: torrent.progress,
downloaded: torrent.downloaded,
downloadSpeed: torrent.downloadSpeed,
uploadSpeed: torrent.uploadSpeed,
numPeers: torrent.numPeers,
length: torrent.length,
bitfield: torrent.bitfield,
files: fileProg
}
})
return {
torrents: torrentProg,
progress,
hasActiveTorrents
}
}
function startServer (infoHash, index) {
var torrent = client.get(infoHash)
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
else torrent.on('ready', () => startServerFromReadyTorrent(torrent, index))
}
function startServerFromReadyTorrent (torrent, index, cb) {
if (server) return
// start the streaming torrent-to-http server
server = torrent.createServer()
server.listen(0, function () {
var port = server.address().port
var urlSuffix = ':' + port + '/' + index
var info = {
torrentKey: torrent.key,
localURL: 'http://localhost' + urlSuffix,
networkURL: 'http://' + networkAddress() + urlSuffix
}
ipc.send('wt-server-running', info)
ipc.send('wt-server-' + torrent.infoHash, info) // TODO: hack
})
}
function stopServer () {
if (!server) return
server.destroy()
server = null
}
function getAudioMetadata (infoHash, index) {
var torrent = client.get(infoHash)
var file = torrent.files[index]
musicmetadata(file.createReadStream(), function (err, info) {
if (err) return
console.log('got audio metadata for %s: %o', file.name, info)
ipc.send('wt-audio-metadata', infoHash, index, info)
})
}
// Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) {
var ret = client.torrents.find((x) => x.key === torrentKey)
if (!ret) throw new Error('missing torrent key ' + torrentKey)
return ret
}
function onError (err) {
console.log(err)
}

Binary file not shown.

Binary file not shown.

BIN
static/WebTorrentSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/cosmosLaundromat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

4
static/linux/postinst Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
set -e
chmod +x /opt/webtorrent-desktop/WebTorrent
ln -s -f /opt/webtorrent-desktop/WebTorrent /usr/bin/webtorrent-desktop

3
static/linux/prerm Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
set -e
rm /usr/bin/webtorrent-desktop

View File

@@ -6,7 +6,7 @@ X-GNOME-FullName=$APP_NAME
Comment=Download and share files over BitTorrent Comment=Download and share files over BitTorrent
Encoding=UTF-8 Encoding=UTF-8
Type=Application Type=Application
Icon=webtorrent Icon=webtorrent-desktop
Terminal=false Terminal=false
Path=$APP_PATH Path=$APP_PATH
Exec=$EXEC_PATH %U Exec=$EXEC_PATH %U

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 KiB

BIN
static/wired-cd.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
static/wired-cd.torrent Normal file

Binary file not shown.