Compare commits

..

127 Commits

Author SHA1 Message Date
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
DC
ab55852bb0 v0.1.1 2016-03-28 00:39:15 -07:00
Feross Aboukhadijeh
46bc1bacdd Merge pull request #250 from rom1504/add_binary
add binary, fix #247
2016-03-27 22:05:47 -07:00
Romain Beaumont
391a2004f4 add binary, fix #247
cmd.js is mostly taken from node_modules/electron-prebuilt/cli.js (what the node_modules/.bin/electron symlink points to)
2016-03-28 02:26:13 +02:00
DC
2341749074 Track progress for currently playing file. Fixes #244 2016-03-27 16:17:35 -07:00
DC
ac7431292e Show filename in window title. Fix #245
Also fix error popover z index
2016-03-27 16:17:35 -07:00
Feross Aboukhadijeh
127b1577ac check for updates 5 seconds after startup 2016-03-27 03:30:29 -07:00
Feross Aboukhadijeh
7562a3856d Merge pull request #240 from feross/changelog
Update CHANGELOG
2016-03-27 03:27:14 -07:00
Feross Aboukhadijeh
bc9ef95790 Update CHANGELOG 2016-03-27 03:13:38 -07:00
DC
3617c17300 Memoize event handlers
Stop virtualdom from swapping out every event handler on every update
2016-03-27 02:58:26 -07:00
Feross Aboukhadijeh
8e344bed20 Merge pull request #239 from feross/absolute-path-urls
Resolve posterURL and torrentPath at runtime
2016-03-27 02:12:41 -07:00
Feross Aboukhadijeh
eb59c11f85 Resolve posterURL and torrentPath at runtime
Fixes bug where posters and torrent files can’t be found in the built
app.
2016-03-27 02:10:58 -07:00
Feross Aboukhadijeh
339f472473 Merge pull request #237 from feross/fix-233
Fixes for PR #233
2016-03-27 01:16:56 -07:00
Feross Aboukhadijeh
75412388e5 Save .torrent dialog: Add "All Files" option 2016-03-27 01:10:45 -07:00
Feross Aboukhadijeh
aad3acfe91 Right click -> "Save torrent file" without using streams
For #233
2016-03-27 01:06:58 -07:00
Feross Aboukhadijeh
b9c012a587 Make right click -> "copy magnet uri" work for default torrents
For #233
2016-03-27 01:06:36 -07:00
Feross Aboukhadijeh
d5bea54a83 sintel.torrent: Use webtorrent.io torrent 2016-03-27 00:58:50 -07:00
Feross Aboukhadijeh
c7ee0aab01 Merge pull request #233 from Flet/torrent-context-menu
add context menu with share/save actions
2016-03-27 00:39:15 -07:00
Feross Aboukhadijeh
9b8a9e5aa3 Merge pull request #236 from feross/fix-mac-flash
Fix OS X flash of white
2016-03-27 00:31:08 -07:00
Feross Aboukhadijeh
40cec3a2f6 Delay lazy load of client
This works great on my slow Macbook 12”, so I assume it will work
without lag on most other people’s computers.
2016-03-27 00:28:33 -07:00
Feross Aboukhadijeh
589880f1e3 OS X: Prevent white flash on window open
We got the window to run less JS but now it’s shown by the main process
too soon! This fixes that with a setTimeout.

Perhaps when this issue is fixed
(https://github.com/atom/electron/issues/861) we can remove the timeout.
2016-03-27 00:27:28 -07:00
Feross Aboukhadijeh
6465c23127 Merge pull request #235 from feross/about-window
Windows/Linux: Add About Window (#220)
2016-03-26 23:53:37 -07:00
Feross Aboukhadijeh
203d058280 About window: increase size slightly 2016-03-26 23:43:24 -07:00
Feross Aboukhadijeh
a116bf976a About window: only allow text selection 2016-03-26 23:43:24 -07:00
Feross Aboukhadijeh
aa117054fb About window: font-size tweaks 2016-03-26 23:43:24 -07:00
Feross Aboukhadijeh
5335bf39b5 Windows/Linux: Hide menu on About Window 2016-03-26 23:43:24 -07:00
Feross Aboukhadijeh
b263a69716 Windows/Linux: Add About Window (#220) 2016-03-26 23:43:24 -07:00
DC
906da4d977 Speed up init() by >= 2x
Lazy load the WebTorrent, Chromecast, and Airplay modules
2016-03-26 23:31:32 -07:00
Dan Flettre
6c07c4763d add context menu with share/save actions 2016-03-26 23:10:27 -05:00
Feross Aboukhadijeh
1e6e101c4e Merge pull request #232 from feross/ui-responsiveness
UI responds instantly to torrent enable/disable (#208)
2016-03-26 20:40:12 -07:00
Feross Aboukhadijeh
4a627b6f03 UI responds instantly to torrent enable/disable (#208) 2016-03-26 20:36:57 -07:00
Feross Aboukhadijeh
a2b9a178b7 Merge pull request #231 from feross/shortcut-fix
Keyboard shortcuts: volume shortcuts should be local
2016-03-26 20:07:30 -07:00
Feross Aboukhadijeh
9ef1d0a605 Keyboard shortcuts: volume shortcuts should be local
`globalShortcut` will register the shortcut at the OS level, even when
the app is not focused.

Using `localShortcut` would work, but let's put it in the top menu
instead, where all the other shortcuts are.
2016-03-26 20:04:29 -07:00
Feross Aboukhadijeh
0cf89600c0 es6ify 2016-03-26 19:58:04 -07:00
DC
3928564314 Add (BETA) to window title
Also fix a bug: fix relative paths to the default torrents.
2016-03-26 18:11:40 -07:00
DC
0d5ff2d964 Use relative paths for default torrents
This keeps them working if a user opens the app from DMG, then installs it to a different path and opens it again
2016-03-26 16:15:24 -07:00
Feross Aboukhadijeh
b85f0b9489 Merge pull request #202 from grunjol/feature-volume-management-clean
Add volume management
2016-03-25 23:46:59 -07:00
Feross Aboukhadijeh
5b6e4ac394 Merge pull request #226 from feross/fix-ubuntu-crash
Linux: Ensure ".local/share/{applications,icons}" exists; plus perf fix
2016-03-25 22:58:16 -07:00
Feross Aboukhadijeh
59a1bc03f2 Perf: Remove all *Sync methods for Linux startup 2016-03-25 21:47:49 -07:00
Feross Aboukhadijeh
01e27b2691 Linux: Ensure ".local/share/{applications,icons}" exists 2016-03-25 18:56:22 -07:00
Feross Aboukhadijeh
656e811e84 Merge pull request #203 from grunjol/feature-unity-desktop-shortcuts
Add unity launcher icons
2016-03-25 18:50:02 -07:00
Feross Aboukhadijeh
db60b99982 window useContentSize 2016-03-25 17:50:29 -07:00
Feross Aboukhadijeh
180d756dc0 OS X packager: Fix missing DMG background image
Remove previous DMG file. This somehow fixes the issue.
2016-03-25 17:50:29 -07:00
Feross Aboukhadijeh
a029ea3b0a Revert "Merge pull request #205 from feross/compress"
This reverts commit bd04d76adf, reversing
changes made to 73d5a4e1ab.
2016-03-25 17:50:29 -07:00
grunjol
4673354703 fixes #116 Add Unity launcher icons 2016-03-25 16:31:12 -03:00
Nate Goldman
ded599328a add version badge, update release info 2016-03-25 11:45:53 -07:00
Nate Goldman
7dfc6fd98c fix package arg 2016-03-25 11:43:13 -07:00
Feross Aboukhadijeh
e0ed255fb4 improve v0.1.0. changelog 2016-03-25 04:14:18 -07:00
Feross Aboukhadijeh
d8f97c3b58 put package.json name back to webtorrent-app
webtorrent-www relies on this name. I originally changed it because I
thought the windows install builder was using it, but I pass all the
options into that explicitly now, and even pass an option to prevent it
from using package.json, so this should be okay.
2016-03-25 04:07:31 -07:00
Feross Aboukhadijeh
753cca7dfb remove duplicate path-exists dep 2016-03-25 03:57:16 -07:00
Feross Aboukhadijeh
7d61968f64 0.1.0 2016-03-25 03:52:35 -07:00
Feross Aboukhadijeh
8637de27b9 Changelog: v0.1.0 2016-03-25 03:51:03 -07:00
Feross Aboukhadijeh
447413e4b9 delete commented out code 2016-03-25 03:40:18 -07:00
Feross Aboukhadijeh
82c8ad7562 windows build: don't use implicit package.json values 2016-03-25 03:37:18 -07:00
Feross Aboukhadijeh
f2bbd97eeb Merge pull request #218 from feross/windows-installer
Create Windows .exe installer
2016-03-25 03:35:43 -07:00
Feross Aboukhadijeh
c788b3358a Windows: fix magnet link handling 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
b0672cce9e npm install before packaging 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
0681169653 Windows: base Squirrel shortcut code on Nylas N1 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
ae6b86d233 Make install.gif not blink 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
8bba565609 Windows: create desktop/start menu shortcuts on install/update 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
78f08487c4 delay install splash screen so user sees it 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
bdf7110135 Move --squirrel-xxxx handling to new file 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
9d35ece954 Windows/linux: Don't autohide top menu bar (it's important) 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
00e4cc1864 Prevent --squirrel arguments from getting added as torrents 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
ad09012587 Windows installer: include icon url, setup icon, loading gif 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
8b5de572f1 package: conditionally require darwin packages 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
20c6b81047 simplify arguments to npm run package 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
aecead4a2d Windows: Create installer .exe file 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
7b02edca0f OS X packager: build to dist/ folder 2016-03-25 03:33:39 -07:00
Feross Aboukhadijeh
109094d0e1 Merge pull request #217 from feross/greenkeeper-webtorrent-0.87.1
Update webtorrent to version 0.87.1 🚀
2016-03-24 20:09:02 -07:00
greenkeeperio-bot
e0856a5274 chore(package): update webtorrent to version 0.87.1
http://greenkeeper.io/
2016-03-24 19:58:13 -07:00
Feross Aboukhadijeh
c8886fb606 Merge pull request #213 from feross/magnet-focus
Show, unminimize, and focus window after opening magnet link (fix #210)
2016-03-24 02:59:39 -07:00
Feross Aboukhadijeh
fd5f4dd139 Show, unminimize, and focus window after opening magnet link (fix #210)
Requires a workaround for this Electron issue:
https://github.com/atom/electron/issues/4338
2016-03-24 02:56:34 -07:00
Feross Aboukhadijeh
0a51da13a4 docs: improve windows build notes 2016-03-24 02:54:49 -07:00
Feross Aboukhadijeh
5540ed9ce1 fix: exception when adding magnet links 2016-03-24 02:54:49 -07:00
Feross Aboukhadijeh
b6516dc40f Merge pull request #212 from feross/header-fix
fix invisible header bug
2016-03-23 23:20:49 -07:00
Nate Goldman
8b57e13735 fix #211 - invisible header bug 2016-03-23 23:16:33 -07:00
Feross Aboukhadijeh
cb3dd716dd Merge pull request #209 from feross/subtler-app-sounds
Sounds: subtler sounds
2016-03-23 21:18:23 -07:00
Feross Aboukhadijeh
4895fb930c Sounds: subtler sounds
This change sets different sounds to different volume levels, and
replaces the Play sound with one that sounds different than the Add
sound.
2016-03-23 20:44:40 -07:00
Feross Aboukhadijeh
1f2985bbc3 Merge pull request #207 from feross/ignore-appdmg
Package: remove optionalDependency "appdmg" from final bundle
2016-03-23 20:28:55 -07:00
Feross Aboukhadijeh
32ad0f0926 Package: remove optionalDependency "appdmg" from final bundle 2016-03-23 20:27:38 -07:00
Feross Aboukhadijeh
3e448da0ba Merge pull request #206 from feross/osx-bundle-id
OS X: pick a better bundle ID
2016-03-23 19:35:16 -07:00
Feross Aboukhadijeh
219e717021 OS X: pick a better bundle ID
The old bundle ID ended in .app, which OS X will interpret as an
executable app. This meant that our preferences folder was treated like
an app, lol.
2016-03-23 19:32:54 -07:00
Feross Aboukhadijeh
1885b6a89e Merge pull request #205 from feross/compress
losslessly compress images (w/ ImageOptim)
2016-03-23 19:28:29 -07:00
Feross Aboukhadijeh
d8a5b8a701 losslessly compress images (w/ ImageOptim) 2016-03-23 19:26:56 -07:00
DC
d41e08b209 Fix magnet link progress bug 2016-03-23 06:43:13 -07:00
grunjol
fc6d8e7b7d add volume management 2016-03-23 09:01:07 -03:00
Feross Aboukhadijeh
9518670c7b Merge pull request #201 from feross/readme
update readme with link to releases
2016-03-22 20:43:29 -07:00
Nate Goldman
eff0b6eb23 Update README.md 2016-03-22 15:30:56 -07:00
DC
f56af6402c Audio metadata 2016-03-22 03:52:27 -07:00
DC
ebcc814ca7 WebTorrent can now play audio 2016-03-22 02:26:28 -07:00
Feross Aboukhadijeh
f7029c811c fix zip bundle
Before this change, the zip command would include the full path on my
machine in the zip file, i.e. /Users/feross/…
2016-03-22 00:02:16 -07:00
Feross Aboukhadijeh
a578d7555f CHANGELOG 2016-03-21 23:39:28 -07:00
Feross Aboukhadijeh
dff969f955 release: create fresh node_modules folder 2016-03-21 23:37:22 -07:00
Feross Aboukhadijeh
b964240a20 0.0.1 2016-03-21 23:34:48 -07:00
Feross Aboukhadijeh
7f7a395d67 AUTHORS.md: exclude duplicate author 2016-03-21 23:34:32 -07:00
Feross Aboukhadijeh
475ef8c6d0 add release scripts 2016-03-21 23:33:31 -07:00
Feross Aboukhadijeh
e269489639 fix changelog format 2016-03-21 23:24:00 -07:00
Feross Aboukhadijeh
20d5d5d60d Sign the app before producing the .zip file 2016-03-21 22:43:17 -07:00
Feross Aboukhadijeh
590dc99c51 Merge pull request #198 from feross/update
Add CHANGELOG.md; set update check delay to 10s
2016-03-21 22:01:58 -07:00
Feross Aboukhadijeh
10e9b36aea AUTO_UPDATE_CHECK_STARTUP_DELAY = 10 seconds 2016-03-21 21:45:27 -07:00
Feross Aboukhadijeh
5250f55bf7 add CHANGELOG.md 2016-03-21 21:44:49 -07:00
45 changed files with 1747 additions and 556 deletions

95
CHANGELOG.md Normal file
View File

@@ -0,0 +1,95 @@
# WebTorrent Desktop Version History
## 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%
- Reduce the number of DOM updates substantially
- Update UI immediately anytime state is changed, instead on 1 second interval
- Added right-click menu
- Save .torrent File
- Copy Instant.io Link to Clipboard
- Copy Magnet Link to Clipbaord
- Added keyboard shortcut for volume up (⌘/Ctrl + ↑) and volume down (⌘/Ctrl + ↓)
- Add desktop launcher shortcuts, like OS X has, for KDE and GNOME (Linux)
- Add "About" window (Windows, Linux)
- Better default window size that fits all the default torrents
- 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
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.
- Installs desktop and start menu shortcuts.
- **Audio file support!**
- Supports playback of .mp3, .aac, .ogg, .wav
- Audio file metadata gets shown in the UI
- Top menu is no longer automatically hidden (Windows)
- When magnet links are opened from third-party apps, the WebTorrent window now gets focus.
- Subtler app sounds.
- Fix for an issue that caused some magnet links to fail to open.
**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, @ungoldman, and @feross for contributing to this release.
## v0.0.1 - 2016-03-21
- Wait 10 seconds (instead of 60 seconds) after app launch before checking for updates.
## v0.0.0 - 2016-03-21
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 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)!
### Features
- **Lightweight, fast torrent client**
- **Beautiful user experience**
- **Instantly stream video and audio** from torrents!
- WebTorrent fetches file pieces from the network **on-demand**, for instant playback.
- Even when the file is not fully downloaded, **seeking still works!** (Seeking just reprioritizes what pieces are fetched from the network.)
- Stream videos to **AirPlay** and **Chromecast**
- **Pure Javascript**, so it's very easy to contribute code!
- Based on the most popular and comprehensive torrent package in Node.js, [`webtorrent`](https://www.npmjs.com/package/webtorrent).
- Lots of **features**, without the bloat:
- Opens magnet links and .torrent files
- Drag-and-drop makes adding torrents easy!
- Seed files/folders by dragging them onto the app
- Discovers peers via tracker servers, DHT (Distributed Hash Table), and peer exchange
- Make the video window "float on top" for watching video while you work!
- Supports WebTorrent protocol  for connecting to WebRTC peers (i.e. web browsers)

View File

@@ -2,7 +2,7 @@
<br> <br>
<a href="https://webtorrent.io"><img src="https://webtorrent.io/img/WebTorrent.png" alt="WebTorrent" width="200"></a> <a href="https://webtorrent.io"><img src="https://webtorrent.io/img/WebTorrent.png" alt="WebTorrent" width="200"></a>
<br> <br>
WebTorrent.app WebTorrent Desktop
<br> <br>
<br> <br>
</h1> </h1>
@@ -18,11 +18,15 @@
<img src="https://img.shields.io/travis/feross/webtorrent-app/master.svg" <img src="https://img.shields.io/travis/feross/webtorrent-app/master.svg"
alt="Travis Build"> alt="Travis Build">
</a> </a>
<a href="https://github.com/feross/webtorrent-app/releases">
<img src="https://img.shields.io/github/release/feross/webtorrent-app.svg"
alt="Latest Release Version">
</a>
</p> </p>
## Install ## Install
**WebTorrent.app** is still under very active development. Expect a release very soon! **WebTorrent Desktop** is still under very active development. You can download the latest version from the [releases](https://github.com/feross/webtorrent-app/releases) page.
## Screenshot ## Screenshot
@@ -58,10 +62,20 @@ To build for one platform:
$ npm run package -- [platform] $ npm run package -- [platform]
``` ```
Where `[platform]` is `--darwin`, `--linux`, or `--win32`. Where `[platform]` is `darwin`, `linux`, or `win32`.
To package a Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs #### Windows build notes
to be installed. On OS X, it is installable via [Homebrew](http://brew.sh/).
To package the Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs
to be installed.
On OS X, first install [XQuartz](http://www.xquartz.org/), then run:
```
brew install wine
```
(Requires the [Homebrew](http://brew.sh/) package manager.)
### Code Style ### Code Style

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* Remove all traces of WebTorrent.app from the system (config and temp files). * Remove all traces of WebTorrent Desktop from the system (config and temp files).
* Useful for developers. * Useful for developers.
*/ */

10
bin/cmd.js Executable file
View File

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

View File

@@ -10,16 +10,17 @@ var electronPackager = require('electron-packager')
var fs = require('fs') var fs = require('fs')
var path = require('path') var path = require('path')
var pkg = require('../package.json') var pkg = require('../package.json')
var rimraf = require('rimraf')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
function build () { function build () {
var platform = process.argv[2] var platform = process.argv[2]
if (platform === '--darwin') { if (platform === 'darwin') {
buildDarwin(printDone) buildDarwin(printDone)
} else if (platform === '--win32') { } else if (platform === 'win32') {
buildWin32(printDone) buildWin32(printDone)
} else if (platform === '--linux') { } else if (platform === 'linux') {
buildLinux(printDone) buildLinux(printDone)
} else { } else {
buildDarwin(function (err, buildPath) { buildDarwin(function (err, buildPath) {
@@ -36,8 +37,8 @@ var all = {
// Build 64 bit binaries only. // Build 64 bit binaries only.
arch: 'x64', arch: 'x64',
// The application source directory. // The human-readable copyright line for the app.
dir: config.ROOT_PATH, 'app-copyright': config.APP_COPYRIGHT,
// The release version of the application. Maps to the `ProductVersion` metadata // The release version of the application. Maps to the `ProductVersion` metadata
// property on Windows, and `CFBundleShortVersionString` on OS X. // property on Windows, and `CFBundleShortVersionString` on OS X.
@@ -57,9 +58,12 @@ var all = {
// Windows requires the build version to start with a number :/ so we stick on a prefix // Windows requires the build version to start with a number :/ so we stick on a prefix
'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''), 'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''),
// The application source directory.
dir: config.ROOT_PATH,
// Pattern which specifies which files to ignore when copying files to create the // Pattern which specifies which files to ignore when copying files to create the
// package(s). // package(s).
ignore: /^\/dist|\/(appveyor.yml|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|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
// The application name. // The application name.
name: config.APP_NAME, name: config.APP_NAME,
@@ -75,21 +79,21 @@ var all = {
prune: true, prune: true,
// The Electron version with which the app is built (without the leading 'v') // The Electron version with which the app is built (without the leading 'v')
version: pkg.devDependencies['electron-prebuilt'] version: pkg.dependencies['electron-prebuilt']
} }
var darwin = { var darwin = {
platform: 'darwin', platform: 'darwin',
// The bundle identifier to use in the application's plist (OS X only). // The bundle identifier to use in the application's plist (OS X only).
'app-bundle-id': 'io.webtorrent.app', 'app-bundle-id': 'io.webtorrent.webtorrent',
// The application category type, as shown in the Finder via "View" -> "Arrange by // The application category type, as shown in the Finder via "View" -> "Arrange by
// Application Category" when viewing the Applications directory (OS X only). // Application Category" when viewing the Applications directory (OS X only).
'app-category-type': 'public.app-category.utilities', 'app-category-type': 'public.app-category.utilities',
// The bundle identifier to use in the application helper's plist (OS X only). // The bundle identifier to use in the application helper's plist (OS X only).
'helper-bundle-id': 'io.webtorrent.app.helper', 'helper-bundle-id': 'io.webtorrent.webtorrent-helper',
// Application icon. // Application icon.
icon: config.APP_ICON + '.icns' icon: config.APP_ICON + '.icns'
@@ -104,9 +108,6 @@ var win32 = {
// Company that produced the file. // Company that produced the file.
CompanyName: config.APP_NAME, CompanyName: config.APP_NAME,
// Copyright notices that apply to the file.
LegalCopyright: config.APP_COPYRIGHT,
// Name of the program, displayed to users // Name of the program, displayed to users
FileDescription: config.APP_NAME, FileDescription: config.APP_NAME,
@@ -137,9 +138,7 @@ var linux = {
build() build()
function buildDarwin (cb) { function buildDarwin (cb) {
var appDmg = require('appdmg')
var plist = require('plist') var plist = require('plist')
var sign = require('electron-osx-sign')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
@@ -150,6 +149,8 @@ function buildDarwin (cb) {
var infoPlistPath = path.join(contentsPath, 'Info.plist') var infoPlistPath = path.join(contentsPath, 'Info.plist')
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8')) var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
// TODO: Use new `extend-info` and `extra-resource` opts to electron-packager,
// available as of v6.
infoPlist.CFBundleDocumentTypes = [ infoPlist.CFBundleDocumentTypes = [
{ {
CFBundleTypeExtensions: [ 'torrent' ], CFBundleTypeExtensions: [ 'torrent' ],
@@ -177,59 +178,76 @@ function buildDarwin (cb) {
} }
] ]
infoPlist.NSHumanReadableCopyright = config.APP_COPYRIGHT
fs.writeFileSync(infoPlistPath, plist.build(infoPlist)) fs.writeFileSync(infoPlistPath, plist.build(infoPlist))
// Copy torrent file icon into app bundle
cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`) cp.execSync(`cp ${config.APP_FILE_ICON + '.icns'} ${resourcesPath}`)
var zipPath = path.join(buildPath[0], BUILD_NAME + '.zip')
cp.execSync(`zip -r -y ${zipPath} ${appPath}`)
/*
* Signing OS X apps for distribution outside the App Store requires:
*
* - Xcode
* - Xcode Command Line Tools (xcode-select --install)
* - Membership in the Apple Developer Program
*/
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
var appDmg = require('appdmg')
var sign = require('electron-osx-sign')
/*
* Sign the app with Apple Developer ID certificate. 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
* Developer" when they open it for the first time (OS X Gatekeeper).
*
* To sign an OS X app for distribution outside the App Store, the following are
* required:
* - Xcode
* - Xcode Command Line Tools (xcode-select --install)
* - Membership in the Apple Developer Program
*/
var signOpts = { var signOpts = {
app: appPath, app: appPath,
platform: 'darwin', platform: 'darwin',
verbose: true verbose: true
} }
var dmgPath = path.join(buildPath[0], BUILD_NAME + '.dmg') // TODO: Use the built-in `sign` opt to electron-packager that takes an options
var dmgOpts = { // object as of v6.
basepath: config.ROOT_PATH,
target: dmgPath,
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' }
]
}
}
sign(signOpts, function (err) { sign(signOpts, function (err) {
if (err) return cb(err) if (err) return cb(err)
// Create .zip file (used by the auto-updater)
var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '.zip')
cp.execSync(`cd ${buildPath[0]} && zip -r -y ${zipPath} ${config.APP_NAME + '.app'}`)
console.log('Created OS X .zip file.')
var targetPath = path.join(config.ROOT_PATH, 'dist', 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) var dmg = appDmg(dmgOpts)
dmg.on('error', cb) dmg.on('error', cb)
dmg.on('progress', function (info) { dmg.on('progress', function (info) {
if (info.type === 'step-begin') console.log(info.title + '...') if (info.type === 'step-begin') console.log(info.title + '...')
}) })
dmg.on('finish', function (info) { dmg.on('finish', function (info) {
console.log('Created OS X disk image (.dmg) file.')
cb(null, buildPath) cb(null, buildPath)
}) })
}) })
@@ -238,11 +256,46 @@ function buildDarwin (cb) {
} }
function buildWin32 (cb) { function buildWin32 (cb) {
electronPackager(Object.assign({}, all, win32), cb) var installer = require('electron-winstaller')
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
if (err) return cb(err)
console.log('Creating Windows installer...')
installer.createWindowsInstaller({
name: config.APP_NAME,
productName: config.APP_NAME,
title: config.APP_NAME,
exe: config.APP_NAME + '.exe',
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)
})
} }
function buildLinux (cb) { function buildLinux (cb) {
electronPackager(Object.assign({}, all, linux), cb) electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
if (err) return cb(err)
// Create .zip file for Linux
var distPath = path.join(config.ROOT_PATH, 'dist')
var zipPath = path.join(config.ROOT_PATH, 'dist', BUILD_NAME + '-linux.zip')
var appFolderName = path.basename(buildPath[0])
cp.execSync(`cd ${distPath} && zip -r -y ${zipPath} ${appFolderName}`)
console.log('Created Linux .zip file.')
})
} }
function printDone (err, buildPath) { function printDone (err, buildPath) {

8
bin/release-_post.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
git diff --exit-code
npm run package
git push
git push --tags
gh-release

9
bin/release-_pre.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/sh
set -e
git pull
npm run update-authors
git diff --exit-code
rm -rf node_modules/
npm install
npm test

7
bin/release-major.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -e
BIN=`dirname $0`
$BIN/release-_pre.sh
npm version major
$BIN/release-_post.sh

7
bin/release-minor.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -e
BIN=`dirname $0`
$BIN/release-_pre.sh
npm version minor
$BIN/release-_post.sh

7
bin/release-patch.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -e
BIN=`dirname $0`
$BIN/release-_pre.sh
npm version patch
$BIN/release-_post.sh

View File

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

View File

@@ -2,37 +2,65 @@ var applicationConfigPath = require('application-config-path')
var path = require('path') var path = require('path')
var APP_NAME = 'WebTorrent' var APP_NAME = 'WebTorrent'
var APP_TEAM = 'The WebTorrent Project'
var APP_VERSION = require('./package.json').version var APP_VERSION = require('./package.json').version
module.exports = { module.exports = {
APP_COPYRIGHT: 'Copyright © 2014-2016 The WebTorrent Project', APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'), APP_FILE_ICON: path.join(__dirname, 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, 'static', 'WebTorrent'), APP_ICON: path.join(__dirname, 'static', 'WebTorrent'),
APP_NAME: APP_NAME, APP_NAME: APP_NAME,
APP_TEAM: APP_TEAM,
APP_VERSION: APP_VERSION, APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
AUTO_UPDATE_URL: 'https://webtorrent.io/app/update?version=' + APP_VERSION, AUTO_UPDATE_URL: 'https://webtorrent.io/app/update?version=' + APP_VERSION,
AUTO_UPDATE_CHECK_STARTUP_DELAY: 60 * 1000 /* 1 minute */, AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
CONFIG_PATH: applicationConfigPath(APP_NAME), CONFIG_PATH: applicationConfigPath(APP_NAME),
CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'), CONFIG_POSTER_PATH: path.join(applicationConfigPath(APP_NAME), 'Posters'),
CONFIG_TORRENT_PATH: path.join(applicationConfigPath(APP_NAME), 'Torrents'), CONFIG_TORRENT_PATH: path.join(applicationConfigPath(APP_NAME), 'Torrents'),
INDEX: 'file://' + path.join(__dirname, 'renderer', 'index.html'),
IS_PRODUCTION: isProduction(), IS_PRODUCTION: isProduction(),
ROOT_PATH: __dirname, ROOT_PATH: __dirname,
STATIC_PATH: path.join(__dirname, 'static'), STATIC_PATH: path.join(__dirname, 'static'),
SOUND_ADD: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'), SOUND_ADD: {
SOUND_DELETE: 'file://' + path.join(__dirname, 'static', 'sound', 'delete.wav'), url: 'file://' + path.join(__dirname, 'static', 'sound', 'add.wav'),
SOUND_DISABLE: 'file://' + path.join(__dirname, 'static', 'sound', 'disable.wav'), volume: 0.2
SOUND_DONE: 'file://' + path.join(__dirname, 'static', 'sound', 'done.wav'), },
SOUND_ENABLE: 'file://' + path.join(__dirname, 'static', 'sound', 'enable.wav'), SOUND_DELETE: {
SOUND_ERROR: 'file://' + path.join(__dirname, 'static', 'sound', 'error.wav'), url: 'file://' + path.join(__dirname, 'static', 'sound', 'delete.wav'),
SOUND_PLAY: 'file://' + path.join(__dirname, 'static', 'sound', 'play.wav'), volume: 0.1
SOUND_STARTUP: 'file://' + path.join(__dirname, 'static', 'sound', 'startup.wav') },
SOUND_DISABLE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'disable.wav'),
volume: 0.2
},
SOUND_DONE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'done.wav'),
volume: 0.2
},
SOUND_ENABLE: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'enable.wav'),
volume: 0.2
},
SOUND_ERROR: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'error.wav'),
volume: 0.2
},
SOUND_PLAY: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'play.wav'),
volume: 0.2
},
SOUND_STARTUP: {
url: 'file://' + path.join(__dirname, 'static', 'sound', 'startup.wav'),
volume: 0.4
},
WINDOW_ABOUT: 'file://' + path.join(__dirname, 'renderer', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, 'renderer', 'main.html')
} }
function isProduction () { function isProduction () {

View File

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

View File

@@ -1,46 +1,86 @@
module.exports = {
init
}
var path = require('path')
var log = require('./log') var log = require('./log')
module.exports = function () { function init () {
if (process.platform === 'win32') { if (process.platform === 'win32') {
var path = require('path') 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)
} }
if (process.platform === 'linux') { if (process.platform === 'linux') {
installDesktopFile() initLinux()
installDesktopIcon()
} }
} }
function installDesktopFile () { function initWindows () {
var config = require('../config') var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
var fs = require('fs') registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, process.execPath)
var path = require('path') registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, process.execPath)
var os = require('os')
var templatePath = path.join(config.STATIC_PATH, 'webtorrent.desktop')
var desktopFile = fs.readFileSync(templatePath, 'utf8')
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
desktopFile = desktopFile.replace(/\$APP_PATH/g, path.dirname(process.execPath))
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, process.execPath)
var desktopFilePath = path.join(os.homedir(), '.local', 'share', 'applications', 'webtorrent.desktop')
fs.writeFileSync(desktopFilePath, desktopFile)
} }
function installDesktopIcon () { function initLinux () {
var config = require('../config') var config = require('../config')
var fs = require('fs') var fs = require('fs')
var path = require('path') var mkdirp = require('mkdirp')
var os = require('os') var os = require('os')
var path = require('path')
var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png') installDesktopFile()
var iconFile = fs.readFileSync(iconStaticPath) installIconFile()
var iconFilePath = path.join(os.homedir(), '.local', 'share', 'icons', 'webtorrent.png') function installDesktopFile () {
fs.writeFileSync(iconFilePath, iconFile) var templatePath = path.join(config.STATIC_PATH, 'webtorrent.desktop')
fs.readFile(templatePath, 'utf8', writeDesktopFile)
}
function writeDesktopFile (err, desktopFile) {
if (err) return console.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
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)
var desktopFilePath = path.join(
os.homedir(),
'.local',
'share',
'applications',
'webtorrent.desktop'
)
mkdirp(path.dirname(desktopFilePath))
fs.writeFile(desktopFilePath, desktopFile, function (err) {
if (err) return console.error(err.message)
})
}
function installIconFile () {
var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
fs.readFile(iconStaticPath, writeIconFile)
}
function writeIconFile (err, iconFile) {
if (err) return console.error(err.message)
var iconFilePath = path.join(
os.homedir(),
'.local',
'share',
'icons',
'webtorrent.png'
)
mkdirp(path.dirname(iconFilePath))
fs.writeFile(iconFilePath, iconFile, function (err) {
if (err) return console.error(err.message)
})
}
} }
/** /**

View File

@@ -4,100 +4,124 @@ var app = electron.app
var autoUpdater = require('./auto-updater') var autoUpdater = require('./auto-updater')
var config = require('../config') var config = require('../config')
var handlers = require('./handlers')
var ipc = require('./ipc') var ipc = require('./ipc')
var log = require('./log') var log = require('./log')
var menu = require('./menu') var menu = require('./menu')
var registerProtocolHandler = require('./register-handlers')
var shortcuts = require('./shortcuts') var shortcuts = require('./shortcuts')
var squirrelWin32 = require('./squirrel-win32')
var windows = require('./windows') var windows = require('./windows')
var tray = require('./tray')
// Prevent multiple instances of the app from running at the same time. New instances var shouldQuit = false
// signal this instance and exit.
var shouldQuit = app.makeSingleInstance(function (newArgv) {
newArgv = sliceArgv(newArgv)
if (app.ipcReady) {
log('Second app instance attempted to open but was prevented')
newArgv.forEach(function (torrentId) {
windows.main.send('dispatch', 'onOpen', torrentId)
})
if (windows.main.isMinimized()) {
windows.main.restore()
}
windows.main.focus()
} else {
argv.push(...newArgv)
}
})
if (shouldQuit) {
app.quit()
}
var argv = sliceArgv(process.argv) var argv = sliceArgv(process.argv)
app.on('open-file', onOpen) if (process.platform === 'win32') {
app.on('open-url', onOpen) shouldQuit = squirrelWin32.handleEvent(argv[0])
app.on('will-finish-launching', function () { argv = argv.filter((arg) => arg.indexOf('--squirrel') === -1)
autoUpdater.init() }
setupCrashReporter()
})
app.ipcReady = false // main window has finished loading and IPC is ready if (!shouldQuit) {
app.isQuitting = false // Prevent multiple instances of app from running at same time. New instances signal
// this instance and quit.
app.on('ready', function () { shouldQuit = app.makeSingleInstance(onAppOpen)
menu.init() if (shouldQuit) {
windows.createMainWindow()
shortcuts.init()
registerProtocolHandler()
})
app.on('ipcReady', function () {
log('IS_PRODUCTION:', config.IS_PRODUCTION)
if (argv.length) {
log('command line args:', process.argv)
}
argv.forEach(function (torrentId) {
windows.main.send('dispatch', 'onOpen', torrentId)
})
})
app.on('before-quit', function () {
app.isQuitting = true
})
app.on('activate', function () {
if (windows.main) {
windows.main.show()
} else {
windows.createMainWindow(menu)
}
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit() app.quit()
} }
}) }
ipc.init() if (!shouldQuit) {
init()
}
function init () {
app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false
// Open handlers must be added as early as possible
app.on('open-file', onOpen)
app.on('open-url', onOpen)
ipc.init()
app.on('will-finish-launching', function () {
autoUpdater.init()
setupCrashReporter()
})
app.on('ready', function () {
menu.init()
windows.createMainWindow()
shortcuts.init()
tray.init()
if (process.platform !== 'win32') handlers.init()
})
app.on('ipcReady', function () {
log('Command line args:', argv)
processArgv(argv)
})
app.on('before-quit', function () {
app.isQuitting = true
})
app.on('activate', function () {
windows.createMainWindow()
})
}
function onOpen (e, torrentId) { function onOpen (e, torrentId) {
e.preventDefault() e.preventDefault()
if (app.ipcReady) { if (app.ipcReady) {
windows.main.send('dispatch', 'onOpen', torrentId) 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.
// Electron issue: https://github.com/atom/electron/issues/4338
setTimeout(function () {
windows.focusWindow(windows.main)
}, 100)
} else { } else {
argv.push(torrentId) argv.push(torrentId)
} }
} }
function onAppOpen (newArgv) {
newArgv = sliceArgv(newArgv)
if (app.ipcReady) {
log('Second app instance opened, but was prevented:', newArgv)
windows.focusWindow(windows.main)
processArgv(newArgv)
} else {
argv.push(...newArgv)
}
}
function sliceArgv (argv) { function sliceArgv (argv) {
return argv.slice(config.IS_PRODUCTION ? 1 : 2) return argv.slice(config.IS_PRODUCTION ? 1 : 2)
} }
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)
}
})
}
function setupCrashReporter () { function setupCrashReporter () {
// require('crash-reporter').start({ // require('crash-reporter').start({
// productName: 'WebTorrent', // productName: 'WebTorrent',

View File

@@ -1,5 +1,5 @@
module.exports = { module.exports = {
init: init init
} }
var debug = require('debug')('webtorrent-app:ipcMain') var debug = require('debug')('webtorrent-app:ipcMain')
@@ -12,20 +12,23 @@ var powerSaveBlocker = electron.powerSaveBlocker
var log = require('./log') var log = require('./log')
var menu = require('./menu') var menu = require('./menu')
var windows = require('./windows') var windows = require('./windows')
var shortcuts = require('./shortcuts')
// has to be a number, not a boolean, and undefined throws an error // has to be a number, not a boolean, and undefined throws an error
var powerSaveBlockID = 0 var powerSaveBlockID = 0
function init () { function init () {
ipcMain.on('ipcReady', function (e) { ipcMain.on('ipcReady', function (e) {
console.timeEnd('init')
app.ipcReady = true app.ipcReady = true
app.emit('ipcReady') app.emit('ipcReady')
setTimeout(function () {
windows.main.show()
console.timeEnd('init')
}, 50)
}) })
ipcMain.on('showOpenTorrentFile', function (e) { ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
menu.showOpenTorrentFile() ipcMain.on('showCreateTorrent', menu.showCreateTorrent)
})
ipcMain.on('setBounds', function (e, bounds, maximize) { ipcMain.on('setBounds', function (e, bounds, maximize) {
setBounds(bounds, maximize) setBounds(bounds, maximize)
@@ -58,6 +61,15 @@ function init () {
ipcMain.on('blockPowerSave', blockPowerSave) ipcMain.on('blockPowerSave', blockPowerSave)
ipcMain.on('unblockPowerSave', unblockPowerSave) ipcMain.on('unblockPowerSave', unblockPowerSave)
ipcMain.on('onPlayerOpen', function () {
menu.onPlayerOpen()
shortcuts.registerPlayerShortcuts()
})
ipcMain.on('onPlayerClose', function () {
menu.onPlayerClose()
shortcuts.unregisterPlayerShortcuts()
})
} }
function setBounds (bounds, maximize) { function setBounds (bounds, maximize) {

View File

@@ -1,10 +1,13 @@
module.exports = { module.exports = {
init: init, init,
onToggleFullScreen: onToggleFullScreen, onToggleFullScreen,
onWindowHide: onWindowHide, onWindowHide,
onWindowShow: onWindowShow, onWindowShow,
showOpenTorrentFile: showOpenTorrentFile, onPlayerOpen,
toggleFullScreen: toggleFullScreen onPlayerClose,
showCreateTorrent,
showOpenTorrentFile,
toggleFullScreen
} }
var debug = require('debug')('webtorrent-app:menu') var debug = require('debug')('webtorrent-app:menu')
@@ -43,6 +46,18 @@ function toggleFloatOnTop (flag) {
} }
} }
function increaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', 0.1)
}
}
function decreaseVolume () {
if (windows.main) {
windows.main.send('dispatch', 'changeVolume', -0.1)
}
}
function toggleDevTools () { function toggleDevTools () {
debug('toggleDevTools') debug('toggleDevTools')
if (windows.main) { if (windows.main) {
@@ -74,6 +89,16 @@ function onWindowHide () {
getMenuItem('Float on Top').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) { function onToggleFullScreen (isFullScreen) {
isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen() isFullScreen = isFullScreen != null ? isFullScreen : windows.main.isFullScreen()
windows.main.setMenuBarVisibility(!isFullScreen) windows.main.setMenuBarVisibility(!isFullScreen)
@@ -195,6 +220,21 @@ function getAppMenuTemplate () {
{ {
type: 'separator' type: 'separator'
}, },
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
enabled: false
},
{
type: 'separator'
},
{ {
label: 'Developer', label: 'Developer',
submenu: [ submenu: [
@@ -260,12 +300,12 @@ function getAppMenuTemplate () {
] ]
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
var name = app.getName() // WebTorrent menu (OS X)
template.unshift({ template.unshift({
label: name, label: config.APP_NAME,
submenu: [ submenu: [
{ {
label: 'About ' + name, label: 'About ' + config.APP_NAME,
role: 'about' role: 'about'
}, },
{ {
@@ -280,7 +320,7 @@ function getAppMenuTemplate () {
type: 'separator' type: 'separator'
}, },
{ {
label: 'Hide ' + name, label: 'Hide ' + config.APP_NAME,
accelerator: 'Command+H', accelerator: 'Command+H',
role: 'hide' role: 'hide'
}, },
@@ -299,12 +339,12 @@ function getAppMenuTemplate () {
{ {
label: 'Quit', label: 'Quit',
accelerator: 'Command+Q', accelerator: 'Command+Q',
click: function () { app.quit() } click: () => app.quit()
} }
] ]
}) })
// Window menu // Window menu (OS X)
template[4].submenu.push( template[4].submenu.push(
{ {
type: 'separator' type: 'separator'
@@ -314,6 +354,17 @@ function getAppMenuTemplate () {
role: 'front' role: 'front'
} }
) )
} else {
// Help menu (Windows, Linux)
template[4].submenu.push(
{
type: 'separator'
},
{
label: 'About ' + config.APP_NAME,
click: windows.createAboutWindow
}
)
} }
return template return template

View File

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

134
main/squirrel-win32.js Normal file
View File

@@ -0,0 +1,134 @@
module.exports = {
handleEvent
}
var cp = require('child_process')
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')
function handleEvent (cmd) {
if (cmd === '--squirrel-install') {
// App was installed.
// Install protocol/file handlers, desktop/start menu shortcuts.
handlers.init()
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)
})
return true
}
if (cmd === '--squirrel-updated') {
// App was updated. (Called on new version of app)
updateShortcuts(function () {
app.quit()
})
return true
}
if (cmd === '--squirrel-uninstall') {
// App was just uninstalled. Undo anything we did in the --squirrel-install and
// --squirrel-updated handlers
removeShortcuts(function () {
app.quit()
})
return true
}
if (cmd === '--squirrel-obsolete') {
// App will be updated. (Called on outgoing version of app)
app.quit()
return true
}
if (cmd === '--squirrel-firstrun') {
// This is called on the app's first run. 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.
function spawn (command, args, cb) {
var stdout = ''
var child
try {
child = cp.spawn(command, args)
} catch (err) {
// Spawn can throw an error
process.nextTick(function () {
cb(error, stdout)
})
return
}
child.stdout.on('data', function (data) {
stdout += data
})
var error = null
child.on('error', function (processError) {
error = processError
})
child.on('close', function (code, signal) {
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
if (error) error.stdout = stdout
cb(error, stdout)
})
}
// Spawn Squirrel's Update.exe with the given arguments and invoke the callback when the
// command completes.
function spawnUpdate (args, cb) {
spawn(updateDotExe, args, cb)
}
// Create desktop/start menu shortcuts using the Squirrel Update.exe command line API
function createShortcuts (cb) {
spawnUpdate(['--createShortcut', exeName], cb)
}
// Update desktop/start menu shortcuts using the Squirrel Update.exe command line API
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) {
createShortcuts(function () {
if (desktopShortcutExists) {
cb()
} else {
// Remove the unwanted desktop shortcut that was recreated
fs.unlink(desktopShortcutPath, cb)
}
})
})
} else {
createShortcuts(cb)
}
}
// Remove desktop/start menu shortcuts using the Squirrel Update.exe command line API
function removeShortcuts (cb) {
spawnUpdate(['--removeShortcut', exeName], cb)
}

31
main/tray.js Normal file
View File

@@ -0,0 +1,31 @@
module.exports = {
init
}
var path = require('path')
var electron = require('electron')
var windows = require('./windows')
function init () {
// No tray icon on OSX
if (process.platform === 'darwin') return
var trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
// On Windows, left click to open the app, right click for context menu
// On Linux, any click (right or left) opens the context menu
trayIcon.on('click', showApp)
var contextMenu = electron.Menu.buildFromTemplate([
{ label: 'Show', click: showApp },
{ label: 'Quit', click: quitApp }
])
trayIcon.setContextMenu(contextMenu)
}
function showApp () {
windows.main.show()
}
function quitApp () {
electron.app.quit()
}

View File

@@ -1,39 +1,74 @@
var windows = module.exports = { var windows = module.exports = {
about: null,
main: null, main: null,
createMainWindow: createMainWindow createAboutWindow: createAboutWindow,
createMainWindow: createMainWindow,
focusWindow: focusWindow
} }
var electron = require('electron') var electron = require('electron')
var app = electron.app
var config = require('../config') var config = require('../config')
var menu = require('./menu') var menu = require('./menu')
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 () { function createMainWindow () {
if (windows.main) {
return focusWindow(windows.main)
}
var win = windows.main = new electron.BrowserWindow({ var win = windows.main = new electron.BrowserWindow({
autoHideMenuBar: true, // Hide top menu bar unless Alt key is pressed (Windows, Linux)
backgroundColor: '#282828', backgroundColor: '#282828',
darkTheme: true, // Forces dark theme (GTK+3) darkTheme: true, // Forces dark theme (GTK+3)
icon: config.APP_ICON + '.png', icon: config.APP_ICON + '.png',
minWidth: 375, minWidth: 375,
minHeight: 38 + (120 * 2), // header height + 2 torrents minHeight: 38 + (120 * 2), // header height + 2 torrents
show: false, // Hide window until DOM finishes loading show: false, // Hide window until DOM finishes loading
title: config.APP_NAME, title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X) titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
width: 450, useContentSize: true, // Specify web page size without OS chrome
height: 38 + (120 * 4) // header height + 4 torrents width: 500,
height: 38 + (120 * 5) // header height + 4 torrents
}) })
win.loadURL(config.INDEX) win.loadURL(config.WINDOW_MAIN)
win.webContents.on('dom-ready', function () { win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen() menu.onToggleFullScreen()
}) })
win.webContents.on('did-finish-load', function () {
win.show()
})
win.on('blur', menu.onWindowHide) win.on('blur', menu.onWindowHide)
win.on('focus', menu.onWindowShow) win.on('focus', menu.onWindowShow)
@@ -41,7 +76,7 @@ function createMainWindow () {
win.on('leave-full-screen', () => menu.onToggleFullScreen(false)) win.on('leave-full-screen', () => menu.onToggleFullScreen(false))
win.on('close', function (e) { win.on('close', function (e) {
if (process.platform === 'darwin' && !app.isQuitting) { if (!electron.app.isQuitting) {
e.preventDefault() e.preventDefault()
win.send('dispatch', 'pause') win.send('dispatch', 'pause')
win.hide() win.hide()
@@ -52,3 +87,10 @@ function createMainWindow () {
windows.main = null windows.main = null
}) })
} }
function focusWindow (win) {
if (win.isMinimized()) {
win.restore()
}
win.show() // shows and gives focus
}

View File

@@ -1,14 +1,14 @@
{ {
"name": "webtorrent-app", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.0.0", "version": "0.2.0",
"author": { "author": {
"name": "Feross Aboukhadijeh", "name": "Feross Aboukhadijeh",
"email": "feross@feross.org", "email": "feross@feross.org",
"url": "http://feross.org" "url": "http://feross.org"
}, },
"bugs": { "bugs": {
"url": "https://github.com/feross/webtorrent-app/issues" "url": "https://github.com/feross/webtorrent-desktop/issues"
}, },
"dependencies": { "dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js", "airplay-js": "guerrerocarlos/node-airplay-js",
@@ -19,45 +19,56 @@
"debug": "^2.2.0", "debug": "^2.2.0",
"drag-drop": "^2.11.0", "drag-drop": "^2.11.0",
"electron-localshortcut": "^0.6.0", "electron-localshortcut": "^0.6.0",
"electron-prebuilt": "0.37.2",
"hyperx": "^2.0.2", "hyperx": "^2.0.2",
"main-loop": "^3.2.0", "main-loop": "^3.2.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"musicmetadata": "^2.0.2",
"network-address": "^1.1.0", "network-address": "^1.1.0",
"path-exists": "^2.1.0",
"prettier-bytes": "^1.0.1", "prettier-bytes": "^1.0.1",
"simple-get": "^2.0.0",
"upload-element": "^1.0.1", "upload-element": "^1.0.1",
"virtual-dom": "^2.1.1", "virtual-dom": "^2.1.1",
"webtorrent": "^0.86.0", "webtorrent": "^0.88.1",
"winreg": "^1.0.1" "winreg": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"appdmg": "^0.3.6",
"electron-osx-sign": "^0.3.0", "electron-osx-sign": "^0.3.0",
"electron-packager": "^5.0.0", "electron-packager": "^6.0.0",
"electron-prebuilt": "0.37.2", "electron-winstaller": "^2.0.5",
"path-exists": "^2.1.0", "gh-release": "^2.0.3",
"plist": "^1.2.0", "plist": "^1.2.0",
"rimraf": "^2.5.2", "rimraf": "^2.5.2",
"standard": "^6.0.5" "standard": "^6.0.5"
}, },
"optionalDependencies": {
"appdmg": "^0.3.6"
},
"homepage": "https://webtorrent.io", "homepage": "https://webtorrent.io",
"keywords": [ "keywords": [
"desktop",
"electron", "electron",
"electron-app" "electron-app",
"webtorrent"
], ],
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"productName": "WebTorrent", "productName": "WebTorrent",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/feross/webtorrent-app.git" "url": "git://github.com/feross/webtorrent-desktop.git"
}, },
"scripts": { "scripts": {
"clean": "node ./bin/clean.js", "clean": "node ./bin/clean.js",
"debug": "DEBUG=* electron .", "debug": "DEBUG=* electron .",
"package": "npm prune && npm dedupe && node ./bin/package.js", "package": "npm install && npm prune && npm dedupe && node ./bin/package.js",
"size": "npm run package -- --darwin && du -ch dist/WebTorrent-darwin-x64 | grep total", "size": "npm run package -- darwin && du -ch dist/WebTorrent-darwin-x64 | grep total",
"start": "electron .", "start": "electron .",
"test": "standard", "test": "standard",
"update-authors": "./bin/update-authors.sh" "update-authors": "./bin/update-authors.sh"
},
"bin": {
"webtorrent-desktop": "./bin/cmd.js"
} }
} }

35
renderer/about.html Normal file
View File

@@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
background-color: #ECECEC;
font-family: BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
text-align: center;
overflow: hidden;
font-size: 16px;
-webkit-user-select: none;
}
img {
width: 65px;
height: 65px;
}
h1 {
font-size: 0.9em;
-webkit-user-select: text;
}
p {
font-size: 0.8em;
-webkit-user-select: text;
}
</style>
</head>
<body>
<img src="../static/WebTorrent.png">
<h1>WebTorrent</h1>
<p>Version <script>document.write(require('../package.json').version)</script></p>
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
</body>
</html>

View File

@@ -49,34 +49,6 @@ table {
background-color: rgb(40, 40, 40); background-color: rgb(40, 40, 40);
} }
.loading {
display: flex;
flex-direction: column;
justify-content: center;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.loading .icon {
font-size: 42px;
display: block;
text-align: center;
animation: spin-ccw 2s infinite linear;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes spin-ccw {
from { transform: rotate(360deg); }
to { transform: rotate(0deg); }
}
@keyframes fadein { @keyframes fadein {
from { opacity: 0; } from { opacity: 0; }
to { opacity: 1; } to { opacity: 1; }
@@ -181,10 +153,10 @@ i:not(.disabled):hover {
left: 0; left: 0;
top: 0; top: 0;
right: 0; right: 0;
z-index: 1000;
transition: opacity 0.15s ease-out; transition: opacity 0.15s ease-out;
font-size: 14px; font-size: 14px;
line-height: 1.5em; line-height: 1.5em;
z-index: 1;
} }
.app:not(.is-focused) .header { .app:not(.is-focused) .header {
@@ -270,6 +242,10 @@ i:not(.disabled):hover {
* MODAL POPOVERS * MODAL POPOVERS
*/ */
.modal {
z-index: 2;
}
.modal .modal-background { .modal .modal-background {
content: ' '; content: ' ';
position: fixed; position: fixed;
@@ -341,30 +317,6 @@ input {
box-shadow: 1px 1px 1px 0px rgba(0,0,0,0.1); 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 LIST
*/ */
@@ -456,12 +408,12 @@ input {
background-color: #F44336; background-color: #F44336;
} }
.torrent.timeout .play, .torrent.timeout .play {
.torrent.unplayable .play {
padding-top: 8px; padding-top: 8px;
} }
.torrent.requested .play { .torrent.requested .play,
.loading-spinner {
border-top: 6px solid rgba(255, 255, 255, 0.2); border-top: 6px solid rgba(255, 255, 255, 0.2);
border-right: 6px solid rgba(255, 255, 255, 0.2); border-right: 6px solid rgba(255, 255, 255, 0.2);
border-bottom: 6px solid rgba(255, 255, 255, 0.2); border-bottom: 6px solid rgba(255, 255, 255, 0.2);
@@ -524,6 +476,17 @@ body.drag .torrent-placeholder span {
color: #def; color: #def;
} }
body.drag .app::after {
content: ' ';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 3;
}
/* /*
* TORRENT LIST: EXPANDED TORRENT DETAILS * TORRENT LIST: EXPANDED TORRENT DETAILS
*/ */
@@ -586,6 +549,28 @@ body.drag .torrent-placeholder span {
text-align: right; text-align: right;
} }
/*
* PLAYER
*/
.player {
position: absolute;
width: 100%;
height: 100%;
background-color: #000;
}
.player .letterbox {
width: 100%;
height: 100%;
display: flex;
}
.player video {
display: block;
width: 100%;
}
/* /*
* PLAYER CONTROLS * PLAYER CONTROLS
*/ */
@@ -732,17 +717,72 @@ body.drag .torrent-placeholder span {
font-weight: bold; font-weight: bold;
} }
/*
* MEDIA OVERLAY / AUDIO DETAILS
*/
.media-overlay-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
background-size: cover;
background-position: center center;
}
.media-overlay {
max-width: calc(100% - 80px);
white-space: nowrap;
text-overflow: ellipsis;
align-self: center;
margin: 0 auto;
font-weight: bold;
font-size: 24px;
line-height: 2;
}
.media-stalled .loading-spinner {
width: 40px;
height: 40px;
margin: 40px auto;
}
.media-stalled .loading-status {
font-size: 18px;
font-weight: normal;
text-align: center;
}
.audio-metadata div {
overflow: hidden;
text-overflow: ellipsis;
}
.audio-metadata .audio-title {
font-size: 32px;
}
.audio-metadata label {
display:inline-block;
width: 100px;
text-align: right;
font-weight: normal;
margin-right: 25px;
}
/* /*
* ERRORS * ERRORS
*/ */
.error-popover { .error-popover {
position: fixed; position: fixed;
z-index: 1001;
top: 36px; top: 36px;
margin: 0; margin: 0;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
z-index: 1;
} }
.app.hide-header .error-popover { .app.hide-header .error-popover {

View File

@@ -8,27 +8,35 @@ var EventEmitter = require('events')
var fs = require('fs') var fs = require('fs')
var mainLoop = require('main-loop') var mainLoop = require('main-loop')
var mkdirp = require('mkdirp') var mkdirp = require('mkdirp')
var musicmetadata = require('musicmetadata')
var networkAddress = require('network-address') var networkAddress = require('network-address')
var path = require('path') var path = require('path')
var remote = require('remote') var remote = require('remote')
var WebTorrent = require('webtorrent')
var createElement = require('virtual-dom/create-element') var createElement = require('virtual-dom/create-element')
var diff = require('virtual-dom/diff') var diff = require('virtual-dom/diff')
var patch = require('virtual-dom/patch') var patch = require('virtual-dom/patch')
var App = require('./views/app') var App = require('./views/app')
var Cast = require('./lib/cast')
var errors = require('./lib/errors') var errors = require('./lib/errors')
var config = require('../config') var config = require('../config')
var TorrentPlayer = require('./lib/torrent-player') var TorrentPlayer = require('./lib/torrent-player')
var torrentPoster = require('./lib/torrent-poster') 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 // 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, // 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 // and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer var ipcRenderer = electron.ipcRenderer
var clipboard = electron.clipboard var clipboard = electron.clipboard
var dialog = remote.require('dialog')
// For easy debugging in Developer Tools // For easy debugging in Developer Tools
var state = global.state = require('./state') var state = global.state = require('./state')
@@ -52,20 +60,11 @@ loadState(init)
function init () { function init () {
state.location.go({ url: 'home' }) state.location.go({ url: 'home' })
// Connect to the WebTorrent and BitTorrent networks // Lazily load the WebTorrent, Chromecast, and Airplay modules
// WebTorrent.app is a hybrid client, as explained here: https://webtorrent.io/faq window.setTimeout(function () {
state.client = new WebTorrent() lazyLoadClient()
state.client.on('warning', onWarning) lazyLoadCast()
state.client.on('error', function (err) { }, 750)
// 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)
}
})
resumeTorrents() /* restart everything we were torrenting last time the app ran */
setInterval(updateTorrentProgress, 1000)
// The UI is built with virtual-dom, a minimalist library extracted from React // 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 // The concepts--one way data flow, a pure function that renders state to a
@@ -78,23 +77,10 @@ function init () {
}) })
document.body.appendChild(vdomLoop.target) document.body.appendChild(vdomLoop.target)
// Calling update() updates the UI given the current state // Save state on exit
// Do this at least once a second to show latest state for each torrent
// (eg % downloaded) and to keep the cursor in sync when playing a video
setInterval(function () {
update()
updateClientProgress()
}, 1000)
window.addEventListener('beforeunload', saveState) window.addEventListener('beforeunload', saveState)
// listen for messages from the main process
setupIpc()
// OS integrations: // OS integrations:
// ...Chromecast and Airplay
Cast.init(update)
// ...drag and drop a torrent or video file to play or seed // ...drag and drop a torrent or video file to play or seed
dragDrop('body', (files) => dispatch('onOpen', files)) dragDrop('body', (files) => dispatch('onOpen', files))
@@ -128,16 +114,65 @@ function init () {
update() update()
}) })
// Listen for messages from the main process
setupIpc()
// Done! Ideally we want to get here <100ms after the user clicks the app // Done! Ideally we want to get here <100ms after the user clicks the app
document.querySelector('.loading').remove() /* TODO: no spinner once fast enough */ playInterfaceSound('STARTUP')
playInterfaceSound(config.SOUND_STARTUP)
console.timeEnd('init') 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 Desktop 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 // 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() // tree. Any events, such as button clicks, will turn into calls to dispatch()
function render (state) { function render (state) {
return App(state, dispatch) return App(state)
} }
// Calls render() to go from state -> UI, then applies to vdom to the real DOM. // Calls render() to go from state -> UI, then applies to vdom to the real DOM.
@@ -163,119 +198,172 @@ function updateElectron () {
// Events from the UI never modify state directly. Instead they call dispatch() // Events from the UI never modify state directly. Instead they call dispatch()
function dispatch (action, ...args) { function dispatch (action, ...args) {
if (['videoMouseMoved', 'playbackJump'].indexOf(action) === -1) { // Log dispatch calls, for debugging
console.log('dispatch: %s %o', action, args) /* log user interactions, but don't spam */ if (action !== 'mediaMouseMoved') {
console.log('dispatch: %s %o', action, args)
} }
if (action === 'onOpen') { if (action === 'onOpen') {
onOpen(args[0] /* files */) onOpen(args[0] /* files */)
} }
if (action === 'addTorrent') { if (action === 'addTorrent') {
addTorrent(args[0] /* torrent */) addTorrent(args[0] /* torrent */)
} }
if (action === 'showCreateTorrent') {
ipcRenderer.send('showCreateTorrent')
}
if (action === 'showOpenTorrentFile') { if (action === 'showOpenTorrentFile') {
ipcRenderer.send('showOpenTorrentFile') ipcRenderer.send('showOpenTorrentFile')
} }
if (action === 'seed') { if (action === 'seed') {
seed(args[0] /* files */) seed(args[0] /* files */)
} }
if (action === 'play') {
state.location.go({
url: 'player',
onbeforeload: function (cb) {
// TODO: handle audio. video only for now.
openPlayer(args[0] /* torrentSummary */, args[1] /* index */, cb)
},
onbeforeunload: closePlayer
})
}
if (action === 'openFile') { if (action === 'openFile') {
openFile(args[0] /* torrentSummary */, args[1] /* index */) openFile(args[0] /* infoHash */, args[1] /* index */)
} }
if (action === 'openFolder') { if (action === 'openFolder') {
openFolder(args[0] /* torrentSummary */) openFolder(args[0] /* infoHash */)
} }
if (action === 'toggleTorrent') { if (action === 'toggleTorrent') {
toggleTorrent(args[0] /* torrentSummary */) toggleTorrent(args[0] /* infoHash */)
} }
if (action === 'deleteTorrent') { if (action === 'deleteTorrent') {
deleteTorrent(args[0] /* torrentSummary */) deleteTorrent(args[0] /* infoHash */)
} }
if (action === 'toggleSelectTorrent') { if (action === 'toggleSelectTorrent') {
toggleSelectTorrent(args[0] /* infoHash */) toggleSelectTorrent(args[0] /* infoHash */)
} }
if (action === 'openTorrentContextMenu') {
openTorrentContextMenu(args[0] /* infoHash */)
}
if (action === 'openChromecast') { if (action === 'openChromecast') {
Cast.openChromecast() lazyLoadCast().openChromecast()
} }
if (action === 'openAirplay') { if (action === 'openAirplay') {
Cast.openAirplay() lazyLoadCast().openAirplay()
} }
if (action === 'stopCasting') { if (action === 'stopCasting') {
Cast.stopCasting() lazyLoadCast().stopCasting()
} }
if (action === 'setDimensions') { if (action === 'setDimensions') {
setDimensions(args[0] /* dimensions */) setDimensions(args[0] /* dimensions */)
} }
if (action === 'back') { if (action === 'back') {
state.location.back() state.location.back()
update()
} }
if (action === 'forward') { if (action === 'forward') {
state.location.forward() state.location.forward()
update()
} }
if (action === 'playPause') { if (action === 'playPause') {
playPause() playPause()
} }
if (action === 'play') { if (action === 'play') {
if (state.location.pending()) return
state.location.go({
url: 'player',
onbeforeload: function (cb) {
openPlayer(args[0] /* infoHash */, args[1] /* index */, cb)
},
onbeforeunload: closePlayer
})
playPause(false) playPause(false)
} }
if (action === 'pause') { if (action === 'pause') {
playPause(true) playPause(true)
// Work around virtual-dom issue: it doesn't expose its redraw function,
// and only redraws on requestAnimationFrame(). That means when the user
// closes the window (hide window / minimize to tray) and we want to pause
// the video, we update the vdom but it keeps playing until you reopen!
var videoTag = document.querySelector('video')
if (videoTag) videoTag.pause()
} }
if (action === 'playbackJump') { if (action === 'playbackJump') {
jumpToTime(args[0] /* seconds */) jumpToTime(args[0] /* seconds */)
} }
if (action === 'videoPlaying') { if (action === 'changeVolume') {
state.video.isPaused = false changeVolume(args[0] /* increase */)
}
if (action === 'mediaPlaying') {
state.playing.isPaused = false
ipcRenderer.send('blockPowerSave') ipcRenderer.send('blockPowerSave')
} }
if (action === 'videoPaused') { if (action === 'mediaPaused') {
state.video.isPaused = true state.playing.isPaused = true
ipcRenderer.send('unblockPowerSave') ipcRenderer.send('unblockPowerSave')
} }
if (action === 'toggleFullScreen') { if (action === 'mediaStalled') {
ipcRenderer.send('toggleFullScreen', args[0]) state.playing.isStalled = true
update()
} }
if (action === 'videoMouseMoved') { if (action === 'mediaTimeUpdate') {
state.video.mouseStationarySince = new Date().getTime() state.playing.lastTimeUpdate = new Date().getTime()
update() state.playing.isStalled = false
}
if (action === 'toggleFullScreen') {
ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
}
if (action === 'mediaMouseMoved') {
state.playing.mouseStationarySince = new Date().getTime()
} }
if (action === 'exitModal') { if (action === 'exitModal') {
state.modal = null state.modal = null
}
if (action === 'updateAvailable') {
updateAvailable(args[0] /* version */)
}
if (action === 'skipVersion') {
if (!state.saved.skippedVersions) state.saved.skippedVersions = []
state.saved.skippedVersions.push(args[0] /* version */)
saveState()
}
// Update the virtual-dom, unless it's just a mouse move event
if (action !== 'mediaMouseMoved') {
update() update()
} }
} }
// Shows a modal saying that we have an update
function updateAvailable (version) {
if (state.saved.skippedVersions && state.saved.skippedVersions.includes(version)) {
console.log('new version skipped by user: v' + version)
return
}
state.modal = { id: 'update-available-modal', version: version }
}
// Plays or pauses the video. If isPaused is undefined, acts as a toggle // Plays or pauses the video. If isPaused is undefined, acts as a toggle
function playPause (isPaused) { function playPause (isPaused) {
if (isPaused === state.video.isPaused) { if (isPaused === state.playing.isPaused) {
return // Nothing to do return // Nothing to do
} }
// Either isPaused is undefined, or it's the opposite of the current state. Toggle. // Either isPaused is undefined, or it's the opposite of the current state. Toggle.
if (Cast.isCasting()) { if (lazyLoadCast().isCasting()) {
Cast.playPause() Cast.playPause()
} }
state.video.isPaused = !state.video.isPaused state.playing.isPaused = !state.playing.isPaused
update()
} }
function jumpToTime (time) { function jumpToTime (time) {
if (Cast.isCasting()) { if (lazyLoadCast().isCasting()) {
Cast.seek(time) Cast.seek(time)
} else { } else {
state.video.jumpToTime = time 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
} }
} }
@@ -288,7 +376,7 @@ function setupIpc () {
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args)) ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
ipcRenderer.on('showOpenTorrentAddress', function (e) { ipcRenderer.on('showOpenTorrentAddress', function (e) {
state.modal = 'open-torrent-address-modal' state.modal = { id: 'open-torrent-address-modal' }
update() update()
}) })
@@ -337,18 +425,6 @@ function saveState () {
}) })
} }
function updateClientProgress () {
var progress = state.client.progress
var activeTorrentsExist = state.client.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
}
state.dock.progress = progress
}
function onOpen (files) { function onOpen (files) {
if (!Array.isArray(files)) files = [ files ] if (!Array.isArray(files)) files = [ files ]
@@ -392,7 +468,9 @@ function getTorrentSummary (infoHash) {
// Get an active torrent from state.client.torrents // Get an active torrent from state.client.torrents
// Returns undefined if we are not currently torrenting that infoHash // Returns undefined if we are not currently torrenting that infoHash
function getTorrent (infoHash) { function getTorrent (infoHash) {
return state.client.torrents.find((x) => x.infoHash === 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 // Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
@@ -418,27 +496,34 @@ function addTorrentToList (torrent) {
state.saved.torrents.push({ state.saved.torrents.push({
status: 'new', status: 'new',
name: torrent.name, name: torrent.name,
magnetURI: torrent.magnetURI, infoHash: torrent.infoHash,
infoHash: torrent.infoHash magnetURI: torrent.magnetURI
}) })
saveState() saveState()
playInterfaceSound(config.SOUND_ADD) playInterfaceSound('ADD')
} }
} }
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object // Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
function startTorrentingSummary (torrentSummary) { function startTorrentingSummary (torrentSummary) {
var s = torrentSummary var s = torrentSummary
if (s.torrentPath) return startTorrentingID(s.torrentPath, s.path) if (s.torrentPath) {
else if (s.magnetURI) return startTorrentingID(s.magnetURI, s.path) var torrentPath = util.getAbsoluteStaticPath(s.torrentPath)
else return startTorrentingID(s.infoHash, s.path) 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 // Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent- // See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
function startTorrentingID (torrentID, path) { function startTorrentingID (torrentID, path) {
console.log('Starting torrent ' + torrentID) console.log('starting torrent ' + torrentID)
var torrent = state.client.add(torrentID, { var torrent = lazyLoadClient().add(torrentID, {
path: path || state.saved.downloadPath // Use downloads folder path: path || state.saved.downloadPath // Use downloads folder
}) })
addTorrentEvents(torrent) addTorrentEvents(torrent)
@@ -454,13 +539,17 @@ function stopTorrenting (infoHash) {
// Creates a torrent for a local file and starts seeding it // Creates a torrent for a local file and starts seeding it
function seed (files) { function seed (files) {
if (files.length === 0) return if (files.length === 0) return
var torrent = state.client.seed(files) var torrent = lazyLoadClient().seed(files)
addTorrentToList(torrent) addTorrentToList(torrent)
addTorrentEvents(torrent) addTorrentEvents(torrent)
} }
function addTorrentEvents (torrent) { function addTorrentEvents (torrent) {
torrent.on('infoHash', update) torrent.on('infoHash', function () {
var infoHash = torrent.infoHash
if (state.pendingTorrents[infoHash]) delete state.pendingTorrents[infoHash]
update()
})
torrent.on('ready', torrentReady) torrent.on('ready', torrentReady)
torrent.on('done', torrentDone) torrent.on('done', torrentDone)
@@ -470,7 +559,6 @@ function addTorrentEvents (torrent) {
torrentSummary.status = 'downloading' torrentSummary.status = 'downloading'
torrentSummary.ready = true torrentSummary.ready = true
torrentSummary.name = torrentSummary.displayName || torrent.name torrentSummary.name = torrentSummary.displayName || torrent.name
torrentSummary.infoHash = torrent.infoHash
torrentSummary.path = torrent.path torrentSummary.path = torrent.path
// Summarize torrent files // Summarize torrent files
@@ -506,12 +594,27 @@ function addTorrentEvents (torrent) {
} }
function updateTorrentProgress () { 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 // TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield // more efficiently than looping over torrent.bitfield
var changed = false lazyLoadClient().torrents.forEach(function (torrent) {
state.client.torrents.forEach(function (torrent) {
var torrentSummary = getTorrentSummary(torrent.infoHash) var torrentSummary = getTorrentSummary(torrent.infoHash)
if (!torrentSummary) return if (!torrentSummary || !torrent.ready) return
torrent.files.forEach(function (file, index) { torrent.files.forEach(function (file, index) {
var numPieces = file._endPiece - file._startPiece + 1 var numPieces = file._endPiece - file._startPiece + 1
var numPiecesPresent = 0 var numPiecesPresent = 0
@@ -529,6 +632,7 @@ function updateTorrentProgress () {
}) })
if (changed) update() if (changed) update()
return changed
} }
function generateTorrentPoster (torrent, torrentSummary) { function generateTorrentPoster (torrent, torrentSummary) {
@@ -541,7 +645,7 @@ function generateTorrentPoster (torrent, torrentSummary) {
fs.writeFile(posterFilePath, buf, function (err) { fs.writeFile(posterFilePath, buf, function (err) {
if (err) return onWarning(err) if (err) return onWarning(err)
// show the poster // show the poster
torrentSummary.posterURL = 'file:///' + posterFilePath torrentSummary.posterURL = posterFilePath
update() update()
}) })
}) })
@@ -572,8 +676,8 @@ function saveTorrentFile (torrentSummary, torrent) {
// Otherwise, save the .torrent file, under the app config folder // Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) { fs.mkdir(config.CONFIG_TORRENT_PATH, function (_) {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) { fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
if (err) return console.log('Error saving torrent file %s: %o', torrentPath, err) if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('Saved torrent file %s', torrentPath) console.log('saved torrent file %s', torrentPath)
torrentSummary.torrentPath = torrentPath torrentSummary.torrentPath = torrentPath
saveState() saveState()
}) })
@@ -601,21 +705,25 @@ function startServer (torrentSummary, index, cb) {
function startServerFromReadyTorrent (torrent, index, cb) { function startServerFromReadyTorrent (torrent, index, cb) {
// automatically choose which file in the torrent to play, if necessary // automatically choose which file in the torrent to play, if necessary
if (!index) { if (index === undefined) index = pickFileToPlay(torrent.files)
// filter out file formats that the <video> tag definitely can't play if (index === undefined) return cb(new errors.UnplayableError())
var files = torrent.files.filter(TorrentPlayer.isPlayable) var file = torrent.files[index]
if (files.length === 0) return cb(new errors.UnplayableError())
// use largest file
var largestFile = files.reduce(function (a, b) {
return a.length > b.length ? a : b
})
index = torrent.files.indexOf(largestFile)
}
// update state // update state
state.playing.infoHash = torrent.infoHash state.playing.infoHash = torrent.infoHash
state.playing.fileIndex = index 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() var server = torrent.createServer()
server.listen(0, function () { server.listen(0, function () {
var port = server.address().port var port = server.address().port
@@ -629,6 +737,28 @@ function startServerFromReadyTorrent (torrent, index, 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 () { function stopServer () {
if (!state.server) return if (!state.server) return
state.server.server.destroy() state.server.server.destroy()
@@ -638,15 +768,16 @@ function stopServer () {
} }
// Opens the video player // Opens the video player
function openPlayer (torrentSummary, index, cb) { function openPlayer (infoHash, index, cb) {
var torrent = state.client.get(torrentSummary.infoHash) var torrentSummary = getTorrentSummary(infoHash)
if (!torrent || !torrent.done) playInterfaceSound(config.SOUND_PLAY) var torrent = lazyLoadClient().get(infoHash)
if (!torrent || !torrent.done) playInterfaceSound('PLAY')
torrentSummary.playStatus = 'requested' torrentSummary.playStatus = 'requested'
update() update()
var timeout = setTimeout(function () { var timeout = setTimeout(function () {
torrentSummary.playStatus = 'timeout' /* no seeders available? */ torrentSummary.playStatus = 'timeout' /* no seeders available? */
playInterfaceSound(config.SOUND_ERROR) playInterfaceSound('ERROR')
update() update()
}, 10000) /* give it a few seconds */ }, 10000) /* give it a few seconds */
@@ -654,7 +785,7 @@ function openPlayer (torrentSummary, index, cb) {
clearTimeout(timeout) clearTimeout(timeout)
if (err) { if (err) {
torrentSummary.playStatus = 'unplayable' torrentSummary.playStatus = 'unplayable'
playInterfaceSound(config.SOUND_ERROR) playInterfaceSound('ERROR')
update() update()
return onError(err) return onError(err)
} }
@@ -665,22 +796,42 @@ function openPlayer (torrentSummary, index, cb) {
if (timedOut) return update() if (timedOut) return update()
// otherwise, play the video // otherwise, play the video
state.window.title = torrentSummary.name state.window.title = torrentSummary.files[state.playing.fileIndex].name
update() update()
ipcRenderer.send('onPlayerOpen')
cb() cb()
}) })
} }
function openFile (torrentSummary, index) { function closePlayer (cb) {
var torrent = state.client.get(torrentSummary.infoHash) 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 if (!torrent) return
var filePath = path.join(torrent.path, torrent.files[index].path) var filePath = path.join(torrent.path, torrent.files[index].path)
ipcRenderer.send('openItem', filePath) ipcRenderer.send('openItem', filePath)
} }
function openFolder (torrentSummary) { function openFolder (infoHash) {
var torrent = state.client.get(torrentSummary.infoHash) var torrent = lazyLoadClient().get(infoHash)
if (!torrent) return if (!torrent) return
var folderPath = path.join(torrent.path, torrent.name) var folderPath = path.join(torrent.path, torrent.name)
@@ -694,36 +845,20 @@ function openFolder (torrentSummary) {
}) })
} }
function closePlayer (cb) { function toggleTorrent (infoHash) {
state.window.title = config.APP_NAME var torrentSummary = getTorrentSummary(infoHash)
update()
if (state.window.isFullScreen) {
dispatch('toggleFullScreen', false)
}
restoreBounds()
stopServer()
update()
ipcRenderer.send('unblockPowerSave')
cb()
}
function toggleTorrent (torrentSummary) {
if (torrentSummary.status === 'paused') { if (torrentSummary.status === 'paused') {
torrentSummary.status = 'new' torrentSummary.status = 'new'
startTorrentingSummary(torrentSummary) startTorrentingSummary(torrentSummary)
playInterfaceSound(config.SOUND_ENABLE) playInterfaceSound('ENABLE')
} else { } else {
torrentSummary.status = 'paused' torrentSummary.status = 'paused'
stopTorrenting(torrentSummary.infoHash) stopTorrenting(torrentSummary.infoHash)
playInterfaceSound(config.SOUND_DISABLE) playInterfaceSound('DISABLE')
} }
} }
function deleteTorrent (torrentSummary) { function deleteTorrent (infoHash) {
var infoHash = torrentSummary.infoHash
var torrent = getTorrent(infoHash) var torrent = getTorrent(infoHash)
if (torrent) torrent.destroy() if (torrent) torrent.destroy()
@@ -731,7 +866,7 @@ function deleteTorrent (torrentSummary) {
if (index > -1) state.saved.torrents.splice(index, 1) if (index > -1) state.saved.torrents.splice(index, 1)
saveState() saveState()
state.location.clearForward() // prevent user from going forward to a deleted torrent state.location.clearForward() // prevent user from going forward to a deleted torrent
playInterfaceSound(config.SOUND_DELETE) playInterfaceSound('DELETE')
} }
function toggleSelectTorrent (infoHash) { function toggleSelectTorrent (infoHash) {
@@ -740,6 +875,48 @@ function toggleSelectTorrent (infoHash) {
update() 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 // Set window dimensions to match video dimensions or fill the screen
function setDimensions (dimensions) { function setDimensions (dimensions) {
// Don't modify the window size if it's already maximized // Don't modify the window size if it's already maximized
@@ -785,7 +962,7 @@ function restoreBounds () {
function onError (err) { function onError (err) {
console.error(err.stack || err) console.error(err.stack || err)
playInterfaceSound(config.SOUND_ERROR) playInterfaceSound('ERROR')
state.errors.push({ state.errors.push({
time: new Date().getTime(), time: new Date().getTime(),
message: err.message || err message: err.message || err
@@ -809,12 +986,15 @@ function showDoneNotification (torrent) {
window.focus() window.focus()
} }
playInterfaceSound(config.SOUND_DONE) playInterfaceSound('DONE')
} }
function playInterfaceSound (url) { function playInterfaceSound (name) {
var sound = config[`SOUND_${name}`]
if (!sound) throw new Error('Invalid sound name')
var audio = new window.Audio() var audio = new window.Audio()
audio.volume = 0.3 audio.volume = sound.volume
audio.src = url audio.src = sound.url
audio.play() audio.play()
} }

View File

@@ -14,6 +14,7 @@ module.exports = {
stopCasting, stopCasting,
playPause, playPause,
seek, seek,
setVolume,
isCasting isCasting
} }
@@ -56,15 +57,19 @@ function addAirplayEvents () {}
function pollCastStatus (state) { function pollCastStatus (state) {
if (state.playing.location === 'chromecast') { if (state.playing.location === 'chromecast') {
state.devices.chromecast.status(function (err, status) { state.devices.chromecast.status(function (err, status) {
if (err) return console.log('Error getting %s status: %o', state.playing.location, err) if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.video.isPaused = status.playerState === 'PAUSED' state.playing.isPaused = status.playerState === 'PAUSED'
state.video.currentTime = status.currentTime state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level
update() update()
}) })
} else if (state.playing.location === 'airplay') { } else if (state.playing.location === 'airplay') {
state.devices.airplay.status(function (status) { state.devices.airplay.status(function (status) {
state.video.isPaused = status.rate === 0 state.playing.isPaused = status.rate === 0
state.video.currentTime = status.position state.playing.currentTime = status.position
// TODO: get airplay volume, implementation needed. meanwhile set value in setVolume
// According to docs is in [-30 - 0] (db) range
// should be converted to [0 - 1] using (val / 30 + 1)
update() update()
}) })
} }
@@ -122,7 +127,7 @@ function stopCasting () {
function stoppedCasting () { function stoppedCasting () {
state.playing.location = 'local' state.playing.location = 'local'
state.video.jumpToTime = state.video.currentTime state.playing.jumpToTime = state.playing.currentTime
update() update()
} }
@@ -137,11 +142,11 @@ function playPause () {
var device var device
if (state.playing.location === 'chromecast') { if (state.playing.location === 'chromecast') {
device = state.devices.chromecast device = state.devices.chromecast
if (!state.video.isPaused) device.pause(castCallback) if (!state.playing.isPaused) device.pause(castCallback)
else device.play(null, null, castCallback) else device.play(null, null, castCallback)
} else if (state.playing.location === 'airplay') { } else if (state.playing.location === 'airplay') {
device = state.devices.airplay device = state.devices.airplay
if (!state.video.isPaused) device.rate(0, castCallback) if (!state.playing.isPaused) device.rate(0, castCallback)
else device.rate(1, castCallback) else device.rate(1, castCallback)
} }
} }
@@ -154,6 +159,17 @@ function seek (time) {
} }
} }
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)
}
}
function castCallback () { function castCallback () {
console.log(state.playing.location + ' callback: %o', arguments) console.log(state.playing.location + ' callback: %o', arguments)
} }

View File

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

View File

@@ -4,6 +4,7 @@ function LocationHistory () {
if (!new.target) return new LocationHistory() if (!new.target) return new LocationHistory()
this._history = [] this._history = []
this._forward = [] this._forward = []
this._pending = null
} }
LocationHistory.prototype.go = function (page) { LocationHistory.prototype.go = function (page) {
@@ -13,9 +14,12 @@ LocationHistory.prototype.go = function (page) {
} }
LocationHistory.prototype._go = function (page) { LocationHistory.prototype._go = function (page) {
if (this._pending) return
if (page.onbeforeload) { if (page.onbeforeload) {
this._pending = page
page.onbeforeload((err) => { page.onbeforeload((err) => {
if (err) return if (err) return
this._pending = null
this._history.push(page) this._history.push(page)
}) })
} else { } else {
@@ -59,3 +63,7 @@ LocationHistory.prototype.hasBack = function () {
LocationHistory.prototype.hasForward = function () { LocationHistory.prototype.hasForward = function () {
return this._forward.length > 0 return this._forward.length > 0
} }
LocationHistory.prototype.pending = function () {
return this._pending
}

View File

@@ -1,5 +1,7 @@
module.exports = { module.exports = {
isPlayable: isPlayable isPlayable,
isVideo,
isAudio
} }
var path = require('path') var path = require('path')
@@ -8,6 +10,15 @@ var path = require('path')
* Determines whether a file in a torrent is audio/video we can play * Determines whether a file in a torrent is audio/video we can play
*/ */
function isPlayable (file) { function isPlayable (file) {
var extname = path.extname(file.name) return isVideo(file) || isAudio(file)
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(extname) !== -1 }
function isVideo (file) {
var ext = path.extname(file.name)
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1
}
function isAudio (file) {
var ext = path.extname(file.name)
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1
} }

View File

@@ -6,7 +6,6 @@
<link rel="stylesheet" href="index.css" charset="utf-8"> <link rel="stylesheet" href="index.css" charset="utf-8">
</head> </head>
<body> <body>
<div class="loading"><i class="icon">sync</i></div>
<script async src="index.js"></script> <script async src="index.js"></script>
</body> </body>
</html> </html>

View File

@@ -17,28 +17,32 @@ module.exports = {
bounds: null, /* {x, y, width, height } */ bounds: null, /* {x, y, width, height } */
isFocused: true, isFocused: true,
isFullScreen: false, isFullScreen: false,
title: config.APP_NAME /* current window title */ title: config.APP_WINDOW_TITLE
}, },
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */ selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
playing: { /* the torrent and file we're currently streaming */ playing: { /* the media (audio or video) that we're currently playing */
infoHash: null, /* the info hash of the torrent we're playing */ infoHash: null, /* the info hash of the torrent we're playing */
fileIndex: null, /* the zero-based index within the torrent */ fileIndex: null, /* the zero-based index within the torrent */
location: 'local' /* 'local', 'chromecast', 'airplay' */ location: 'local', /* 'local', 'chromecast', 'airplay' */
}, type: null, /* 'audio' or 'video' */
devices: { /* playback devices like Chromecast and AppleTV */
airplay: null, /* airplay client. finds and manages AppleTVs */
chromecast: null /* chromecast client. finds and manages Chromecasts */
},
video: { /* state of the video player screen */
currentTime: 0, /* seconds */ currentTime: 0, /* seconds */
duration: 1, /* seconds */ duration: 1, /* seconds */
isPaused: true, isPaused: true,
isStalled: false,
lastTimeUpdate: 0, /* Unix time in ms */
mouseStationarySince: 0 /* Unix time in ms */ 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: { dock: {
badge: 0, badge: 0,
progress: 0 progress: 0
}, },
modal: null, /* modal popover */
errors: [], /* user-facing errors */ errors: [], /* user-facing errors */
/* /*
@@ -64,9 +68,10 @@ module.exports = {
{ {
status: 'paused', status: 'paused',
infoHash: '88594aaacbde40ef3e2510c47374ec0aa396c08e', 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', displayName: 'Big Buck Bunny',
posterURL: path.join(config.ROOT_PATH, 'static', 'bigBuckBunny.jpg'), posterURL: 'bigBuckBunny.jpg',
torrentPath: path.join(config.ROOT_PATH, 'static', 'bigBuckBunny.torrent'), torrentPath: 'bigBuckBunny.torrent',
files: [ files: [
{ {
'name': 'bbb_sunflower_1080p_30fps_normal.mp4', 'name': 'bbb_sunflower_1080p_30fps_normal.mp4',
@@ -79,9 +84,10 @@ module.exports = {
{ {
status: 'paused', status: 'paused',
infoHash: '6a9759bffd5c0af65319979fb7832189f4f3c35d', 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', displayName: 'Sintel',
posterURL: path.join(config.ROOT_PATH, 'static', 'sintel.jpg'), posterURL: 'sintel.jpg',
torrentPath: path.join(config.ROOT_PATH, 'static', 'sintel.torrent'), torrentPath: 'sintel.torrent',
files: [ files: [
{ {
'name': 'sintel.mp4', 'name': 'sintel.mp4',
@@ -94,9 +100,10 @@ module.exports = {
{ {
status: 'paused', status: 'paused',
infoHash: '02767050e0be2fd4db9a2ad6c12416ac806ed6ed', 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', displayName: 'Tears of Steel',
posterURL: path.join(config.ROOT_PATH, 'static', 'tearsOfSteel.jpg'), posterURL: 'tearsOfSteel.jpg',
torrentPath: path.join(config.ROOT_PATH, 'static', 'tearsOfSteel.torrent'), torrentPath: 'tearsOfSteel.torrent',
files: [ files: [
{ {
'name': 'tears_of_steel_1080p.webm', 'name': 'tears_of_steel_1080p.webm',
@@ -105,6 +112,70 @@ module.exports = {
'numPieces': 2180 'numPieces': 2180
} }
] ]
},
{
status: 'paused',
infoHash: '6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5',
magnetURI: 'magnet:?xt=urn:btih:6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5&dn=CosmosLaundromatFirstCycle&tr=http%3A%2F%2Fbt1.archive.org%3A6969%2Fannounce&tr=http%3A%2F%2Fbt2.archive.org%3A6969%2Fannounce&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com&tr=wss%3A%2F%2Ftracker.webtorrent.io&ws=http%3A%2F%2Fia601508.us.archive.org%2F14%2Fitems%2F&ws=http%3A%2F%2Fia801508.us.archive.org%2F14%2Fitems%2F&ws=https%3A%2F%2Farchive.org%2Fdownload%2F',
displayName: 'Cosmos Laundromat (Preview)',
posterURL: 'cosmosLaundromat.jpg',
torrentPath: 'cosmosLaundromat.torrent',
files: [
{
'name': 'Cosmos Laundromat - First Cycle (1080p).gif',
'length': 223580,
'numPiecesPresent': 0,
'numPieces': 1
},
{
'name': 'Cosmos Laundromat - First Cycle (1080p).mp4',
'length': 220087570,
'numPiecesPresent': 0,
'numPieces': 421
},
{
'name': 'Cosmos Laundromat - First Cycle (1080p).ogv',
'length': 56832560,
'numPiecesPresent': 0,
'numPieces': 109
},
{
'name': 'CosmosLaundromat-FirstCycle1080p.en.srt',
'length': 3949,
'numPiecesPresent': 0,
'numPieces': 1
},
{
'name': 'CosmosLaundromat-FirstCycle1080p.es.srt',
'length': 3907,
'numPiecesPresent': 0,
'numPieces': 1
},
{
'name': 'CosmosLaundromat-FirstCycle1080p.fr.srt',
'length': 4119,
'numPiecesPresent': 0,
'numPieces': 1
},
{
'name': 'CosmosLaundromat-FirstCycle1080p.it.srt',
'length': 3941,
'numPiecesPresent': 0,
'numPieces': 1
},
{
'name': 'CosmosLaundromatFirstCycle_meta.sqlite',
'length': 11264,
'numPiecesPresent': 0,
'numPieces': 1
},
{
'name': 'CosmosLaundromatFirstCycle_meta.xml',
'length': 1204,
'numPiecesPresent': 0,
'numPieces': 1
}
]
} }
], ],
downloadPath: path.join(os.homedir(), 'Downloads') downloadPath: path.join(os.homedir(), 'Downloads')

9
renderer/util.js Normal file
View File

@@ -0,0 +1,9 @@
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

@@ -8,7 +8,8 @@ var Header = require('./header')
var Player = require('./player') var Player = require('./player')
var TorrentList = require('./torrent-list') var TorrentList = require('./torrent-list')
var Modals = { var Modals = {
'open-torrent-address-modal': require('./open-torrent-address-modal') 'open-torrent-address-modal': require('./open-torrent-address-modal'),
'update-available-modal': require('./update-available-modal')
} }
function App (state, dispatch) { function App (state, dispatch) {
@@ -18,9 +19,9 @@ function App (state, dispatch) {
// * The video is paused // * The video is paused
// * The video is playing remotely on Chromecast or Airplay // * The video is playing remotely on Chromecast or Airplay
var hideControls = state.location.current().url === 'player' && var hideControls = state.location.current().url === 'player' &&
state.video.mouseStationarySince !== 0 && state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.video.mouseStationarySince > 2000 && new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.video.isPaused && !state.playing.isPaused &&
state.playing.location === 'local' state.playing.location === 'local'
// Hide the header on Windows/Linux when in the player // Hide the header on Windows/Linux when in the player
@@ -62,7 +63,7 @@ function App (state, dispatch) {
function getModal () { function getModal () {
if (state.modal) { if (state.modal) {
var contents = Modals[state.modal](state, dispatch) var contents = Modals[state.modal.id](state, dispatch)
return hx` return hx`
<div class='modal'> <div class='modal'>
<div class='modal-background'></div> <div class='modal-background'></div>

View File

@@ -4,7 +4,9 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx') var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
function Header (state, dispatch) { var {dispatcher} = require('../lib/dispatcher')
function Header (state) {
return hx` return hx`
<div class='header'> <div class='header'>
${getTitle()} ${getTitle()}
@@ -12,13 +14,13 @@ function Header (state, dispatch) {
<i.icon.back <i.icon.back
class=${state.location.hasBack() ? '' : 'disabled'} class=${state.location.hasBack() ? '' : 'disabled'}
title='Back' title='Back'
onclick=${() => dispatch('back')}> onclick=${dispatcher('back')}>
chevron_left chevron_left
</i> </i>
<i.icon.forward <i.icon.forward
class=${state.location.hasForward() ? '' : 'disabled'} class=${state.location.hasForward() ? '' : 'disabled'}
title='Forward' title='Forward'
onclick=${() => dispatch('forward')}> onclick=${dispatcher('forward')}>
chevron_right chevron_right
</i> </i>
</div> </div>
@@ -40,7 +42,7 @@ function Header (state, dispatch) {
<i <i
class='icon add' class='icon add'
title='Add torrent' title='Add torrent'
onclick=${() => dispatch('showOpenTorrentFile')}> onclick=${dispatcher('showOpenTorrentFile')}>
add add
</i> </i>
` `

View File

@@ -4,7 +4,9 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx') var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
function OpenTorrentAddressModal (state, dispatch) { var {dispatch} = require('../lib/dispatcher')
function OpenTorrentAddressModal (state) {
return hx` return hx`
<div class='open-torrent-address-modal'> <div class='open-torrent-address-modal'>
<p><strong>Enter torrent address or magnet link</strong></p> <p><strong>Enter torrent address or magnet link</strong></p>
@@ -15,17 +17,17 @@ function OpenTorrentAddressModal (state, dispatch) {
</p> </p>
</div> </div>
` `
}
function handleKeyPress (e) {
if (e.which === 13) handleOK() /* hit Enter to submit */ function handleKeyPress (e) {
} if (e.which === 13) handleOK() /* hit Enter to submit */
}
function handleOK () {
dispatch('exitModal') function handleOK () {
dispatch('addTorrent', document.querySelector('#add-torrent-url').value) dispatch('exitModal')
} dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
}
function handleCancel () {
dispatch('exitModal') function handleCancel () {
} dispatch('exitModal')
} }

View File

@@ -4,58 +4,84 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx') var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var prettyBytes = require('prettier-bytes')
var util = require('../util')
var {dispatch, dispatcher} = require('../lib/dispatcher')
// Shows a streaming video player. Standard features + Chromecast + Airplay // Shows a streaming video player. Standard features + Chromecast + Airplay
function Player (state, dispatch) { function Player (state) {
// Show the video as large as will fit in the window, play immediately // Show the video as large as will fit in the window, play immediately
// If the video is on Chromecast or Airplay, show a title screen instead // If the video is on Chromecast or Airplay, show a title screen instead
var showVideo = state.playing.location === 'local' var showVideo = state.playing.location === 'local'
return hx` return hx`
<div <div
class='player' class='player'
onmousemove=${() => dispatch('videoMouseMoved')}> onmousemove=${dispatcher('mediaMouseMoved')}>
${showVideo ? renderVideo(state, dispatch) : renderCastScreen(state, dispatch)} ${showVideo ? renderMedia(state) : renderCastScreen(state)}
${renderPlayerControls(state, dispatch)} ${renderPlayerControls(state)}
</div> </div>
` `
} }
function renderVideo (state, dispatch) { function renderMedia (state) {
if (!state.server) return
// Unfortunately, play/pause can't be done just by modifying HTML. // Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary // Instead, grab the DOM node and play/pause it if necessary
var videoElement = document.querySelector('video') var mediaType = state.playing.type /* 'audio' or 'video' */
if (videoElement !== null) { var mediaElement = document.querySelector(mediaType) /* get the <video> or <audio> tag */
if (state.video.isPaused && !videoElement.paused) { if (mediaElement !== null) {
videoElement.pause() if (state.playing.isPaused && !mediaElement.paused) {
} else if (!state.video.isPaused && videoElement.paused) { mediaElement.pause()
videoElement.play() } else if (!state.playing.isPaused && mediaElement.paused) {
mediaElement.play()
} }
// When the user clicks or drags on the progress bar, jump to that position // When the user clicks or drags on the progress bar, jump to that position
if (state.video.jumpToTime) { if (state.playing.jumpToTime) {
videoElement.currentTime = state.video.jumpToTime mediaElement.currentTime = state.playing.jumpToTime
state.video.jumpToTime = null state.playing.jumpToTime = null
} }
state.video.currentTime = videoElement.currentTime // Set volume
state.video.duration = videoElement.duration 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
state.playing.volume = mediaElement.volume
} }
// Create the <audio> or <video> tag
var mediaTag = hx`
<div
src='${state.server.localURL}'
ondblclick=${dispatcher('toggleFullScreen')}
onloadedmetadata=${onLoadedMetadata}
onended=${onEnded}
onplay=${dispatcher('mediaPlaying')}
onpause=${dispatcher('mediaPaused')}
onstalling=${dispatcher('mediaStalled')}
ontimeupdate=${dispatcher('mediaTimeUpdate')}
autoplay>
</div>
`
mediaTag.tagName = mediaType
// Show the media.
return hx` return hx`
<div <div
class='letterbox' class='letterbox'
onmousemove=${() => dispatch('videoMouseMoved')}> onmousemove=${dispatcher('mediaMouseMoved')}>
<video ${mediaTag}
src='${state.server.localURL}' ${renderOverlay(state)}
ondblclick=${() => dispatch('toggleFullScreen')}
onloadedmetadata=${onLoadedMetadata}
onended=${onEnded}
onplay=${() => dispatch('videoPlaying')}
onpause=${() => dispatch('videoPaused')}
autoplay>
</video>
</div> </div>
` `
// As soon as the video loads enough to know the video dimensions, resize the window // As soon as the video loads enough to know the video dimensions, resize the window
function onLoadedMetadata (e) { function onLoadedMetadata (e) {
if (mediaType !== 'video') return
var video = e.target var video = e.target
var dimensions = { var dimensions = {
width: video.videoWidth, width: video.videoWidth,
@@ -66,23 +92,100 @@ function renderVideo (state, dispatch) {
// When the video completes, pause the video instead of looping // When the video completes, pause the video instead of looping
function onEnded (e) { function onEnded (e) {
state.video.isPaused = true state.playing.isPaused = true
} }
} }
function renderCastScreen (state, dispatch) { function renderOverlay (state) {
var elems = []
var audioMetadataElem = renderAudioMetadata(state)
var spinnerElem = renderLoadingSpinner(state)
if (audioMetadataElem) elems.push(audioMetadataElem)
if (spinnerElem) elems.push(spinnerElem)
// Video fills the window, centered with black bars if necessary
// Audio gets a static poster image and a summary of the file metadata.
var style
if (state.playing.type === 'audio') {
style = { backgroundImage: cssBackgroundImagePoster(state) }
} else if (elems.length !== 0) {
style = { backgroundImage: cssBackgroundImageDarkGradient() }
} else {
return /* Video, not audio, and it isn't stalled, so no spinner. No overlay needed. */
}
return hx`
<div class='media-overlay-background' style=${style}>
<div class='media-overlay'>${elems}</div>
</div>
`
}
function renderAudioMetadata (state) {
if (!state.playing.audioInfo) return
var info = state.playing.audioInfo
// Get audio track info
var title = info.title
if (!title) {
var torrentSummary = getPlayingTorrentSummary(state)
title = torrentSummary.files[state.playing.fileIndex].name
}
var artist = info.artist && info.artist[0]
var album = info.album
if (album && info.year && !album.includes(info.year)) {
album += ' (' + info.year + ')'
}
var track
if (info.track && info.track.no && info.track.of) {
track = info.track.no + ' of ' + info.track.of
}
// Show a small info box in the middle of the screen with title/album/artist/etc
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 artist/etc info, if available. Otherwise, center the title
var emptyLabel = hx`<label></label>`
elems.unshift(hx`<div class='audio-title'>${elems.length ? emptyLabel : undefined}${title}</div>`)
return hx`<div class='audio-metadata'>${elems}</div>`
}
function renderLoadingSpinner (state) {
if (state.playing.isPaused) return
var isProbablyStalled = state.playing.isStalled ||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
if (!isProbablyStalled) return
var torrentSummary = getPlayingTorrentSummary(state)
var torrent = state.client.get(torrentSummary.infoHash)
var file = torrentSummary.files[state.playing.fileIndex]
var progress = 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'>${progress}%</span> downloaded,
<span>↓ ${prettyBytes(torrent.downloadSpeed || 0)}/s</span>
<span>↑ ${prettyBytes(torrent.uploadSpeed || 0)}/s</span>
</div>
</div>
`
}
function renderCastScreen (state) {
var isChromecast = state.playing.location.startsWith('chromecast') var isChromecast = state.playing.location.startsWith('chromecast')
var isAirplay = state.playing.location.startsWith('airplay') var isAirplay = state.playing.location.startsWith('airplay')
var isStarting = state.playing.location.endsWith('-pending') var isStarting = state.playing.location.endsWith('-pending')
if (!isChromecast && !isAirplay) throw new Error('Unimplemented cast type') if (!isChromecast && !isAirplay) throw new Error('Unimplemented cast type')
// Show a nice title image, if possible // Show a nice title image, if possible
var style = {} var style = {
var infoHash = state.playing.infoHash backgroundImage: cssBackgroundImagePoster(state)
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === infoHash)
if (torrentSummary && torrentSummary.posterURL) {
var cleanURL = torrentSummary.posterURL.replace(/\\/g, '/')
style.backgroundImage = `radial-gradient(circle at center, rgba(0,0,0,0.4) 0%,rgba(0,0,0,1) 100%), url(${cleanURL})`
} }
// Show whether we're connected to Chromecast / Airplay // Show whether we're connected to Chromecast / Airplay
@@ -98,8 +201,27 @@ function renderCastScreen (state, dispatch) {
` `
} }
function renderPlayerControls (state, dispatch) { // Returns the CSS background-image string for a poster image + dark vignette
var positionPercent = 100 * state.video.currentTime / state.video.duration function cssBackgroundImagePoster (state) {
var torrentSummary = getPlayingTorrentSummary(state)
if (!torrentSummary || !torrentSummary.posterURL) return ''
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
var cleanURL = posterURL.replace(/\\/g, '/')
return cssBackgroundImageDarkGradient() + `, url(${cleanURL})`
}
function cssBackgroundImageDarkGradient () {
return 'radial-gradient(circle at center, ' +
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
}
function getPlayingTorrentSummary (state) {
var infoHash = state.playing.infoHash
return state.saved.torrents.find((x) => x.infoHash === infoHash)
}
function renderPlayerControls (state) {
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' } var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
var elements = [ var elements = [
@@ -115,7 +237,7 @@ function renderPlayerControls (state, dispatch) {
`, `,
hx` hx`
<i class='icon fullscreen' <i class='icon fullscreen'
onclick=${() => dispatch('toggleFullScreen')}> onclick=${dispatcher('toggleFullScreen')}>
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'} ${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
</i> </i>
` `
@@ -128,18 +250,18 @@ function renderPlayerControls (state, dispatch) {
if (isOnChromecast) { if (isOnChromecast) {
chromecastClass = 'active' chromecastClass = 'active'
airplayClass = 'disabled' airplayClass = 'disabled'
chromecastHandler = () => dispatch('stopCasting') chromecastHandler = dispatcher('stopCasting')
airplayHandler = undefined airplayHandler = undefined
} else if (isOnAirplay) { } else if (isOnAirplay) {
chromecastClass = 'disabled' chromecastClass = 'disabled'
airplayClass = 'active' airplayClass = 'active'
chromecastHandler = undefined chromecastHandler = undefined
airplayHandler = () => dispatch('stopCasting') airplayHandler = dispatcher('stopCasting')
} else { } else {
chromecastClass = '' chromecastClass = ''
airplayClass = '' airplayClass = ''
chromecastHandler = () => dispatch('openChromecast') chromecastHandler = dispatcher('openChromecast')
airplayHandler = () => dispatch('openAirplay') airplayHandler = dispatcher('openAirplay')
} }
if (state.devices.chromecast || isOnChromecast) { if (state.devices.chromecast || isOnChromecast) {
elements.push(hx` elements.push(hx`
@@ -165,7 +287,7 @@ function renderPlayerControls (state, dispatch) {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
elements.push(hx` elements.push(hx`
<i.icon.back <i.icon.back
onclick=${() => dispatch('back')}> onclick=${dispatcher('back')}>
chevron_left chevron_left
</i> </i>
`) `)
@@ -173,8 +295,8 @@ function renderPlayerControls (state, dispatch) {
// Finally, the big button in the center plays or pauses the video // Finally, the big button in the center plays or pauses the video
elements.push(hx` elements.push(hx`
<i class='icon play-pause' onclick=${() => dispatch('playPause')}> <i class='icon play-pause' onclick=${dispatcher('playPause')}>
${state.video.isPaused ? 'play_arrow' : 'pause'} ${state.playing.isPaused ? 'play_arrow' : 'pause'}
</i> </i>
`) `)
@@ -182,10 +304,10 @@ function renderPlayerControls (state, dispatch) {
// Handles a click or drag to scrub (jump to another position in the video) // Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) { function handleScrub (e) {
dispatch('videoMouseMoved') dispatch('mediaMouseMoved')
var windowWidth = document.querySelector('body').clientWidth var windowWidth = document.querySelector('body').clientWidth
var fraction = e.clientX / windowWidth var fraction = e.clientX / windowWidth
var position = fraction * state.video.duration /* seconds */ var position = fraction * state.playing.duration /* seconds */
dispatch('playbackJump', position) dispatch('playbackJump', position)
} }
} }
@@ -197,22 +319,23 @@ function renderLoadingBar (state) {
if (torrent === null) { if (torrent === null) {
return [] return []
} }
var file = torrent.files[state.playing.fileIndex]
// Find all contiguous parts of the torrent which are loaded // Find all contiguous parts of the torrent which are loaded
var parts = [] var parts = []
var lastPartPresent = false var lastPartPresent = false
var numParts = torrent.pieces.length var numParts = file._endPiece - file._startPiece + 1
for (var i = 0; i < numParts; i++) { for (var i = file._startPiece; i <= file._endPiece; i++) {
var partPresent = torrent.bitfield.get(i) var partPresent = torrent.bitfield.get(i)
if (partPresent && !lastPartPresent) { if (partPresent && !lastPartPresent) {
parts.push({start: i, count: 1}) parts.push({start: i - file._startPiece, count: 1})
} else if (partPresent) { } else if (partPresent) {
parts[parts.length - 1].count++ parts[parts.length - 1].count++
} }
lastPartPresent = partPresent lastPartPresent = partPresent
} }
// Output an list of rectangles to show loading progress // Output some bars to show which parts of the file are loaded
return hx` return hx`
<div class='loading-bar'> <div class='loading-bar'>
${parts.map(function (part) { ${parts.map(function (part) {

View File

@@ -5,9 +5,12 @@ var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var TorrentPlayer = require('../lib/torrent-player') var util = require('../util')
function TorrentList (state, dispatch) { var TorrentPlayer = require('../lib/torrent-player')
var {dispatcher} = require('../lib/dispatcher')
function TorrentList (state) {
var torrentRows = state.saved.torrents.map( var torrentRows = state.saved.torrents.map(
(torrentSummary) => renderTorrent(torrentSummary)) (torrentSummary) => renderTorrent(torrentSummary))
return hx` return hx`
@@ -24,7 +27,9 @@ function TorrentList (state, dispatch) {
function renderTorrent (torrentSummary) { function renderTorrent (torrentSummary) {
// Get ephemeral data (like progress %) directly from the WebTorrent handle // Get ephemeral data (like progress %) directly from the WebTorrent handle
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var torrent = state.client.torrents.find((x) => x.infoHash === infoHash) var torrent = state.client
? state.client.torrents.find((x) => x.infoHash === infoHash)
: null
var isSelected = state.selectedInfoHash === infoHash var isSelected = state.selectedInfoHash === infoHash
// Background image: show some nice visuals, like a frame from the movie, if possible // Background image: show some nice visuals, like a frame from the movie, if possible
@@ -33,9 +38,10 @@ function TorrentList (state, dispatch) {
var gradient = isSelected var gradient = isSelected
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)' ? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)' : 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
var posterURL = util.getAbsoluteStaticPath(torrentSummary.posterURL)
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron): // Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
// Backslashes in URLS in CSS cause bizarre string encoding issues // Backslashes in URLS in CSS cause bizarre string encoding issues
var cleanURL = torrentSummary.posterURL.replace(/\\/g, '/') var cleanURL = posterURL.replace(/\\/g, '/')
style.backgroundImage = gradient + `, url('${cleanURL}')` style.backgroundImage = gradient + `, url('${cleanURL}')`
} }
@@ -47,7 +53,9 @@ function TorrentList (state, dispatch) {
if (isSelected) classes.push('selected') if (isSelected) classes.push('selected')
classes = classes.join(' ') classes = classes.join(' ')
return hx` return hx`
<div style=${style} class=${classes} onclick=${() => dispatch('toggleSelectTorrent', infoHash)}> <div style=${style} class=${classes}
oncontextmenu=${dispatcher('openTorrentContextMenu', infoHash)}
onclick=${dispatcher('toggleSelectTorrent', infoHash)}>
${renderTorrentMetadata(torrent, torrentSummary)} ${renderTorrentMetadata(torrent, torrentSummary)}
${renderTorrentButtons(torrentSummary)} ${renderTorrentButtons(torrentSummary)}
${isSelected ? renderTorrentDetails(torrent, torrentSummary) : ''} ${isSelected ? renderTorrentDetails(torrent, torrentSummary) : ''}
@@ -103,9 +111,12 @@ function TorrentList (state, dispatch) {
// Download button toggles between torrenting (DL/seed) and paused // Download button toggles between torrenting (DL/seed) and paused
// Play button starts streaming the torrent immediately, unpausing if needed // Play button starts streaming the torrent immediately, unpausing if needed
function renderTorrentButtons (torrentSummary) { function renderTorrentButtons (torrentSummary) {
var playIcon, playTooltip var infoHash = torrentSummary.infoHash
var playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'unplayable') { if (torrentSummary.playStatus === 'unplayable') {
playIcon = 'warning' playIcon = 'play_arrow'
playClass = 'disabled'
playTooltip = 'Sorry, WebTorrent can\'t play any of the files in this torrent. ' + 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.' 'View details and click on individual files to open them in another program.'
} else if (torrentSummary.playStatus === 'timeout') { } else if (torrentSummary.playStatus === 'timeout') {
@@ -131,34 +142,30 @@ function TorrentList (state, dispatch) {
return hx` return hx`
<div class='buttons'> <div class='buttons'>
<i.btn.icon.play <i.btn.icon.play
title='${playTooltip}' title=${playTooltip}
onclick=${(e) => handleButton('play', e)}> class=${playClass}
onclick=${dispatcher('play', infoHash)}>
${playIcon} ${playIcon}
</i> </i>
<i.btn.icon.download <i.btn.icon.download
class='${torrentSummary.status}' class=${torrentSummary.status}
title='${downloadTooltip}' title=${downloadTooltip}
onclick=${(e) => handleButton('toggleTorrent', e)}> onclick=${dispatcher('toggleTorrent', infoHash)}>
${downloadIcon} ${downloadIcon}
</i> </i>
<i <i
class='icon delete' class='icon delete'
title='Remove torrent' title='Remove torrent'
onclick=${(e) => handleButton('deleteTorrent', e)}> onclick=${dispatcher('deleteTorrent', infoHash)}>
close close
</i> </i>
</div> </div>
` `
function handleButton (action, e) {
// Prevent propagation so that we don't select/unselect the torrent
e.stopPropagation()
dispatch(action, torrentSummary)
}
} }
// Show files, per-file download status and play buttons, and so on // Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrent, torrentSummary) { function renderTorrentDetails (torrent, torrentSummary) {
var infoHash = torrentSummary.infoHash
var filesElement var filesElement
if (!torrentSummary.files) { if (!torrentSummary.files) {
// We don't know what files this torrent contains // We don't know what files this torrent contains
@@ -173,7 +180,10 @@ function TorrentList (state, dispatch) {
filesElement = hx` filesElement = hx`
<div class='files'> <div class='files'>
<strong>Files</strong> <strong>Files</strong>
<span class='open-folder' onclick=${handleOpenFolder}>Open folder</span> <span class='open-folder'
onclick=${dispatcher('openFolder', infoHash)}>
Open folder
</span>
<table> <table>
${fileRows} ${fileRows}
</table> </table>
@@ -186,11 +196,6 @@ function TorrentList (state, dispatch) {
${filesElement} ${filesElement}
</div> </div>
` `
function handleOpenFolder (e) {
e.stopPropagation()
dispatch('openFolder', torrentSummary)
}
} }
// Show a single torrentSummary file in the details view for a single torrent // Show a single torrentSummary file in the details view for a single torrent
@@ -200,15 +205,20 @@ function TorrentList (state, dispatch) {
var progress = Math.round(100 * file.numPiecesPresent / (file.numPieces || 0)) + '%' var progress = Math.round(100 * file.numPiecesPresent / (file.numPieces || 0)) + '%'
// Second, render the file as a table row // Second, render the file as a table row
var infoHash = torrentSummary.infoHash
var icon var icon
var rowClass = '' var rowClass = ''
if (state.playing.infoHash === torrentSummary.infoHash && state.playing.fileIndex === index) { var handleClick
if (state.playing.infoHash === infoHash && state.playing.fileIndex === index) {
icon = 'pause_arrow' /* playing? add option to pause */ icon = 'pause_arrow' /* playing? add option to pause */
handleClick = undefined // TODO: pause audio
} else if (TorrentPlayer.isPlayable(file)) { } else if (TorrentPlayer.isPlayable(file)) {
icon = 'play_arrow' /* playable? add option to play */ icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index)
} else { } else {
icon = 'description' /* file icon, opens in OS default app */ icon = 'description' /* file icon, opens in OS default app */
rowClass = isDone ? '' : 'disabled' rowClass = isDone ? '' : 'disabled'
handleClick = dispatcher('openFile', infoHash, index)
} }
return hx` return hx`
<tr onclick=${handleClick} class='${rowClass}'> <tr onclick=${handleClick} class='${rowClass}'>
@@ -220,17 +230,5 @@ function TorrentList (state, dispatch) {
<td class='col-size'>${prettyBytes(file.length)}</td> <td class='col-size'>${prettyBytes(file.length)}</td>
</tr> </tr>
` `
// Finally, let the user click on the row to play media or open files
function handleClick (e) {
e.stopPropagation()
if (icon === 'pause_arrow') {
throw new Error('Unimplemented') // TODO: pause audio
} else if (icon === 'play_arrow') {
dispatch('play', torrentSummary, index)
} else if (isDone) {
dispatch('openFile', torrentSummary, index)
}
}
} }
} }

View File

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

BIN
static/WebTorrentSmall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
static/cosmosLaundromat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

BIN
static/loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -10,7 +10,24 @@ Icon=webtorrent
Terminal=false Terminal=false
Path=$APP_PATH Path=$APP_PATH
Exec=$EXEC_PATH %U Exec=$EXEC_PATH %U
TryExec=$EXEC_PATH TryExec=$TRY_EXEC_PATH
StartupNotify=false StartupNotify=false
Categories=Network;FileTransfer;P2P; Categories=Network;FileTransfer;P2P;
MimeType=application/x-bittorrent;x-scheme-handler/magnet; MimeType=application/x-bittorrent;x-scheme-handler/magnet;
Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
[Desktop Action CreateNewTorrent]
Name=Create New Torrent...
Exec=$EXEC_PATH -n
Path=$APP_PATH
[Desktop Action OpenTorrentFile]
Name=Open Torrent File...
Exec=$EXEC_PATH -o
Path=$APP_PATH
[Desktop Action OpenTorrentAddress]
Name=Open Torrent Address...
Exec=$EXEC_PATH -u
Path=$APP_PATH