Compare commits

...

496 Commits

Author SHA1 Message Date
Feross Aboukhadijeh
9d3e26f15a 0.7.2 2016-06-02 22:46:10 -07:00
Feross Aboukhadijeh
8a95895254 Ensure state.saved.prefs exists
Fixes a bug I introduced in the migrations
2016-06-02 22:33:22 -07:00
Feross Aboukhadijeh
5d71f9e9c6 code cleanup 2016-06-02 22:32:01 -07:00
Feross Aboukhadijeh
5d410457ce Merge pull request #622 from feross/about
Fix title in About Window
2016-06-02 21:12:58 -07:00
Feross Aboukhadijeh
c6cd21b8ff Fix title in About Window
Shows up as 'Electron' on Windows
2016-06-02 20:50:04 -07:00
Feross Aboukhadijeh
2235b2fa82 changelog 2016-06-02 20:10:40 -07:00
Feross Aboukhadijeh
65e0b5d6e7 0.7.1 2016-06-02 19:58:43 -07:00
Feross Aboukhadijeh
ea64411570 Merge pull request #621 from feross/f/fix
Fix v0.7 bug - refactor state
2016-06-02 19:53:53 -07:00
Feross Aboukhadijeh
9348c61a84 stray console.log 2016-06-02 19:53:37 -07:00
Feross Aboukhadijeh
d9aa3822ee Set selections by default
In case the user tries to change a file selection state before enabling
the torrent.
2016-06-02 19:52:35 -07:00
Feross Aboukhadijeh
e86bd26800 Refactor state save/load
- Fix bug where new install was relying on the migration to run on
startup to fix up the default config
- Moved save/load functions into state.js
- Removed exported getInitialState, getDefaultSavedState since that's
leaky. The state module should take care of that.
2016-06-02 19:46:29 -07:00
Feross Aboukhadijeh
6d8cec17de npm run open-config -- open folder 2016-06-02 19:40:12 -07:00
Feross Aboukhadijeh
572f084570 Merge pull request #620 from feross/skip-keys
Change step Forward/Backward shortcuts to match VLC's
2016-06-02 17:23:37 -07:00
Feross Aboukhadijeh
4a3ca5459d Change step Forward/Backward shortcuts to match VLC's
We got this feature idea from VLC, so let's align with the keyboard
shortcuts that they use :)

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

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

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

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

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

* Mock AirPlay volume support

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

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

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

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

* Codestyle fix

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

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

* resolve conflicts

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

* intendation fix

* conflict resolve

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

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

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

This new approach improves:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Don't unlink deselected files

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

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

Fixes #360
2016-05-17 07:13:38 -07:00
DC
0095687bf5 Simplify subtitles code 2016-05-17 06:27:58 -07:00
DC
d466ed085a When manually adding subtitle track(s), always switch to a new track
Also fix a bug I added in the parent commit
2016-05-17 05:50:36 -07:00
DC
eeda7c17c5 Wait for the app ready event before creating windows
Fixes #524
2016-05-17 05:12:42 -07:00
DC
b89deb46db Remove debug console.logs 2016-05-16 08:35:00 -07:00
DC
951a89c6c9 Add Subtitles File menu item 2016-05-16 08:21:03 -07:00
DC
d4e6c84279 Automatically add subtitle tracks
Currently, add all .SRT and .VTT subtitle files in the same torrent as a video file
2016-05-16 08:03:21 -07:00
DC
9731d85ca3 Simplify subtitles code 2016-05-16 03:41:27 -07:00
DC
98f7ba8931 Fix a bad bug when creating multifile torrents 2016-05-16 01:09:21 -07:00
Feross Aboukhadijeh
24c775608e Merge pull request #513 from feross/detect-win32
Fix missing 'About WebTorrent' menu item
2016-05-16 03:22:50 +02:00
Feross Aboukhadijeh
f4eab12c3f Merge pull request #518 from feross/osx-magnet-exception
OS X: Fix magnet links throwing exception on launch
2016-05-16 03:04:17 +02:00
Feross Aboukhadijeh
8eeddeb4bc OS X: Fix magnet links throwing exception on launch
Push page into location right away
2016-05-15 18:02:11 -07:00
Feross Aboukhadijeh
58f1594d9e Fix missing 'About WebTorrent' menu item 2016-05-14 01:51:47 -07:00
Feross Aboukhadijeh
c126ac0a84 fix test script on windows 2016-05-13 23:11:55 -07:00
Feross Aboukhadijeh
6768be710e changelog fixes 2016-05-13 23:11:27 -07:00
Feross Aboukhadijeh
b63aa090dc fix release script 2016-05-13 23:11:23 -07:00
Feross Aboukhadijeh
05ef8be5bc 0.4.0 2016-05-13 22:49:38 -07:00
Feross Aboukhadijeh
1a09249bc3 changelog 2016-05-13 22:46:47 -07:00
Feross Aboukhadijeh
803820dfca authors 2016-05-13 22:01:00 -07:00
Feross Aboukhadijeh
deb111bf62 Merge pull request #512 from feross/isnan-string
check if the subtitle label ends with a number
2016-05-13 21:49:14 -07:00
grunjol
7d64c7e308 check if the subtitle label ends with a number 2016-05-13 23:00:17 -03:00
Feross Aboukhadijeh
ffb7183f51 Win32: Look on Desktop for cert files 2016-05-13 18:04:24 -07:00
Feross Aboukhadijeh
20c6737aba Merge pull request #511 from feross/fix-cpu
HACK: OS X: Disable WebRTC peers to fix 100% CPU issue
2016-05-13 17:37:12 -07:00
Feross Aboukhadijeh
959fb20b61 HACK: OS X: Disable WebRTC peers to fix 100% CPU issue
HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome
bug.

Fixed in Chrome 51, so we can remove this hack once Electron updates
Chrome.

For #353.
2016-05-13 17:32:06 -07:00
Feross Aboukhadijeh
5d14c923fa Merge pull request #510 from feross/dc/fix
Allow seeding torrents that contain subtitles
2016-05-13 17:16:16 -07:00
DC
5ffa7c4465 Drag drop: subtitles only in video screen, torrents only in home screen 2016-05-13 17:15:10 -07:00
DC
461744da5b Allow seeding torrents that contain subtitles
Fixes a bug in our drag-drop handling: before, it was impossible to create a torrent containing .torrent, .srt, or .vtt files
2016-05-13 16:59:49 -07:00
Feross Aboukhadijeh
6df33bc58b remove stray console.log 2016-05-13 16:37:17 -07:00
Feross Aboukhadijeh
b5ae8f56cf Merge pull request #509 from feross/win-32-bit
Only build 32-bit binaries for Windows
2016-05-13 16:27:40 -07:00
DC
2e0de52520 Fix torrentPath migration (#479)
Fixes #448
2016-05-13 15:18:08 -07:00
Feross Aboukhadijeh
7b1ff0efc6 Only build 32-bit binaries for Windows 2016-05-13 15:15:51 -07:00
Feross Aboukhadijeh
4002392b7f Merge pull request #508 from feross/fix-handler
Windows: Fix handler registration for development version of app
2016-05-13 14:45:01 -07:00
Feross Aboukhadijeh
ee4b84fc11 Windows: Fix handler registration for development version of app
Closes #497.
2016-05-13 14:24:22 -07:00
Feross Aboukhadijeh
90a0ce4a4d Merge pull request #507 from feross/fix-is-production
Fix isProduction() detection
2016-05-13 13:38:41 -07:00
Feross Aboukhadijeh
80faba8234 Fix isProduction() detection
In the renderer process on OS X, config.IS_PRODUCTION was always true
because process.execPath is to "Electron Helper", so the detection
regex was being overly specific.
2016-05-13 13:29:25 -07:00
Feross Aboukhadijeh
ac0574a473 Fixes for PR #486 2016-05-13 13:21:31 -07:00
Feross Aboukhadijeh
792e3430f1 Merge pull request #486 from rguedes/soundwheelvideo
Increase/Decrease Sound with mouse wheel on video hover
2016-05-13 13:18:56 -07:00
Rolando Guedes
9e33be0ab1 Undo spaces changes 2016-05-13 21:05:26 +01:00
Rolando Guedes
c343c008ed Undo spaces changes 2016-05-13 21:02:35 +01:00
Feross Aboukhadijeh
6405be5144 Merge pull request #506 from feross/update-deps
OS X: Bounce the Downloads stack when download completes
2016-05-13 12:26:52 -07:00
Feross Aboukhadijeh
db743daae5 Merge pull request #505 from feross/update-deps
Update deps
2016-05-13 12:25:27 -07:00
Feross Aboukhadijeh
290a25c393 OS X: Bounce the Downloads stack when download completes
(If the download is inside the Downloads folder.)
2016-05-13 12:25:03 -07:00
Feross Aboukhadijeh
6589e134b3 code style 2016-05-13 12:23:59 -07:00
Feross Aboukhadijeh
a2aa5e4271 electron-prebuilt@1.0.2 2016-05-13 12:09:03 -07:00
Feross Aboukhadijeh
205e2eb551 dlnacasts@0.1 2016-05-13 12:02:24 -07:00
grunjol
53209a9da3 push/unshift from submenu in linux/windows (#504) 2016-05-13 02:02:08 -07:00
Feross Aboukhadijeh
2a23611c5f Merge pull request #502 from feross/add-mpg
Add .mpg video extension to supported list
2016-05-12 20:32:41 -07:00
Feross Aboukhadijeh
cb71913cbe Add .mpg video extension to supported list 2016-05-12 20:16:12 -07:00
Feross Aboukhadijeh
836d7c6664 Use Array.prototype.includes 2016-05-12 20:14:24 -07:00
Feross Aboukhadijeh
4cef9f2911 Merge pull request #499 from feross/fix-fullscreen-button
Fix for overflowing captions icon
2016-05-12 17:57:51 -07:00
Feross Aboukhadijeh
0913988d53 Merge pull request #498 from feross/fix-volume-drag
OS X: Volume slider nub should not move window
2016-05-12 17:55:01 -07:00
Feross Aboukhadijeh
6468f82a7f Small comment fix 2016-05-12 17:54:23 -07:00
Feross Aboukhadijeh
fd0fc769b1 Fix for overflowing captions icon
Closes #467.
2016-05-12 17:54:16 -07:00
Feross Aboukhadijeh
e5b648dfc6 OS X: Volume slider nub should not move window
Before, grabbing the volume slider nub would move the window.
2016-05-12 17:23:51 -07:00
Feross Aboukhadijeh
7701c5f097 remove unused css 2016-05-12 17:11:11 -07:00
Feross Aboukhadijeh
e5eddce868 Merge pull request #495 from feross/osx-fullscreenst
OS X: Make controls use full window in fullscreen
2016-05-12 17:10:34 -07:00
Feross Aboukhadijeh
72f917a744 OS X: Make controls use full window in fullscreen
This bug was subtle. Basically, on OS X only, we use
window.setAspectRatio() to make the player window match the video size.

But this is maintained even in fullscreen mode, which makes the window
actually not use up the fullscreen, and there are black bars above and
below the video player controls, which looks really weird.

Unset the aspect ratio in fullscreen mode, then set it again upon
leaving fullscreen mode.
2016-05-12 17:09:10 -07:00
Feross Aboukhadijeh
0b82c83d44 style: remove extraneous parameters 2016-05-12 17:09:10 -07:00
Feross Aboukhadijeh
602654cc1d Merge pull request #494 from feross/perf
Improve app startup time by ~350ms
2016-05-12 17:06:23 -07:00
Feross Aboukhadijeh
350bed53a3 Perf: Send 'ipcReady' before all requires (300ms improvement!)
This improves the time to the main window showing by 300ms on my
Macbook 12"!

Before: ~800ms
After: ~500ms
2016-05-12 17:00:34 -07:00
Feross Aboukhadijeh
840754fb59 Perf: Lazy load srt-to-vtt and languagedetect 2016-05-12 17:00:34 -07:00
Feross Aboukhadijeh
ed46583226 Perf: Send 'ipcReady' as soon as possible
This slightly improves app startup time
2016-05-12 17:00:34 -07:00
Feross Aboukhadijeh
93252d430e Delay calling tray.init() and handlers.init() 2016-05-12 17:00:34 -07:00
Feross Aboukhadijeh
bfd09a058e Small style tweaks 2016-05-12 17:00:34 -07:00
Feross Aboukhadijeh
b1a7543d37 Perf: Use electron.* getter inline, rather than upfront 2016-05-12 17:00:34 -07:00
Feross Aboukhadijeh
39195fe8c4 Rename auto-updater.js -> updater.js
To remove confusion between Electron autoUpdater and our autoUpdater
module.
2016-05-12 16:58:18 -07:00
Feross Aboukhadijeh
ea1c66b3fc Reduce delayedInit to 3 seconds 2016-05-12 16:58:18 -07:00
Feross Aboukhadijeh
f35eb73d50 Refactor auto-updater.js for lazy loading
By removing the upfront electron.autoUpdater, we can delay loading it
until init() is called.
2016-05-12 16:58:18 -07:00
Feross Aboukhadijeh
c99af4718e Perf: Inline electron.* usage
Apparently, electron.* is actually a getter, so whenever a component of
electron is referenced for the first time, it's require()'d. So, there
are theoretical performance benefits to not declaring all electron.*
upfront.

Instead of:

var autoUpdater = electron.autoUpdater

Just use electron.autoUpdater directly when needed.
2016-05-12 16:58:18 -07:00
Feross Aboukhadijeh
dbef07e334 Merge pull request #492 from feross/accelerator
Shortcuts improvements
2016-05-12 16:57:48 -07:00
Feross Aboukhadijeh
969ad64c47 Merge pull request #493 from feross/fix-screen
Remove require('screen')
2016-05-12 16:57:42 -07:00
Feross Aboukhadijeh
5dd5e8661b Remove require('screen')
This is deprecated usage and was just removed in Electron v1.
2016-05-12 16:57:09 -07:00
Feross Aboukhadijeh
5c9265fc99 Move Escape keyboard shortcut to shortcuts.js 2016-05-12 16:52:13 -07:00
Feross Aboukhadijeh
1deab08d38 Playback menu: Add "Play/Pause" item
The goal here is to remove shortcut handling from the renderer and
unify it all in menu.js and shortcuts.ks (for alternate shortcuts).

I would rather name it "Play" and change to "Pause" when video is
playing, but Electron doesn't support this (yet).
2016-05-12 16:52:13 -07:00
Feross Aboukhadijeh
3d6da99e8e Bug: Space key triggers power save block from torrent list
Hitting Space from the torrent list should not cause power save to be
blocked.
2016-05-12 16:52:13 -07:00
Feross Aboukhadijeh
2005ee4d0b shortcuts.js: Consistent exported method naming
Exposed methods whose sole purpose is notify the module of an event
firing, should start with "on".
2016-05-12 16:52:13 -07:00
Feross Aboukhadijeh
c99da2ccaa Remove Window menu on Linux and Windows
The Window menu is apparently an OS X only convention. I couldn't find
a single app on Windows or Linux that had this menu or even a
"minimize" menu item.
2016-05-12 16:52:13 -07:00
Feross Aboukhadijeh
4bffb6634c Add Playback menu for playback-related functionality 2016-05-12 16:52:13 -07:00
Feross Aboukhadijeh
504aca747d main/menu.js: minor refactor
Just some code cleanup to make menu.js more internally consistent.

- Name the electron.dialog returned value `selectedPaths` which is more
accurate.

- Move the file menu into the `template` object, like the rest of the
menus. Then reach in afterwards for OS-specific tweaks.
2016-05-12 16:52:13 -07:00
Feross Aboukhadijeh
2085312c34 Merge pull request #490 from feross/smaller-ui
UI tweaks: Reduce font size, list item height, single torrent status line
2016-05-12 16:51:20 -07:00
Feross Aboukhadijeh
744d38259e Put peers before speeds, to reduce bouncing
When speed goes to zero, it disappears, which looks weird when it's not
the last item on the status line.
2016-05-12 16:50:58 -07:00
Feross Aboukhadijeh
868739445a Merge pull request #489 from feross/fix-add-duplicate
Fix duplicate torrent handling
2016-05-12 16:47:48 -07:00
Feross Aboukhadijeh
98d8a798ce Merge pull request #488 from feross/electron-1
electron-prebuilt@1.0.1
2016-05-12 15:32:53 -07:00
Feross Aboukhadijeh
fe31cfaa3e electron-prebuilt@1.0.1 2016-05-12 15:22:39 -07:00
Feross Aboukhadijeh
17d5490448 Merge pull request #487 from furstenheim/master
Avoid TypeError out of OS X
2016-05-12 14:49:37 -07:00
gabriel
d4c415d585 Avoid TypeError out of OS X 2016-05-12 20:09:20 +02:00
Rolando Guedes
cb8f7f53c2 Fix Cli Test fails: JavaScript Standard Style 2016-05-12 15:56:29 +01:00
Rolando Guedes
8d93641ebe Fix Cli Test fails: JavaScript Standard Style 2016-05-12 15:47:48 +01:00
Rolando Guedes
4faf30e0a1 Fix Cli Test fails: JavaScript Standard Style 2016-05-12 15:31:28 +01:00
Rolando Guedes
ed1b27ede0 Increase/Decrease Sound with mouse wheel on video 2016-05-12 15:09:01 +01:00
Feross Aboukhadijeh
252443a529 UX: Improve torrent status line
The goal of this commit is to merge the two torrent status lines onto a
single, concise line which has high signal and information density.

- Hide download speed, upload speed, and number of peers when 0,
because that's just noise.
- Remove number of files, because that information can be found by
expanding the torrent.

This also allowed the further reduction of the torrent item height from
110px to 100px.
2016-05-11 21:29:46 +02:00
Feross Aboukhadijeh
86f5a1a54e Default window height shows all torrents 2016-05-11 21:26:18 +02:00
Feross Aboukhadijeh
0b1872fa28 UI: Reduce font size, list item height
- Reduce torrent list item from 120px to 110px height
- Vertically center torrent list buttons
- Reduce font sizes (torrent list, modal labels)
2016-05-11 20:49:41 +02:00
Feross Aboukhadijeh
9eeb8133af Fix duplicate torrent handling
WebTorrent 0.91 changed how duplicate torrents are handled, which broke
handling in WebTorrent Desktop.

After this PR:

- No more try-catch on client.add -- this has never thrown errors.

- No check for duplicate torrent.key value since client.add no longer
returns the same torrent object when adding a duplicate torrent. It
emits 'error' instead, and that case is already handled :)
2016-05-11 18:36:20 +02:00
Feross Aboukhadijeh
1eb5504029 move console.time/timeEnd to same file 2016-05-11 17:56:20 +02:00
Feross Aboukhadijeh
dfe8c3eb6b remove unneeded console.log 2016-05-11 17:52:18 +02:00
DC
2b8c1fe709 Fix incorrect path when for single-file torrents
Fixes #457
2016-05-10 22:54:14 -07:00
DC
905cc527d0 Add ogv as a video type 2016-05-10 22:31:24 -07:00
Feross Aboukhadijeh
95019453fd Clearer build output 2016-05-09 19:20:14 +02:00
Feross Aboukhadijeh
e46a7f42df Merge pull request #476 from feross/webtorrent-version
About WebTorrent: Show `webtorrent` library version
2016-05-09 18:31:40 +02:00
Feross Aboukhadijeh
15a59f445b About WebTorrent: Remove git hash from build version 2016-05-09 18:29:22 +02:00
Feross Aboukhadijeh
dea951fc42 About WebTorrent: Show webtorrent library version
Closes #475
2016-05-09 18:21:52 +02:00
Feross Aboukhadijeh
347eb2c7f0 Merge pull request #474 from feross/application-config
application-config@^0.2.1
2016-05-09 17:55:23 +02:00
Feross Aboukhadijeh
4221883eb4 application-config@^0.2.1
My fixes were merged upstream -- no need to depend on my fork anymore.
2016-05-09 17:18:53 +02:00
Feross Aboukhadijeh
27f729250f Merge pull request #473 from feross/npm-run-open-config
add `npm run open-config` to open config file quickly
2016-05-09 17:17:29 +02:00
Feross Aboukhadijeh
452bbb60c4 use path.join 2016-05-09 17:12:17 +02:00
Feross Aboukhadijeh
9d4aeaedd3 add npm run open-config to get to config file quickly 2016-05-09 17:10:51 +02:00
Feross Aboukhadijeh
558b6c1648 add new package.json keywords 2016-05-09 17:10:35 +02:00
Feross Aboukhadijeh
98e263e69a Remove path-exists
This package trivially wraps core node.js functionality. Let's do
without it.
2016-05-09 16:59:57 +02:00
Feross Aboukhadijeh
18b126e0d2 Remove unnecessary IPC 2016-05-09 16:14:46 +02:00
Feross Aboukhadijeh
82dff65572 Merge pull request #465 from furstenheim/master
Allow to torrent a single file
2016-05-09 16:00:36 +02:00
Feross Aboukhadijeh
d60d298b8f Merge pull request #471 from feross/set-sheet-offset
Set sheet offset
2016-05-09 02:10:24 +02:00
Feross Aboukhadijeh
ffbd8184b5 Set sheet offset on OS X 2016-05-09 02:06:17 +02:00
Feross Aboukhadijeh
11cf4aeecd electron-prebuilt@0.37.8
All bug fixes. The only new feature that's relevant to WebTorrent
Desktop is `setSheetOffset`
2016-05-09 02:00:02 +02:00
Feross Aboukhadijeh
b0b8b56816 Merge pull request #470 from feross/reduce-sfx-volume
Reduce sfx volume
2016-05-09 01:40:05 +02:00
Feross Aboukhadijeh
967e5ecb9c Merge pull request #469 from feross/show-in-folder
Add "Show in Folder" to context menu
2016-05-09 01:36:27 +02:00
Feross Aboukhadijeh
f0315f7f77 Reduce sound effect volume by 25%
except for delete -- which this pr just makes consistent
2016-05-09 01:36:22 +02:00
Feross Aboukhadijeh
facb07cbb1 Add "Show in Folder" to context menu
Based on @watson's PR #463.

Differences:

- Remove the "Open Folder" link from expanded torrent view.
- Use showItemInFolder instead of openItem electron API
- Add a separator
- Use IPC to invoke electron.shell.showItemInFolder from main process
2016-05-09 01:34:35 +02:00
gabriel
41910aea9c Do not show torrent file option on OS X 2016-05-08 23:39:32 +02:00
gabriel
8fcfa3b97a Allow to torrent a single file 2016-05-07 22:05:46 +02:00
DC
8ebb2349dd External VLC on Windows
Turns out we can't use vlc --version because it pops up a command prompt :/
2016-05-04 04:33:35 -07:00
DC
1e487a3c2a Use vlc-command 2016-05-04 01:48:39 -07:00
DC
291ea94a10 Cross platform VLC detection 2016-05-04 00:48:34 -07:00
DC
ade6c1e4a0 Add more media file extensions 2016-05-04 00:48:34 -07:00
DC
bde5dc14c3 Play unsupported files in VLC 2016-05-04 00:48:34 -07:00
DC
0a005eb054 Check for missing or unused dependencies 2016-05-03 00:08:53 -07:00
Feross Aboukhadijeh
735851486e remove unnecessary escape 2016-05-02 20:42:05 +02:00
Feross Aboukhadijeh
56ba5c705a add missing mkdirp dep 2016-05-02 20:39:06 +02:00
Feross Aboukhadijeh
cdab2dbc65 add missing rimraf dep 2016-05-02 20:39:06 +02:00
Feross Aboukhadijeh
4284eb8f75 add missing path-exists dep 2016-05-02 20:39:06 +02:00
Feross Aboukhadijeh
2707fc9053 Merge pull request #454 from feross/greenkeeper-standard-7.0.0
Update standard to version 7.0.0 🚀
2016-05-02 17:16:25 +02:00
greenkeeperio-bot
1d4d4319e4 chore(package): update standard to version 7.0.0
https://greenkeeper.io/
2016-05-02 16:22:55 +02:00
Feross Aboukhadijeh
c5cc0ce09d Merge pull request #447 from feross/small-fixes
Small fixes
2016-04-28 12:22:28 +02:00
Feross Aboukhadijeh
fdd7dab76f electron-winstaller@2.3.0 2016-04-28 12:18:31 +02:00
Feross Aboukhadijeh
7624f2da98 fixes for cross-zip@2 2016-04-28 12:14:39 +02:00
Feross Aboukhadijeh
ef51f827dc fix exception in webtorrent process 2016-04-28 12:10:33 +02:00
Greenkeeper
011ab13c83 chore(package): update cross-zip to version 2.0.1 (#445)
https://greenkeeper.io/
2016-04-28 12:10:05 +02:00
DC
017d61815f Create Torrent: fix for single file torrents 2016-04-27 07:46:55 -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
82 changed files with 7454 additions and 3286 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?**

2
.gitignore vendored
View File

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

View File

@@ -10,6 +10,17 @@
- Romain Beaumont <romain.rom1@gmail.com>
- Dan Flettre <fletd01@yahoo.com>
- Liam Gray <liam.r.gray@gmail.com>
- grunjol <grunjol@argenteam.net>
- grunjol <grunjol@users.noreply.github.com>
- Rémi Jouannet <remijouannet@users.noreply.github.com>
- Evan Miller <miller.evan815@gmail.com>
- Alex <alxmorais8@msn.com>
- Diego Rodríguez Baquero <diegorbaquero@gmail.com>
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
- gabriel <furstenheim@gmail.com>
- Rolando Guedes <rolando.guedes@3gnt.net>
- Benjamin Tan <demoneaux@gmail.com>
- Mathias Rasmussen <mathiasvr@gmail.com>
- Sergey Bargamon <sergey@bargamon.ru>
- Thomas Watson Steen <w@tson.dk>
#### Generated by bin/update-authors.sh.

View File

@@ -1,6 +1,271 @@
# WebTorrent.app Version History
# WebTorrent Desktop Version History
## v0.1.1
## v0.7.1 - 2016-06-02
### Changed
- Change "Step Forward" keyboard shortcut to `Alt+Left`
- Change "Step Backward" keyboard shortcut to to `Alt+Right`
### Fixed
- First time startup bug -- invalid torrent/poster paths
## v0.7.0 - 2016-06-02
### Added
- Improved AirPlay support -- using the new [`airplayer`](https://www.npmjs.com/package/airplayer) package
- Remember volume setting in player, for as long as the app is open
### Changed
- Add (+) button now also accepts non .torrent files and creates a torrent from
those files
- Show prompt text in title bar for open dialogs (OS X)
- Upgrade Electron to 1.2.1
- Improve window resizing when aspect ratio is enforced (OS X)
- Use .ico format for better icon rendering quality (Windows)
- Fix crash reporter not working (Windows)
### Fixed
- Re-enable WebRTC (web peers)! (OS X, Windows)
- Windows support was disabled in v0.6.1 to work around a bug in Electron
- OS X support was disabled in v0.4.0 to work around a 100% CPU bug
- Fix subtitle selector radio button UI size glitch
- Fix race condition causing exeption on app startup
- Fix duplicate torrent detection in some cases
- Fix "gray screen" exception caused by incorrect file list order
- Fix torrent loading message UI misalignment
### Known issues
- When upgrading to WebTorrent Desktop v0.7.0, some torrent metadata (file list,
selected files, whether torrent is streamable) will be cleared. Just start the
torrent to re-populate the metadata.
## v0.6.1 - 2016-05-26
### Fixed
- Disable WebRTC to work around Electron crash (Windows)
- Will be re-enabled in the next version of WebTorrent, which will be based on
the next version of Electron, where the bug is fixed.
- Fix crash when updating from WebTorrent 0.5.x in some situtations (#583)
- Fix crash when dropping files onto the dock icon (OS X)
- Fix keyboard shortcuts Space and ESC being captured globally (#585)
- Fix crash, show error when drag-dropping hidden files (#586)
## v0.6.0 - 2016-05-24
### Added
- Added Preferences page to set Download folder
- Save video position, resume playback from saved position
- Add additional video player keyboard shortcuts (#275)
- Use `poster.jpg` file as the poster image if available (#558)
- Associate .torrent files to WebTorrent Desktop (OS X) (#553)
- Add support for pasting `instant.io` links (#559)
- Add announcement feature
### Changed
- Nicer player UI
- Reduce startup jank, improve startup time (#568)
- Cleanup unsupported codec detection (#569, #570)
- Cleaner look for the torrent file list
- Improve subtitle positioning (#551)
### Fixed
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
- Fix bugs in LocationHistory
- When player is active, and magnet link is pasted, go back to list
- After deleting torrent, remove just the player from forward stack
- After creating torrent, remove create torrent page from forward stack
- Cancel button on create torrent page should only go back one page
## v0.5.1 - 2016-05-18
### Fixed
- Fix auto-updater (OS X, Windows).
## v0.5.0 - 2016-05-17
### Added
- Select/deselect individual files to torrent.
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
- "Add Subtitle File..." menu item.
### Changed
- When manually adding subtitle track(s), always switch to the new track.
### Fixed
- Magnet links throw exception on app launch. (OS X)
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
- Missing 'About WebTorrent' menu item. (Windows)
- Rare exception. ("Cannot create BrowserWindow before app is ready")
## v0.4.0 - 2016-05-13
### Added
- Better Windows support!
- Windows 32-bit build.
- Windows Portable App build.
- Windows app signing, for fewer install warnings.
- Better Linux support!
- Linux 32-bit build.
- Subtitles support!
- .srt and .vtt file support.
- Drag-and-drop files on video, or choose from file selector.
- Multiple subtitle files support.
- Stream to VLC when the audio codec is unplayable (e.g. AC3, EAC3).
- "Show in Folder" item in context menu.
- Volume slider, with mute/unmute button.
- New "Create torrent" page to modify:
- Torrent comment.
- Trackers.
- Private torrent flag.
- Use mouse wheel to increase/decrease volume.
- Bounce the Downloads stack when download completes. (OS X)
- New default torrent on first launch: The WIRED CD.
### Changed
- Improve app startup time by 40%.
- UI tweaks: Reduce font size, reduce torrent list item height.
- Add Playback menu for playback-related functionality.
- Fix installing when the app is already installed. (Windows)
- Don't kill unrelated processes on uninstall. (Windows)
- Set "sheet offset" correctly for create torrent dialog. (OS X)
- Remove OS X-style Window menu. (Linux, Windows)
- Remove "Add Fake Airplay/Chromecast" menu items.
### Fixed
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is
temporary. (OS X)
- When fullscreen, make controls use the full window. (OS X)
- Support creating torrents that contain .torrent files.
- Block power save while casting to a remote device.
- Do not block power save when the space key is pressed from the torrent list.
- Support playing .mpg and .ogv extensions in the app.
- Fix video centering for multi-screen setups.
- Show an error when adding a duplicate torrent.
- Show an error when adding an invalid magnet link.
- Do not stop music when tabbing to another program (OS X)
- Properly size the Windows volume mixer icon.
- Default to the user's OS-defined, localized "Downloads" folder.
- Enforce minimimum window size when resizing player to prevent window disappearing.
- Fix rare race condition error on app quit.
- Don't use zero-byte torrent "poster" images.
Thanks to @grunjol, @rguedes, @furstenheim, @karloluis, @DiegoRBaquero, @alxhotel,
@AgentEpsilon, @remijouannet, Rolando Guedes, @dcposch, and @feross for contributing
to this release!
## v0.3.3 - 2016-04-07
### 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
- Improve app startup time by over 100%
@@ -17,8 +282,13 @@
- Fixed
- Crash when ".local/share/{applications,icons}" path did not exist (Linux)
- 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!**
- Includes auto-updater, just like the OS X version.
@@ -33,20 +303,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.
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.
## 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.
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
[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

View File

@@ -4,44 +4,72 @@ Contributions welcome!
**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.
- **Writing**: contribute your expertise in an area by helping expand the included content.
- **Copy editing**: fix typos, clarify language, and generally improve the quality of the content.
- **Formatting**: help keep content easy to read with consistent formatting.
- **Writing**: contribute your expertise in an area by helping expand the included docs.
- **Copy editing**: fix typos, clarify language, and improve the quality of the docs.
- **Formatting**: help keep docs easy to read with consistent formatting.
## Code Style
[![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-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:
1. **No `--force` pushes** or modifying the Git history in any way.
1. **Non-master branches** ought to 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.
1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor.
1. Contributors should attempt to adhere to the prevailing code style.
2. **Non-master branches** should be used for ongoing work.
3. **Significant modifications** like API changes should be subject to a **pull request**
to solicit feedback from other contributors.
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.
## 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

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

View File

@@ -2,7 +2,7 @@
<br>
<a href="https://webtorrent.io"><img src="https://webtorrent.io/img/WebTorrent.png" alt="WebTorrent" width="200"></a>
<br>
WebTorrent.app
WebTorrent Desktop
<br>
<br>
</h1>
@@ -10,28 +10,19 @@
<h4 align="center">The streaming torrent client. For OS X, Windows, and Linux.</h4>
<p align="center">
<a href="https://gitter.im/feross/webtorrent">
<img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg"
alt="Gitter">
</a>
<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>
<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>
<a href="https://travis-ci.org/feross/webtorrent-desktop"><img src="https://img.shields.io/travis/feross/webtorrent-desktop/master.svg" alt="Travis"></a>
<a href="https://github.com/feross/webtorrent-desktop/releases"><img src="https://img.shields.io/github/release/feross/webtorrent-desktop.svg" alt="Release"></a>
</p>
## 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
<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>
## How to Contribute
@@ -62,7 +53,20 @@ To build for one 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
@@ -83,5 +87,4 @@ brew install wine
## License
MIT. Copyright (c) [Feross Aboukhadijeh](http://feross.org).
MIT. Copyright (c) [WebTorrent, LLC](https://webtorrent.io).

54
bin/check-deps.js Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env node
var fs = require('fs')
var cp = require('child_process')
var BUILT_IN_DEPS = ['child_process', 'electron', 'fs', 'os', 'path']
var EXECUTABLE_DEPS = ['gh-release', 'standard']
main()
// Scans codebase for missing or unused dependencies. Exits with code 0 on success.
function main () {
if (process.platform === 'win32') {
console.error('Sorry, check-deps only works on Mac and Linux')
return
}
var usedDeps = findUsedDeps()
var packageDeps = findPackageDeps()
var missingDeps = usedDeps.filter(
(dep) => !packageDeps.includes(dep) && !BUILT_IN_DEPS.includes(dep)
)
var unusedDeps = packageDeps.filter(
(dep) => !usedDeps.includes(dep) && !EXECUTABLE_DEPS.includes(dep)
)
if (missingDeps.length > 0) {
console.error('Missing package dependencies: ' + missingDeps)
}
if (unusedDeps.length > 0) {
console.error('Unused package dependencies: ' + unusedDeps)
}
if (missingDeps.length + unusedDeps.length > 0) {
process.exitCode = 1
}
}
// Finds all dependencies specified in `package.json`
function findPackageDeps () {
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
var deps = Object.keys(pkg.dependencies)
var devDeps = Object.keys(pkg.devDependencies)
var optionalDeps = Object.keys(pkg.optionalDependencies)
return [].concat(deps, devDeps, optionalDeps)
}
// Finds all dependencies that used with `require()`
function findUsedDeps () {
var stdout = cp.execSync('./bin/list-deps.sh')
return stdout.toString().trim().split('\n')
}

View File

@@ -1,17 +1,27 @@
#!/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.
*/
var config = require('../config')
var fs = require('fs')
var os = require('os')
var path = require('path')
var pathExists = require('path-exists')
var rimraf = require('rimraf')
var config = require('../config')
var handlers = require('../main/handlers')
rimraf.sync(config.CONFIG_PATH)
var tmpPath = path.join(pathExists.sync('/tmp') ? '/tmp' : os.tmpDir(), 'webtorrent')
var tmpPath
try {
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
} catch (err) {
tmpPath = path.join(os.tmpDir(), 'webtorrent')
}
rimraf.sync(tmpPath)
// Uninstall .torrent file and magnet link handlers
handlers.uninstall()

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env node
var electron = require('electron-prebuilt')
var proc = require('child_process')
var cp = require('child_process')
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) {
process.exit(code)
process.exitCode = code
})

10
bin/list-deps.sh Executable file
View File

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

6
bin/open-config.js Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env node
var config = require('../config')
var open = require('open')
open(config.CONFIG_PATH)

View File

@@ -4,18 +4,39 @@
* Builds app binaries for OS X, Linux, and Windows.
*/
var config = require('../config')
var cp = require('child_process')
var electronPackager = require('electron-packager')
var fs = require('fs')
var minimist = require('minimist')
var mkdirp = require('mkdirp')
var os = require('os')
var path = require('path')
var pkg = require('../package.json')
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 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 () {
var platform = process.argv[2]
rimraf.sync(DIST_PATH)
var platform = argv._[0]
if (platform === 'darwin') {
buildDarwin(printDone)
} else if (platform === 'win32') {
@@ -23,10 +44,10 @@ function build () {
} else if (platform === 'linux') {
buildLinux(printDone)
} else {
buildDarwin(function (err, buildPath) {
printDone(err, buildPath)
buildWin32(function (err, buildPath) {
printDone(err, buildPath)
buildDarwin(function (err) {
printDone(err)
buildWin32(function (err) {
printDone(err)
buildLinux(printDone)
})
})
@@ -34,11 +55,9 @@ function build () {
}
var all = {
// Build 64 bit binaries only.
arch: 'x64',
// The application source directory.
dir: config.ROOT_PATH,
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
// property on Windows, and `NSHumanReadableCopyright` on OS X.
'app-copyright': config.APP_COPYRIGHT,
// The release version of the application. Maps to the `ProductVersion` metadata
// property on Windows, and `CFBundleShortVersionString` on OS X.
@@ -54,19 +73,22 @@ var all = {
'asar-unpack': 'WebTorrent*',
// The build version of the application. Maps to the FileVersion metadata property on
// Windows, and CFBundleVersion on OS X. We're using the short git hash (e.g. 'e7d837e')
// 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', ''),
// Windows, and CFBundleVersion on OS X. Note: Windows requires the build version to
// start with a number. We're using the version of the underlying WebTorrent library.
'build-version': require('webtorrent/package.json').version,
// The application source directory.
dir: config.ROOT_PATH,
// Pattern which specifies which files to ignore when copying files to create the
// 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.
name: config.APP_NAME,
// 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.
overwrite: true,
@@ -76,12 +98,16 @@ var all = {
prune: true,
// The Electron version with which the app is built (without the leading 'v')
version: pkg.dependencies['electron-prebuilt']
version: require('electron-prebuilt/package.json').version
}
var darwin = {
// Build for OS X
platform: 'darwin',
// Build 64 bit binaries only.
arch: 'x64',
// The bundle identifier to use in the application's plist (OS X only).
'app-bundle-id': 'io.webtorrent.webtorrent',
@@ -97,24 +123,25 @@ var darwin = {
}
var win32 = {
// Build for Windows.
platform: 'win32',
// Build 32 bit binaries only.
arch: 'ia32',
// Object hash of application metadata to embed into the executable (Windows only)
'version-string': {
// Company that produced the file.
CompanyName: config.APP_NAME,
// Copyright notices that apply to the file.
LegalCopyright: config.APP_COPYRIGHT,
// Name of the program, displayed to users
FileDescription: config.APP_NAME,
// 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
// 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.
ProductName: config.APP_NAME,
@@ -130,7 +157,11 @@ var win32 = {
}
var linux = {
platform: 'linux'
// Build for Linux.
platform: 'linux',
// Build 32 and 64 bit binaries.
arch: 'all'
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
}
@@ -140,8 +171,10 @@ build()
function buildDarwin (cb) {
var plist = require('plist')
console.log('OS X: Packaging electron...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err)
console.log('OS X: Packaged electron. ' + buildPath)
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
var contentsPath = path.join(appPath, 'Contents')
@@ -176,7 +209,24 @@ function buildDarwin (cb) {
}
]
infoPlist.NSHumanReadableCopyright = config.APP_COPYRIGHT
infoPlist.UTExportedTypeDeclarations = [
{
UTTypeConformsTo: [
'public.data',
'public.item',
'com.bittorrent.torrent'
],
UTTypeDescription: 'BitTorrent Document',
UTTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
UTTypeIdentifier: 'org.bittorrent.torrent',
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
UTTypeTagSpecification: {
'com.apple.ostype': 'TORR',
'public.filename-extension': [ 'torrent' ],
'public.mime-type': 'application/x-bittorrent'
}
}
]
fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
@@ -184,11 +234,24 @@ function buildDarwin (cb) {
cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`)
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')
/*
* 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
* the same author as the current version.
* - So users will not a see a warning about the app coming from an "Unidentified
@@ -206,48 +269,71 @@ function buildDarwin (cb) {
verbose: true
}
console.log('OS X: Signing app...')
sign(signOpts, function (err) {
if (err) return cb(err)
console.log('OS X: Signed app.')
cb(null)
})
}
// Create .zip file (used by the auto-updater)
var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.zip')
cp.execSync(`pushd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'} && popd`)
console.log('Created OS X .zip file.')
function pack (cb) {
packageZip() // always produce .zip file, used for automatic updates
var targetPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.dmg')
rimraf.sync(targetPath)
if (argv.package === 'dmg' || argv.package === 'all') {
packageDmg(cb)
}
}
// 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' }
]
}
function packageZip () {
// Create .zip file (used by the auto-updater)
console.log('OS X: Creating zip...')
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
zip.zipSync(inPath, outPath)
console.log('OS X: Created zip.')
}
function packageDmg (cb) {
console.log('OS X: Creating dmg...')
var appDmg = require('appdmg')
var targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
rimraf.sync(targetPath)
// Create a .dmg (OS X disk image) file, for easy user installation.
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)
dmg.on('error', cb)
dmg.on('progress', function (info) {
if (info.type === 'step-begin') console.log(info.title + '...')
})
dmg.on('finish', function (info) {
console.log('Created OS X disk image (.dmg) file.')
cb(null, buildPath)
})
var dmg = appDmg(dmgOpts)
dmg.once('error', cb)
dmg.on('progress', function (info) {
if (info.type === 'step-begin') console.log(info.title + '...')
})
dmg.once('finish', function (info) {
console.log('OS X: Created dmg.')
cb(null)
})
}
})
@@ -255,39 +341,164 @@ function buildDarwin (cb) {
function buildWin32 (cb) {
var installer = require('electron-winstaller')
console.log('Windows: Packaging electron...')
/*
* Path to folder with the following files:
* - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt)
*/
var CERT_PATH
try {
fs.accessSync('D:')
CERT_PATH = 'D:'
} catch (err) {
CERT_PATH = path.join(os.homedir(), 'Desktop')
}
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
if (err) return cb(err)
console.log('Windows: Packaged electron. ' + buildPath)
console.log('Creating Windows installer...')
installer.createWindowsInstaller({
name: config.APP_NAME,
productName: config.APP_NAME,
title: config.APP_NAME,
exe: config.APP_NAME + '.exe',
var signWithParams
if (process.platform === 'win32') {
if (argv.sign) {
var certificateFile = path.join(CERT_PATH, 'authenticode.p12')
var certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
var timestampServer = 'http://timestamp.comodoca.com'
signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256`
} else {
printWarning()
}
} else {
printWarning()
}
appDirectory: buildPath[0],
outputDirectory: path.join(config.ROOT_PATH, 'dist'),
version: pkg.version,
description: config.APP_NAME,
authors: config.APP_TEAM,
iconUrl: config.APP_ICON + '.ico',
setupIcon: config.APP_ICON + '.ico',
// certificateFile: '', // TODO
usePackageJson: false,
loadingGif: path.join(config.STATIC_PATH, 'loading.gif')
}).then(function () {
console.log('Created Windows installer.')
cb(null, buildPath)
}).catch(cb)
var tasks = []
if (argv.package === 'exe' || argv.package === 'all') {
tasks.push((cb) => packageInstaller(cb))
}
if (argv.package === 'portable' || argv.package === 'all') {
tasks.push((cb) => packagePortable(cb))
}
series(tasks, cb)
function packageInstaller (cb) {
console.log('Windows: Creating installer...')
installer.createWindowsInstaller({
appDirectory: buildPath[0],
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) {
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.zipSync(inPath, outPath)
console.log('Windows: Created portable app.')
cb(null)
}
})
}
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)
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.zipSync(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`)
cb(null)
}
}
function printDone (err, buildPath) {
function printDone (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

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

View File

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

View File

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

12
bin/warning.txt Normal file
View File

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

132
config.js
View File

@@ -1,11 +1,16 @@
var applicationConfigPath = require('application-config-path')
var appConfig = require('application-config')('WebTorrent')
var fs = require('fs')
var path = require('path')
var APP_NAME = 'WebTorrent'
var APP_TEAM = 'The WebTorrent Project'
var APP_TEAM = 'WebTorrent, LLC'
var APP_VERSION = require('./package.json').version
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
@@ -14,53 +19,96 @@ module.exports = {
APP_VERSION: APP_VERSION,
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_URL: 'https://webtorrent.io/desktop/update',
CONFIG_PATH: applicationConfigPath(APP_NAME),
CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'),
CONFIG_TORRENT_PATH: path.join(applicationConfigPath(APP_NAME), 'Torrents'),
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
CONFIG_PATH: getConfigPath(),
DEFAULT_TORRENTS: [
{
name: 'Big Buck Bunny',
posterFileName: 'bigBuckBunny.jpg',
torrentFileName: 'bigBuckBunny.torrent'
},
{
name: 'Cosmos Laundromat (Preview)',
posterFileName: 'cosmosLaundromat.jpg',
torrentFileName: 'cosmosLaundromat.torrent'
},
{
name: 'Sintel',
posterFileName: 'sintel.jpg',
torrentFileName: 'sintel.torrent'
},
{
name: 'Tears of Steel',
posterFileName: 'tearsOfSteel.jpg',
torrentFileName: 'tearsOfSteel.torrent'
},
{
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
posterFileName: 'wiredCd.jpg',
torrentFileName: 'wiredCd.torrent'
}
],
DELAYED_INIT: 3000 /* 3 seconds */,
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
HOME_PAGE_URL: 'https://webtorrent.io',
IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(),
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: __dirname,
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
},
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html')
WINDOW_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 getDefaultDownloadPath () {
if (!process || !process.type) {
return ''
}
if (isPortable()) {
return path.join(getConfigPath(), 'Downloads')
}
var electron = require('electron')
return process.type === 'renderer'
? electron.remote.app.getPath('downloads')
: electron.app.getPath('downloads')
}
function isPortable () {
try {
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
} catch (err) {
return false
}
}
function isProduction () {
@@ -68,7 +116,7 @@ function isProduction () {
return false
}
if (process.platform === 'darwin') {
return !/\/Electron\.app\/Contents\/MacOS\/Electron$/.test(process.execPath)
return !/\/Electron\.app\//.test(process.execPath)
}
if (process.platform === 'win32') {
return !/\\electron\.exe$/.test(process.execPath)

14
crash-reporter.js Normal file
View File

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

View File

@@ -1,2 +1 @@
console.time('init')
require('./main')

57
main/announcement.js Normal file
View File

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

View File

@@ -1,31 +0,0 @@
module.exports = {
init
}
var electron = require('electron')
var config = require('../config')
var log = require('./log')
var autoUpdater = electron.autoUpdater
function init () {
autoUpdater.on('error', function (err) {
log.error('App update error: ' + err.message || err)
})
autoUpdater.setFeedURL(config.AUTO_UPDATE_URL)
/*
* 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.
*/
setTimeout(() => autoUpdater.checkForUpdates(), config.AUTO_UPDATE_CHECK_STARTUP_DELAY)
autoUpdater.on('checking-for-update', () => log('Checking for app update'))
autoUpdater.on('update-available', () => log('App update available'))
autoUpdater.on('update-not-available', () => log('App update not available'))
autoUpdater.on('update-downloaded', function (e, releaseNotes, releaseName, releaseDate, updateURL) {
log('App update downloaded: ', releaseName, updateURL)
})
}

122
main/dialog.js Normal file
View File

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

59
main/dock.js Normal file
View File

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

View File

@@ -1,63 +1,309 @@
module.exports = {
init
install,
uninstall
}
var config = require('../config')
var path = require('path')
var log = require('./log')
function init () {
function install () {
if (process.platform === 'darwin') {
installDarwin()
}
if (process.platform === 'win32') {
initWindows()
installWin32()
}
if (process.platform === 'linux') {
initLinux()
installLinux()
}
}
function initWindows () {
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)
function uninstall () {
if (process.platform === 'darwin') {
uninstallDarwin()
}
if (process.platform === 'win32') {
uninstallWin32()
}
if (process.platform === 'linux') {
uninstallLinux()
}
}
function initLinux () {
var config = require('../config')
var fs = require('fs')
var mkdirp = require('mkdirp')
function installDarwin () {
var electron = require('electron')
var app = electron.app
// On OS X, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime.
app.setAsDefaultProtocolClient('magnet')
// File handlers are defined in `Info.plist`.
}
function uninstallDarwin () {}
var EXEC_COMMAND = [ process.execPath ]
if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH)
}
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,
EXEC_COMMAND
)
registerFileHandlerWin32(
'.torrent',
'io.webtorrent.torrent',
'BitTorrent Document',
iconPath,
EXEC_COMMAND
)
/**
* To add a protocol handler, the following keys must be added to the Windows registry:
*
* 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, `${commandToArgs(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, `${commandToArgs(command)} "%1"`, done)
}
function done (err) {
if (err) log.error(err.message)
}
}
}
function uninstallWin32 () {
var Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
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(commandToArgs(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 commandToArgs (command) {
return command.map((arg) => `"${arg}"`).join(' ')
}
function installLinux () {
var fs = require('fs-extra')
var os = require('os')
var path = require('path')
var config = require('../config')
var log = require('./log')
installDesktopFile()
installIconFile()
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)
}
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 execPath = process.execPath + (config.IS_PRODUCTION ? '' : ' \.')
var tryExecPath = process.execPath
var appPath = config.IS_PRODUCTION
? path.dirname(process.execPath)
: config.ROOT_PATH
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, execPath)
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, tryExecPath)
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
var desktopFilePath = path.join(
os.homedir(),
'.local',
'share',
'applications',
'webtorrent.desktop'
'webtorrent-desktop.desktop'
)
mkdirp(path.dirname(desktopFilePath))
fs.mkdirp(path.dirname(desktopFilePath))
fs.writeFile(desktopFilePath, desktopFile, function (err) {
if (err) return console.error(err.message)
if (err) return log.error(err.message)
})
}
@@ -67,99 +313,42 @@ function initLinux () {
}
function writeIconFile (err, iconFile) {
if (err) return console.error(err.message)
if (err) return log.error(err.message)
var iconFilePath = path.join(
os.homedir(),
'.local',
'share',
'icons',
'webtorrent.png'
'webtorrent-desktop.png'
)
mkdirp(path.dirname(iconFilePath))
fs.mkdirp(path.dirname(iconFilePath))
fs.writeFile(iconFilePath, iconFile, function (err) {
if (err) return console.error(err.message)
if (err) return log.error(err.message)
})
}
}
/**
* To add a protocol handler on Windows, 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 uninstallLinux () {
var os = require('os')
var path = require('path')
var fs = require('fs-extra')
function registerProtocolHandlerWin32 (protocol, name, icon, command) {
var Registry = require('winreg')
var desktopFilePath = path.join(
os.homedir(),
'.local',
'share',
'applications',
'webtorrent-desktop.desktop'
)
fs.removeSync(desktopFilePath)
var protocolKey = new Registry({
hive: Registry.HKCU, // HKEY_CURRENT_USER
key: '\\Software\\Classes\\' + protocol
})
protocolKey.set('', Registry.REG_SZ, name, callback)
protocolKey.set('URL Protocol', Registry.REG_SZ, '', callback)
var iconKey = new Registry({
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)
}
var iconFilePath = path.join(
os.homedir(),
'.local',
'share',
'icons',
'webtorrent-desktop.png'
)
fs.removeSync(iconFilePath)
}

View File

@@ -1,15 +1,22 @@
console.time('init')
var electron = require('electron')
var app = electron.app
var ipcMain = electron.ipcMain
var autoUpdater = require('./auto-updater')
var announcement = require('./announcement')
var config = require('../config')
var crashReporter = require('../crash-reporter')
var dialog = require('./dialog')
var dock = require('./dock')
var handlers = require('./handlers')
var ipc = require('./ipc')
var log = require('./log')
var menu = require('./menu')
var shortcuts = require('./shortcuts')
var squirrelWin32 = require('./squirrel-win32')
var tray = require('./tray')
var updater = require('./updater')
var windows = require('./windows')
var shouldQuit = false
@@ -34,6 +41,11 @@ if (!shouldQuit) {
}
function init () {
if (config.IS_PORTABLE) {
app.setPath('userData', config.CONFIG_PATH)
}
var isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false
@@ -43,49 +55,60 @@ function init () {
ipc.init()
app.on('will-finish-launching', function () {
autoUpdater.init()
setupCrashReporter()
app.once('will-finish-launching', function () {
crashReporter.init()
})
app.on('ready', function () {
isReady = true
windows.main.init()
windows.webtorrent.init()
menu.init()
windows.createMainWindow()
shortcuts.init()
if (process.platform !== 'win32') handlers.init()
// To keep app startup fast, some code is delayed.
setTimeout(delayedInit, config.DELAYED_INIT)
})
app.on('ipcReady', function () {
app.once('ipcReady', function () {
log('Command line args:', argv)
processArgv(argv)
console.timeEnd('init')
})
app.on('before-quit', function () {
app.on('before-quit', function (e) {
if (app.isQuitting) return
app.isQuitting = true
e.preventDefault()
windows.main.dispatch('saveState') // try to save state on exit
ipcMain.once('savedState', () => app.quit())
setTimeout(() => app.quit(), 2000) // quit after 2 secs, at most
})
app.on('activate', function () {
windows.createMainWindow()
if (isReady) windows.main.show()
})
}
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
function delayedInit () {
announcement.init()
dock.init()
handlers.install()
tray.init()
updater.init()
}
function onOpen (e, torrentId) {
e.preventDefault()
if (app.ipcReady) {
windows.main.send('dispatch', 'onOpen', torrentId)
// Magnet links opened from Chrome won't focus the app without a setTimeout. The
// confirmation dialog Chrome shows causes Chrome to steal back the focus.
// Magnet links opened from Chrome won't focus the app without a setTimeout.
// The confirmation dialog Chrome shows causes Chrome to steal back the focus.
// Electron issue: https://github.com/atom/electron/issues/4338
setTimeout(function () {
windows.focusWindow(windows.main)
}, 100)
setTimeout(() => windows.main.show(), 100)
processArgv([ torrentId ])
} else {
argv.push(torrentId)
}
@@ -96,7 +119,7 @@ function onAppOpen (newArgv) {
if (app.ipcReady) {
log('Second app instance opened, but was prevented:', newArgv)
windows.focusWindow(windows.main)
windows.main.show()
processArgv(newArgv)
} else {
@@ -109,28 +132,22 @@ function sliceArgv (argv) {
}
function processArgv (argv) {
argv.forEach(function (argvi) {
switch (argvi) {
case '-n':
windows.main.send('dispatch', 'showCreateTorrent')
break
case '-o':
windows.main.send('dispatch', 'showOpenTorrentFile')
break
case '-u':
windows.main.send('showOpenTorrentAddress')
break
default:
windows.main.send('dispatch', 'onOpen', argvi)
var torrentIds = []
argv.forEach(function (arg) {
if (arg === '-n') {
dialog.openSeedDirectory()
} else if (arg === '-o') {
dialog.openTorrentFile()
} else if (arg === '-u') {
dialog.openTorrentAddress()
} else if (arg.startsWith('-psn')) {
// Ignore OS X launchd "process serial number" argument
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
} else {
torrentIds.push(arg)
}
})
}
function setupCrashReporter () {
// require('crash-reporter').start({
// productName: 'WebTorrent',
// companyName: 'WebTorrent',
// submitURL: 'https://webtorrent.io/crash-report',
// autoSubmit: true
// })
if (torrentIds.length > 0) {
windows.main.dispatch('onOpen', torrentIds)
}
}

View File

@@ -2,120 +2,172 @@ module.exports = {
init
}
var debug = require('debug')('webtorrent-app:ipcMain')
var electron = require('electron')
var app = electron.app
var ipcMain = electron.ipcMain
var powerSaveBlocker = electron.powerSaveBlocker
var dialog = require('./dialog')
var dock = require('./dock')
var log = require('./log')
var menu = require('./menu')
var powerSaveBlocker = require('./power-save-blocker')
var shell = require('./shell')
var shortcuts = require('./shortcuts')
var vlc = require('./vlc')
var windows = require('./windows')
// has to be a number, not a boolean, and undefined throws an error
var powerSaveBlockID = 0
// Messages from the main process, to be sent once the WebTorrent process starts
var messageQueueMainToWebTorrent = []
// holds a ChildProcess while we're playing a video in VLC, null otherwise
var vlcProcess
function init () {
ipcMain.on('ipcReady', function (e) {
var ipc = electron.ipcMain
ipc.once('ipcReady', function (e) {
app.ipcReady = true
app.emit('ipcReady')
setTimeout(function () {
windows.main.show()
console.timeEnd('init')
}, 50)
})
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
ipcMain.on('showCreateTorrent', menu.showCreateTorrent)
ipcMain.on('setBounds', function (e, bounds, maximize) {
setBounds(bounds, maximize)
ipc.once('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window',
messageQueueMainToWebTorrent.length)
messageQueueMainToWebTorrent.forEach(function (message) {
windows.webtorrent.send(message.name, ...message.args)
log('webtorrent: sent queued %s', message.name)
})
})
ipcMain.on('setAspectRatio', function (e, aspectRatio, extraSize) {
setAspectRatio(aspectRatio, extraSize)
/**
* Dialog
*/
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
ipc.on('openFiles', () => dialog.openFiles())
/**
* Dock
*/
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
/**
* Events
*/
ipc.on('onPlayerOpen', function () {
menu.onPlayerOpen()
shortcuts.onPlayerOpen()
})
ipcMain.on('setBadge', function (e, text) {
setBadge(text)
ipc.on('onPlayerClose', function () {
menu.onPlayerClose()
shortcuts.onPlayerOpen()
})
ipcMain.on('setProgress', function (e, progress) {
setProgress(progress)
/**
* Power Save Blocker
*/
ipc.on('blockPowerSave', () => powerSaveBlocker.start())
ipc.on('unblockPowerSave', () => powerSaveBlocker.stop())
/**
* Shell
*/
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
/**
* Windows: Main
*/
var main = windows.main
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
ipc.on('setProgress', (e, ...args) => main.setProgress(...args))
ipc.on('setTitle', (e, ...args) => main.setTitle(...args))
ipc.on('show', () => main.show())
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
/**
* VLC
* TODO: Move most of this code to vlc.js
*/
ipc.on('checkForVLC', function (e) {
vlc.checkForVLC(function (isInstalled) {
windows.main.send('checkForVLC', isInstalled)
})
})
ipcMain.on('toggleFullScreen', function (e, flag) {
menu.toggleFullScreen(flag)
ipc.on('vlcPlay', function (e, url) {
var args = ['--play-and-exit', '--video-on-top', '--no-video-title-show', '--quiet', url]
log('Running vlc ' + args.join(' '))
vlc.spawn(args, function (err, proc) {
if (err) return windows.main.dispatch('vlcNotFound')
vlcProcess = proc
// If it works, close the modal after a second
var closeModalTimeout = setTimeout(() =>
windows.main.dispatch('exitModal'), 1000)
vlcProcess.on('close', function (code) {
clearTimeout(closeModalTimeout)
if (!vlcProcess) return // Killed
log('VLC exited with code ', code)
if (code === 0) {
windows.main.dispatch('backToList')
} else {
windows.main.dispatch('vlcNotFound')
}
vlcProcess = null
})
vlcProcess.on('error', function (e) {
log('VLC error', e)
})
})
})
ipcMain.on('setTitle', function (e, title) {
windows.main.setTitle(title)
ipc.on('vlcQuit', function () {
if (!vlcProcess) return
log('Killing VLC, pid ' + vlcProcess.pid)
vlcProcess.kill('SIGKILL') // kill -9
vlcProcess = null
})
ipcMain.on('openItem', function (e, path) {
log('opening file or folder: ' + path)
electron.shell.openItem(path)
})
// Capture all events
var oldEmit = ipc.emit
ipc.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) {
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') {
// Send message to main window
windows.main.send(name, ...args)
log('webtorrent: got %s', name)
} else if (app.ipcReadyWebTorrent) {
// Send message to webtorrent window
windows.webtorrent.send(name, ...args)
log('webtorrent: sent %s', name)
} else {
// Queue message for webtorrent window, it hasn't finished loading yet
messageQueueMainToWebTorrent.push({
name: name,
args: args
})
log('webtorrent: queueing %s', name)
}
return
}
ipcMain.on('blockPowerSave', blockPowerSave)
ipcMain.on('unblockPowerSave', unblockPowerSave)
ipcMain.on('onPlayerOpen', menu.onPlayerOpen)
ipcMain.on('onPlayerClose', menu.onPlayerClose)
}
function setBounds (bounds, maximize) {
// Do nothing in fullscreen
if (!windows.main || windows.main.isFullScreen()) return
// Maximize or minimize, if the second argument is present
var willBeMaximized
if (maximize === true) {
if (!windows.main.isMaximized()) windows.main.maximize()
willBeMaximized = true
} else if (maximize === false) {
if (windows.main.isMaximized()) windows.main.unmaximize()
willBeMaximized = false
} else {
willBeMaximized = windows.main.isMaximized()
}
// Assuming we're not maximized or maximizing, set the window size
if (!willBeMaximized) {
windows.main.setBounds(bounds, true)
}
}
function setAspectRatio (aspectRatio, extraSize) {
debug('setAspectRatio %o %o', aspectRatio, extraSize)
if (windows.main) {
windows.main.setAspectRatio(aspectRatio, extraSize)
}
}
// Display string in dock badging area (OS X)
function setBadge (text) {
debug('setBadge %s', text)
if (app.dock) app.dock.setBadge(String(text))
}
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
function setProgress (progress) {
debug('setProgress %s', progress)
if (windows.main) {
windows.main.setProgressBar(progress)
}
}
function blockPowerSave () {
powerSaveBlockID = powerSaveBlocker.start('prevent-display-sleep')
debug('blockPowerSave %d', powerSaveBlockID)
}
function unblockPowerSave () {
if (powerSaveBlocker.isStarted(powerSaveBlockID)) {
powerSaveBlocker.stop(powerSaveBlockID)
debug('unblockPowerSave %d', powerSaveBlockID)
// Emit all other events normally
oldEmit.call(ipc, name, e, ...args)
}
}

View File

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

View File

@@ -1,176 +1,107 @@
module.exports = {
init,
onToggleFullScreen,
onWindowHide,
onWindowShow,
onPlayerOpen,
onPlayerClose,
showCreateTorrent,
showOpenTorrentFile,
toggleFullScreen
onPlayerOpen,
onToggleAlwaysOnTop,
onToggleFullScreen,
onWindowBlur,
onWindowFocus
}
var debug = require('debug')('webtorrent-app:menu')
var electron = require('electron')
var app = electron.app
var config = require('../config')
var dialog = require('./dialog')
var shell = require('./shell')
var windows = require('./windows')
var appMenu, dockMenu
var menu
function init () {
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
electron.Menu.setApplicationMenu(appMenu)
dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
if (app.dock) app.dock.setMenu(dockMenu)
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(menu)
}
function toggleFullScreen (flag) {
debug('toggleFullScreen %s', flag)
if (windows.main && windows.main.isVisible()) {
flag = flag != null ? flag : !windows.main.isFullScreen()
windows.main.setFullScreen(flag)
}
function onPlayerClose () {
getMenuItem('Play/Pause').enabled = false
getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false
getMenuItem('Step Forward').enabled = false
getMenuItem('Step Backward').enabled = false
getMenuItem('Increase Speed').enabled = false
getMenuItem('Decrease Speed').enabled = false
getMenuItem('Add Subtitles File...').enabled = false
}
// Sets whether the window should always show on top of other windows
function toggleFloatOnTop (flag) {
debug('toggleFloatOnTop %s', flag)
if (windows.main) {
flag = flag != null ? flag : !windows.main.isAlwaysOnTop()
windows.main.setAlwaysOnTop(flag)
getMenuItem('Float on Top').checked = flag
}
function onPlayerOpen () {
getMenuItem('Play/Pause').enabled = true
getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true
getMenuItem('Step Forward').enabled = true
getMenuItem('Step Backward').enabled = true
getMenuItem('Increase Speed').enabled = true
getMenuItem('Decrease Speed').enabled = true
getMenuItem('Add Subtitles File...').enabled = true
}
function increaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', 0.1)
}
function onToggleAlwaysOnTop (flag) {
getMenuItem('Float on Top').checked = flag
}
function decreaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', -0.1)
}
function onToggleFullScreen (flag) {
getMenuItem('Full Screen').checked = flag
}
function toggleDevTools () {
debug('toggleDevTools')
if (windows.main) {
windows.main.toggleDevTools()
}
}
function reloadWindow () {
debug('reloadWindow')
if (windows.main) {
windows.main.webContents.reloadIgnoringCache()
}
}
function addFakeDevice (device) {
debug('addFakeDevice %s', device)
windows.main.send('addFakeDevice', device)
}
function onWindowShow () {
debug('onWindowShow')
getMenuItem('Full Screen').enabled = true
getMenuItem('Float on Top').enabled = true
}
function onWindowHide () {
debug('onWindowHide')
function onWindowBlur () {
getMenuItem('Full Screen').enabled = false
getMenuItem('Float on Top').enabled = false
}
function onPlayerOpen () {
getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true
}
function onPlayerClose () {
getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false
}
function onToggleFullScreen (isFullScreen) {
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen()
windows.main.setMenuBarVisibility(!isFullScreen)
getMenuItem('Full Screen').checked = isFullScreen
windows.main.send('fullscreenChanged', isFullScreen)
function onWindowFocus () {
getMenuItem('Full Screen').enabled = true
getMenuItem('Float on Top').enabled = true
}
function getMenuItem (label) {
for (var i = 0; i < appMenu.items.length; i++) {
var menuItem = appMenu.items[i].submenu.items.find(function (item) {
for (var i = 0; i < menu.items.length; i++) {
var menuItem = menu.items[i].submenu.items.find(function (item) {
return item.label === label
})
if (menuItem) return menuItem
}
}
// Prompts the user for a file or folder, then makes a torrent out of the data
function showCreateTorrent () {
electron.dialog.showOpenDialog({
title: 'Select a file or folder for the torrent file.',
properties: [ 'openFile', 'openDirectory', 'multiSelections' ]
}, function (filenames) {
if (!Array.isArray(filenames)) return
windows.main.send('dispatch', 'seed', filenames)
})
}
// Prompts the user to choose a torrent file, then adds it to the app
function showOpenTorrentFile () {
electron.dialog.showOpenDialog(windows.main, {
title: 'Select a .torrent file to open.',
properties: [ 'openFile', 'multiSelections' ]
}, function (filenames) {
if (!Array.isArray(filenames)) return
filenames.forEach(function (filename) {
windows.main.send('dispatch', 'addTorrent', filename)
})
})
}
// Prompts the user for the URL of a torrent file, then downloads and adds it
function showOpenTorrentAddress () {
windows.main.send('showOpenTorrentAddress')
}
function getAppMenuTemplate () {
function getMenuTemplate () {
var template = [
{
label: 'File',
submenu: [
{
label: 'Create New Torrent...',
label: process.platform === 'darwin'
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N',
click: showCreateTorrent
click: () => dialog.openSeedDirectory()
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
click: () => dialog.openTorrentFile()
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
click: () => dialog.openTorrentAddress()
},
{
type: 'separator'
},
{
label: process.platform === 'darwin'
? 'Close Window'
: 'Close',
label: process.platform === 'win32'
? 'Close'
: 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
@@ -198,6 +129,14 @@ function getAppMenuTemplate () {
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall'
},
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'CmdOrCtrl+,',
click: () => windows.main.dispatch('preferences')
}
]
},
@@ -210,27 +149,20 @@ function getAppMenuTemplate () {
accelerator: process.platform === 'darwin'
? 'Ctrl+Command+F'
: 'F11',
click: () => toggleFullScreen()
click: () => windows.main.toggleFullScreen()
},
{
label: 'Float on Top',
type: 'checkbox',
click: () => toggleFloatOnTop()
click: () => windows.main.toggleAlwaysOnTop()
},
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
enabled: false
label: 'Go Back',
accelerator: 'Esc',
click: () => windows.main.dispatch('escapeBack')
},
{
type: 'separator'
@@ -238,41 +170,89 @@ function getAppMenuTemplate () {
{
label: 'Developer',
submenu: [
{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click: reloadWindow
},
{
label: 'Developer Tools',
accelerator: process.platform === 'darwin'
? 'Alt+Command+I'
: 'Ctrl+Shift+I',
click: toggleDevTools
click: () => windows.main.toggleDevTools()
},
{
type: 'separator'
},
{
label: 'Add Fake Airplay',
click: () => addFakeDevice('airplay')
},
{
label: 'Add Fake Chromecast',
click: () => addFakeDevice('chromecast')
label: 'Show WebTorrent Process',
accelerator: process.platform === 'darwin'
? 'Alt+Command+P'
: 'Ctrl+Shift+P',
click: () => windows.webtorrent.toggleDevTools()
}
]
}
]
},
{
label: 'Window',
role: 'window',
label: 'Playback',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
label: 'Play/Pause',
accelerator: 'Space',
click: () => windows.main.dispatch('playPause'),
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: () => windows.main.dispatch('changeVolume', 0.1),
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: () => windows.main.dispatch('changeVolume', -0.1),
enabled: false
},
{
type: 'separator'
},
{
label: 'Step Forward',
accelerator: process.platform === 'darwin'
? 'CmdOrCtrl+Alt+Right'
: 'Alt+Right',
click: () => windows.main.dispatch('skip', 1),
enabled: false
},
{
label: 'Step Backward',
accelerator: process.platform === 'darwin'
? 'CmdOrCtrl+Alt+Left'
: 'Alt+Left',
click: () => windows.main.dispatch('skip', -1),
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Speed',
accelerator: 'CmdOrCtrl+=',
click: () => windows.main.dispatch('changePlaybackRate', 1),
enabled: false
},
{
label: 'Decrease Speed',
accelerator: 'CmdOrCtrl+-',
click: () => windows.main.dispatch('changePlaybackRate', -1),
enabled: false
},
{
type: 'separator'
},
{
label: 'Add Subtitles File...',
click: () => windows.main.dispatch('openSubtitles'),
enabled: false
}
]
},
@@ -282,25 +262,25 @@ function getAppMenuTemplate () {
submenu: [
{
label: 'Learn more about ' + config.APP_NAME,
click: () => electron.shell.openExternal('https://webtorrent.io')
click: () => shell.openExternal(config.HOME_PAGE_URL)
},
{
label: 'Contribute on GitHub',
click: () => electron.shell.openExternal('https://github.com/feross/webtorrent-app')
click: () => shell.openExternal(config.GITHUB_URL)
},
{
type: 'separator'
},
{
label: 'Report an Issue...',
click: () => electron.shell.openExternal('https://github.com/feross/webtorrent-app/issues')
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
}
]
}
]
if (process.platform === 'darwin') {
// WebTorrent menu (OS X)
// Add WebTorrent app menu (OS X)
template.unshift({
label: config.APP_NAME,
submenu: [
@@ -311,6 +291,14 @@ function getAppMenuTemplate () {
{
type: 'separator'
},
{
label: 'Preferences',
accelerator: 'Cmd+,',
click: () => windows.main.dispatch('preferences')
},
{
type: 'separator'
},
{
label: 'Services',
role: 'services',
@@ -344,17 +332,36 @@ function getAppMenuTemplate () {
]
})
// Window menu (OS X)
template[4].submenu.push(
{
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
)
} else {
// Add Window menu (OS X)
template.splice(5, 0, {
label: 'Window',
role: 'window',
submenu: [
{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
},
{
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
]
})
}
// On Windows and Linux, open dialogs do not support selecting both files and
// folders and files, so add an extra menu item so there is one for each type.
if (process.platform === 'linux' || process.platform === 'win32') {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: () => dialog.openSeedFile()
})
// Help menu (Windows, Linux)
template[4].submenu.push(
{
@@ -362,30 +369,19 @@ function getAppMenuTemplate () {
},
{
label: 'About ' + config.APP_NAME,
click: windows.createAboutWindow
click: () => windows.about.init()
}
)
}
// Add "File > Quit" menu item so Linux distros where the system tray icon is
// missing will have a way to quit the app.
if (process.platform === 'linux') {
// File menu (Linux)
template[0].submenu.push({
label: 'Quit',
click: () => app.quit()
})
}
return template
}
function getDockMenuTemplate () {
return [
{
label: 'Create New Torrent...',
accelerator: 'CmdOrCtrl+N',
click: showCreateTorrent
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
}
]
}

View File

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

32
main/shell.js Normal file
View File

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

View File

@@ -1,21 +1,20 @@
module.exports = {
init
onPlayerClose,
onPlayerOpen
}
var electron = require('electron')
var localShortcut = require('electron-localshortcut')
var globalShortcut = electron.globalShortcut
var menu = require('./menu')
var windows = require('./windows')
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.
// Electron does not support multiple accelerators for a single menu item, so this
// is registered separately here.
localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen)
function onPlayerOpen () {
// Register play/pause media key, available on some keyboards.
electron.globalShortcut.register(
'MediaPlayPause',
() => windows.main.dispatch('playPause')
)
}
function onPlayerClose () {
// Return the media key to the OS, so other apps can use it.
electron.globalShortcut.unregister('MediaPlayPause')
}

View File

@@ -7,28 +7,23 @@ var electron = require('electron')
var fs = require('fs')
var os = require('os')
var path = require('path')
var pathExists = require('path-exists')
var app = electron.app
var handlers = require('./handlers')
var exeName = path.basename(process.execPath)
var updateDotExe = path.join(process.execPath, '..', '..', 'Update.exe')
var EXE_NAME = path.basename(process.execPath)
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
function handleEvent (cmd) {
if (cmd === '--squirrel-install') {
// App was installed.
// Install protocol/file handlers, desktop/start menu shortcuts.
handlers.init()
// App was installed. Install desktop/start menu shortcuts.
createShortcuts(function () {
// Ensure user sees install splash screen so they realize that Setup.exe actually
// installed an application and isn't the application itself.
setTimeout(function () {
app.quit()
}, 5000)
}, 3000)
})
return true
}
@@ -44,9 +39,18 @@ function handleEvent (cmd) {
if (cmd === '--squirrel-uninstall') {
// App was just uninstalled. Undo anything we did in the --squirrel-install and
// --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
}
@@ -57,15 +61,17 @@ function handleEvent (cmd) {
}
if (cmd === '--squirrel-firstrun') {
// This is called on the app's first run. Do not quit, allow startup to continue.
// App is running for the first time. Do not quit, allow startup to continue.
return false
}
return false
}
// Spawn a command and invoke the callback when it completes with an error and the output
// from standard out.
/**
* Spawn a command and invoke the callback when it completes with an error and
* the output from standard out.
*/
function spawn (command, args, cb) {
var stdout = ''
@@ -95,25 +101,33 @@ function spawn (command, args, cb) {
})
}
// Spawn Squirrel's Update.exe with the given arguments and invoke the callback when the
// command completes.
/**
* Spawn the Squirrel `Update.exe` command with the given arguments and invoke
* the callback when the command completes.
*/
function spawnUpdate (args, cb) {
spawn(updateDotExe, args, cb)
spawn(UPDATE_EXE, args, cb)
}
// Create desktop/start menu shortcuts using the Squirrel Update.exe command line API
/**
* Create desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function createShortcuts (cb) {
spawnUpdate(['--createShortcut', exeName], cb)
spawnUpdate(['--createShortcut', EXE_NAME], cb)
}
// Update desktop/start menu shortcuts using the Squirrel Update.exe command line API
/**
* Update desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function updateShortcuts (cb) {
var homeDir = os.homedir()
if (homeDir) {
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
// Check if the desktop shortcut has been previously deleted and and keep it deleted
// if it was
pathExists(desktopShortcutPath).then(function (desktopShortcutExists) {
// If the desktop shortcut was deleted by the user, then keep it deleted.
fs.access(desktopShortcutPath, function (err) {
var desktopShortcutExists = !err
createShortcuts(function () {
if (desktopShortcutExists) {
cb()
@@ -128,7 +142,10 @@ function updateShortcuts (cb) {
}
}
// Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API
/**
* Remove desktop and start menu shortcuts using the Squirrel `Update.exe`
* command.
*/
function removeShortcuts (cb) {
spawnUpdate(['--removeShortcut', exeName], cb)
spawnUpdate(['--removeShortcut', EXE_NAME], cb)
}

115
main/tray.js Normal file
View File

@@ -0,0 +1,115 @@
module.exports = {
hasTray,
init,
onWindowBlur,
onWindowFocus
}
var electron = require('electron')
var app = electron.app
var config = require('../config')
var windows = require('./windows')
var tray
function init () {
if (process.platform === 'linux') {
initLinux()
}
if (process.platform === 'win32') {
initWin32()
}
// OS X apps generally do not have menu bar icons
}
/**
* Returns true if there a tray icon is active.
*/
function hasTray () {
return !!tray
}
function onWindowBlur () {
if (!tray) return
updateTrayMenu()
}
function onWindowFocus () {
if (!tray) return
updateTrayMenu()
}
function initLinux () {
checkLinuxTraySupport(function (supportsTray) {
if (supportsTray) createTray()
})
}
function initWin32 () {
createTray()
}
/**
* Check for libappindicator1 support before creating tray icon
*/
function checkLinuxTraySupport (cb) {
var cp = require('child_process')
// Check that we're on Ubuntu (or another debian system) and that we have
// libappindicator1. If WebTorrent was installed from the deb file, we should
// always have it. If it was installed from the zip file, we might not.
cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) {
if (err) return cb(false)
// Unfortunately there's no cleaner way, as far as I can tell, to check
// whether a debian package is installed:
cb(stdout.endsWith('\tinstall\n'))
})
}
function createTray () {
tray = new electron.Tray(getIconPath())
// On Windows, left click opens the app, right click opens the context menu.
// On Linux, any click (left or right) opens the context menu.
tray.on('click', () => windows.main.show())
// Show the tray context menu, and keep the available commands up to date
updateTrayMenu()
}
function updateTrayMenu () {
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
tray.setContextMenu(contextMenu)
}
function getMenuTemplate () {
return [
getToggleItem(),
{
label: 'Quit',
click: () => app.quit()
}
]
function getToggleItem () {
if (windows.main.win.isVisible()) {
return {
label: 'Hide to tray',
click: () => windows.main.hide()
}
} else {
return {
label: 'Show WebTorrent',
click: () => windows.main.show()
}
}
}
}
function getIconPath () {
return process.platform === 'win32'
? config.APP_ICON + '.ico'
: config.APP_ICON + '.png'
}

76
main/updater.js Normal file
View File

@@ -0,0 +1,76 @@
module.exports = {
init
}
var electron = require('electron')
var get = require('simple-get')
var config = require('../config')
var log = require('./log')
var windows = require('./windows')
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform
function init () {
if (process.platform === 'linux') {
initLinux()
} else {
initDarwinWin32()
}
}
// The Electron auto-updater does not support Linux yet, so manually check for
// updates and show the user a modal notification.
function initLinux () {
get.concat(AUTO_UPDATE_URL, onResponse)
}
function onResponse (err, res, data) {
if (err) return log(`Update error: ${err.message}`)
if (res.statusCode === 200) {
// Update available
try {
data = JSON.parse(data)
} catch (err) {
return log(`Update error: Invalid JSON response: ${err.message}`)
}
windows.main.dispatch('updateAvailable', data.version)
} else if (res.statusCode === 204) {
// No update available
} else {
// Unexpected status code
log(`Update error: Unexpected status code: ${res.statusCode}`)
}
}
function initDarwinWin32 () {
electron.autoUpdater.on(
'error',
(err) => log.error(`Update error: ${err.message}`)
)
electron.autoUpdater.on(
'checking-for-update',
() => log('Checking for update')
)
electron.autoUpdater.on(
'update-available',
() => log('Update available')
)
electron.autoUpdater.on(
'update-not-available',
() => log('Update not available')
)
electron.autoUpdater.on(
'update-downloaded',
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
)
electron.autoUpdater.setFeedURL(AUTO_UPDATE_URL)
electron.autoUpdater.checkForUpdates()
}

22
main/vlc.js Normal file
View File

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

View File

@@ -1,98 +0,0 @@
var windows = module.exports = {
about: null,
main: null,
createAboutWindow: createAboutWindow,
createMainWindow: createMainWindow,
focusWindow: focusWindow
}
var electron = require('electron')
var app = electron.app
var config = require('../config')
var menu = require('./menu')
function createAboutWindow () {
if (windows.about) {
return focusWindow(windows.about)
}
var win = windows.about = new electron.BrowserWindow({
backgroundColor: '#ECECEC',
show: false,
center: true,
resizable: false,
icon: config.APP_ICON + '.png',
title: process.platform !== 'darwin'
? 'About ' + config.APP_WINDOW_TITLE
: '',
useContentSize: true, // Specify web page size without OS chrome
width: 300,
height: 170,
minimizable: false,
maximizable: false,
fullscreen: false,
skipTaskbar: true
})
win.loadURL(config.WINDOW_ABOUT)
// No window menu
win.setMenu(null)
win.webContents.on('did-finish-load', function () {
win.show()
})
win.once('closed', function () {
windows.about = null
})
}
function createMainWindow () {
if (windows.main) {
return focusWindow(windows.main)
}
var win = windows.main = new electron.BrowserWindow({
backgroundColor: '#282828',
darkTheme: true, // Forces dark theme (GTK+3)
icon: config.APP_ICON + '.png',
minWidth: 375,
minHeight: 38 + (120 * 2), // header height + 2 torrents
show: false, // Hide window until DOM finishes loading
title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
useContentSize: true, // Specify web page size without OS chrome
width: 450,
height: 38 + (120 * 4) // header height + 4 torrents
})
win.loadURL(config.WINDOW_MAIN)
win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen()
})
win.on('blur', menu.onWindowHide)
win.on('focus', menu.onWindowShow)
win.on('enter-full-screen', () => menu.onToggleFullScreen(true))
win.on('leave-full-screen', () => menu.onToggleFullScreen(false))
win.on('close', function (e) {
if (process.platform === 'darwin' && !app.isQuitting) {
e.preventDefault()
win.send('dispatch', 'pause')
win.hide()
}
})
win.once('closed', function () {
windows.main = null
})
}
function focusWindow (win) {
if (win.isMinimized()) {
win.restore()
}
win.show() // shows and gives focus
}

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

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

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

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

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

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

View File

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

View File

@@ -1,71 +1,90 @@
{
"name": "webtorrent-app",
"name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.1.1",
"version": "0.7.2",
"author": {
"name": "Feross Aboukhadijeh",
"name": "WebTorrent, LLC",
"email": "feross@feross.org",
"url": "http://feross.org"
"url": "https://webtorrent.io"
},
"bin": {
"webtorrent-desktop": "./bin/cmd.js"
},
"bugs": {
"url": "https://github.com/feross/webtorrent-app/issues"
"url": "https://github.com/feross/webtorrent-desktop/issues"
},
"dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js",
"application-config": "^0.2.0",
"application-config-path": "^0.1.0",
"airplayer": "^2.0.0",
"application-config": "^0.2.1",
"bitfield": "^1.0.2",
"chromecasts": "^1.8.0",
"create-torrent": "^3.22.1",
"debug": "^2.2.0",
"create-torrent": "^3.24.5",
"deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0",
"drag-drop": "^2.11.0",
"electron-localshortcut": "^0.6.0",
"electron-prebuilt": "0.37.2",
"electron-prebuilt": "1.2.1",
"fs-extra": "^0.27.0",
"hyperx": "^2.0.2",
"iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1",
"main-loop": "^3.2.0",
"mkdirp": "^0.5.1",
"musicmetadata": "^2.0.2",
"network-address": "^1.1.0",
"path-exists": "^2.1.0",
"parse-torrent": "^5.7.3",
"prettier-bytes": "^1.0.1",
"upload-element": "^1.0.1",
"run-parallel": "^1.1.6",
"semver": "^5.1.0",
"simple-concat": "^1.0.0",
"simple-get": "^2.0.0",
"srt-to-vtt": "^1.1.1",
"virtual-dom": "^2.1.1",
"webtorrent": "^0.87.1",
"winreg": "^1.0.1"
"vlc-command": "^1.0.1",
"webtorrent": "0.x",
"winreg": "^1.2.0",
"zero-fill": "^2.2.3"
},
"devDependencies": {
"cross-zip": "^2.0.1",
"electron-osx-sign": "^0.3.0",
"electron-packager": "^5.0.0",
"electron-winstaller": "^2.0.5",
"electron-packager": "^7.0.0",
"electron-winstaller": "^2.3.0",
"gh-release": "^2.0.3",
"minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"nobin-debian-installer": "^0.0.10",
"open": "0.0.5",
"plist": "^1.2.0",
"rimraf": "^2.5.2",
"standard": "^6.0.5"
},
"optionalDependencies": {
"appdmg": "^0.3.6"
"run-series": "^1.1.4",
"standard": "^7.0.0"
},
"homepage": "https://webtorrent.io",
"keywords": [
"desktop",
"electron",
"electron-app"
"electron-app",
"hybrid webtorrent client",
"mad science",
"torrent client",
"torrent",
"webtorrent"
],
"license": "MIT",
"main": "index.js",
"optionalDependencies": {
"appdmg": "^0.4.3"
},
"productName": "WebTorrent",
"repository": {
"type": "git",
"url": "git://github.com/feross/webtorrent-app.git"
"url": "git://github.com/feross/webtorrent-desktop.git"
},
"scripts": {
"clean": "node ./bin/clean.js",
"debug": "DEBUG=* electron .",
"package": "npm install && npm prune && npm dedupe && node ./bin/package.js",
"size": "npm run package -- darwin && du -ch dist/WebTorrent-darwin-x64 | grep total",
"open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js",
"start": "electron .",
"test": "standard",
"test": "standard && node ./bin/check-deps.js",
"update-authors": "./bin/update-authors.sh"
},
"bin": {
"webtorrent-app": "./bin/cmd.js"
}
}

View File

@@ -29,7 +29,10 @@
<body>
<img src="../static/WebTorrent.png">
<h1>WebTorrent</h1>
<p>Version <script>document.write(require('../package.json').version)</script></p>
<p>
Version <script>document.write(require('../package.json').version)</script>
(<script>document.write(require('webtorrent/package.json').version)</script>)
</p>
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
</body>
</html>

View File

@@ -1,799 +0,0 @@
/*
* BASIC STYLES
*/
*,
*:after,
*:before {
box-sizing: border-box;
}
html,
body {
background: rgb(40, 40, 40);
cursor: default;
height: 100%;
margin: 0;
padding: 0;
width: 100%;
overflow: hidden;
}
body {
color: #FFF;
font-family: BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
font-size: 14px;
line-height: 1.5em;
}
table {
table-layout: fixed;
}
::-webkit-scrollbar {
width: 10px;
background-color: rgb(40, 40, 40);
}
::-webkit-scrollbar-corner {
background-color: rgb(40, 40, 40);
}
::-webkit-scrollbar-thumb {
border: 1px solid rgb(40, 40, 40);
border-radius: 10px;
background: linear-gradient(to right, rgb(90, 90, 90), rgb(80, 80, 80))
}
::-webkit-scrollbar-track {
background-color: rgb(40, 40, 40);
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
.app {
-webkit-user-select: none;
-webkit-app-region: drag;
height: 100%;
display: flex;
flex-flow: column;
animation: fadein 0.3s;
background: rgb(40, 40, 40);
}
.app:not(.is-focused) {
background: rgb(50, 50, 50);
}
/*
* MATERIAL ICONS
*/
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(../static/MaterialIcons-Regular.woff2) format('woff2');
}
.icon {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px; /* Preferred icon size */
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
/*
* UTILITY CLASSES
*/
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.disabled {
opacity: 0.3;
}
/*
* BUTTONS
*/
a,
i {
cursor: default;
-webkit-app-region: no-drag;
}
a:not(.disabled):hover,
i:not(.disabled):hover {
-webkit-filter: brightness(1.3);
}
.btn {
width: 40px;
height: 40px;
border-radius: 20px;
font-size: 22px;
transition: all 0.1s ease-out;
text-align: center;
}
/*
* HEADER
*/
.header {
background: rgb(40, 40, 40);
border-bottom: 1px solid rgb(20, 20, 20);
height: 37px; /* vertically center OS menu buttons (OS X) */
padding-top: 6px;
overflow: hidden;
flex: 0 1 auto;
opacity: 1;
position: fixed;
left: 0;
top: 0;
right: 0;
transition: opacity 0.15s ease-out;
font-size: 14px;
line-height: 1.5em;
z-index: 1;
}
.app:not(.is-focused) .header {
background: rgb(50, 50, 50);
}
.app.view-player .header {
opacity: 0.8;
}
.app.hide-video-controls.view-player .header {
opacity: 0;
cursor: none;
}
.app.hide-header .header {
display: none;
}
.header .title {
opacity: 0.6;
position: absolute;
margin-top: 1px;
padding: 0 150px 0 150px;
width: 100%;
text-align: center;
pointer-events: none;
}
.header .nav {
font-weight: bold;
margin-right: 9px;
}
.header .nav.left {
float: left;
}
.app.is-darwin:not(.is-fullscreen) .header .nav.left {
margin-left: 78px;
}
.header .nav.right {
float: right;
}
.header .nav * {
opacity: 0.6;
}
.header .nav .disabled {
opacity: 0.1;
}
.header .nav *:not(.disabled):hover {
opacity: 1;
}
.header .nav .back,
.header .nav .forward {
font-size: 30px;
margin-top: -3px;
}
/*
* CONTENT
*/
.content {
position: relative;
width: 100%;
overflow-x: hidden;
overflow-y: overlay;
flex: 1 1 auto;
margin-top: 37px;
}
.app.view-player .content {
margin-top: 0;
}
/*
* MODAL POPOVERS
*/
.modal .modal-background {
content: ' ';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
opacity: 0.5;
}
.modal .modal-content {
position: fixed;
top: 45px;
left: 0;
right: 0;
margin: 0 auto;
width: calc(100% - 20px);
max-width: 600px;
box-shadow: 2px 2px 10px 0px rgba(0,0,0,0.4);
background-color: white;
color: #222;
padding: 20px;
}
.open-torrent-address-modal input {
width: calc(100% - 100px)
}
/*
* BUTTONS
*/
button {
background: transparent;
margin-left: 10px;
padding: 0;
border: none;
font-size: 14px;
font-weight: bold;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
cursor: pointer;
color: #aaa;
}
button.primary {
color: #0cf;
}
button:hover {
-webkit-filter: brightness(1.1);
}
button:active {
-webkit-filter: brightness(1.1);
text-shadow: none;
}
/*
* OTHER FORM ELEMENT DEFAULTS
*/
input {
background: transparent;
width: 300px;
padding: 6px;
border: 1px solid #bbb;
border-radius: 3px;
box-shadow: 1px 1px 1px 0px rgba(0,0,0,0.1);
}
/*
* 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%;
}
/*
* TORRENT LIST
*/
.torrent {
background: linear-gradient(to bottom right, #4B79A1, #283E51);
background-repeat: no-repeat;
background-size: cover;
background-position: 0 50%;
transition: -webkit-filter 0.1s ease-out;
position: relative;
animation: fadein .4s;
}
.torrent,
.torrent-placeholder {
height: 120px;
}
.torrent:not(:last-child) {
border-bottom: 1px solid rgb(20, 20, 20);
}
.torrent:hover {
-webkit-filter: brightness(1.1);
}
.torrent .metadata {
position: absolute;
top: 20px;
left: 20px;
width: calc(100% - 150px);
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
}
.torrent .metadata span:not(:last-child)::after {
content: ' — ';
}
.torrent .buttons {
position: absolute;
top: 25px;
right: 10px;
align-items: center;
display: none;
}
.torrent:hover .buttons {
display: flex;
}
.torrent .buttons > * {
margin-left: 6px; /* space buttons apart */
}
.torrent .buttons .download {
background-color: #2233BB;
width: 28px;
height: 28px;
border-radius: 14px;
font-size: 18px;
padding-top: 6px;
}
.torrent .buttons .download.downloading {
animation-name: greenpulse;
animation-duration: 0.8s;
animation-direction: alternate;
animation-iteration-count: infinite;
}
@keyframes greenpulse {
0% {
color: #ffffff;
padding-top: 4px;
}
100% {
color: #44dd44;
padding-top: 6px;
}
}
.torrent .buttons .download.seeding {
color: #44dd44;
}
.torrent .buttons .play {
padding-top: 10px;
background-color: #F44336;
}
.torrent.timeout .play {
padding-top: 8px;
}
.torrent.requested .play {
border-top: 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-left: 6px solid #ffffff;
border-radius: 50%;
color: transparent;
animation: load8 1.1s infinite linear;
}
@keyframes load8 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.torrent .buttons .delete {
opacity: 0.5;
}
.torrent .buttons .delete:hover {
opacity: 0.7;
}
.torrent .name {
font-size: 1.5em;
font-weight: bold;
line-height: 1.5em;
}
.torrent .status,
.torrent .status2 {
font-size: 1em;
line-height: 1.5em;
}
/*
* TORRENT LIST: DRAG-DROP TARGET
*/
.torrent-placeholder {
padding: 10px;
font-size: 1.1em;
}
.torrent-placeholder span {
border: 5px #444 dashed;
border-radius: 5px;
color: #666;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
body.drag .torrent-placeholder span {
border-color: #def;
color: #def;
}
/*
* TORRENT LIST: EXPANDED TORRENT DETAILS
*/
.torrent.selected {
height: auto;
}
.torrent-details {
padding: 8em 20px 20px 20px;
}
.torrent-details .open-folder {
float: right;
}
.torrent-details table {
width: 100%;
white-space: nowrap;
border: none;
border-spacing: 0;
}
.torrent-details tr {
height: 28px;
}
.torrent-details tr:hover,
.torrent-details .open-folder:hover {
background-color: rgba(200, 200, 200, 0.3);
}
.torrent-details td {
overflow: hidden;
padding: 0;
vertical-align: bottom;
}
.torrent-details td.col-icon {
width: 2em;
}
.torrent-details td.col-icon .icon {
font-size: 18px;
position: relative;
top: 3px;
}
.torrent-details td.col-name {
width: auto;
text-overflow: ellipsis;
}
.torrent-details td.col-progress {
width: 4em;
text-align: right;
}
.torrent-details td.col-size {
width: 4em;
text-align: right;
}
/*
* PLAYER CONTROLS
*/
.player-controls {
position: fixed;
background: rgba(40, 40, 40, 0.8);
width: 100%;
height: 38px;
bottom: 0;
transition: opacity 0.15s ease-out;
}
.app.hide-video-controls .player-controls {
opacity: 0;
}
.app.hide-video-controls .player {
cursor: none;
}
.app.hide-video-controls .player .player-controls:hover {
opacity: 1;
cursor: default;
}
/* invisible click target for scrubbing */
.player-controls .scrub-bar {
position: absolute;
width: 100%;
height: 23px; /* 3px .loading-bar plus 10px above and below */
top: -10px;
left: 0;
-webkit-app-region: no-drag;
}
.player-controls .loading-bar {
position: relative;
width: 100%;
top: -3px;
height: 3px;
background-color: rgba(0, 0, 0, 0.3);
transition: all 0.1s ease-out;
position: absolute;
}
.player-controls .loading-bar-part {
position: absolute;
background-color: #dd0000;
top: 0;
height: 100%;
}
.player-controls .playback-cursor {
position: absolute;
top: -3px;
background-color: #FFF;
width: 3px;
height: 3px;
border-radius: 50%;
margin-top: 0;
margin-left: 0;
transition-property: width, height, border-radius, margin-top, margin-left;
transition-duration: 0.1s;
transition-timing-function: ease-out;
}
.player-controls .play-pause {
display: block;
width: 30px;
height: 30px;
padding: 5px;
margin: 0 auto;
}
.player-controls .chromecast,
.player-controls .airplay,
.player-controls .fullscreen,
.player-controls .back {
display: block;
width: 20px;
height: 20px;
margin: 5px;
}
.player-controls .back {
float: left;
}
.player-controls .chromecast,
.player-controls .airplay,
.player-controls .fullscreen {
float: right;
}
.player-controls .fullscreen {
margin-right: 15px;
}
.player-controls .chromecast,
.player-controls .airplay {
font-size: 18px; /* make the cast icons less huge */
margin-top: 8px !important;
}
.player-controls .chromecast.active,
.player-controls .airplay.active {
color: #9af;
}
.player .playback-bar:hover .loading-bar {
height: 5px;
}
.player .playback-bar:hover .playback-cursor {
top: -8px;
width: 14px;
height: 14px;
}
/*
* CHROMECAST / AIRPLAY CONTROLS
*/
.cast-screen {
width: 100%;
height: 200px;
color: #eee;
text-align: center;
line-height: 2;
align-self: center;
}
.cast-screen .icon {
font-size: 50px;
}
.cast-screen .cast-type,
.cast-screen .cast-status {
font-size: 32px;
}
.cast-screen .cast-type {
font-weight: bold;
}
/*
* AUDIO DETAILS
*/
.audio-metadata {
width: 500px;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
align-self: center;
margin: 0 auto;
font-weight: bold;
font-size: 24px;
line-height: 2;
}
.audio-metadata .audio-title {
font-size: 32px;
}
.audio-metadata label {
display:inline-block;
width: 100px;
text-align: right;
font-weight: normal;
margin-right: 25px;
}
/*
* ERRORS
*/
.error-popover {
position: fixed;
top: 36px;
margin: 0;
width: 100%;
overflow: hidden;
z-index: 1;
}
.app.hide-header .error-popover {
top: 0px;
}
.error-popover.hidden {
display: none;
}
.error-popover .error,
.error-popover .title {
padding: 10px;
background-color: rgba(0, 0, 0, 0.8);
border-bottom: 1px solid #444;
}
.error-popover .title {
font-weight: bold;
color: #c44;
}
.error-popover .error {
color: #bbb;
}
.error-popover .error:last-child {
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;
}
}

View File

@@ -1,971 +0,0 @@
console.time('init')
var cfg = require('application-config')('WebTorrent')
var createTorrent = require('create-torrent')
var dragDrop = require('drag-drop')
var electron = require('electron')
var EventEmitter = require('events')
var fs = require('fs')
var mainLoop = require('main-loop')
var mkdirp = require('mkdirp')
var musicmetadata = require('musicmetadata')
var networkAddress = require('network-address')
var path = require('path')
var remote = require('remote')
var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff')
var patch = require('virtual-dom/patch')
var App = require('./views/app')
var errors = require('./lib/errors')
var config = require('../config')
var TorrentPlayer = require('./lib/torrent-player')
var torrentPoster = require('./lib/torrent-poster')
var util = require('./util')
var {setDispatch} = require('./lib/dispatcher')
setDispatch(dispatch)
// These two dependencies are the slowest-loading, so we lazy load them
// This cuts time from icon click to rendered window from ~550ms to ~150ms on my laptop
var WebTorrent = null
var Cast = null
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
var clipboard = electron.clipboard
var dialog = remote.require('dialog')
// For easy debugging in Developer Tools
var state = global.state = require('./state')
// Force use of webtorrent trackers on all torrents
global.WEBTORRENT_ANNOUNCE = createTorrent.announceList
.map((arr) => arr[0])
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
var vdomLoop
// All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app.
loadState(init)
/**
* Called once when the application loads. (Not once per window.)
* Connects to the torrent networks, sets up the UI and OS integrations like
* the dock icon and drag+drop.
*/
function init () {
state.location.go({ url: 'home' })
// Lazily load the WebTorrent, Chromecast, and Airplay modules
window.setTimeout(function () {
lazyLoadClient()
lazyLoadCast()
}, 750)
// The UI is built with virtual-dom, a minimalist library extracted from React
// The concepts--one way data flow, a pure function that renders state to a
// virtual DOM tree, and a diff that applies changes in the vdom to the real
// DOM, are all the same. Learn more: https://facebook.github.io/react/
vdomLoop = mainLoop(state, render, {
create: createElement,
diff: diff,
patch: patch
})
document.body.appendChild(vdomLoop.target)
// Save state on exit
window.addEventListener('beforeunload', saveState)
// OS integrations:
// ...drag and drop a torrent or video file to play or seed
dragDrop('body', (files) => dispatch('onOpen', files))
// ...same thing if you paste a torrent
document.addEventListener('paste', onPaste)
// ...keyboard shortcuts
document.addEventListener('keydown', function (e) {
if (e.which === 27) { /* ESC means either exit fullscreen or go back */
if (state.modal) {
dispatch('exitModal')
} else if (state.window.isFullScreen) {
dispatch('toggleFullScreen')
} else {
dispatch('back')
}
} else if (e.which === 32) { /* spacebar pauses or plays the video */
dispatch('playPause')
}
})
// ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
window.addEventListener('focus', function () {
state.window.isFocused = true
state.dock.badge = 0
update()
})
window.addEventListener('blur', function () {
state.window.isFocused = false
update()
})
// Listen for messages from the main process
setupIpc()
// Done! Ideally we want to get here <100ms after the user clicks the app
playInterfaceSound('STARTUP')
console.timeEnd('init')
}
// Lazily loads the WebTorrent module and creates the WebTorrent client
function lazyLoadClient () {
if (!WebTorrent) initWebtorrent()
return state.client
}
// Lazily loads Chromecast and Airplay support
function lazyLoadCast () {
if (!Cast) {
Cast = require('./lib/cast')
Cast.init(update) // Search the local network for Chromecast and Airplays
}
return Cast
}
// Load the WebTorrent module, connect to both the WebTorrent and BitTorrent
// networks, resume torrents, start monitoring torrent progress
function initWebtorrent () {
WebTorrent = require('webtorrent')
// Connect to the WebTorrent and BitTorrent networks
// WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq
state.client = new WebTorrent()
state.client.on('warning', onWarning)
state.client.on('error', function (err) {
// TODO: WebTorrent should have semantic errors
if (err.message.startsWith('There is already a swarm')) {
onError(new Error('Couldn\'t add duplicate torrent'))
} else {
onError(err)
}
})
// Restart everything we were torrenting last time the app ran
resumeTorrents()
// Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary
// a progress bar and to keep the cursor in sync when playing a video
setInterval(function () {
if (!updateTorrentProgress()) {
update() // If we didn't just update(), do so now, for the video cursor
}
}, 1000)
}
// This is the (mostly) pure function from state -> UI. Returns a virtual DOM
// tree. Any events, such as button clicks, will turn into calls to dispatch()
function render (state) {
return App(state)
}
// Calls render() to go from state -> UI, then applies to vdom to the real DOM.
function update () {
vdomLoop.update(state)
updateElectron()
}
function updateElectron () {
if (state.window.title !== state.prev.title) {
state.prev.title = state.window.title
ipcRenderer.send('setTitle', state.window.title)
}
if (state.dock.progress !== state.prev.progress) {
state.prev.progress = state.dock.progress
ipcRenderer.send('setProgress', state.dock.progress)
}
if (state.dock.badge !== state.prev.badge) {
state.prev.badge = state.dock.badge
ipcRenderer.send('setBadge', state.dock.badge || '')
}
}
// Events from the UI never modify state directly. Instead they call dispatch()
function dispatch (action, ...args) {
// Log dispatch calls, for debugging
if (action !== 'mediaMouseMoved') {
console.log('dispatch: %s %o', action, args)
}
if (action === 'onOpen') {
onOpen(args[0] /* files */)
}
if (action === 'addTorrent') {
addTorrent(args[0] /* torrent */)
}
if (action === 'showCreateTorrent') {
ipcRenderer.send('showCreateTorrent')
}
if (action === 'showOpenTorrentFile') {
ipcRenderer.send('showOpenTorrentFile')
}
if (action === 'seed') {
seed(args[0] /* files */)
}
if (action === 'openFile') {
openFile(args[0] /* infoHash */, args[1] /* index */)
}
if (action === 'openFolder') {
openFolder(args[0] /* infoHash */)
}
if (action === 'toggleTorrent') {
toggleTorrent(args[0] /* infoHash */)
}
if (action === 'deleteTorrent') {
deleteTorrent(args[0] /* infoHash */)
}
if (action === 'toggleSelectTorrent') {
toggleSelectTorrent(args[0] /* infoHash */)
}
if (action === 'openTorrentContextMenu') {
openTorrentContextMenu(args[0] /* infoHash */)
}
if (action === 'openChromecast') {
lazyLoadCast().openChromecast()
}
if (action === 'openAirplay') {
lazyLoadCast().openAirplay()
}
if (action === 'stopCasting') {
lazyLoadCast().stopCasting()
}
if (action === 'setDimensions') {
setDimensions(args[0] /* dimensions */)
}
if (action === 'back') {
state.location.back()
}
if (action === 'forward') {
state.location.forward()
}
if (action === 'playPause') {
playPause()
}
if (action === 'play') {
state.location.go({
url: 'player',
onbeforeload: function (cb) {
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
},
onbeforeunload: closePlayer
})
playPause(false)
}
if (action === 'pause') {
playPause(true)
}
if (action === 'playbackJump') {
jumpToTime(args[0] /* seconds */)
}
if (action === 'changeVolume') {
changeVolume(args[0] /* increase */)
}
if (action === 'mediaPlaying') {
state.playing.isPaused = false
ipcRenderer.send('blockPowerSave')
}
if (action === 'mediaPaused') {
state.playing.isPaused = true
ipcRenderer.send('unblockPowerSave')
}
if (action === 'toggleFullScreen') {
ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
}
if (action === 'mediaMouseMoved') {
state.playing.mouseStationarySince = new Date().getTime()
}
if (action === 'exitModal') {
state.modal = null
}
// Update the virtual-dom, unless it's just a mouse move event
if (action !== 'mediaMouseMoved') {
update()
}
}
// Plays or pauses the video. If isPaused is undefined, acts as a toggle
function playPause (isPaused) {
if (isPaused === state.playing.isPaused) {
return // Nothing to do
}
// Either isPaused is undefined, or it's the opposite of the current state. Toggle.
if (lazyLoadCast().isCasting()) {
Cast.playPause()
}
state.playing.isPaused = !state.playing.isPaused
update()
}
function jumpToTime (time) {
if (lazyLoadCast().isCasting()) {
Cast.seek(time)
} else {
state.playing.jumpToTime = time
update()
}
}
function changeVolume (delta) {
// change volume with delta value
setVolume(state.playing.volume + delta)
}
function setVolume (volume) {
// check if its in [0.0 - 1.0] range
volume = Math.max(0, Math.min(1, volume))
if (lazyLoadCast().isCasting()) {
Cast.setVolume(volume)
} else {
state.playing.setVolume = volume
update()
}
}
function setupIpc () {
ipcRenderer.send('ipcReady')
ipcRenderer.on('log', (e, ...args) => console.log(...args))
ipcRenderer.on('error', (e, ...args) => console.error(...args))
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
ipcRenderer.on('showOpenTorrentAddress', function (e) {
state.modal = 'open-torrent-address-modal'
update()
})
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
state.window.isFullScreen = isFullScreen
update()
})
ipcRenderer.on('addFakeDevice', function (e, device) {
var player = new EventEmitter()
player.play = (networkURL) => console.log(networkURL)
state.devices[device] = player
update()
})
}
// Load state.saved from the JSON state file
function loadState (cb) {
cfg.read(function (err, data) {
if (err) console.error(err)
console.log('loaded state from ' + cfg.filePath)
// populate defaults if they're not there
state.saved = Object.assign({}, state.defaultSavedState, data)
state.saved.torrents.forEach(function (torrentSummary) {
if (torrentSummary.displayName) torrentSummary.name = torrentSummary.displayName
})
if (cb) cb()
})
}
// Starts all torrents that aren't paused on program startup
function resumeTorrents () {
state.saved.torrents
.filter((x) => x.status !== 'paused')
.forEach((x) => startTorrentingSummary(x))
}
// Write state.saved to the JSON state file
function saveState () {
console.log('saving state to ' + cfg.filePath)
cfg.write(state.saved, function (err) {
if (err) console.error(err)
update()
})
}
function onOpen (files) {
if (!Array.isArray(files)) files = [ files ]
// .torrent file = start downloading the torrent
files.filter(isTorrent).forEach(function (torrentFile) {
addTorrent(torrentFile)
})
// everything else = seed these files
seed(files.filter(isNotTorrent))
}
function onPaste (e) {
if (e.target.tagName.toLowerCase() === 'input') return
var torrentIds = clipboard.readText().split('\n')
torrentIds.forEach(function (torrentId) {
torrentId = torrentId.trim()
if (torrentId.length === 0) return
dispatch('addTorrent', torrentId)
})
}
function isTorrent (file) {
var name = typeof file === 'string' ? file : file.name
var isTorrentFile = path.extname(name).toLowerCase() === '.torrent'
var isMagnet = typeof file === 'string' && /^magnet:/.test(file)
return isTorrentFile || isMagnet
}
function isNotTorrent (file) {
return !isTorrent(file)
}
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents
// Returns undefined if we don't know that infoHash
function getTorrentSummary (infoHash) {
return state.saved.torrents.find((x) => x.infoHash === infoHash)
}
// Get an active torrent from state.client.torrents
// Returns undefined if we are not currently torrenting that infoHash
function getTorrent (infoHash) {
var pending = state.pendingTorrents[infoHash]
if (pending) return pending
return lazyLoadClient().torrents.find((x) => x.infoHash === infoHash)
}
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
function addTorrent (torrentId) {
var torrent = startTorrentingID(torrentId)
torrent.on('infoHash', function () {
addTorrentToList(torrent)
})
}
function addTorrentToList (torrent) {
if (getTorrentSummary(torrent.infoHash)) {
return // Skip, torrent is already in state.saved
}
// If torrentId is a remote torrent (filesystem path, http url, etc.), wait for
// WebTorrent to finish reading it
if (torrent.infoHash) onInfoHash()
else torrent.on('infoHash', onInfoHash)
function onInfoHash () {
state.saved.torrents.push({
status: 'new',
name: torrent.name,
infoHash: torrent.infoHash,
magnetURI: torrent.magnetURI
})
saveState()
playInterfaceSound('ADD')
}
}
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
function startTorrentingSummary (torrentSummary) {
var s = torrentSummary
if (s.torrentPath) {
var torrentPath = util.getAbsoluteStaticPath(s.torrentPath)
var ret = startTorrentingID(torrentPath, s.path)
if (s.infoHash) state.pendingTorrents[s.infoHash] = ret
return ret
} else if (s.magnetURI) {
return startTorrentingID(s.magnetURI, s.path)
} else {
return startTorrentingID(s.infoHash, s.path)
}
}
// 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 startTorrentingID (torrentID, path) {
console.log('starting torrent ' + torrentID)
var torrent = lazyLoadClient().add(torrentID, {
path: path || state.saved.downloadPath // Use downloads folder
})
addTorrentEvents(torrent)
return torrent
}
// Stops downloading and/or seeding
function stopTorrenting (infoHash) {
var torrent = getTorrent(infoHash)
if (torrent) torrent.destroy()
}
// Creates a torrent for a local file and starts seeding it
function seed (files) {
if (files.length === 0) return
var torrent = lazyLoadClient().seed(files)
addTorrentToList(torrent)
addTorrentEvents(torrent)
}
function addTorrentEvents (torrent) {
torrent.on('infoHash', function () {
var infoHash = torrent.infoHash
if (state.pendingTorrents[infoHash]) delete state.pendingTorrents[infoHash]
update()
})
torrent.on('ready', torrentReady)
torrent.on('done', torrentDone)
function torrentReady () {
// Summarize torrent
var torrentSummary = getTorrentSummary(torrent.infoHash)
torrentSummary.status = 'downloading'
torrentSummary.ready = true
torrentSummary.name = torrentSummary.displayName || torrent.name
torrentSummary.path = torrent.path
// Summarize torrent files
torrentSummary.files = torrent.files.map(summarizeFileInTorrent)
updateTorrentProgress()
// Save the .torrent file, if it hasn't been saved already
if (!torrentSummary.torrentPath) saveTorrentFile(torrentSummary, torrent)
// Auto-generate a poster image, if it hasn't been generated already
if (!torrentSummary.posterURL) generateTorrentPoster(torrent, torrentSummary)
update()
}
function torrentDone () {
// Update the torrent summary
var torrentSummary = getTorrentSummary(torrent.infoHash)
torrentSummary.status = 'seeding'
updateTorrentProgress()
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
// Don't notify if we merely finished verifying data files that were already on disk.
if (torrent.received > 0) {
if (!state.window.isFocused) {
state.dock.badge += 1
}
showDoneNotification(torrent)
}
update()
}
}
function updateTorrentProgress () {
var changed = false
// First, track overall progress
var progress = lazyLoadClient().progress
var activeTorrentsExist = lazyLoadClient().torrents.some(function (torrent) {
return torrent.progress !== 1
})
// Hide progress bar when client has no torrents, or progress is 100%
if (!activeTorrentsExist || progress === 1) {
progress = -1
}
// Show progress bar under the WebTorrent taskbar icon, on OSX
if (state.dock.progress !== progress) changed = true
state.dock.progress = progress
// Track progress for every file in each torrentSummary
// TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield
lazyLoadClient().torrents.forEach(function (torrent) {
var torrentSummary = getTorrentSummary(torrent.infoHash)
if (!torrentSummary || !torrent.ready) return
torrent.files.forEach(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++
}
var fileSummary = torrentSummary.files[index]
if (fileSummary.numPiecesPresent !== numPiecesPresent || fileSummary.numPieces !== numPieces) {
fileSummary.numPieces = numPieces
fileSummary.numPiecesPresent = numPiecesPresent
changed = true
}
})
})
if (changed) update()
return changed
}
function generateTorrentPoster (torrent, torrentSummary) {
torrentPoster(torrent, function (err, buf, extension) {
if (err) return onWarning(err)
// save it for next time
mkdirp(config.CONFIG_POSTER_PATH, function (err) {
if (err) return onWarning(err)
var posterFilePath = path.join(config.CONFIG_POSTER_PATH, torrent.infoHash + extension)
fs.writeFile(posterFilePath, buf, function (err) {
if (err) return onWarning(err)
// show the poster
torrentSummary.posterURL = posterFilePath
update()
})
})
})
}
// Produces a JSON saveable summary of a file in a torrent
function summarizeFileInTorrent (file) {
return {
name: file.name,
length: file.length,
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 (torrentSummary, torrent) {
checkIfTorrentFileExists(torrentSummary.infoHash, function (torrentPath, exists) {
if (exists) {
// We've already saved the file
torrentSummary.torrentPath = torrentPath
saveState()
return
}
// 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)
torrentSummary.torrentPath = torrentPath
saveState()
})
})
})
}
// 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)
})
}
function startServer (torrentSummary, index, cb) {
if (state.server) return cb()
var torrent = getTorrent(torrentSummary.infoHash)
if (!torrent) torrent = startTorrentingSummary(torrentSummary)
if (torrent.ready) startServerFromReadyTorrent(torrent, index, cb)
else torrent.on('ready', () => startServerFromReadyTorrent(torrent, index, cb))
}
function startServerFromReadyTorrent (torrent, index, cb) {
// automatically choose which file in the torrent to play, if necessary
if (index === undefined) index = pickFileToPlay(torrent.files)
if (index === undefined) return cb(new errors.UnplayableError())
var file = torrent.files[index]
// update state
state.playing.infoHash = torrent.infoHash
state.playing.fileIndex = index
state.playing.type = TorrentPlayer.isVideo(file) ? 'video' : 'audio'
state.playing.audioInfo = null
// if it's audio, parse out the metadata (artist, title, etc)
musicmetadata(file.createReadStream(), function (err, info) {
if (err) return
console.log('got audio metadata for %s: %v', file.name, info)
state.playing.audioInfo = info
update()
})
// either way, start a streaming torrent-to-http server
var server = torrent.createServer()
server.listen(0, function () {
var port = server.address().port
var urlSuffix = ':' + port + '/' + index
state.server = {
server: server,
localURL: 'http://localhost' + urlSuffix,
networkURL: 'http://' + networkAddress() + urlSuffix
}
cb()
})
}
// Picks the default file to play from a list of torrent or torrentSummary files
// Returns an index or undefined, if no files are playable
function pickFileToPlay (files) {
// first, try to find the biggest video file
var videoFiles = files.filter(TorrentPlayer.isVideo)
if (videoFiles.length > 0) {
var largestVideoFile = videoFiles.reduce(function (a, b) {
return a.length > b.length ? a : b
})
return files.indexOf(largestVideoFile)
}
// if there are no videos, play the first audio file
var audioFiles = files.filter(TorrentPlayer.isAudio)
if (audioFiles.length > 0) {
return files.indexOf(audioFiles[0])
}
// no video or audio means nothing is playable
return undefined
}
function stopServer () {
if (!state.server) return
state.server.server.destroy()
state.server = null
state.playing.infoHash = null
state.playing.fileIndex = null
}
// Opens the video player
function openPlayer (infoHash, index, cb) {
var torrentSummary = getTorrentSummary(infoHash)
var torrent = lazyLoadClient().get(infoHash)
if (!torrent || !torrent.done) playInterfaceSound('PLAY')
torrentSummary.playStatus = 'requested'
update()
var timeout = setTimeout(function () {
torrentSummary.playStatus = 'timeout' /* no seeders available? */
playInterfaceSound('ERROR')
update()
}, 10000) /* give it a few seconds */
startServer(torrentSummary, index, function (err) {
clearTimeout(timeout)
if (err) {
torrentSummary.playStatus = 'unplayable'
playInterfaceSound('ERROR')
update()
return onError(err)
}
// if we timed out (user clicked play a long time ago), don't autoplay
var timedOut = torrentSummary.playStatus === 'timeout'
delete torrentSummary.playStatus
if (timedOut) return update()
// otherwise, play the video
state.window.title = torrentSummary.files[state.playing.fileIndex].name
update()
ipcRenderer.send('onPlayerOpen')
cb()
})
}
function closePlayer (cb) {
state.window.title = config.APP_WINDOW_TITLE
update()
if (state.window.isFullScreen) {
dispatch('toggleFullScreen', false)
}
restoreBounds()
stopServer()
update()
ipcRenderer.send('unblockPowerSave')
ipcRenderer.send('onPlayerClose')
cb()
}
function openFile (infoHash, index) {
var torrent = lazyLoadClient().get(infoHash)
if (!torrent) return
var filePath = path.join(torrent.path, torrent.files[index].path)
ipcRenderer.send('openItem', filePath)
}
function openFolder (infoHash) {
var torrent = lazyLoadClient().get(infoHash)
if (!torrent) return
var folderPath = path.join(torrent.path, torrent.name)
// Multi-file torrents create their own folder, single file torrents just
// drop the file directly into the Downloads folder
fs.stat(folderPath, function (err, stats) {
if (err || !stats.isDirectory()) {
folderPath = torrent.path
}
ipcRenderer.send('openItem', folderPath)
})
}
function toggleTorrent (infoHash) {
var torrentSummary = getTorrentSummary(infoHash)
if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new'
startTorrentingSummary(torrentSummary)
playInterfaceSound('ENABLE')
} else {
torrentSummary.status = 'paused'
stopTorrenting(torrentSummary.infoHash)
playInterfaceSound('DISABLE')
}
}
function deleteTorrent (infoHash) {
var torrent = getTorrent(infoHash)
if (torrent) torrent.destroy()
var index = state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
if (index > -1) state.saved.torrents.splice(index, 1)
saveState()
state.location.clearForward() // prevent user from going forward to a deleted torrent
playInterfaceSound('DELETE')
}
function toggleSelectTorrent (infoHash) {
// toggle selection
state.selectedInfoHash = state.selectedInfoHash === infoHash ? null : infoHash
update()
}
function openTorrentContextMenu (infoHash) {
var torrentSummary = getTorrentSummary(infoHash)
var menu = new remote.Menu()
menu.append(new remote.MenuItem({
label: 'Save Torrent File As...',
click: () => saveTorrentFileAs(torrentSummary)
}))
menu.append(new remote.MenuItem({
label: 'Copy Instant.io Link to Clipboard',
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
}))
menu.append(new remote.MenuItem({
label: 'Copy Magnet Link to Clipboard',
click: () => clipboard.writeText(torrentSummary.magnetURI)
}))
menu.popup(remote.getCurrentWindow())
}
function saveTorrentFileAs (torrentSummary) {
var newFileName = `${path.parse(torrentSummary.name).name}.torrent`
var opts = {
title: 'Save Torrent File',
defaultPath: path.join(state.saved.downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
]
}
dialog.showSaveDialog(remote.getCurrentWindow(), opts, (savePath) => {
var torrentPath = util.getAbsoluteStaticPath(torrentSummary.torrentPath)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return onError(err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return onError(err)
})
})
})
}
// Set window dimensions to match video dimensions or fill the screen
function setDimensions (dimensions) {
// Don't modify the window size if it's already maximized
if (remote.getCurrentWindow().isMaximized()) {
state.window.bounds = null
return
}
// Save the bounds of the window for later. See restoreBounds()
state.window.bounds = {
x: window.screenX,
y: window.screenY,
width: window.outerWidth,
height: window.outerHeight
}
state.window.wasMaximized = remote.getCurrentWindow().isMaximized
// Limit window size to screen size
var screenWidth = window.screen.width
var screenHeight = window.screen.height
var aspectRatio = dimensions.width / dimensions.height
var scaleFactor = Math.min(
Math.min(screenWidth / dimensions.width, 1),
Math.min(screenHeight / dimensions.height, 1)
)
var width = Math.floor(dimensions.width * scaleFactor)
var height = Math.floor(dimensions.height * scaleFactor)
// Center window on screen
var x = Math.floor((screenWidth - width) / 2)
var y = Math.floor((screenHeight - height) / 2)
ipcRenderer.send('setAspectRatio', aspectRatio)
ipcRenderer.send('setBounds', {x, y, width, height})
}
function restoreBounds () {
ipcRenderer.send('setAspectRatio', 0)
if (state.window.bounds) {
ipcRenderer.send('setBounds', state.window.bounds, false)
}
}
function onError (err) {
console.error(err.stack || err)
playInterfaceSound('ERROR')
state.errors.push({
time: new Date().getTime(),
message: err.message || err
})
update()
}
function onWarning (err) {
console.log('warning: %s', err.message)
}
function showDoneNotification (torrent) {
if (state.window.isFocused) return
var notif = new window.Notification('Download Complete', {
body: torrent.name,
silent: true
})
notif.onclick = function () {
window.focus()
}
playInterfaceSound('DONE')
}
function playInterfaceSound (name) {
var sound = config[`SOUND_${name}`]
if (!sound) throw new Error('Invalid sound name')
var audio = new window.Audio()
audio.volume = sound.volume
audio.src = sound.url
audio.play()
}

View File

@@ -1,126 +1,335 @@
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
// * Modifies state when things change
// * Starts and stops casting, provides remote video controls
module.exports = {
init,
openChromecast,
openAirplay,
stopCasting,
playPause,
open,
close,
play,
pause,
seek,
setVolume,
isCasting
setRate
}
var airplayer = require('airplayer')()
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
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
// Start polling Chromecast or Airplay, whenever we're connected
setInterval(() => pollCastStatus(state), 1000)
// Listen for devices: Chromecast and Airplay
// Listen for devices: Chromecast, DLNA and Airplay
chromecasts.on('update', function (player) {
state.devices.chromecast = player
addChromecastEvents()
state.devices.chromecast = chromecastPlayer(player)
})
var browser = airplay.createBrowser()
browser.on('deviceOn', function (player) {
state.devices.airplay = player
addAirplayEvents()
}).start()
}
function addChromecastEvents () {
state.devices.chromecast.on('error', function (err) {
state.devices.chromecast.errorMessage = err.message
update()
dlnacasts.on('update', function (player) {
state.devices.dlna = dlnaPlayer(player)
})
state.devices.chromecast.on('disconnect', function () {
state.playing.location = 'local'
update()
airplayer.on('update', function (player) {
state.devices.airplay = airplayPlayer(player)
})
}
function addAirplayEvents () {}
// chromecast player implementation
function chromecastPlayer (player) {
function addEvents () {
player.on('error', function (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to Chromecast. ' + err.message
})
update()
})
player.on('disconnect', function () {
state.playing.location = 'local'
update()
})
}
// Update our state from the remote TV
function pollCastStatus (state) {
if (state.playing.location === 'chromecast') {
state.devices.chromecast.status(function (err, status) {
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()
})
}
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.muted ? 0 : status.volume.level
update()
})
} else if (state.playing.location === 'airplay') {
state.devices.airplay.status(function (status) {
state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume
// According to docs is in [-30 - 0] (db) range
// should be converted to [0 - 1] using (val / 30 + 1)
}
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 addEvents () {
player.on('event', function (event) {
switch (event.state) {
case 'loading':
break
case 'playing':
state.playing.isPaused = false
break
case 'paused':
state.playing.isPaused = true
break
case 'stopped':
break
}
update()
})
}
}
function openChromecast () {
if (state.playing.location !== 'local') {
throw new Error('You can\'t connect to Chromecast when already connected to another device')
function open () {
player.play(state.server.networkURL, function (err, res) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to AirPlay. ' + err.message
})
} else {
state.playing.location = 'airplay'
}
update()
})
}
state.playing.location = 'chromecast-pending'
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
state.devices.chromecast.play(state.server.networkURL, {
type: 'video/mp4',
title: config.APP_NAME + ' — ' + torrentSummary.name
}, 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')
function play (callback) {
player.resume(callback)
}
state.playing.location = 'airplay-pending'
state.devices.airplay.play(state.server.networkURL, 0, function (res) {
if (res.statusCode !== 200) {
function pause (callback) {
player.pause(callback)
}
function stop (callback) {
player.stop(callback)
}
function status () {
player.playbackInfo(function (err, res, status) {
if (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Could not connect to AirPlay. ' + err.message
})
} else {
state.playing.isPaused = status.rate === 0
state.playing.currentTime = status.position
update()
}
})
}
function seek (time, callback) {
player.scrub(time, callback)
}
function volume (volume, callback) {
// AirPlay doesn't support volume
// TODO: We should just disable the volume slider
state.playing.volume = volume
}
addEvents()
return {
player: player,
open: open,
play: play,
pause: pause,
stop: stop,
status: status,
seek: seek,
volume: volume
}
}
// DLNA player implementation
function dlnaPlayer (player) {
function addEvents () {
player.on('error', function (err) {
state.playing.location = 'local'
state.errors.push({
time: new Date().getTime(),
message: 'Couldn\'t connect to Airplay'
message: 'Could not connect to DLNA. ' + err.message
})
} else {
state.playing.location = 'airplay'
update()
})
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()
}
// Stops Chromecast or Airplay, move video back to local screen
function stopCasting () {
if (state.playing.location === 'chromecast') {
state.devices.chromecast.stop(stoppedCasting)
} else if (state.playing.location === 'airplay') {
state.devices.airplay.stop(stoppedCasting)
} else if (state.playing.location.endsWith('-pending')) {
// Connecting to Chromecast took too long or errored out. Let the user cancel
// Stops casting, move video back to local screen
function close () {
var device = getDevice()
if (device) {
device.stop(stoppedCasting)
clearInterval(statusInterval)
} else {
stoppedCasting()
}
}
@@ -131,45 +340,64 @@ function stoppedCasting () {
update()
}
// Checks whether we are connected and already casting
// Returns false if we not casting (state.playing.location === 'local')
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
function isCasting () {
return state.playing.location === 'chromecast' || state.playing.location === 'airplay'
}
function playPause () {
var device
if (state.playing.location === 'chromecast') {
device = state.devices.chromecast
if (!state.playing.isPaused) device.pause(castCallback)
else device.play(null, null, castCallback)
function getDevice (location) {
if (location && state.devices[location]) {
return state.devices[location]
} else if (state.playing.location === 'chromecast') {
return state.devices.chromecast
} else if (state.playing.location === 'airplay') {
device = state.devices.airplay
if (!state.playing.isPaused) device.rate(0, castCallback)
else device.rate(1, castCallback)
return state.devices.airplay
} else if (state.playing.location === 'dlna') {
return state.devices.dlna
} else {
return null
}
}
function seek (time) {
function play () {
var device = getDevice()
if (device) {
device.play(castCallback)
}
}
function pause () {
var device = getDevice()
if (device) {
device.pause(castCallback)
}
}
function setRate (rate) {
var device
var result = true
if (state.playing.location === 'chromecast') {
state.devices.chromecast.seek(time, castCallback)
// TODO find how to control playback rate on chromecast
castCallback()
result = false
} else if (state.playing.location === 'airplay') {
state.devices.airplay.scrub(time, castCallback)
device = state.devices.airplay
device.rate(rate, castCallback)
} else {
result = false
}
return result
}
function seek (time) {
var device = getDevice()
if (device) {
device.seek(time, castCallback)
}
}
function setVolume (volume) {
if (state.playing.location === 'chromecast') {
state.devices.chromecast.volume(volume, castCallback)
} else if (state.playing.location === 'airplay') {
// 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)
var device = getDevice()
if (device) {
device.volume(volume, castCallback)
}
}
function castCallback () {
console.log(state.playing.location + ' callback: %o', arguments)
console.log('%s callback: %o', state.playing.location, arguments)
}

View File

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

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

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

View File

@@ -4,58 +4,123 @@ function LocationHistory () {
if (!new.target) return new LocationHistory()
this._history = []
this._forward = []
this._pending = false
}
LocationHistory.prototype.go = function (page) {
console.log('go', page)
this.clearForward()
this._go(page)
}
LocationHistory.prototype._go = function (page) {
if (page.onbeforeload) {
page.onbeforeload((err) => {
if (err) return
this._history.push(page)
})
} else {
this._history.push(page)
}
}
LocationHistory.prototype.back = function () {
if (this._history.length <= 1) return
var page = this._history.pop()
if (page.onbeforeunload) {
page.onbeforeunload(() => {
this._forward.push(page)
})
} else {
this._forward.push(page)
}
}
LocationHistory.prototype.forward = function () {
if (this._forward.length === 0) return
var page = this._forward.pop()
this._go(page)
}
LocationHistory.prototype.clearForward = function () {
this._forward = []
LocationHistory.prototype.url = function () {
return this.current() && this.current().url
}
LocationHistory.prototype.current = function () {
return this._history[this._history.length - 1]
}
LocationHistory.prototype.go = function (page, cb) {
if (!cb) cb = noop
if (this._pending) return cb(null)
console.log('go', page)
this.clearForward()
this._go(page, cb)
}
LocationHistory.prototype.back = function (cb) {
var self = this
if (!cb) cb = noop
if (self._history.length <= 1 || self._pending) return cb(null)
var page = self._history.pop()
self._unload(page, done)
function done (err) {
if (err) return cb(err)
self._forward.push(page)
self._load(self.current(), cb)
}
}
LocationHistory.prototype.hasBack = function () {
return this._history.length > 1
}
LocationHistory.prototype.forward = function (cb) {
if (!cb) cb = noop
if (this._forward.length === 0 || this._pending) return cb(null)
var page = this._forward.pop()
this._go(page, cb)
}
LocationHistory.prototype.hasForward = function () {
return this._forward.length > 0
}
LocationHistory.prototype.clearForward = function (url) {
if (url == null) {
this._forward = []
} else {
console.log(this._forward)
console.log(url)
this._forward = this._forward.filter(function (page) {
return page.url !== url
})
}
}
LocationHistory.prototype.backToFirst = function (cb) {
var self = this
if (!cb) cb = noop
if (self._history.length <= 1) return cb(null)
self.back(function (err) {
if (err) return cb(err)
self.backToFirst(cb)
})
}
LocationHistory.prototype._go = function (page, cb) {
var self = this
if (!cb) cb = noop
self._unload(self.current(), done1)
function done1 (err) {
if (err) return cb(err)
self._load(page, done2)
}
function done2 (err) {
if (err) return cb(err)
self._history.push(page)
cb(null)
}
}
LocationHistory.prototype._load = function (page, cb) {
var self = this
self._pending = true
if (page && page.onbeforeload) page.onbeforeload(done)
else done(null)
function done (err) {
self._pending = false
cb(err)
}
}
LocationHistory.prototype._unload = function (page, cb) {
var self = this
self._pending = true
if (page && page.onbeforeunload) page.onbeforeunload(done)
else done(null)
function done (err) {
self._pending = false
cb(err)
}
}
function noop () {}

View File

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

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

@@ -0,0 +1,73 @@
module.exports = {
preload,
play
}
var config = require('../../config')
var path = require('path')
var VOLUME = 0.15
/* Cache of Audio elements, for instant playback */
var cache = {}
var sounds = {
ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
volume: VOLUME
},
DELETE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
volume: VOLUME
},
DISABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
volume: VOLUME
},
DONE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
volume: VOLUME
},
ENABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
volume: VOLUME
},
ERROR: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
volume: VOLUME
},
PLAY: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
volume: VOLUME
},
STARTUP: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
volume: VOLUME * 2
}
}
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()
}

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

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

View File

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

View File

@@ -4,12 +4,18 @@ var captureVideoFrame = require('./capture-video-frame')
var path = require('path')
function torrentPoster (torrent, cb) {
// First, try to use the largest video file
// First, try to use a poster image if available
var posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
// Second, try to use the largest video file
// Filter out file formats that the <video> tag definitely can't play
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
// Second, try to use the largest image file
// Third, try to use the largest image file
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
@@ -20,7 +26,7 @@ function torrentPoster (torrent, cb) {
function getLargestFileByExtension (torrent, extensions) {
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
})
if (files.length === 0) return undefined
@@ -64,6 +70,8 @@ function torrentPosterFromVideo (file, torrent, cb) {
server.destroy()
if (buf.length === 0) return cb(new Error('Generated poster contains no data'))
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.TORRENT_PATH, torrentSummary.torrentFileName)
}
// Expects a torrentSummary
// Returns an absolute path to the poster image, or null if unavailable
function getPosterPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.posterFileName) return null
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
// Backslashes in URLS in CSS cause bizarre string encoding issues
return posterPath.replace(/\\/g, '/')
}

1239
renderer/main.css Normal file

File diff suppressed because it is too large Load Diff

View File

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

1298
renderer/main.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,116 +0,0 @@
var os = require('os')
var path = require('path')
var config = require('../config')
var LocationHistory = require('./lib/location-history')
module.exports = {
/*
* Temporary state disappears once the program exits.
* It can contain complex objects like open connections, etc.
*/
client: null, /* the WebTorrent client */
server: null, /* local WebTorrent-to-HTTP server */
prev: {}, /* used for state diffing in updateElectron() */
location: new LocationHistory(),
window: {
bounds: null, /* {x, y, width, height } */
isFocused: true,
isFullScreen: false,
title: config.APP_WINDOW_TITLE
},
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
playing: { /* the media (audio or video) that we're currently playing */
infoHash: null, /* the info hash of the torrent we're playing */
fileIndex: null, /* the zero-based index within the torrent */
location: 'local', /* 'local', 'chromecast', 'airplay' */
type: null, /* 'audio' or 'video' */
currentTime: 0, /* seconds */
duration: 1, /* seconds */
isPaused: true,
mouseStationarySince: 0 /* Unix time in ms */
},
audioInfo: null, /* set whenever an audio file is playing */
pendingTorrents: {}, /* infohash to WebTorrent handle */
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
},
errors: [], /* user-facing errors */
/*
* 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.
*
* 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 */
torrents: [
{
status: 'paused',
infoHash: '88594aaacbde40ef3e2510c47374ec0aa396c08e',
magnetURI: 'magnet:?xt=urn:btih:88594aaacbde40ef3e2510c47374ec0aa396c08e&dn=bbb_sunflower_1080p_30fps_normal.mp4&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fdistribution.bbb3d.renderfarming.net%2Fvideo%2Fmp4%2Fbbb_sunflower_1080p_30fps_normal.mp4',
displayName: 'Big Buck Bunny',
posterURL: 'bigBuckBunny.jpg',
torrentPath: 'bigBuckBunny.torrent',
files: [
{
'name': 'bbb_sunflower_1080p_30fps_normal.mp4',
'length': 276134947,
'numPiecesPresent': 0,
'numPieces': 527
}
]
},
{
status: 'paused',
infoHash: '6a9759bffd5c0af65319979fb7832189f4f3c35d',
magnetURI: 'magnet:?xt=urn:btih:6a9759bffd5c0af65319979fb7832189f4f3c35d&dn=sintel.mp4&tr=udp%3A%2F%2Fexodus.desync.com%3A6969&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=https%3A%2F%2Fwebtorrent.io%2Ftorrents%2Fsintel-1024-surround.mp4',
displayName: 'Sintel',
posterURL: 'sintel.jpg',
torrentPath: 'sintel.torrent',
files: [
{
'name': 'sintel.mp4',
'length': 129241752,
'numPiecesPresent': 0,
'numPieces': 987
}
]
},
{
status: 'paused',
infoHash: '02767050e0be2fd4db9a2ad6c12416ac806ed6ed',
magnetURI: 'magnet:?xt=urn:btih:02767050e0be2fd4db9a2ad6c12416ac806ed6ed&dn=tears_of_steel_1080p.webm&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io',
displayName: 'Tears of Steel',
posterURL: 'tearsOfSteel.jpg',
torrentPath: 'tearsOfSteel.torrent',
files: [
{
'name': 'tears_of_steel_1080p.webm',
'length': 571346576,
'numPiecesPresent': 0,
'numPieces': 2180
}
]
}
],
downloadPath: path.join(os.homedir(), '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

@@ -1,84 +1,82 @@
module.exports = App
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var hx = require('../lib/hx')
var Header = require('./header')
var Player = require('./player')
var TorrentList = require('./torrent-list')
var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal')
var Views = {
'home': require('./home'),
'player': require('./player'),
'create-torrent': require('./create-torrent'),
'preferences': require('./preferences')
}
function App (state, dispatch) {
var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal'),
'update-available-modal': require('./update-available-modal'),
'unsupported-media-modal': require('./unsupported-media-modal')
}
function App (state) {
// Hide player controls while playing video, if the mouse stays still for a while
// Never hide the controls when:
// * The mouse is over the controls or we're scrubbing (see CSS)
// * The video is paused
// * The video is playing remotely on Chromecast or Airplay
var hideControls = state.location.current().url === 'player' &&
var hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local'
// Hide the header on Windows/Linux when in the player
// On OSX, the header appears as part of the title bar
var hideHeader = process.platform !== 'darwin' && state.location.current().url === 'player'
state.playing.location === 'local' &&
state.playing.playbackRate === 1
var cls = [
'view-' + state.location.current().url, /* e.g. view-home, view-player */
'view-' + state.location.url(), /* e.g. view-home, view-player */
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
]
if (state.window.isFullScreen) cls.push('is-fullscreen')
if (state.window.isFocused) cls.push('is-focused')
if (hideControls) cls.push('hide-video-controls')
if (hideHeader) cls.push('hide-header')
return hx`
<div class='app ${cls.join(' ')}'>
${Header(state, dispatch)}
${getErrorPopover()}
<div class='content'>${getView()}</div>
${getModal()}
${Header(state)}
${getErrorPopover(state)}
<div class='content'>${getView(state)}</div>
${getModal(state)}
</div>
`
function getErrorPopover () {
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>`
})
return hx`
<div class='error-popover ${recentErrors.length > 0 ? 'visible' : 'hidden'}'>
<div class='title'>Error</div>
${errorElems}
</div>
`
}
function getModal () {
if (state.modal) {
var contents = Modals[state.modal](state, dispatch)
return hx`
<div class='modal'>
<div class='modal-background'></div>
<div class='modal-content add-file-modal'>
${contents}
</div>
</div>
`
}
}
function getView () {
if (state.location.current().url === 'home') {
return TorrentList(state, dispatch)
} else if (state.location.current().url === 'player') {
return Player(state, dispatch)
}
}
}
function getErrorPopover (state) {
var now = new Date().getTime()
var recentErrors = state.errors.filter((x) => now - x.time < 5000)
var hasErrors = recentErrors.length > 0
var errorElems = recentErrors.map(function (error) {
return hx`<div class='error'>${error.message}</div>`
})
return hx`
<div class='error-popover ${hasErrors ? 'visible' : 'hidden'}'>
<div class='title'>Error</div>
${errorElems}
</div>
`
}
function getModal (state) {
if (!state.modal) return
var contents = Modals[state.modal.id](state)
return hx`
<div class='modal'>
<div class='modal-background'></div>
<div class='modal-content'>
${contents}
</div>
</div>
`
}
function getView (state) {
var url = state.location.url()
return Views[url](state)
}

View File

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

View File

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

296
renderer/views/home.js Normal file
View File

@@ -0,0 +1,296 @@
module.exports = TorrentList
var prettyBytes = require('prettier-bytes')
var hx = require('../lib/hx')
var TorrentSummary = require('../lib/torrent-summary')
var TorrentPlayer = require('../lib/torrent-player')
var {dispatcher} = require('../lib/dispatcher')
function TorrentList (state) {
var torrentRows = state.saved.torrents.map(
(torrentSummary) => renderTorrent(torrentSummary)
)
return hx`
<div class='torrent-list'>
${torrentRows}
<div class='torrent-placeholder'>
<span class='ellipsis'>Drop a torrent file here or paste a magnet link</span>
</div>
</div>`
function renderTorrent (torrentSummary) {
var infoHash = torrentSummary.infoHash
var isSelected = infoHash && state.selectedInfoHash === infoHash
// Background image: show some nice visuals, like a frame from the movie, if possible
var style = {}
if (torrentSummary.posterFileName) {
var gradient = isSelected
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
style.backgroundImage = gradient + `, url('${posterPath}')`
}
// Foreground: name of the torrent, basic info like size, play button,
// cast buttons if available, and delete
var classes = ['torrent']
// playStatus turns the play button into a loading spinner or error icon
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled')
classes = classes.join(' ')
return hx`
<div style=${style} class=${classes}
oncontextmenu=${infoHash && dispatcher('openTorrentContextMenu', infoHash)}
onclick=${infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
${renderTorrentMetadata(torrentSummary)}
${infoHash ? renderTorrentButtons(torrentSummary) : ''}
${isSelected ? renderTorrentDetails(torrentSummary) : ''}
</div>
`
}
// Show name, download status, % complete
function renderTorrentMetadata (torrentSummary) {
var name = torrentSummary.name || 'Loading torrent...'
var elements = [hx`
<div class='name ellipsis'>${name}</div>
`]
// If it's downloading/seeding then show progress info
var prog = torrentSummary.progress
if (torrentSummary.status !== 'paused' && prog) {
elements.push(hx`
<div class='ellipsis'>
${renderPercentProgress()}
${renderTotalProgress()}
${renderPeers()}
${renderDownloadSpeed()}
${renderUploadSpeed()}
</div>
`)
}
return hx`<div class='metadata'>${elements}</div>`
function renderPercentProgress () {
var progress = Math.floor(100 * prog.progress)
return hx`<span>${progress}%</span>`
}
function renderTotalProgress () {
var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0)
if (downloaded === total) {
return hx`<span>${downloaded}</span>`
} else {
return hx`<span>${downloaded} / ${total}</span>`
}
}
function renderPeers () {
if (prog.numPeers === 0) return
var count = prog.numPeers === 1 ? 'peer' : 'peers'
return hx`<span>${prog.numPeers} ${count}</span>`
}
function renderDownloadSpeed () {
if (prog.downloadSpeed === 0) return
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>`
}
function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>`
}
}
// Download button toggles between torrenting (DL/seed) and paused
// Play button starts streaming the torrent immediately, unpausing if needed
function renderTorrentButtons (torrentSummary) {
var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else {
playIcon = 'play_arrow'
playTooltip = 'Start streaming'
}
var downloadIcon, downloadTooltip
if (torrentSummary.status === 'seeding') {
downloadIcon = 'file_upload'
downloadTooltip = 'Seeding. Click to stop.'
} else if (torrentSummary.status === 'downloading') {
downloadIcon = 'file_download'
downloadTooltip = 'Torrenting. Click to stop.'
} else {
downloadIcon = 'file_download'
downloadTooltip = 'Click to start torrenting.'
}
// Do we have a saved position? Show it using a radial progress bar on top
// of the play button, unless already showing a spinner there:
var positionElem
var willShowSpinner = torrentSummary.playStatus === 'requested'
var defaultFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
var fraction = defaultFile.currentTime / defaultFile.duration
positionElem = renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position'
}
// Only show the play button for torrents that contain playable media
var playButton
if (TorrentPlayer.isPlayableTorrent(torrentSummary)) {
playButton = hx`
<i.button-round.icon.play
title=${playTooltip}
class=${playClass}
onclick=${dispatcher('play', infoHash)}>
${playIcon}
</i>
`
}
return hx`
<div class='buttons'>
${positionElem}
${playButton}
<i.button-round.icon.download
class=${torrentSummary.status}
title=${downloadTooltip}
onclick=${dispatcher('toggleTorrent', infoHash)}>
${downloadIcon}
</i>
<i
class='icon delete'
title='Remove torrent'
onclick=${dispatcher('deleteTorrent', infoHash)}>
close
</i>
</div>
`
}
// Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrentSummary) {
var filesElement
if (!torrentSummary.files) {
// We don't know what files this torrent contains
var message = torrentSummary.status === 'paused'
? 'Failed to load torrent info. Click the download button to try again...'
: 'Downloading torrent info...'
filesElement = hx`<div class='files warning'>${message}</div>`
} else {
// We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files
.map((file, index) => ({ file, index }))
.sort(function (a, b) {
if (a.file.name < b.file.name) return -1
if (b.file.name < a.file.name) return 1
return 0
})
.map((object) => renderFileRow(torrentSummary, object.file, object.index))
filesElement = hx`
<div class='files'>
<table>
${fileRows}
</table>
</div>
`
}
return hx`
<div class='torrent-details'>
${filesElement}
</div>
`
}
// Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded
// Are we even torrenting it?
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
var isDone = false // Are we finished torrenting it?
var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files) {
var fileProg = torrentSummary.progress.files[index]
isDone = fileProg.numPiecesPresent === fileProg.numPieces
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
}
// Second, for media files where we saved our position, show how far we got
var positionElem
if (file.currentTime) {
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
positionElem = renderRadialProgressBar(file.currentTime / file.duration)
}
// Finally, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash
var icon
var handleClick
if (isPlayable) {
icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index)
} else {
icon = 'description' /* file icon, opens in OS default app */
handleClick = dispatcher('openItem', infoHash, index)
}
var rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return hx`
<tr onclick=${handleClick}>
<td class='col-icon ${rowClass}'>
${positionElem}
<i class='icon'>${icon}</i>
</td>
<td class='col-name ${rowClass}'>
${file.name}
</td>
<td class='col-progress ${rowClass}'>
${isSelected ? progress : ''}
</td>
<td class='col-size ${rowClass}'>
${prettyBytes(file.length)}
</td>
<td class='col-select'
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
</td>
</tr>
`
}
}
function renderRadialProgressBar (fraction, cssClass) {
var rotation = 360 * fraction
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
return hx`
<div class="radial-progress ${cssClass}">
<div class="circle">
<div class="mask full" style=${transformFill}>
<div class="fill" style=${transformFill}></div>
</div>
<div class="mask half">
<div class="fill" style=${transformFill}></div>
<div class="fill fix" style=${transformFix}></div>
</div>
</div>
<div class="inset"></div>
</div>
`
}

View File

@@ -1,20 +1,20 @@
module.exports = OpenTorrentAddressModal
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var {dispatch} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function OpenTorrentAddressModal (state) {
return hx`
<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>
<input id='add-torrent-url' type='text' autofocus onkeypress=${handleKeyPress} />
<button class='primary' onclick=${handleOK}>OK</button>
<button class='cancel' onclick=${handleCancel}>Cancel</button>
<input id='add-torrent-url' type='text' onkeypress=${handleKeyPress} />
</p>
<p class='float-right'>
<button class='button button-flat' onclick=${handleCancel}>CANCEL</button>
<button class='button button-raised' onclick=${handleOK}>OK</button>
</p>
<script>document.querySelector('#add-torrent-url').focus()</script>
</div>
`
}

View File

@@ -1,10 +1,11 @@
module.exports = Player
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var Bitfield = require('bitfield')
var prettyBytes = require('prettier-bytes')
var zeroFill = require('zero-fill')
var util = require('../util')
var hx = require('../lib/hx')
var TorrentSummary = require('../lib/torrent-summary')
var {dispatch, dispatcher} = require('../lib/dispatcher')
// Shows a streaming video player. Standard features + Chromecast + Airplay
@@ -15,20 +16,26 @@ function Player (state) {
return hx`
<div
class='player'
onwheel=${handleVolumeWheel}
onmousemove=${dispatcher('mediaMouseMoved')}>
${showVideo ? renderMedia(state) : renderCastScreen(state)}
${renderPlayerControls(state)}
</div>
</div>
`
}
// Handles volume change by wheel
function handleVolumeWheel (e) {
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
}
function renderMedia (state) {
if (!state.server) return
// Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary
var mediaType = state.playing.type /* 'audio' or 'video' */
var mediaElement = document.querySelector(mediaType) /* get the <video> or <audio> tag */
// Get the <video> or <audio> tag
var mediaElement = document.querySelector(state.playing.type)
if (mediaElement !== null) {
if (state.playing.isPaused && !mediaElement.paused) {
mediaElement.pause()
@@ -40,17 +47,51 @@ function renderMedia (state) {
mediaElement.currentTime = state.playing.jumpToTime
state.playing.jumpToTime = null
}
// set volume
if (state.playing.playbackRate !== mediaElement.playbackRate) {
mediaElement.playbackRate = state.playing.playbackRate
}
// Recover previous volume
if (state.previousVolume !== null && isFinite(state.previousVolume)) {
mediaElement.volume = state.previousVolume
state.previousVolume = null
}
// Set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
mediaElement.volume = state.playing.setVolume
state.playing.setVolume = null
}
state.playing.currentTime = mediaElement.currentTime
state.playing.duration = mediaElement.duration
// Switch to the newly added subtitle track, if available
var tracks = mediaElement.textTracks || []
for (var j = 0; j < tracks.length; j++) {
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
}
// Save video position
var file = state.getPlayingFileSummary()
file.currentTime = state.playing.currentTime = mediaElement.currentTime
file.duration = state.playing.duration = mediaElement.duration
state.playing.volume = mediaElement.volume
}
// Add subtitles to the <video> tag
var trackTags = []
if (state.playing.subtitles.selectedIndex >= 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i]
var isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push(hx`
<track
${isSelected ? 'default' : ''}
label=${track.label}
type='subtitles'
src=${track.buffer}>
`)
}
}
// Create the <audio> or <video> tag
var mediaTag = hx`
<div
@@ -58,33 +99,29 @@ function renderMedia (state) {
ondblclick=${dispatcher('toggleFullScreen')}
onloadedmetadata=${onLoadedMetadata}
onended=${onEnded}
onplay=${dispatcher('mediaPlaying')}
onpause=${dispatcher('mediaPaused')}
autoplay>
onstalling=${dispatcher('mediaStalled')}
onerror=${dispatcher('mediaError')}
ontimeupdate=${dispatcher('mediaTimeUpdate')}
onencrypted=${dispatcher('mediaEncrypted')}
oncanplay=${onCanPlay}>
${trackTags}
</div>
`
mediaTag.tagName = mediaType
mediaTag.tagName = state.playing.type // conditional tag name
// 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`
<div
class='letterbox'
style=${style}
onmousemove=${dispatcher('mediaMouseMoved')}>
${mediaTag}
${renderAudioMetadata(state)}
${renderOverlay(state)}
</div>
`
// As soon as the video loads enough to know the video dimensions, resize the window
// As soon as we know the video dimensions, resize the window
function onLoadedMetadata (e) {
if (mediaType !== 'video') return
if (state.playing.type !== 'video') return
var video = e.target
var dimensions = {
width: video.videoWidth,
@@ -97,17 +134,55 @@ function renderMedia (state) {
function onEnded (e) {
state.playing.isPaused = true
}
function onCanPlay (e) {
var elem = e.target
if (state.playing.type === 'video' &&
elem.webkitVideoDecodedByteCount === 0) {
dispatch('mediaError', 'Video codec unsupported')
} else if (elem.webkitAudioDecodedByteCount === 0) {
dispatch('mediaError', 'Audio codec unsupported')
} else {
elem.play()
}
}
}
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 {
// Video playing, so no spinner. No overlay needed
return
}
return hx`
<div class='media-overlay-background' style=${style}>
<div class='media-overlay'>${elems}</div>
</div>
`
}
function renderAudioMetadata (state) {
if (!state.playing.audioInfo) return
var info = state.playing.audioInfo
var fileSummary = state.getPlayingFileSummary()
if (!fileSummary.audioInfo) return
var info = fileSummary.audioInfo
// Get audio track info
var title = info.title
if (!title) {
var torrentSummary = getPlayingTorrentSummary(state)
title = torrentSummary.files[state.playing.fileIndex].name
title = fileSummary.name
}
var artist = info.artist && info.artist[0]
var album = info.album
@@ -119,135 +194,319 @@ function renderAudioMetadata (state) {
track = info.track.no + ' of ' + info.track.of
}
// Show a small info box in the middle of the screen
var elems = [hx`<div class='audio-title'><label></label>${title}</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 (track) elems.push(hx`<div class='audio-track'><label>Track</label>${track}</div>`)
// Show a small info box in the middle of the screen with title/album/etc
var elems = []
if (artist) {
elems.push(hx`
<div class='audio-artist'>
<label>Artist</label>${artist}
</div>
`)
}
if (album) {
elems.push(hx`
<div class='audio-album'>
<label>Album</label>${album}
</div>
`)
}
if (track) {
elems.push(hx`
<div class='audio-track'>
<label>Track</label>${track}
</div>
`)
}
// Align the title with the other info, if available. Otherwise, center 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>`
}
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 = state.getPlayingTorrentSummary().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) {
var isChromecast = state.playing.location.startsWith('chromecast')
var isAirplay = state.playing.location.startsWith('airplay')
var castIcon, castType, isCast
if (state.playing.location.startsWith('chromecast')) {
castIcon = 'cast_connected'
castType = 'Chromecast'
isCast = true
} else if (state.playing.location.startsWith('airplay')) {
castIcon = 'airplay'
castType = 'AirPlay'
isCast = true
} else if (state.playing.location.startsWith('dlna')) {
castIcon = 'tv'
castType = 'DLNA'
isCast = true
} else if (state.playing.location === 'vlc') {
castIcon = 'tv'
castType = 'VLC'
isCast = false
} else if (state.playing.location === 'error') {
castIcon = 'error_outline'
castType = 'Error'
isCast = false
}
var isStarting = state.playing.location.endsWith('-pending')
if (!isChromecast && !isAirplay) throw new Error('Unimplemented cast type')
var castStatus
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
else castStatus = ''
// Show a nice title image, if possible
var style = {
backgroundImage: cssBackgroundImagePoster(state)
}
// Show whether we're connected to Chromecast / Airplay
var castStatus = isStarting ? 'Connecting...' : 'Connected'
return hx`
<div class='letterbox' style=${style}>
<div class='cast-screen'>
<i class='icon'>${isAirplay ? 'airplay' : 'cast'}</i>
<div class='cast-type'>${isAirplay ? 'AirPlay' : 'Chromecast'}</div>
<i class='icon'>${castIcon}</i>
<div class='cast-type'>${castType}</div>
<div class='cast-status'>${castStatus}</div>
</div>
</div>
`
}
// Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) {
var torrentSummary = getPlayingTorrentSummary(state)
if (!torrentSummary || !torrentSummary.posterURL) return ''
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
var cleanURL = posterURL.replace(/\\/g, '/')
return 'radial-gradient(circle at center, ' +
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' +
`, url(${cleanURL})`
}
function renderSubtitlesOptions (state) {
var subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return
function getPlayingTorrentSummary (state) {
var infoHash = state.playing.infoHash
return state.saved.torrents.find((x) => x.infoHash === infoHash)
var items = subtitles.tracks.map(function (track, ix) {
var isSelected = state.playing.subtitles.selectedIndex === ix
return hx`
<li onclick=${dispatcher('selectSubtitle', ix)}>
<i.icon>${'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
${track.label}
</li>
`
})
var noneSelected = state.playing.subtitles.selectedIndex === -1
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
return hx`
<ul.subtitles-list>
${items}
<li onclick=${dispatcher('selectSubtitle', -1)}>
<i.icon>${noneClass}</i>
None
</li>
</ul>
`
}
function renderPlayerControls (state) {
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled'
: state.playing.subtitles.selectedIndex >= 0
? 'active'
: ''
var elements = [
hx`
<div class='playback-bar'>
${renderLoadingBar(state)}
<div class='playback-cursor' style=${playbackCursorStyle}></div>
<div class='scrub-bar'
<div
class='playback-cursor'
style=${playbackCursorStyle}>
</div>
<div
class='scrub-bar'
draggable='true'
ondragstart=${handleDragStart}
onclick=${handleScrub},
ondrag=${handleScrub}></div>
ondrag=${handleScrub}>
</div>
</div>
`,
hx`
<i class='icon fullscreen'
<i class='icon play-pause float-left' onclick=${dispatcher('playPause')}>
${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>
`,
hx`
<i
class='icon fullscreen float-right'
onclick=${dispatcher('toggleFullScreen')}>
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i>
`
]
if (state.playing.type === 'video') {
// show closed captions icon
elements.push(hx`
<i.icon.closed-caption.float-right
class=${captionsClass}
onclick=${handleSubtitles}>
closed_caption
</i>
`)
}
// If we've detected a Chromecast or AppleTV, the user can play video there
var isOnChromecast = state.playing.location.startsWith('chromecast')
var isOnAirplay = state.playing.location.startsWith('airplay')
var chromecastClass, chromecastHandler, airplayClass, airplayHandler
var isOnDlna = state.playing.location.startsWith('dlna')
var chromecastClass, chromecastHandler
var airplayClass, airplayHandler
var dlnaClass, dlnaHandler
if (isOnChromecast) {
chromecastClass = 'active'
dlnaClass = 'disabled'
airplayClass = 'disabled'
chromecastHandler = dispatcher('stopCasting')
chromecastHandler = dispatcher('closeDevice')
airplayHandler = undefined
dlnaHandler = undefined
} else if (isOnAirplay) {
chromecastClass = 'disabled'
dlnaClass = 'disabled'
airplayClass = 'active'
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 {
chromecastClass = ''
airplayClass = ''
chromecastHandler = dispatcher('openChromecast')
airplayHandler = dispatcher('openAirplay')
dlnaClass = ''
chromecastHandler = dispatcher('openDevice', 'chromecast')
airplayHandler = dispatcher('openDevice', 'airplay')
dlnaHandler = dispatcher('openDevice', 'dlna')
}
if (state.devices.chromecast || isOnChromecast) {
var castIcon = isOnChromecast ? 'cast_connected' : 'cast'
elements.push(hx`
<i.icon.chromecast
<i.icon.device.float-right
class=${chromecastClass}
onclick=${chromecastHandler}>
cast
${castIcon}
</i>
`)
}
if (state.devices.airplay || isOnAirplay) {
elements.push(hx`
<i.icon.airplay
<i.icon.device.float-right
class=${airplayClass}
onclick=${airplayHandler}>
airplay
</i>
`)
}
// On OSX, the back button is in the title bar of the window; see app.js
// On other platforms, we render one over the video on mouseover
if (process.platform !== 'darwin') {
if (state.devices.dlna || isOnDlna) {
elements.push(hx`
<i.icon.back
onclick=${dispatcher('back')}>
chevron_left
<i
class='icon device float-right'
class=${dlnaClass}
onclick=${dlnaHandler}>
tv
</i>
`)
}
// Finally, the big button in the center plays or pauses the video
// 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`
<i class='icon play-pause' onclick=${dispatcher('playPause')}>
${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i>
<div class='volume float-left'>
<i
class='icon volume-icon float-left'
onmousedown=${handleVolumeMute}>
${volumeIcon}
</i>
<input
class='volume-slider float-right'
type='range' min='0' max='1' step='0.05'
value=${volumeChanging !== false ? volumeChanging : volume}
onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub}
style=${volumeStyle}
/>
</div>
`)
return hx`<div class='player-controls'>${elements}</div>`
// Show video playback progress
var currentTimeStr = formatTime(state.playing.currentTime)
var durationStr = formatTime(state.playing.duration)
elements.push(hx`
<span class='time float-left'>
${currentTimeStr} / ${durationStr}
</span>
`)
// render playback rate
if (state.playing.playbackRate !== 1) {
elements.push(hx`
<span class='rate float-left'>
${state.playing.playbackRate}x
</span>
`)
}
return hx`
<div class='controls'>
${elements}
${renderSubtitlesOptions(state)}
</div>
`
function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) {
var dt = e.dataTransfer
dt.effectAllowed = 'none'
}
}
// Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) {
@@ -257,38 +516,78 @@ function renderPlayerControls (state) {
var position = fraction * state.playing.duration /* seconds */
dispatch('playbackJump', position)
}
// 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('toggleSubtitlesMenu')
}
}
}
// lets scrub without sending to volume backend
var volumeChanging = false
// Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be "spongey" / non-contiguous
function renderLoadingBar (state) {
var torrent = state.client.get(state.playing.infoHash)
if (torrent === null) {
var torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary.progress) {
return []
}
var file = torrent.files[state.playing.fileIndex]
// Find all contiguous parts of the torrent which are loaded
var prog = torrentSummary.progress
var fileProg = prog.files[state.playing.fileIndex]
var parts = []
var lastPartPresent = false
var numParts = file._endPiece - file._startPiece + 1
for (var i = file._startPiece; i <= file._endPiece; i++) {
var partPresent = torrent.bitfield.get(i)
if (partPresent && !lastPartPresent) {
parts.push({start: i - file._startPiece, count: 1})
var lastPiecePresent = false
for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
var partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
if (partPresent && !lastPiecePresent) {
parts.push({start: i - fileProg.startPiece, count: 1})
} else if (partPresent) {
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`
<div class='loading-bar'>
${parts.map(function (part) {
var style = {
left: (100 * part.start / numParts) + '%',
width: (100 * part.count / numParts) + '%'
left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / fileProg.numPieces) + '%'
}
return hx`<div class='loading-bar-part' style=${style}></div>`
@@ -296,3 +595,31 @@ function renderLoadingBar (state) {
</div>
`
}
// Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) {
var torrentSummary = state.getPlayingTorrentSummary()
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 formatTime (time) {
if (typeof time !== 'number' || Number.isNaN(time)) {
return '0:00'
}
var hours = Math.floor(time / 3600)
var minutes = Math.floor(time % 3600 / 60)
if (hours > 0) {
minutes = zeroFill(2, minutes)
}
var seconds = zeroFill(2, Math.floor(time % 60))
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
}

View File

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

View File

@@ -1,234 +0,0 @@
module.exports = TorrentList
var h = require('virtual-dom/h')
var hyperx = require('hyperx')
var hx = hyperx(h)
var prettyBytes = require('prettier-bytes')
var util = require('../util')
var TorrentPlayer = require('../lib/torrent-player')
var {dispatcher} = require('../lib/dispatcher')
function TorrentList (state) {
var torrentRows = state.saved.torrents.map(
(torrentSummary) => renderTorrent(torrentSummary))
return hx`
<div class='torrent-list'>
${torrentRows}
<div class='torrent-placeholder'>
<span class='ellipsis'>Drop a torrent file here or paste a magnet link</span>
</div>
</div>`
// Renders a torrent in the torrent list
// Includes name, download status, play button, background image
// May be expanded for additional info, including the list of files inside
function renderTorrent (torrentSummary) {
// Get ephemeral data (like progress %) directly from the WebTorrent handle
var infoHash = torrentSummary.infoHash
var torrent = state.client
? 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
var style = {}
if (torrentSummary.posterURL) {
var gradient = isSelected
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
// 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,
// cast buttons if available, and delete
var classes = ['torrent']
// playStatus turns the play button into a loading spinner or error icon
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected')
classes = classes.join(' ')
return hx`
<div style=${style} class=${classes}
oncontextmenu=${dispatcher('openTorrentContextMenu', infoHash)}
onclick=${dispatcher('toggleSelectTorrent', infoHash)}>
${renderTorrentMetadata(torrent, torrentSummary)}
${renderTorrentButtons(torrentSummary)}
${isSelected ? renderTorrentDetails(torrent, torrentSummary) : ''}
</div>
`
}
// Show name, download status, % complete
function renderTorrentMetadata (torrent, torrentSummary) {
var name = torrentSummary.name || 'Loading torrent...'
var elements = [hx`
<div class='name ellipsis'>${name}</div>
`]
// If a torrent is paused and we only get the torrentSummary
// If it's downloading/seeding then we have more information
if (torrent) {
var progress = Math.floor(100 * torrent.progress)
var downloaded = prettyBytes(torrent.downloaded)
var total = prettyBytes(torrent.length || 0)
if (downloaded !== total) downloaded += ` / ${total}`
elements.push(hx`
<div class='status ellipsis'>
${getFilesLength()}
<span>${getPeers()}</span>
<span>↓ ${prettyBytes(torrent.downloadSpeed || 0)}/s</span>
<span>↑ ${prettyBytes(torrent.uploadSpeed || 0)}/s</span>
</div>
`)
elements.push(hx`
<div class='status2 ellipsis'>
<span class='progress'>${progress}%</span>
<span>${downloaded}</span>
</div>
`)
}
return hx`<div class='metadata'>${elements}</div>`
function getPeers () {
var count = torrent.numPeers === 1 ? 'peer' : 'peers'
return `${torrent.numPeers} ${count}`
}
function getFilesLength () {
if (torrent.ready && torrent.files.length > 1) {
return hx`<span class='files'>${torrent.files.length} files</span>`
}
}
}
// Download button toggles between torrenting (DL/seed) and paused
// Play button starts streaming the torrent immediately, unpausing if needed
function renderTorrentButtons (torrentSummary) {
var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'unplayable') {
playIcon = 'play_arrow'
playClass = 'disabled'
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' +
'View details and click on individual files to open them in another program.'
} else if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else {
playIcon = 'play_arrow'
playTooltip = 'Start streaming'
}
var downloadIcon, downloadTooltip
if (torrentSummary.status === 'seeding') {
downloadIcon = 'file_upload'
downloadTooltip = 'Seeding. Click to stop.'
} else if (torrentSummary.status === 'downloading') {
downloadIcon = 'file_download'
downloadTooltip = 'Torrenting. Click to stop.'
} else {
downloadIcon = 'file_download'
downloadTooltip = 'Click to start torrenting.'
}
return hx`
<div class='buttons'>
<i.btn.icon.play
title=${playTooltip}
class=${playClass}
onclick=${dispatcher('play', infoHash)}>
${playIcon}
</i>
<i.btn.icon.download
class=${torrentSummary.status}
title=${downloadTooltip}
onclick=${dispatcher('toggleTorrent', infoHash)}>
${downloadIcon}
</i>
<i
class='icon delete'
title='Remove torrent'
onclick=${dispatcher('deleteTorrent', infoHash)}>
close
</i>
</div>
`
}
// Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrent, torrentSummary) {
var infoHash = torrentSummary.infoHash
var filesElement
if (!torrentSummary.files) {
// We don't know what files this torrent contains
var message = torrent
? 'Downloading torrent data using magnet link...'
: 'Failed to download torrent data from magnet link. Click the download button to try again...'
filesElement = hx`<div class='files warning'>${message}</div>`
} else {
// We do know the files. List them and show download stats for each one
var fileRows = torrentSummary.files.map(
(file, index) => renderFileRow(torrent, torrentSummary, file, index))
filesElement = hx`
<div class='files'>
<strong>Files</strong>
<span class='open-folder'
onclick=${dispatcher('openFolder', infoHash)}>
Open folder
</span>
<table>
${fileRows}
</table>
</div>
`
}
return hx`
<div class='torrent-details'>
${filesElement}
</div>
`
}
// Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrent, torrentSummary, file, index) {
// First, find out how much of the file we've downloaded
var isDone = file.numPiecesPresent === file.numPieces
var progress = Math.round(100 * file.numPiecesPresent / (file.numPieces || 0)) + '%'
// Second, render the file as a table row
var infoHash = torrentSummary.infoHash
var icon
var rowClass = ''
var handleClick
if (state.playing.infoHash === infoHash && state.playing.fileIndex === index) {
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 */
handleClick = dispatcher('play', infoHash, index)
} else {
icon = 'description' /* file icon, opens in OS default app */
rowClass = isDone ? '' : 'disabled'
handleClick = dispatcher('openFile', infoHash, index)
}
return hx`
<tr onclick=${handleClick} class='${rowClass}'>
<td class='col-icon'>
<i class='icon'>${icon}</i>
</td>
<td class='col-name'>${file.name}</td>
<td class='col-progress'>${progress}</td>
<td class='col-size'>${prettyBytes(file.length)}</td>
</tr>
`
}
}

View File

@@ -0,0 +1,39 @@
module.exports = UnsupportedMediaModal
var electron = require('electron')
var {dispatch, dispatcher} = require('../lib/dispatcher')
var hx = require('../lib/hx')
function UnsupportedMediaModal (state) {
var err = state.modal.error
var message = (err && err.getMessage)
? err.getMessage()
: err
var actionButton = state.modal.vlcInstalled
? hx`<button class="button-raised" onclick=${onPlay}>Play in VLC</button>`
: hx`<button class="button-raised" onclick=${onInstall}>Install VLC</button>`
var vlcMessage = state.modal.vlcNotFound
? 'Couldn\'t run VLC. Please make sure it\'s installed.'
: ''
return hx`
<div>
<p><strong>Sorry, we can't play that file.</strong></p>
<p>${message}</p>
<p class='float-right'>
<button class="button-flat" onclick=${dispatcher('backToList')}>Cancel</button>
${actionButton}
</p>
<p class='error-text'>${vlcMessage}</p>
</div>
`
function onInstall () {
electron.shell.openExternal('http://www.videolan.org/vlc/')
state.modal.vlcInstalled = true // Assume they'll install it successfully
}
function onPlay () {
dispatch('vlcPlay')
}
}

View File

@@ -0,0 +1,29 @@
module.exports = UpdateAvailableModal
var electron = require('electron')
var {dispatch} = require('../lib/dispatcher')
var hx = require('../lib/hx')
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>

359
renderer/webtorrent.js Normal file
View File

@@ -0,0 +1,359 @@
// 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 = new WebTorrent()
// WebTorrent-to-HTTP streaming sever
var server = null
// Used for diffing, so we only send progress updates when necessary
var 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, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
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.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections))
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, selections) {
console.log('starting torrent %s: %s', torrentKey, torrentID)
var torrent = client.add(torrentID, {
path: path,
fileModtimes: fileModtimes
})
torrent.key = torrentKey
// Listen for ready event, progress notifications, etc
addTorrentEvents(torrent)
// Only download the files the user wants, not necessarily all files
torrent.once('ready', () => selectFiles(torrent, selections))
return torrent
}
function stopTorrenting (infoHash) {
var torrent = client.get(infoHash)
if (torrent) 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
}
}
// 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.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.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.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.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.once('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)
})
}
function selectFiles (torrentOrInfoHash, selections) {
// Get the torrent object
var torrent
if (typeof torrentOrInfoHash === 'string') {
torrent = client.get(torrentOrInfoHash)
} else {
torrent = torrentOrInfoHash
}
// Selections not specified?
// Load all files. We still need to replace the default whole-torrent
// selection with individual selections for each file, so we can
// select/deselect files later on
if (!selections) {
selections = torrent.files.map((x) => true)
}
// Selections specified incorrectly?
if (selections.length !== torrent.files.length) {
throw new Error('got ' + selections.length + ' file selections, ' +
'but the torrent contains ' + torrent.files.length + ' files')
}
// Remove default selection (whole torrent)
torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files)
for (var i = 0; i < selections.length; i++) {
var file = torrent.files[i]
if (selections[i]) {
file.select()
} else {
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
file.deselect()
}
}
}
// Gets a WebTorrent handle by torrentKey
// 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/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
Encoding=UTF-8
Type=Application
Icon=webtorrent
Icon=webtorrent-desktop
Terminal=false
Path=$APP_PATH
Exec=$EXEC_PATH %U

Binary file not shown.

Before

Width:  |  Height:  |  Size: 726 KiB

BIN
static/wiredCd.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

BIN
static/wiredCd.torrent Normal file

Binary file not shown.