Compare commits

..

147 Commits

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

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

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

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

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

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

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

Instead of:

var autoUpdater = electron.autoUpdater

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

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

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

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

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

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

After this PR:

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

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

Differences:

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

View File

@@ -11,5 +11,12 @@
- Dan Flettre <fletd01@yahoo.com> - Dan Flettre <fletd01@yahoo.com>
- Liam Gray <liam.r.gray@gmail.com> - Liam Gray <liam.r.gray@gmail.com>
- grunjol <grunjol@argenteam.net> - grunjol <grunjol@argenteam.net>
- Rémi Jouannet <remijouannet@users.noreply.github.com>
- Evan Miller <miller.evan815@gmail.com>
- Alex <alxmorais8@msn.com>
- Diego Rodríguez Baquero <diegorbaquero@gmail.com>
- Karlo Luis Martinez Martos <karlo.luis.m@gmail.com>
- gabriel <furstenheim@gmail.com>
- Rolando Guedes <rolando.guedes@3gnt.net>
#### Generated by bin/update-authors.sh. #### Generated by bin/update-authors.sh.

View File

@@ -1,16 +1,83 @@
# WebTorrent Desktop Version History # WebTorrent Desktop Version History
## UNRELEASED ## v0.5.0 - 2016-05-17
### Added ### Added
- Select/deselect individual files to torrent.
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
- "Add Subtitle File..." menu item.
### Changed ### Changed
- Use Squirrel.Windows 1.3.0 - When manually adding subtitle track(s), always switch to the new track.
- Fix installing when the app is already installed
- Don't kill unrelated processes on uninstall
### Fixed ### Fixed
- Magnet links throw exception on app launch. (OS X)
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
- Missing 'About WebTorrent' menu item. (Windows)
- Rare exception. ("Cannot create BrowserWindow before app is ready")
## v0.4.0 - 2016-05-13
### Added
- Better Windows support!
- Windows 32-bit build.
- Windows Portable App build.
- Windows app signing, for fewer install warnings.
- Better Linux support!
- Linux 32-bit build.
- Subtitles support!
- .srt and .vtt file support.
- Drag-and-drop files on video, or choose from file selector.
- Multiple subtitle files support.
- Stream to VLC when the audio codec is unplayable (e.g. AC3, EAC3).
- "Show in Folder" item in context menu.
- Volume slider, with mute/unmute button.
- New "Create torrent" page to modify:
- Torrent comment.
- Trackers.
- Private torrent flag.
- Use mouse wheel to increase/decrease volume.
- Bounce the Downloads stack when download completes. (OS X)
- New default torrent on first launch: The WIRED CD.
### Changed
- Improve app startup time by 40%.
- UI tweaks: Reduce font size, reduce torrent list item height.
- Add Playback menu for playback-related functionality.
- Fix installing when the app is already installed. (Windows)
- Don't kill unrelated processes on uninstall. (Windows)
- Set "sheet offset" correctly for create torrent dialog. (OS X)
- Remove OS X-style Window menu. (Linux, Windows)
- Remove "Add Fake Airplay/Chromecast" menu items.
### Fixed
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is
temporary. (OS X)
- When fullscreen, make controls use the full window. (OS X)
- Support creating torrents that contain .torrent files.
- Block power save while casting to a remote device.
- Do not block power save when the space key is pressed from the torrent list.
- Support playing .mpg and .ogv extensions in the app.
- Fix video centering for multi-screen setups.
- Show an error when adding a duplicate torrent.
- Show an error when adding an invalid magnet link.
- Do not stop music when tabbing to another program (OS X)
- Properly size the Windows volume mixer icon.
- Default to the user's OS-defined, localized "Downloads" folder.
- Enforce minimimum window size when resizing player to prevent window disappearing.
- Fix rare race condition error on app quit.
- Don't use zero-byte torrent "poster" images.
Thanks to @grunjol, @rguedes, @furstenheim, @karloluis, @DiegoRBaquero, @alxhotel,
@AgentEpsilon, @remijouannet, Rolando Guedes, @dcposch, and @feross for contributing
to this release!
## v0.3.3 - 2016-04-07 ## v0.3.3 - 2016-04-07
### Fixed ### Fixed

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

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

View File

@@ -5,9 +5,9 @@
* Useful for developers. * Useful for developers.
*/ */
var fs = require('fs')
var os = require('os') var os = require('os')
var path = require('path') var path = require('path')
var pathExists = require('path-exists')
var rimraf = require('rimraf') var rimraf = require('rimraf')
var config = require('../config') var config = require('../config')
@@ -15,7 +15,12 @@ var handlers = require('../main/handlers')
rimraf.sync(config.CONFIG_PATH) rimraf.sync(config.CONFIG_PATH)
var tmpPath = path.join(pathExists.sync('/tmp') ? '/tmp' : os.tmpDir(), 'webtorrent') var tmpPath
try {
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
} catch (err) {
tmpPath = path.join(os.tmpDir(), 'webtorrent')
}
rimraf.sync(tmpPath) rimraf.sync(tmpPath)
// Uninstall .torrent file and magnet link handlers // Uninstall .torrent file and magnet link handlers

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

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

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

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

View File

@@ -9,6 +9,7 @@ var electronPackager = require('electron-packager')
var fs = require('fs') var fs = require('fs')
var minimist = require('minimist') var minimist = require('minimist')
var mkdirp = require('mkdirp') var mkdirp = require('mkdirp')
var os = require('os')
var path = require('path') var path = require('path')
var rimraf = require('rimraf') var rimraf = require('rimraf')
var series = require('run-series') var series = require('run-series')
@@ -18,16 +19,6 @@ var config = require('../config')
var pkg = require('../package.json') var pkg = require('../package.json')
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
/*
* Path to folder with the following files:
* - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt)
*/
var CERT_PATH = process.platform === 'win32'
? 'D:'
: '/Volumes/Certs'
var DIST_PATH = path.join(config.ROOT_PATH, 'dist') var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
var argv = minimist(process.argv.slice(2), { var argv = minimist(process.argv.slice(2), {
@@ -64,9 +55,6 @@ function build () {
} }
var all = { var all = {
// Build 64 bit binaries only.
arch: 'x64',
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata // The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
// property on Windows, and `NSHumanReadableCopyright` on OS X. // property on Windows, and `NSHumanReadableCopyright` on OS X.
'app-copyright': config.APP_COPYRIGHT, 'app-copyright': config.APP_COPYRIGHT,
@@ -85,9 +73,9 @@ var all = {
'asar-unpack': 'WebTorrent*', 'asar-unpack': 'WebTorrent*',
// The build version of the application. Maps to the FileVersion metadata property on // The build version of the application. Maps to the FileVersion metadata property on
// Windows, and CFBundleVersion on OS X. We're using the short git hash (e.g. 'e7d837e') // Windows, and CFBundleVersion on OS X. Note: Windows requires the build version to
// Windows requires the build version to start with a number :/ so we stick on a prefix // start with a number. We're using the version of the underlying WebTorrent library.
'build-version': '0-' + cp.execSync('git rev-parse --short HEAD').toString().replace('\n', ''), 'build-version': require('webtorrent/package.json').version,
// The application source directory. // The application source directory.
dir: config.ROOT_PATH, dir: config.ROOT_PATH,
@@ -110,12 +98,16 @@ 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.dependencies['electron-prebuilt'] version: require('electron-prebuilt/package.json').version
} }
var darwin = { var darwin = {
// Build for OS X
platform: 'darwin', platform: 'darwin',
// Build 64 bit binaries only.
arch: 'x64',
// The bundle identifier to use in the application's plist (OS X only). // The bundle identifier to use in the application's plist (OS X only).
'app-bundle-id': 'io.webtorrent.webtorrent', 'app-bundle-id': 'io.webtorrent.webtorrent',
@@ -131,8 +123,12 @@ var darwin = {
} }
var win32 = { var win32 = {
// Build for Windows.
platform: 'win32', platform: 'win32',
// Build 32 bit binaries only.
arch: 'ia32',
// Object hash of application metadata to embed into the executable (Windows only) // Object hash of application metadata to embed into the executable (Windows only)
'version-string': { 'version-string': {
@@ -161,9 +157,10 @@ var win32 = {
} }
var linux = { var linux = {
// Build for Linux.
platform: 'linux', platform: 'linux',
// Build 32/64 bit binaries. // Build 32 and 64 bit binaries.
arch: 'all' arch: 'all'
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option. // Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
@@ -177,7 +174,7 @@ function buildDarwin (cb) {
console.log('OS X: Packaging electron...') console.log('OS X: Packaging electron...')
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) { electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('OS X: Packaged electron. ' + buildPath[0]) console.log('OS X: Packaged electron. ' + buildPath)
var appPath = path.join(buildPath[0], config.APP_NAME + '.app') var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
var contentsPath = path.join(appPath, 'Contents') var contentsPath = path.join(appPath, 'Contents')
@@ -277,7 +274,7 @@ function buildDarwin (cb) {
var inPath = path.join(buildPath[0], config.APP_NAME + '.app') var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
zip(inPath, outPath) zip.zipSync(inPath, outPath)
console.log('OS X: Created zip.') console.log('OS X: Created zip.')
} }
@@ -327,11 +324,24 @@ function buildDarwin (cb) {
function buildWin32 (cb) { function buildWin32 (cb) {
var installer = require('electron-winstaller') var installer = require('electron-winstaller')
console.log('Windows: Packaging electron...') console.log('Windows: Packaging electron...')
/*
* Path to folder with the following files:
* - Windows Authenticode private key and cert (authenticode.p12)
* - Windows Authenticode password file (authenticode.txt)
*/
var CERT_PATH
try {
fs.accessSync('D:')
CERT_PATH = 'D:'
} catch (err) {
CERT_PATH = path.join(os.homedir(), 'Desktop')
}
electronPackager(Object.assign({}, all, win32), function (err, buildPath) { electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('Windows: Packaged electron. ' + buildPath[0]) console.log('Windows: Packaged electron. ' + buildPath)
var signWithParams var signWithParams
if (process.platform === 'win32') { if (process.platform === 'win32') {
@@ -358,6 +368,7 @@ function buildWin32 (cb) {
function packageInstaller (cb) { function packageInstaller (cb) {
console.log('Windows: Creating installer...') console.log('Windows: Creating installer...')
installer.createWindowsInstaller({ installer.createWindowsInstaller({
appDirectory: buildPath[0], appDirectory: buildPath[0],
authors: config.APP_TEAM, authors: config.APP_TEAM,
@@ -376,14 +387,15 @@ function buildWin32 (cb) {
title: config.APP_NAME, title: config.APP_NAME,
usePackageJson: false, usePackageJson: false,
version: pkg.version version: pkg.version
}).then(function () { })
.then(function () {
console.log('Windows: Created installer.') console.log('Windows: Created installer.')
cb(null) cb(null)
}).catch(cb) })
.catch(cb)
} }
function packagePortable (cb) { function packagePortable (cb) {
// Create Windows portable app
console.log('Windows: Creating portable app...') console.log('Windows: Creating portable app...')
var portablePath = path.join(buildPath[0], 'Portable Settings') var portablePath = path.join(buildPath[0], 'Portable Settings')
@@ -391,7 +403,7 @@ function buildWin32 (cb) {
var inPath = path.join(DIST_PATH, path.basename(buildPath[0])) var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
zip(inPath, outPath) zip.zipSync(inPath, outPath)
console.log('Windows: Created portable app.') console.log('Windows: Created portable app.')
cb(null) cb(null)
@@ -403,7 +415,7 @@ function buildLinux (cb) {
console.log('Linux: Packaging electron...') console.log('Linux: Packaging electron...')
electronPackager(Object.assign({}, all, linux), function (err, buildPath) { electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
if (err) return cb(err) if (err) return cb(err)
console.log('Linux: Packaged electron. ' + buildPath[0]) console.log('Linux: Packaged electron. ' + buildPath)
var tasks = [] var tasks = []
buildPath.forEach(function (filesPath) { buildPath.forEach(function (filesPath) {
@@ -455,7 +467,7 @@ function buildLinux (cb) {
var inPath = path.join(DIST_PATH, path.basename(filesPath)) var inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip') var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
zip(inPath, outPath) zip.zipSync(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`) console.log(`Linux: Created ${destArch} zip.`)
cb(null) cb(null)

View File

@@ -6,4 +6,4 @@ npm run package -- --sign
git push git push
git push --tags git push --tags
npm publish npm publish
gh-release ./node_modules/.bin/gh-release

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
var appConfig = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
var fs = require('fs')
var path = require('path') var path = require('path')
var pathExists = require('path-exists')
var APP_NAME = 'WebTorrent' var APP_NAME = 'WebTorrent'
var APP_TEAM = 'The WebTorrent Project' var APP_TEAM = 'The WebTorrent Project'
@@ -17,7 +17,6 @@ module.exports = {
APP_VERSION: APP_VERSION, APP_VERSION: APP_VERSION,
APP_WINDOW_TITLE: APP_NAME + ' (BETA)', APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
AUTO_UPDATE_CHECK_STARTUP_DELAY: 5 * 1000 /* 5 seconds */,
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' + AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update' +
'?version=' + APP_VERSION + '&platform=' + process.platform, '?version=' + APP_VERSION + '&platform=' + process.platform,
@@ -27,9 +26,14 @@ module.exports = {
CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'), CONFIG_POSTER_PATH: path.join(getConfigPath(), 'Posters'),
CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'), CONFIG_TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
DELAYED_INIT: 3000 /* 3 seconds */,
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop', GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master', GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
HOME_PAGE_URL: 'https://webtorrent.io',
IS_PORTABLE: isPortable(), IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(), IS_PRODUCTION: isProduction(),
@@ -53,7 +57,11 @@ function getConfigPath () {
} }
function isPortable () { function isPortable () {
return process.platform === 'win32' && isProduction() && pathExists(PORTABLE_PATH) try {
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
} catch (err) {
return false
}
} }
function isProduction () { function isProduction () {
@@ -61,7 +69,7 @@ function isProduction () {
return false return false
} }
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
return !/\/Electron\.app\/Contents\/MacOS\/Electron$/.test(process.execPath) return !/\/Electron\.app\//.test(process.execPath)
} }
if (process.platform === 'win32') { if (process.platform === 'win32') {
return !/\\electron\.exe$/.test(process.execPath) return !/\\electron\.exe$/.test(process.execPath)

View File

@@ -11,5 +11,4 @@ function init () {
productName: config.APP_NAME, productName: config.APP_NAME,
submitURL: config.CRASH_REPORT_URL submitURL: config.CRASH_REPORT_URL
}) })
console.log('crash reporter started')
} }

View File

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

View File

@@ -1,50 +0,0 @@
module.exports = {
init
}
var electron = require('electron')
var get = require('simple-get')
var config = require('../config')
var log = require('./log')
var windows = require('./windows')
var autoUpdater = electron.autoUpdater
function init () {
autoUpdater.on('error', function (err) {
log.error('App update error: ' + err.message || err)
})
autoUpdater.setFeedURL(config.AUTO_UPDATE_URL)
/*
* We always check for updates on app startup. To keep app startup fast, we delay this
* first check so it happens when there is less going on.
*/
setTimeout(checkForUpdates, config.AUTO_UPDATE_CHECK_STARTUP_DELAY)
autoUpdater.on('checking-for-update', () => log('Checking for app update'))
autoUpdater.on('update-available', () => log('App update available'))
autoUpdater.on('update-not-available', () => log('App update not available'))
autoUpdater.on('update-downloaded', function (e, releaseNotes, releaseName, releaseDate, updateURL) {
log('App update downloaded: ', releaseName, updateURL)
})
}
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

@@ -5,6 +5,8 @@ module.exports = {
var path = require('path') var path = require('path')
var config = require('../config')
function install () { function install () {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
installDarwin() installDarwin()
@@ -42,6 +44,12 @@ function installDarwin () {
function uninstallDarwin () {} function uninstallDarwin () {}
var EXEC_COMMAND = [ process.execPath ]
if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH)
}
function installWin32 () { function installWin32 () {
var Registry = require('winreg') var Registry = require('winreg')
@@ -49,8 +57,8 @@ function installWin32 () {
var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico') var iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, process.execPath) registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, process.execPath) registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND)
/** /**
* To add a protocol handler, the following keys must be added to the Windows registry: * To add a protocol handler, the following keys must be added to the Windows registry:
@@ -108,7 +116,7 @@ function installWin32 () {
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command' key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
}) })
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done) commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
} }
function done (err) { function done (err) {
@@ -169,7 +177,7 @@ function installWin32 () {
hive: Registry.HKCU, hive: Registry.HKCU,
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command' key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
}) })
commandKey.set('', Registry.REG_SZ, '"' + command + '" "%1"', done) commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
} }
function done (err) { function done (err) {
@@ -181,8 +189,8 @@ function installWin32 () {
function uninstallWin32 () { function uninstallWin32 () {
var Registry = require('winreg') var Registry = require('winreg')
unregisterProtocolHandlerWin32('magnet', process.execPath) unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', process.execPath) unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)
function unregisterProtocolHandlerWin32 (protocol, command) { function unregisterProtocolHandlerWin32 (protocol, command) {
getCommand() getCommand()
@@ -193,7 +201,7 @@ function uninstallWin32 () {
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command' key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
}) })
commandKey.get('', function (err, item) { commandKey.get('', function (err, item) {
if (!err && item.value.indexOf(command) >= 0) { if (!err && item.value.indexOf(commandToArgs(command)) >= 0) {
destroyProtocol() destroyProtocol()
} }
}) })
@@ -241,6 +249,10 @@ function uninstallWin32 () {
} }
} }
function commandToArgs (command) {
return command.map((arg) => `"${arg}"`).join(' ')
}
function installLinux () { function installLinux () {
var fs = require('fs-extra') var fs = require('fs-extra')
var os = require('os') var os = require('os')
@@ -260,14 +272,14 @@ function installLinux () {
function writeDesktopFile (err, desktopFile) { function writeDesktopFile (err, desktopFile) {
if (err) return log.error(err.message) if (err) return log.error(err.message)
var appPath = config.IS_PRODUCTION ? path.dirname(process.execPath) : config.ROOT_PATH var appPath = config.IS_PRODUCTION
var execPath = process.execPath + (config.IS_PRODUCTION ? '' : ' \.') ? path.dirname(process.execPath)
var tryExecPath = process.execPath : config.ROOT_PATH
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME) desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath) desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, execPath) desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, tryExecPath) desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
var desktopFilePath = path.join( var desktopFilePath = path.join(
os.homedir(), os.homedir(),

View File

@@ -1,9 +1,10 @@
console.time('init')
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
var ipcMain = electron.ipcMain var ipcMain = electron.ipcMain
var autoUpdater = require('./auto-updater')
var config = require('../config') var config = require('../config')
var crashReporter = require('../crash-reporter') var crashReporter = require('../crash-reporter')
var handlers = require('./handlers') var handlers = require('./handlers')
@@ -12,8 +13,9 @@ var log = require('./log')
var menu = require('./menu') var menu = require('./menu')
var shortcuts = require('./shortcuts') var shortcuts = require('./shortcuts')
var squirrelWin32 = require('./squirrel-win32') var squirrelWin32 = require('./squirrel-win32')
var windows = require('./windows')
var tray = require('./tray') var tray = require('./tray')
var updater = require('./updater')
var windows = require('./windows')
var shouldQuit = false var shouldQuit = false
var argv = sliceArgv(process.argv) var argv = sliceArgv(process.argv)
@@ -41,6 +43,7 @@ function init () {
app.setPath('userData', config.CONFIG_PATH) app.setPath('userData', config.CONFIG_PATH)
} }
var isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false app.isQuitting = false
@@ -52,21 +55,24 @@ function init () {
app.on('will-finish-launching', function () { app.on('will-finish-launching', function () {
crashReporter.init() crashReporter.init()
autoUpdater.init()
}) })
app.on('ready', function () { app.on('ready', function () {
menu.init() isReady = true
windows.createMainWindow() windows.createMainWindow()
windows.createWebTorrentHiddenWindow() windows.createWebTorrentHiddenWindow()
menu.init()
shortcuts.init() shortcuts.init()
tray.init()
handlers.install() // To keep app startup fast, some code is delayed.
setTimeout(delayedInit, config.DELAYED_INIT)
}) })
app.on('ipcReady', function () { app.on('ipcReady', function () {
log('Command line args:', argv) log('Command line args:', argv)
processArgv(argv) processArgv(argv)
console.timeEnd('init')
}) })
app.on('before-quit', function (e) { app.on('before-quit', function (e) {
@@ -80,10 +86,16 @@ function init () {
}) })
app.on('activate', function () { app.on('activate', function () {
windows.createMainWindow() if (isReady) windows.createMainWindow()
}) })
} }
function delayedInit () {
tray.init()
handlers.install()
updater.init()
}
function onOpen (e, torrentId) { function onOpen (e, torrentId) {
e.preventDefault() e.preventDefault()
@@ -120,11 +132,11 @@ function sliceArgv (argv) {
function processArgv (argv) { function processArgv (argv) {
argv.forEach(function (arg) { argv.forEach(function (arg) {
if (arg === '-n') { if (arg === '-n') {
windows.main.send('dispatch', 'showOpenSeedFiles') menu.showOpenSeedFiles()
} else if (arg === '-o') { } else if (arg === '-o') {
windows.main.send('dispatch', 'showOpenTorrentFile') menu.showOpenTorrentFile()
} else if (arg === '-u') { } else if (arg === '-u') {
windows.main.send('showOpenTorrentAddress') menu.showOpenTorrentAddress()
} else if (arg.startsWith('-psn')) { } else if (arg.startsWith('-psn')) {
// Ignore OS X launchd "process serial number" argument // Ignore OS X launchd "process serial number" argument
// More: https://github.com/feross/webtorrent-desktop/issues/214 // More: https://github.com/feross/webtorrent-desktop/issues/214

View File

@@ -6,25 +6,29 @@ var electron = require('electron')
var app = electron.app var app = electron.app
var ipcMain = electron.ipcMain var ipcMain = electron.ipcMain
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') var shortcuts = require('./shortcuts')
var vlc = require('./vlc')
// 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 powerSaveBlockerId = 0
// messages from the main process, to be sent once the WebTorrent process starts
var messageQueueMainToWebTorrent = []
// holds a ChildProcess while we're playing a video in VLC, null otherwise
var vlcProcess
function init () { function init () {
ipcMain.on('ipcReady', function (e) { ipcMain.on('ipcReady', function (e) {
windows.main.show()
app.ipcReady = true app.ipcReady = true
app.emit('ipcReady') app.emit('ipcReady')
windows.main.show()
console.timeEnd('init')
}) })
var messageQueueMainToWebTorrent = []
ipcMain.on('ipcReadyWebTorrent', function (e) { ipcMain.on('ipcReadyWebTorrent', function (e) {
app.ipcReadyWebTorrent = true app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window', log('sending %d queued messages from the main win to the webtorrent window',
@@ -36,14 +40,13 @@ function init () {
}) })
ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile) ipcMain.on('showOpenTorrentFile', menu.showOpenTorrentFile)
ipcMain.on('showOpenSeedFiles', menu.showOpenSeedFiles)
ipcMain.on('setBounds', function (e, bounds, maximize) { ipcMain.on('setBounds', function (e, bounds, maximize) {
setBounds(bounds, maximize) setBounds(bounds, maximize)
}) })
ipcMain.on('setAspectRatio', function (e, aspectRatio, extraSize) { ipcMain.on('setAspectRatio', function (e, aspectRatio) {
setAspectRatio(aspectRatio, extraSize) setAspectRatio(aspectRatio)
}) })
ipcMain.on('setBadge', function (e, text) { ipcMain.on('setBadge', function (e, text) {
@@ -63,26 +66,83 @@ function init () {
}) })
ipcMain.on('openItem', function (e, path) { ipcMain.on('openItem', function (e, path) {
log('opening file or folder: ' + path) log('open item: ' + path)
electron.shell.openItem(path) electron.shell.openItem(path)
}) })
ipcMain.on('showItemInFolder', function (e, path) {
log('show item in folder: ' + path)
electron.shell.showItemInFolder(path)
})
ipcMain.on('blockPowerSave', blockPowerSave) ipcMain.on('blockPowerSave', blockPowerSave)
ipcMain.on('unblockPowerSave', unblockPowerSave) ipcMain.on('unblockPowerSave', unblockPowerSave)
ipcMain.on('onPlayerOpen', function () { ipcMain.on('onPlayerOpen', function () {
menu.onPlayerOpen() menu.onPlayerOpen()
shortcuts.registerPlayerShortcuts() shortcuts.onPlayerOpen()
}) })
ipcMain.on('onPlayerClose', function () { ipcMain.on('onPlayerClose', function () {
menu.onPlayerClose() menu.onPlayerClose()
shortcuts.unregisterPlayerShortcuts() shortcuts.onPlayerOpen()
}) })
ipcMain.on('focusWindow', function (e, windowName) { ipcMain.on('focusWindow', function (e, windowName) {
windows.focusWindow(windows[windowName]) windows.focusWindow(windows[windowName])
}) })
ipcMain.on('downloadFinished', function (e, filePath) {
if (app.dock) {
// Bounces the Downloads stack if the filePath is inside the Downloads folder.
app.dock.downloadFinished(filePath)
}
})
ipcMain.on('checkForVLC', function (e) {
vlc.checkForVLC(function (isInstalled) {
windows.main.send('checkForVLC', isInstalled)
})
})
ipcMain.on('vlcPlay', function (e, url) {
var args = ['--play-and-exit', '--quiet', url]
console.log('Running vlc ' + args.join(' '))
vlc.spawn(args, function (err, proc) {
if (err) windows.main.send('dispatch', 'vlcNotFound')
vlcProcess = proc
// If it works, close the modal after a second
var closeModalTimeout = setTimeout(() =>
windows.main.send('dispatch', 'exitModal'), 1000)
vlcProcess.on('close', function (code) {
clearTimeout(closeModalTimeout)
if (!vlcProcess) return // Killed
console.log('VLC exited with code ', code)
if (code === 0) {
windows.main.send('dispatch', 'backToList')
} else {
windows.main.send('dispatch', 'vlcNotFound')
}
vlcProcess = null
})
vlcProcess.on('error', function (e) {
console.log('VLC error', e)
})
})
})
ipcMain.on('vlcQuit', function () {
if (!vlcProcess) return
console.log('Killing VLC, pid ' + vlcProcess.pid)
vlcProcess.kill('SIGKILL') // kill -9
vlcProcess = null
})
// Capture all events // Capture all events
var oldEmit = ipcMain.emit var oldEmit = ipcMain.emit
ipcMain.emit = function (name, e, ...args) { ipcMain.emit = function (name, e, ...args) {
@@ -142,8 +202,7 @@ function setBounds (bounds, maximize) {
log('setBounds: setting bounds to ' + JSON.stringify(bounds)) log('setBounds: setting bounds to ' + JSON.stringify(bounds))
if (bounds.x === null && bounds.y === null) { if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen // X and Y not specified? By default, center on current screen
var screen = require('screen') var scr = electron.screen.getDisplayMatching(windows.main.getBounds())
var scr = screen.getDisplayMatching(windows.main.getBounds())
bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2) bounds.x = Math.round(scr.bounds.x + scr.bounds.width / 2 - bounds.width / 2)
bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2) bounds.y = Math.round(scr.bounds.y + scr.bounds.height / 2 - bounds.height / 2)
log('setBounds: centered to ' + JSON.stringify(bounds)) log('setBounds: centered to ' + JSON.stringify(bounds))
@@ -154,17 +213,19 @@ function setBounds (bounds, maximize) {
} }
} }
function setAspectRatio (aspectRatio, extraSize) { function setAspectRatio (aspectRatio) {
log('setAspectRatio %o %o', aspectRatio, extraSize) log('setAspectRatio %o', aspectRatio)
if (windows.main) { if (windows.main) {
windows.main.setAspectRatio(aspectRatio, extraSize) windows.main.setAspectRatio(aspectRatio)
} }
} }
// Display string in dock badging area (OS X) // Display string in dock badging area (OS X)
function setBadge (text) { function setBadge (text) {
log('setBadge %s', text) log('setBadge %s', text)
if (app.dock) app.dock.setBadge(String(text)) if (app.dock) {
app.dock.setBadge(String(text))
}
} }
// Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1. // Show progress bar. Valid range is [0, 1]. Remove when < 0; indeterminate when > 1.
@@ -176,13 +237,13 @@ function setProgress (progress) {
} }
function blockPowerSave () { function blockPowerSave () {
powerSaveBlockID = powerSaveBlocker.start('prevent-display-sleep') powerSaveBlockerId = electron.powerSaveBlocker.start('prevent-display-sleep')
log('blockPowerSave %d', powerSaveBlockID) log('blockPowerSave %d', powerSaveBlockerId)
} }
function unblockPowerSave () { function unblockPowerSave () {
if (powerSaveBlocker.isStarted(powerSaveBlockID)) { if (electron.powerSaveBlocker.isStarted(powerSaveBlockerId)) {
powerSaveBlocker.stop(powerSaveBlockID) electron.powerSaveBlocker.stop(powerSaveBlockerId)
log('unblockPowerSave %d', powerSaveBlockID) log('unblockPowerSave %d', powerSaveBlockerId)
} }
} }

View File

@@ -1,11 +1,14 @@
module.exports = { module.exports = {
init, init,
onPlayerClose,
onPlayerOpen,
onToggleFullScreen, onToggleFullScreen,
onWindowHide, onWindowHide,
onWindowShow, onWindowShow,
onPlayerOpen,
onPlayerClose, // TODO: move these out of menu.js -- they don't belong here
showOpenSeedFiles, showOpenSeedFiles,
showOpenTorrentAddress,
showOpenTorrentFile, showOpenTorrentFile,
toggleFullScreen toggleFullScreen
} }
@@ -18,20 +21,26 @@ var config = require('../config')
var log = require('./log') var log = require('./log')
var windows = require('./windows') var windows = require('./windows')
var appMenu, dockMenu var appMenu
function init () { function init () {
appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate()) appMenu = electron.Menu.buildFromTemplate(getAppMenuTemplate())
electron.Menu.setApplicationMenu(appMenu) electron.Menu.setApplicationMenu(appMenu)
dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate()) if (app.dock) {
if (app.dock) app.dock.setMenu(dockMenu) var dockMenu = electron.Menu.buildFromTemplate(getDockMenuTemplate())
app.dock.setMenu(dockMenu)
}
} }
function toggleFullScreen (flag) { function toggleFullScreen (flag) {
log('toggleFullScreen %s', flag) log('toggleFullScreen %s', flag)
if (windows.main && windows.main.isVisible()) { if (windows.main && windows.main.isVisible()) {
flag = flag != null ? flag : !windows.main.isFullScreen() flag = flag != null ? flag : !windows.main.isFullScreen()
if (flag) {
// Allows the window to use the full screen in fullscreen mode (OS X).
windows.main.setAspectRatio(0)
}
windows.main.setFullScreen(flag) windows.main.setFullScreen(flag)
} }
} }
@@ -46,6 +55,25 @@ function toggleFloatOnTop (flag) {
} }
} }
function toggleDevTools () {
log('toggleDevTools')
if (windows.main) {
windows.main.toggleDevTools()
}
}
function showWebTorrentWindow () {
log('showWebTorrentWindow')
windows.webtorrent.show()
windows.webtorrent.webContents.openDevTools({ detach: true })
}
function playPause () {
if (windows.main) {
windows.main.send('dispatch', 'playPause')
}
}
function increaseVolume () { function increaseVolume () {
if (windows.main) { if (windows.main) {
windows.main.send('dispatch', 'changeVolume', 0.1) windows.main.send('dispatch', 'changeVolume', 0.1)
@@ -58,18 +86,12 @@ function decreaseVolume () {
} }
} }
function toggleDevTools () { function openSubtitles () {
log('toggleDevTools')
if (windows.main) { if (windows.main) {
windows.main.toggleDevTools() windows.main.send('dispatch', 'openSubtitles')
} }
} }
function showWebTorrentWindow () {
windows.webtorrent.show()
windows.webtorrent.webContents.openDevTools({ detach: true })
}
function onWindowShow () { function onWindowShow () {
log('onWindowShow') log('onWindowShow')
getMenuItem('Full Screen').enabled = true getMenuItem('Full Screen').enabled = true
@@ -83,13 +105,19 @@ function onWindowHide () {
} }
function onPlayerOpen () { function onPlayerOpen () {
log('onPlayerOpen')
getMenuItem('Play/Pause').enabled = true
getMenuItem('Increase Volume').enabled = true getMenuItem('Increase Volume').enabled = true
getMenuItem('Decrease Volume').enabled = true getMenuItem('Decrease Volume').enabled = true
getMenuItem('Add Subtitles File...').enabled = true
} }
function onPlayerClose () { function onPlayerClose () {
log('onPlayerClose')
getMenuItem('Play/Pause').enabled = false
getMenuItem('Increase Volume').enabled = false getMenuItem('Increase Volume').enabled = false
getMenuItem('Decrease Volume').enabled = false getMenuItem('Decrease Volume').enabled = false
getMenuItem('Add Subtitles File...').enabled = false
} }
function onToggleFullScreen (isFullScreen) { function onToggleFullScreen (isFullScreen) {
@@ -108,17 +136,29 @@ function getMenuItem (label) {
} }
} }
// Prompts the user for a file or folder, then makes a torrent out of the data // Prompts the user for a file, then creates a torrent. Only allows a single file
// selection.
function showOpenSeedFile () {
electron.dialog.showOpenDialog({
title: 'Select a file for the torrent file.',
properties: [ 'openFile' ]
}, function (selectedPaths) {
if (!Array.isArray(selectedPaths)) return
var selectedPath = selectedPaths[0]
windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
})
}
// Prompts the user for a file or directory, then creates a torrent. Only allows a single
// selection. To create a multi-file torrent, the user must select a directory.
function showOpenSeedFiles () { function showOpenSeedFiles () {
// Allow only a single selection
// To create a multi-file torrent, the user must select a folder
electron.dialog.showOpenDialog({ electron.dialog.showOpenDialog({
title: 'Select a file or folder for the torrent file.', title: 'Select a file or folder for the torrent file.',
properties: [ 'openFile', 'openDirectory' ] properties: [ 'openFile', 'openDirectory' ]
}, function (filenames) { }, function (selectedPaths) {
if (!Array.isArray(filenames)) return if (!Array.isArray(selectedPaths)) return
var fileOrFolder = filenames[0] var selectedPath = selectedPaths[0]
windows.main.send('dispatch', 'showCreateTorrent', fileOrFolder) windows.main.send('dispatch', 'showCreateTorrent', selectedPath)
}) })
} }
@@ -128,10 +168,10 @@ function showOpenTorrentFile () {
title: 'Select a .torrent file to open.', title: 'Select a .torrent file to open.',
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }], filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
properties: [ 'openFile', 'multiSelections' ] properties: [ 'openFile', 'multiSelections' ]
}, function (filenames) { }, function (selectedPaths) {
if (!Array.isArray(filenames)) return if (!Array.isArray(selectedPaths)) return
filenames.forEach(function (filename) { selectedPaths.forEach(function (selectedPath) {
windows.main.send('dispatch', 'addTorrent', filename) windows.main.send('dispatch', 'addTorrent', selectedPath)
}) })
}) })
} }
@@ -142,44 +182,38 @@ function showOpenTorrentAddress () {
} }
function getAppMenuTemplate () { function getAppMenuTemplate () {
var fileMenu = [
{
label: 'Create New Torrent...',
accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
},
{
type: 'separator'
},
{
label: process.platform === 'windows' ? 'Close' : 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
// File > Quit for Linux users with distros where the system tray is broken
if (process.platform === 'linux') {
fileMenu.push({
label: 'Quit',
click: () => app.quit()
})
}
var template = [ var template = [
{ {
label: 'File', label: 'File',
submenu: fileMenu submenu: [
{
label: process.platform === 'darwin'
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N',
click: showOpenSeedFiles
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: showOpenTorrentFile
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: showOpenTorrentAddress
},
{
type: 'separator'
},
{
label: process.platform === 'win32'
? 'Close'
: 'Close Window',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
}, },
{ {
label: 'Edit', label: 'Edit',
@@ -225,21 +259,6 @@ 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: [
@@ -262,13 +281,36 @@ function getAppMenuTemplate () {
] ]
}, },
{ {
label: 'Window', label: 'Playback',
role: 'window',
submenu: [ submenu: [
{ {
label: 'Minimize', label: 'Play/Pause',
accelerator: 'CmdOrCtrl+M', accelerator: 'CmdOrCtrl+P',
role: 'minimize' click: playPause,
enabled: false
},
{
type: 'separator'
},
{
label: 'Increase Volume',
accelerator: 'CmdOrCtrl+Up',
click: increaseVolume,
enabled: false
},
{
label: 'Decrease Volume',
accelerator: 'CmdOrCtrl+Down',
click: decreaseVolume,
enabled: false
},
{
type: 'separator'
},
{
label: 'Add Subtitles File...',
click: openSubtitles,
enabled: false
} }
] ]
}, },
@@ -278,7 +320,7 @@ function getAppMenuTemplate () {
submenu: [ submenu: [
{ {
label: 'Learn more about ' + config.APP_NAME, label: 'Learn more about ' + config.APP_NAME,
click: () => electron.shell.openExternal('https://webtorrent.io') click: () => electron.shell.openExternal(config.HOME_PAGE_URL)
}, },
{ {
label: 'Contribute on GitHub', label: 'Contribute on GitHub',
@@ -289,14 +331,14 @@ function getAppMenuTemplate () {
}, },
{ {
label: 'Report an Issue...', label: 'Report an Issue...',
click: () => electron.shell.openExternal(config.GITHUB_URL + '/issues') click: () => electron.shell.openExternal(config.GITHUB_URL_ISSUES)
} }
] ]
} }
] ]
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// WebTorrent menu (OS X) // Add WebTorrent app menu (OS X)
template.unshift({ template.unshift({
label: config.APP_NAME, label: config.APP_NAME,
submenu: [ submenu: [
@@ -340,17 +382,35 @@ function getAppMenuTemplate () {
] ]
}) })
// Window menu (OS X) // Add Window menu (OS X)
template[4].submenu.push( template.splice(5, 0, {
{ label: 'Window',
type: 'separator' role: 'window',
}, submenu: [
{ {
label: 'Bring All to Front', label: 'Minimize',
role: 'front' accelerator: 'CmdOrCtrl+M',
} role: 'minimize'
) },
} else { {
type: 'separator'
},
{
label: 'Bring All to Front',
role: 'front'
}
]
})
}
// In Linux and Windows it is not possible to open both folders and files
if (process.platform === 'linux' || process.platform === 'win32') {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: showOpenSeedFile
})
// Help menu (Windows, Linux) // Help menu (Windows, Linux)
template[4].submenu.push( template[4].submenu.push(
{ {
@@ -362,6 +422,15 @@ function getAppMenuTemplate () {
} }
) )
} }
// Add "File > Quit" menu item so Linux distros where the system tray icon is missing
// will have a way to quit the app.
if (process.platform === 'linux') {
// File menu (Linux)
template[0].submenu.push({
label: 'Quit',
click: () => app.quit()
})
}
return template return template
} }

View File

@@ -1,29 +1,34 @@
module.exports = { module.exports = {
init, init,
registerPlayerShortcuts, onPlayerClose,
unregisterPlayerShortcuts onPlayerOpen
} }
var electron = require('electron') var electron = require('electron')
var localShortcut = require('electron-localshortcut')
var globalShortcut = electron.globalShortcut
var menu = require('./menu') var menu = require('./menu')
var windows = require('./windows') var windows = require('./windows')
function init () { function init () {
// ⌘+Shift+F is an alternative fullscreen shortcut to the ones defined in menu.js. var localShortcut = require('electron-localshortcut')
// Electron does not support multiple accelerators for a single menu item, so this
// is registered separately here. // Alternate shortcuts. Most shortcuts are registered in menu,js, but Electron
// does not support multiple shortcuts for a single menu item.
localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen) localShortcut.register('CmdOrCtrl+Shift+F', menu.toggleFullScreen)
localShortcut.register('Space', () => windows.main.send('dispatch', 'playPause'))
// Hidden shortcuts, i.e. not shown in the menu
localShortcut.register('Esc', () => windows.main.send('dispatch', 'escapeBack'))
} }
function registerPlayerShortcuts () { function onPlayerOpen () {
// Special "media key" for play/pause, available on some keyboards // Register special "media key" for play/pause, available on some keyboards
globalShortcut.register('MediaPlayPause', () => windows.main.send('dispatch', 'playPause')) electron.globalShortcut.register(
'MediaPlayPause',
() => windows.main.send('dispatch', 'playPause')
)
} }
function unregisterPlayerShortcuts () { function onPlayerClose () {
globalShortcut.unregister('MediaPlayPause') electron.globalShortcut.unregister('MediaPlayPause')
} }

View File

@@ -8,8 +8,6 @@ var path = require('path')
var electron = require('electron') var electron = require('electron')
var app = electron.app var app = electron.app
var Menu = electron.Menu
var Tray = electron.Tray
var windows = require('./windows') var windows = require('./windows')
@@ -35,7 +33,7 @@ function hasTray () {
} }
function createTrayIcon () { function createTrayIcon () {
trayIcon = new Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png')) trayIcon = new electron.Tray(path.join(__dirname, '..', 'static', 'WebTorrentSmall.png'))
// On Windows, left click to open the app, right click for context menu // On Windows, left click to open the app, right click for context menu
// On Linux, any click (right or left) opens the context menu // On Linux, any click (right or left) opens the context menu
@@ -66,7 +64,7 @@ function updateTrayMenu () {
} else { } else {
showHideMenuItem = { label: 'Show', click: showApp } showHideMenuItem = { label: 'Show', click: showApp }
} }
var contextMenu = Menu.buildFromTemplate([ var contextMenu = electron.Menu.buildFromTemplate([
showHideMenuItem, showHideMenuItem,
{ label: 'Quit', click: () => app.quit() } { label: 'Quit', click: () => app.quit() }
]) ])

72
main/updater.js Normal file
View File

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

22
main/vlc.js Normal file
View File

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

View File

@@ -9,6 +9,8 @@ var windows = module.exports = {
var electron = require('electron') var electron = require('electron')
var app = electron.app
var config = require('../config') var config = require('../config')
var menu = require('./menu') var menu = require('./menu')
var tray = require('./tray') var tray = require('./tray')
@@ -68,7 +70,7 @@ function createWebTorrentHiddenWindow () {
// Prevent killing the WebTorrent process // Prevent killing the WebTorrent process
win.on('close', function (e) { win.on('close', function (e) {
if (!electron.app.isQuitting) { if (!app.isQuitting) {
e.preventDefault() e.preventDefault()
win.hide() win.hide()
} }
@@ -79,6 +81,9 @@ function createWebTorrentHiddenWindow () {
}) })
} }
var HEADER_HEIGHT = 37
var TORRENT_HEIGHT = 100
function createMainWindow () { function createMainWindow () {
if (windows.main) { if (windows.main) {
return focusWindow(windows.main) return focusWindow(windows.main)
@@ -89,14 +94,17 @@ function createMainWindow () {
icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon. icon: config.APP_ICON + 'Smaller.png', // Window and Volume Mixer icon.
minWidth: config.WINDOW_MIN_WIDTH, minWidth: config.WINDOW_MIN_WIDTH,
minHeight: config.WINDOW_MIN_HEIGHT, minHeight: config.WINDOW_MIN_HEIGHT,
show: false, // Hide window until DOM finishes loading show: false, // Hide window until renderer sends 'ipcReady' event
title: config.APP_WINDOW_TITLE, title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X) titleBarStyle: 'hidden-inset', // Hide OS chrome, except traffic light buttons (OS X)
useContentSize: true, // Specify web page size without OS chrome useContentSize: true, // Specify web page size without OS chrome
width: 500, width: 500,
height: 38 + (120 * 5) // header height + 4 torrents height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
}) })
win.loadURL(config.WINDOW_MAIN) win.loadURL(config.WINDOW_MAIN)
if (process.platform === 'darwin') {
win.setSheetOffset(HEADER_HEIGHT)
}
win.webContents.on('dom-ready', function () { win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen() menu.onToggleFullScreen()
@@ -110,8 +118,8 @@ function createMainWindow () {
win.on('close', function (e) { win.on('close', function (e) {
if (process.platform !== 'darwin' && !tray.hasTray()) { if (process.platform !== 'darwin' && !tray.hasTray()) {
electron.app.quit() app.quit()
} else if (!electron.app.isQuitting) { } else if (!app.isQuitting) {
e.preventDefault() e.preventDefault()
win.hide() win.hide()
win.send('dispatch', 'backToList') win.send('dispatch', 'backToList')

View File

@@ -1,7 +1,7 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For OS X, Windows, and Linux.",
"version": "0.3.3", "version": "0.5.1",
"author": { "author": {
"name": "Feross Aboukhadijeh", "name": "Feross Aboukhadijeh",
"email": "feross@feross.org", "email": "feross@feross.org",
@@ -15,18 +15,20 @@
}, },
"dependencies": { "dependencies": {
"airplay-js": "guerrerocarlos/node-airplay-js", "airplay-js": "guerrerocarlos/node-airplay-js",
"application-config": "feross/node-application-config", "application-config": "^0.2.1",
"async": "^2.0.0-rc.5",
"bitfield": "^1.0.2", "bitfield": "^1.0.2",
"chromecasts": "^1.8.0", "chromecasts": "^1.8.0",
"concat-stream": "^1.5.1", "concat-stream": "^1.5.1",
"create-torrent": "^3.24.5", "create-torrent": "^3.24.5",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"dlnacasts": "^0.0.3", "dlnacasts": "^0.1.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.6", "electron-prebuilt": "1.0.2",
"fs-extra": "^0.27.0", "fs-extra": "^0.27.0",
"hyperx": "^2.0.2", "hyperx": "^2.0.2",
"iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1", "languagedetect": "^1.1.1",
"main-loop": "^3.2.0", "main-loop": "^3.2.0",
"musicmetadata": "^2.0.2", "musicmetadata": "^2.0.2",
@@ -34,36 +36,41 @@
"prettier-bytes": "^1.0.1", "prettier-bytes": "^1.0.1",
"simple-get": "^2.0.0", "simple-get": "^2.0.0",
"srt-to-vtt": "^1.1.1", "srt-to-vtt": "^1.1.1",
"upload-element": "^1.0.1",
"virtual-dom": "^2.1.1", "virtual-dom": "^2.1.1",
"wcjs-player": "^0.5.7", "vlc-command": "^1.0.1",
"webchimera.js": "^0.2.3",
"webtorrent": "0.x", "webtorrent": "0.x",
"winreg": "^1.1.1" "winreg": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"cross-zip": "^1.0.0", "cross-zip": "^2.0.1",
"electron-osx-sign": "^0.3.0", "electron-osx-sign": "^0.3.0",
"electron-packager": "^7.0.0", "electron-packager": "^7.0.0",
"electron-winstaller": "feross/windows-installer#build", "electron-winstaller": "^2.3.0",
"gh-release": "^2.0.3", "gh-release": "^2.0.3",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"mkdirp": "^0.5.1",
"nobin-debian-installer": "^0.0.9", "nobin-debian-installer": "^0.0.9",
"open": "0.0.5",
"plist": "^1.2.0", "plist": "^1.2.0",
"rimraf": "^2.5.2",
"run-series": "^1.1.4", "run-series": "^1.1.4",
"standard": "^6.0.5" "standard": "^7.0.0"
}, },
"homepage": "https://webtorrent.io", "homepage": "https://webtorrent.io",
"keywords": [ "keywords": [
"desktop", "desktop",
"electron", "electron",
"electron-app", "electron-app",
"hybrid webtorrent client",
"mad science",
"torrent client",
"torrent",
"webtorrent" "webtorrent"
], ],
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.js",
"optionalDependencies": { "optionalDependencies": {
"appdmg": "^0.3.6" "appdmg": "^0.4.3"
}, },
"productName": "WebTorrent", "productName": "WebTorrent",
"repository": { "repository": {
@@ -72,13 +79,10 @@
}, },
"scripts": { "scripts": {
"clean": "node ./bin/clean.js", "clean": "node ./bin/clean.js",
"open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js", "package": "node ./bin/package.js",
"start": "electron .", "start": "electron .",
"test": "standard", "test": "standard && node ./bin/check-deps.js",
"update-authors": "./bin/update-authors.sh" "update-authors": "./bin/update-authors.sh"
},
"cmake-js": {
"runtime": "electron",
"runtimeVersion": "0.37.5"
} }
} }

View File

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

View File

@@ -117,10 +117,6 @@ table {
float: right; float: right;
} }
.expand-collapse {
cursor: pointer;
}
.expand-collapse.expanded::before { .expand-collapse.expanded::before {
content: '▲' content: '▲'
} }
@@ -276,7 +272,6 @@ table {
} }
.modal label { .modal label {
font-size: 16px;
font-weight: bold; font-weight: bold;
} }
@@ -367,7 +362,6 @@ button { /* Rectangular text buttons */
border-radius: 3px; border-radius: 3px;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
cursor: pointer;
color: #aaa; color: #aaa;
outline: none; outline: none;
} }
@@ -434,7 +428,7 @@ input {
.torrent, .torrent,
.torrent-placeholder { .torrent-placeholder {
height: 120px; height: 100px;
} }
.torrent:not(:last-child) { .torrent:not(:last-child) {
@@ -447,9 +441,9 @@ input {
.torrent .metadata { .torrent .metadata {
position: absolute; position: absolute;
top: 20px; top: 25px;
left: 20px; left: 15px;
right: 20px; right: 15px;
width: calc(100% - 40px); width: calc(100% - 40px);
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px; text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
} }
@@ -459,12 +453,15 @@ input {
} }
.torrent .metadata span:not(:last-child)::after { .torrent .metadata span:not(:last-child)::after {
content: ' '; content: ' ';
opacity: 0.7;
padding-left: 4px;
padding-right: 4px;
} }
.torrent .buttons { .torrent .buttons {
position: absolute; position: absolute;
top: 25px; top: 29px;
right: 10px; right: 10px;
align-items: center; align-items: center;
display: none; display: none;
@@ -547,17 +544,11 @@ input {
} }
.torrent .name { .torrent .name {
font-size: 1.5em; font-size: 18px;
font-weight: bold; font-weight: bold;
line-height: 1.5em; line-height: 1.5em;
} }
.torrent .status,
.torrent .status2 {
font-size: 1em;
line-height: 1.5em;
}
/* /*
* TORRENT LIST: DRAG-DROP TARGET * TORRENT LIST: DRAG-DROP TARGET
*/ */
@@ -601,11 +592,7 @@ body.drag .app::after {
} }
.torrent-details { .torrent-details {
padding: 8em 20px 20px 20px; padding: 8em 12px 20px 20px;
}
.torrent-details .open-folder {
float: right;
} }
.torrent-details table { .torrent-details table {
@@ -619,8 +606,11 @@ body.drag .app::after {
height: 28px; height: 28px;
} }
.torrent-details tr:hover, .torrent-details td {
.torrent-details .open-folder:hover { vertical-align: center;
}
.torrent-details tr:hover {
background-color: rgba(200, 200, 200, 0.3); background-color: rgba(200, 200, 200, 0.3);
} }
@@ -630,16 +620,16 @@ body.drag .app::after {
vertical-align: bottom; vertical-align: bottom;
} }
.torrent-details td.col-icon { .torrent-details td .icon {
width: 2em;
}
.torrent-details td.col-icon .icon {
font-size: 18px; font-size: 18px;
position: relative; position: relative;
top: 3px; top: 3px;
} }
.torrent-details td.col-icon {
width: 2em;
}
.torrent-details td.col-name { .torrent-details td.col-name {
width: auto; width: auto;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -655,6 +645,11 @@ body.drag .app::after {
text-align: right; text-align: right;
} }
.torrent-details td.col-select {
width: 2em;
text-align: right;
}
/* /*
* PLAYER * PLAYER
*/ */
@@ -674,11 +669,9 @@ body.drag .app::after {
background-position: center; background-position: center;
} }
.player .video-player { .player video {
display: block; display: block;
width: 100%; width: 100%;
height: 100%;
position: relative;
} }
/* /*
@@ -762,9 +755,15 @@ body.drag .app::after {
.player-controls .volume-icon, .player-controls .volume-icon,
.player-controls .back { .player-controls .back {
display: block; display: block;
width: 20px;
height: 20px; height: 20px;
margin: 5px; margin: 5px;
/*
* Fix for overflowing captions icon
* https://github.com/feross/webtorrent-desktop/issues/467
*/
max-width: 22px;
overflow: hidden;
} }
.player-controls .volume, .player-controls .volume,
@@ -810,6 +809,7 @@ body.drag .app::after {
border: none; border: none;
padding: 0; padding: 0;
vertical-align: sub; vertical-align: sub;
-webkit-app-region: no-drag;
} }
.player-controls .volume-slider::-webkit-slider-thumb { .player-controls .volume-slider::-webkit-slider-thumb {
@@ -820,6 +820,7 @@ body.drag .app::after {
height: 10px; height: 10px;
border: 1px solid #303233; border: 1px solid #303233;
border-radius: 50%; border-radius: 50%;
-webkit-app-region: no-drag;
} }
.player-controls .volume-slider:focus { .player-controls .volume-slider:focus {
@@ -989,3 +990,7 @@ body.drag .app::after {
.error-popover .error:last-child { .error-popover .error:last-child {
border-bottom: none; border-bottom: none;
} }
.error-text {
color: #c44;
}

View File

@@ -1,14 +1,26 @@
console.time('init') console.time('init')
var crashReporter = require('../crash-reporter')
crashReporter.init()
var electron = require('electron')
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
// Listen for messages from the main process
setupIpc()
var appConfig = require('application-config')('WebTorrent') var appConfig = require('application-config')('WebTorrent')
var Async = require('async')
var concat = require('concat-stream') var concat = require('concat-stream')
var dragDrop = require('drag-drop') var dragDrop = require('drag-drop')
var electron = require('electron')
var fs = require('fs-extra') var fs = require('fs-extra')
var iso639 = require('iso-639-1')
var mainLoop = require('main-loop') var mainLoop = require('main-loop')
var path = require('path') var path = require('path')
var srtToVtt = require('srt-to-vtt')
var LanguageDetect = require('languagedetect')
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')
@@ -16,7 +28,6 @@ var patch = require('virtual-dom/patch')
var App = require('./views/app') var App = require('./views/app')
var config = require('../config') var config = require('../config')
var crashReporter = require('../crash-reporter')
var errors = require('./lib/errors') var errors = require('./lib/errors')
var sound = require('./lib/sound') var sound = require('./lib/sound')
var State = require('./state') var State = require('./state')
@@ -26,18 +37,7 @@ var TorrentSummary = require('./lib/torrent-summary')
var {setDispatch} = require('./lib/dispatcher') var {setDispatch} = require('./lib/dispatcher')
setDispatch(dispatch) setDispatch(dispatch)
appConfig.filePath = config.CONFIG_PATH + path.sep + 'config.json' appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
var ipcRenderer = electron.ipcRenderer
var clipboard = electron.clipboard
var dialog = electron.remote.dialog
var Menu = electron.remote.Menu
var MenuItem = electron.remote.MenuItem
var remote = electron.remote
// This dependency is the slowest-loading, so we lazy load it // This dependency is the slowest-loading, so we lazy load it
var Cast = null var Cast = null
@@ -45,11 +45,10 @@ var Cast = null
// For easy debugging in Developer Tools // For easy debugging in Developer Tools
var state = global.state = State.getInitialState() var state = global.state = State.getInitialState()
var vdomLoop // Push the first page into the location history
state.location.go({ url: 'home' })
// Report crashes back to our server. var vdomLoop
// Not global JS exceptions, not like Rollbar, handles segfaults/core dumps only
crashReporter.init()
// All state lives in state.js. `state.saved` is read from and written to a file. // All state lives in state.js. `state.saved` is read from and written to a file.
// All other state is ephemeral. First we load state.saved then initialize the app. // All other state is ephemeral. First we load state.saved then initialize the app.
@@ -64,14 +63,11 @@ function init () {
// Clean up the freshly-loaded config file, which may be from an older version // Clean up the freshly-loaded config file, which may be from an older version
cleanUpConfig() cleanUpConfig()
// Push the first page into the location history
state.location.go({ url: 'home' })
// Restart everything we were torrenting last time the app ran // Restart everything we were torrenting last time the app ran
resumeTorrents() resumeTorrents()
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast // Lazy-load other stuff, like the AppleTV module, later to keep startup fast
window.setTimeout(delayedInit, 5000) window.setTimeout(delayedInit, config.DELAYED_INIT)
// 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
@@ -96,16 +92,10 @@ function init () {
// ...same thing if you paste a torrent // ...same thing if you paste a torrent
document.addEventListener('paste', onPaste) document.addEventListener('paste', onPaste)
// ...keyboard shortcuts
document.addEventListener('keydown', onKeyDown)
// ...focus and blur. Needed to show correct dock icon text ("badge") in OSX // ...focus and blur. Needed to show correct dock icon text ("badge") in OSX
window.addEventListener('focus', onFocus) window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur) window.addEventListener('blur', onBlur)
// 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
sound.play('STARTUP') sound.play('STARTUP')
@@ -126,10 +116,19 @@ function cleanUpConfig () {
// Migration: replace torrentPath with torrentFileName // Migration: replace torrentPath with torrentFileName
var src, dst var src, dst
if (ts.torrentPath) { if (ts.torrentPath) {
// There are a number of cases to handle here:
// * Originally we used absolute paths
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
// * Finally, now we're getting rid of torrentPath altogether
console.log('migration: replacing torrentPath %s', ts.torrentPath) console.log('migration: replacing torrentPath %s', ts.torrentPath)
src = path.isAbsolute(ts.torrentPath) if (path.isAbsolute(ts.torrentPath)) {
? ts.torrentPath src = ts.torrentPath
: path.join(config.STATIC_PATH, ts.torrentPath) } else if (ts.torrentPath.startsWith('..')) {
src = ts.torrentPath
} else {
src = path.join(config.STATIC_PATH, ts.torrentPath)
}
dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent') dst = path.join(config.CONFIG_TORRENT_PATH, infoHash + '.torrent')
// Synchronous FS calls aren't ideal, but probably OK in a migration // Synchronous FS calls aren't ideal, but probably OK in a migration
// that only runs once // that only runs once
@@ -154,6 +153,11 @@ function cleanUpConfig () {
delete ts.posterURL delete ts.posterURL
ts.posterFileName = infoHash + extension ts.posterFileName = infoHash + extension
} }
// Migration: add per-file selections
if (!ts.selections) {
ts.selections = ts.files.map((x) => true)
}
}) })
} }
@@ -211,9 +215,6 @@ function dispatch (action, ...args) {
if (action === 'addTorrent') { if (action === 'addTorrent') {
addTorrent(args[0] /* torrent */) addTorrent(args[0] /* torrent */)
} }
if (action === 'showOpenSeedFiles') {
ipcRenderer.send('showOpenSeedFiles') /* open file or folder to seed */
}
if (action === 'showOpenTorrentFile') { if (action === 'showOpenTorrentFile') {
ipcRenderer.send('showOpenTorrentFile') /* open torrent file */ ipcRenderer.send('showOpenTorrentFile') /* open torrent file */
} }
@@ -226,9 +227,6 @@ function dispatch (action, ...args) {
if (action === 'openFile') { if (action === 'openFile') {
openFile(args[0] /* infoHash */, args[1] /* index */) openFile(args[0] /* infoHash */, args[1] /* index */)
} }
if (action === 'openFolder') {
openFolder(args[0] /* infoHash */)
}
if (action === 'toggleTorrent') { if (action === 'toggleTorrent') {
toggleTorrent(args[0] /* infoHash */) toggleTorrent(args[0] /* infoHash */)
} }
@@ -238,6 +236,9 @@ function dispatch (action, ...args) {
if (action === 'toggleSelectTorrent') { if (action === 'toggleSelectTorrent') {
toggleSelectTorrent(args[0] /* infoHash */) toggleSelectTorrent(args[0] /* infoHash */)
} }
if (action === 'toggleTorrentFile') {
toggleTorrentFile(args[0] /* infoHash */, args[1] /* index */)
}
if (action === 'openTorrentContextMenu') { if (action === 'openTorrentContextMenu') {
openTorrentContextMenu(args[0] /* infoHash */) openTorrentContextMenu(args[0] /* infoHash */)
} }
@@ -251,6 +252,8 @@ function dispatch (action, ...args) {
setDimensions(args[0] /* dimensions */) setDimensions(args[0] /* dimensions */)
} }
if (action === 'backToList') { if (action === 'backToList') {
// Exit any modals and screens with a back button
state.modal = null
while (state.location.hasBack()) state.location.back() while (state.location.hasBack()) state.location.back()
// Work around virtual-dom issue: it doesn't expose its redraw function, // Work around virtual-dom issue: it doesn't expose its redraw function,
@@ -260,6 +263,15 @@ function dispatch (action, ...args) {
var mediaTag = document.querySelector('video,audio') var mediaTag = document.querySelector('video,audio')
if (mediaTag) mediaTag.pause() if (mediaTag) mediaTag.pause()
} }
if (action === 'escapeBack') {
if (state.modal) {
dispatch('exitModal')
} else if (state.window.isFullScreen) {
dispatch('toggleFullScreen')
} else {
dispatch('back')
}
}
if (action === 'back') { if (action === 'back') {
state.location.back() state.location.back()
} }
@@ -293,29 +305,46 @@ function dispatch (action, ...args) {
openSubtitles() openSubtitles()
} }
if (action === 'selectSubtitle') { if (action === 'selectSubtitle') {
selectSubtitle(args[0] /* label */) selectSubtitle(args[0] /* index */)
} }
if (action === 'showSubtitles') { if (action === 'toggleSubtitlesMenu') {
showSubtitles() toggleSubtitlesMenu()
} }
if (action === 'mediaStalled') { if (action === 'mediaStalled') {
state.playing.isStalled = true state.playing.isStalled = true
} }
if (action === 'mediaError') { if (action === 'mediaError') {
state.location.back(function () { if (state.location.current().url === 'player') {
onError(new Error('Unsupported file format')) state.playing.location = 'error'
}) ipcRenderer.send('checkForVLC')
ipcRenderer.once('checkForVLC', function (e, isInstalled) {
state.modal = {
id: 'unsupported-media-modal',
error: args[0],
vlcInstalled: isInstalled
}
})
}
} }
if (action === 'mediaTimeUpdate') { if (action === 'mediaTimeUpdate') {
state.playing.lastTimeUpdate = new Date().getTime() state.playing.lastTimeUpdate = new Date().getTime()
state.playing.isStalled = false state.playing.isStalled = false
} }
if (action === 'toggleFullScreen') {
ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
}
if (action === 'mediaMouseMoved') { if (action === 'mediaMouseMoved') {
state.playing.mouseStationarySince = new Date().getTime() state.playing.mouseStationarySince = new Date().getTime()
} }
if (action === 'vlcPlay') {
ipcRenderer.send('vlcPlay', state.server.localURL)
state.playing.location = 'vlc'
}
if (action === 'vlcNotFound') {
if (state.modal && state.modal.id === 'unsupported-media-modal') {
state.modal.vlcNotFound = true
}
}
if (action === 'toggleFullScreen') {
ipcRenderer.send('toggleFullScreen', args[0] /* optional bool */)
}
if (action === 'exitModal') { if (action === 'exitModal') {
state.modal = null state.modal = null
} }
@@ -365,6 +394,7 @@ function pause () {
} }
function playPause () { function playPause () {
if (state.location.current().url !== 'player') return
if (state.playing.isPaused) { if (state.playing.isPaused) {
play() play()
} else { } else {
@@ -396,13 +426,13 @@ function setVolume (volume) {
} }
function openSubtitles () { function openSubtitles () {
dialog.showOpenDialog({ electron.remote.dialog.showOpenDialog({
title: 'Select a subtitles file.', title: 'Select a subtitles file.',
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ], filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
properties: [ 'openFile' ] properties: [ 'openFile' ]
}, function (filenames) { }, function (filenames) {
if (!Array.isArray(filenames)) return if (!Array.isArray(filenames)) return
addSubtitle({path: filenames[0]}) addSubtitles(filenames, true)
}) })
} }
@@ -430,6 +460,10 @@ function setupIpc () {
ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) { ipcRenderer.on('fullscreenChanged', function (e, isFullScreen) {
state.window.isFullScreen = isFullScreen state.window.isFullScreen = isFullScreen
if (!isFullScreen) {
// Aspect ratio gets reset in fullscreen mode, so restore it (OS X)
ipcRenderer.send('setAspectRatio', state.playing.aspectRatio)
}
update() update()
}) })
@@ -515,15 +549,23 @@ function saveState () {
function onOpen (files) { function onOpen (files) {
if (!Array.isArray(files)) files = [ files ] if (!Array.isArray(files)) files = [ files ]
// .torrent file = start downloading the torrent // In the player, the only drag-drop function is adding subtitles
files.filter(isTorrent).forEach(addTorrent) var isInPlayer = state.location.current().url === 'player'
if (isInPlayer) {
return addSubtitles(files.filter(isSubtitle), true)
}
// subtitle file // Otherwise, you can only drag-drop onto the home screen
files.filter(isSubtitle).forEach(addSubtitle) var isHome = state.location.current().url === 'home' && !state.modal
if (isHome) {
// everything else = seed these files if (files.every(isTorrent)) {
var rest = files.filter(not(isTorrent)).filter(not(isSubtitle)) // All .torrent files? Start downloading
if (rest.length > 0) showCreateTorrent(rest) files.forEach(addTorrent)
} else {
// Show the Create Torrent screen. Let's seed those files.
showCreateTorrent(files)
}
}
} }
function isTorrent (file) { function isTorrent (file) {
@@ -539,12 +581,6 @@ function isSubtitle (file) {
return ext === '.srt' || ext === '.vtt' return ext === '.srt' || ext === '.vtt'
} }
function not (test) {
return function (...args) {
return !test(...args)
}
}
// Gets a torrent summary {name, infoHash, status} from state.saved.torrents // Gets a torrent summary {name, infoHash, status} from state.saved.torrents
// Returns undefined if we don't know that infoHash // Returns undefined if we don't know that infoHash
function getTorrentSummary (torrentKey) { function getTorrentSummary (torrentKey) {
@@ -565,46 +601,102 @@ function addTorrent (torrentId) {
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path) ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
} }
function addSubtitle (file) { function addSubtitles (files, autoSelect) {
// Subtitles are only supported while playing video
if (state.playing.type !== 'video') return if (state.playing.type !== 'video') return
fs.createReadStream(file.path || file).pipe(srtToVtt()).pipe(concat(function (buf) {
// Read the files concurrently, then add all resulting subtitle tracks
console.log(files)
var subs = state.playing.subtitles
Async.map(files, loadSubtitle, function (err, tracks) {
if (err) return onError(err)
for (var i = 0; i < tracks.length; i++) {
// No dupes allowed
var track = tracks[i]
if (subs.tracks.some((t) => track.filePath === t.filePath)) continue
// Add the track
subs.tracks.push(track)
// If we're auto-selecting a track, try to find one in the user's language
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
state.playing.subtitles.selectedIndex = subs.tracks.length - 1
}
}
// Finally, make sure no two tracks have the same label
relabelSubtitles()
})
}
function loadSubtitle (file, cb) {
var srtToVtt = require('srt-to-vtt')
var LanguageDetect = require('languagedetect')
// Read the .SRT or .VTT file, parse it, add subtitle track
var filePath = file.path || file
fs.createReadStream(filePath).pipe(srtToVtt()).pipe(concat(function (buf) {
// Detect what language the subtitles are in
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
// Set the cue text position so it appears above the player controls. // Set the cue text position so it appears above the player controls.
// The only way to change cue text position is by modifying the VTT. It is not // The only way to change cue text position is by modifying the VTT. It is not
// possible via CSS. // possible via CSS.
var langDetected = (new LanguageDetect()).detect(buf.toString().replace(/(.*-->.*)/g, ''), 2)
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%')) var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%'))
var track = { var track = {
buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'), buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'),
language: langDetected,
label: langDetected, label: langDetected,
selected: true filePath: filePath
} }
state.playing.subtitles.tracks.forEach(function (trackItem) {
trackItem.selected = false cb(null, track)
if (trackItem.label === track.label) {
track.label = Number.isNaN(track.label.slice(-1))
? track.label + ' 2'
: track.label.slice(0, -1) + (parseInt(track.label.slice(-1)) + 1)
}
})
state.playing.subtitles.change = track.label
state.playing.subtitles.tracks.push(track)
state.playing.subtitles.enabled = true
})) }))
} }
function selectSubtitle (label) { function selectSubtitle (ix) {
state.playing.subtitles.tracks.forEach(function (track) { state.playing.subtitles.selectedIndex = ix
track.selected = (track.label === label)
})
state.playing.subtitles.enabled = !!label
state.playing.subtitles.change = label
state.playing.subtitles.show = false
} }
function showSubtitles () { // Checks whether a language name like "English" or "German" matches the system
state.playing.subtitles.show = !state.playing.subtitles.show // language, aka the current locale
function isSystemLanguage (language) {
var osLangISO = window.navigator.language.split('-')[0] // eg "en"
var langIso = iso639.getCode(language) // eg "de" if language is "German"
return langIso === osLangISO
}
// Make sure we don't have two subtitle tracks with the same label
// Labels each track by language, eg "German", "English", "English 2", ...
function relabelSubtitles () {
var counts = {}
state.playing.subtitles.tracks.forEach(function (track) {
var lang = track.language
counts[lang] = (counts[lang] || 0) + 1
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang
})
}
function checkForSubtitles () {
if (state.playing.type !== 'video') return
var torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary || !torrentSummary.progress) return
torrentSummary.progress.files.forEach(function (fp, ix) {
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
var file = torrentSummary.files[ix]
if (!isSubtitle(file.name)) return
var filePath = path.join(torrentSummary.path, file.path)
addSubtitles([filePath], false)
})
}
function toggleSubtitlesMenu () {
state.playing.subtitles.showMenu = !state.playing.subtitles.showMenu
} }
// Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object // Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object
@@ -624,7 +716,8 @@ function startTorrentingSummary (torrentSummary) {
torrentID = s.magnetURI || s.infoHash torrentID = s.magnetURI || s.infoHash
} }
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes) console.log('start torrenting %s %s', s.torrentKey, torrentID)
ipcRenderer.send('wt-start-torrenting', s.torrentKey, torrentID, path, s.fileModtimes, s.selections)
} }
// //
@@ -710,16 +803,16 @@ function torrentWarning (torrentKey, message) {
} }
function torrentError (torrentKey, message) { function torrentError (torrentKey, message) {
var torrentSummary = getTorrentSummary(torrentKey) // TODO: WebTorrent needs semantic errors
if (message.startsWith('Cannot add duplicate torrent')) {
// Remove infohash from the message
message = 'Cannot add duplicate torrent'
}
onError(message)
// TODO: WebTorrent should have semantic errors var torrentSummary = getTorrentSummary(torrentKey)
if (message.startsWith('There is already a swarm')) { if (torrentSummary) {
onError(new Error('Can\'t add duplicate torrent')) console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
} else if (!torrentSummary) {
onError(message)
} else {
console.log('error, stopping torrent %s (%s):\n\t%o',
torrentSummary.name, torrentSummary.infoHash, message)
torrentSummary.status = 'paused' torrentSummary.status = 'paused'
update() update()
} }
@@ -733,6 +826,9 @@ function torrentMetadata (torrentKey, torrentInfo) {
torrentSummary.path = torrentInfo.path torrentSummary.path = torrentInfo.path
torrentSummary.files = torrentInfo.files torrentSummary.files = torrentInfo.files
torrentSummary.magnetURI = torrentInfo.magnetURI torrentSummary.magnetURI = torrentInfo.magnetURI
if (!torrentSummary.selections) {
torrentSummary.selections = torrentSummary.files.map((x) => true)
}
update() update()
// Save the .torrent file, if it hasn't been saved already // Save the .torrent file, if it hasn't been saved already
@@ -754,6 +850,7 @@ function torrentDone (torrentKey, torrentInfo) {
state.dock.badge += 1 state.dock.badge += 1
} }
showDoneNotification(torrentSummary) showDoneNotification(torrentSummary)
ipcRenderer.send('downloadFinished', getTorrentPath(torrentSummary))
} }
update() update()
@@ -783,6 +880,8 @@ function torrentProgress (progressInfo) {
torrentSummary.progress = p torrentSummary.progress = p
}) })
checkForSubtitles()
update() update()
} }
@@ -882,6 +981,9 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index) ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
} }
// if it's video, check for subtitles files that are done downloading
checkForSubtitles()
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index) ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, function (e, info) { ipcRenderer.once('wt-server-' + torrentSummary.infoHash, function (e, info) {
clearTimeout(timeout) clearTimeout(timeout)
@@ -908,6 +1010,9 @@ function closePlayer (cb) {
if (isCasting()) { if (isCasting()) {
Cast.close() Cast.close()
} }
if (state.playing.location === 'vlc') {
ipcRenderer.send('vlcQuit')
}
state.window.title = config.APP_WINDOW_TITLE state.window.title = config.APP_WINDOW_TITLE
state.playing = State.getDefaultPlayState() state.playing = State.getDefaultPlayState()
state.server = null state.server = null
@@ -933,17 +1038,6 @@ function openFile (infoHash, index) {
ipcRenderer.send('openItem', filePath) ipcRenderer.send('openItem', filePath)
} }
function openFolder (infoHash) {
var torrentSummary = getTorrentSummary(infoHash)
var firstFilePath = path.join(
torrentSummary.path,
torrentSummary.files[0].path)
var folderPath = path.dirname(firstFilePath)
ipcRenderer.send('openItem', folderPath)
}
// TODO: use torrentKey, not infoHash // TODO: use torrentKey, not infoHash
function toggleTorrent (infoHash) { function toggleTorrent (infoHash) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
@@ -975,25 +1069,56 @@ function toggleSelectTorrent (infoHash) {
update() update()
} }
function toggleTorrentFile (infoHash, index) {
var torrentSummary = getTorrentSummary(infoHash)
torrentSummary.selections[index] = !torrentSummary.selections[index]
// Let the WebTorrent process know to start or stop fetching that file
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
}
function openTorrentContextMenu (infoHash) { function openTorrentContextMenu (infoHash) {
var torrentSummary = getTorrentSummary(infoHash) var torrentSummary = getTorrentSummary(infoHash)
var menu = new Menu() var menu = new electron.remote.Menu()
menu.append(new MenuItem({
if (torrentSummary.files) {
menu.append(new electron.remote.MenuItem({
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
click: () => showItemInFolder(torrentSummary)
}))
menu.append(new electron.remote.MenuItem({
type: 'separator'
}))
}
menu.append(new electron.remote.MenuItem({
label: 'Copy Magnet Link to Clipboard',
click: () => electron.clipboard.writeText(torrentSummary.magnetURI)
}))
menu.append(new electron.remote.MenuItem({
label: 'Copy Instant.io Link to Clipboard',
click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
}))
menu.append(new electron.remote.MenuItem({
label: 'Save Torrent File As...', label: 'Save Torrent File As...',
click: () => saveTorrentFileAs(torrentSummary) click: () => saveTorrentFileAs(torrentSummary)
})) }))
menu.append(new MenuItem({ menu.popup(electron.remote.getCurrentWindow())
label: 'Copy Instant.io Link to Clipboard', }
click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
}))
menu.append(new MenuItem({ function getTorrentPath (torrentSummary) {
label: 'Copy Magnet Link to Clipboard', var itemPath = path.join(torrentSummary.path, torrentSummary.files[0].path)
click: () => clipboard.writeText(torrentSummary.magnetURI) if (torrentSummary.files.length > 1) {
})) itemPath = path.dirname(itemPath)
}
return itemPath
}
menu.popup(remote.getCurrentWindow()) function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', getTorrentPath(torrentSummary))
} }
function saveTorrentFileAs (torrentSummary) { function saveTorrentFileAs (torrentSummary) {
@@ -1006,7 +1131,7 @@ function saveTorrentFileAs (torrentSummary) {
{ name: 'All Files', extensions: ['*'] } { name: 'All Files', extensions: ['*'] }
] ]
} }
dialog.showSaveDialog(remote.getCurrentWindow(), opts, function (savePath) { electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary) var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) { fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return onError(err) if (err) return onError(err)
@@ -1020,7 +1145,7 @@ function saveTorrentFileAs (torrentSummary) {
// 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
if (remote.getCurrentWindow().isMaximized()) { if (electron.remote.getCurrentWindow().isMaximized()) {
state.window.bounds = null state.window.bounds = null
return return
} }
@@ -1032,7 +1157,7 @@ function setDimensions (dimensions) {
width: window.outerWidth, width: window.outerWidth,
height: window.outerHeight height: window.outerHeight
} }
state.window.wasMaximized = remote.getCurrentWindow().isMaximized state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
// Limit window size to screen size // Limit window size to screen size
var screenWidth = window.screen.width var screenWidth = window.screen.width
@@ -1053,6 +1178,7 @@ function setDimensions (dimensions) {
ipcRenderer.send('setAspectRatio', aspectRatio) ipcRenderer.send('setAspectRatio', aspectRatio)
ipcRenderer.send('setBounds', {x: null, y: null, width, height}) ipcRenderer.send('setBounds', {x: null, y: null, width, height})
state.playing.aspectRatio = aspectRatio
} }
function restoreBounds () { function restoreBounds () {
@@ -1112,7 +1238,7 @@ function onWarning (err) {
function onPaste (e) { function onPaste (e) {
if (e.target.tagName.toLowerCase() === 'input') return if (e.target.tagName.toLowerCase() === 'input') return
var torrentIds = clipboard.readText().split('\n') var torrentIds = electron.clipboard.readText().split('\n')
torrentIds.forEach(function (torrentId) { torrentIds.forEach(function (torrentId) {
torrentId = torrentId.trim() torrentId = torrentId.trim()
if (torrentId.length === 0) return if (torrentId.length === 0) return
@@ -1120,20 +1246,6 @@ function onPaste (e) {
}) })
} }
function onKeyDown (e) {
if (e.which === 27) { /* ESC means either exit fullscreen or go back */
if (state.modal) {
dispatch('exitModal')
} else if (state.window.isFullScreen) {
dispatch('toggleFullScreen')
} else {
dispatch('back')
}
} else if (e.which === 32) { /* spacebar pauses or plays the video */
dispatch('playPause')
}
}
function onFocus (e) { function onFocus (e) {
state.window.isFocused = true state.window.isFocused = true
state.dock.badge = 0 state.dock.badge = 0

View File

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

View File

@@ -6,41 +6,43 @@ module.exports = {
var config = require('../../config') var config = require('../../config')
var path = require('path') var path = require('path')
var VOLUME = 0.15
/* Cache of Audio elements, for instant playback */ /* Cache of Audio elements, for instant playback */
var cache = {} var cache = {}
var sounds = { var sounds = {
ADD: { ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
volume: 0.2 volume: VOLUME
}, },
DELETE: { DELETE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
volume: 0.1 volume: VOLUME
}, },
DISABLE: { DISABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
volume: 0.2 volume: VOLUME
}, },
DONE: { DONE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
volume: 0.2 volume: VOLUME
}, },
ENABLE: { ENABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
volume: 0.2 volume: VOLUME
}, },
ERROR: { ERROR: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
volume: 0.2 volume: VOLUME
}, },
PLAY: { PLAY: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
volume: 0.2 volume: VOLUME
}, },
STARTUP: { STARTUP: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
volume: 0.4 volume: VOLUME * 2
} }
} }

View File

@@ -16,12 +16,27 @@ function isPlayable (file) {
function isVideo (file) { function isVideo (file) {
var ext = path.extname(file.name).toLowerCase() var ext = path.extname(file.name).toLowerCase()
return ['.mp4', '.m4v', '.webm', '.mov', '.mkv'].indexOf(ext) !== -1 return [
'.avi',
'.m4v',
'.mkv',
'.mov',
'.mp4',
'.mpg',
'.ogv',
'.webm'
].includes(ext)
} }
function isAudio (file) { function isAudio (file) {
var ext = path.extname(file.name).toLowerCase() var ext = path.extname(file.name).toLowerCase()
return ['.mp3', '.aac', '.ogg', '.wav'].indexOf(ext) !== -1 return [
'.aac',
'.ac3',
'.mp3',
'.ogg',
'.wav'
].includes(ext)
} }
function isPlayableTorrent (torrentSummary) { function isPlayableTorrent (torrentSummary) {

View File

@@ -9,7 +9,8 @@ var LocationHistory = require('./lib/location-history')
module.exports = { module.exports = {
getInitialState, getInitialState,
getDefaultPlayState, getDefaultPlayState,
getDefaultSavedState getDefaultSavedState,
getPlayingTorrentSummary
} }
function getInitialState () { function getInitialState () {
@@ -57,7 +58,12 @@ function getInitialState () {
* *
* Also accessible via `require('application-config')('WebTorrent').filePath` * Also accessible via `require('application-config')('WebTorrent').filePath`
*/ */
saved: {} saved: {},
/*
* Getters, for convenience
*/
getPlayingTorrentSummary
} }
} }
@@ -75,9 +81,11 @@ function getDefaultPlayState () {
lastTimeUpdate: 0, /* Unix time in ms */ lastTimeUpdate: 0, /* Unix time in ms */
mouseStationarySince: 0, /* Unix time in ms */ mouseStationarySince: 0, /* Unix time in ms */
subtitles: { subtitles: {
tracks: [], /* subtitles file (Buffer) */ tracks: [], /* subtitle tracks, each {label, language, ...} */
enabled: false selectedIndex: -1, /* current subtitle track */
} showMenu: false /* popover menu, above the video */
},
aspectRatio: 0 /* aspect ratio of the video */
} }
} }
@@ -262,3 +270,8 @@ function getDefaultSavedState () {
: remote.app.getPath('downloads') : remote.app.getPath('downloads')
} }
} }
function getPlayingTorrentSummary () {
var infoHash = this.playing.infoHash
return this.saved.torrents.find((x) => x.infoHash === infoHash)
}

View File

@@ -12,7 +12,8 @@ var Views = {
} }
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') 'update-available-modal': require('./update-available-modal'),
'unsupported-media-modal': require('./unsupported-media-modal')
} }
function App (state) { function App (state) {

View File

@@ -33,7 +33,6 @@ function CreateTorrentPage (state) {
// Sanity check: show the number of files and total size // Sanity check: show the number of files and total size
var numFiles = files.length var numFiles = files.length
console.log('FILES', files)
var totalBytes = files var totalBytes = files
.map((f) => f.size) .map((f) => f.size)
.reduce((a, b) => a + b, 0) .reduce((a, b) => a + b, 0)
@@ -41,8 +40,16 @@ function CreateTorrentPage (state) {
// Then, use the name of the base folder (or sole file, for a single file torrent) // Then, use the name of the base folder (or sole file, for a single file torrent)
// as the default name. Show all files relative to the base folder. // as the default name. Show all files relative to the base folder.
var defaultName = path.basename(pathPrefix) var defaultName, basePath
var basePath = path.dirname(pathPrefix) if (files.length === 1) {
// Single file torrent: /a/b/foo.jpg -> torrent name "foo.jpg", path "/a/b"
defaultName = files[0].name
basePath = pathPrefix
} else {
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name "b", path "/a"
defaultName = path.basename(pathPrefix)
basePath = path.dirname(pathPrefix)
}
var maxFileElems = 100 var maxFileElems = 100
var fileElems = files.slice(0, maxFileElems).map(function (file) { var fileElems = files.slice(0, maxFileElems).map(function (file) {
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path) var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)

View File

@@ -4,7 +4,6 @@ var h = require('virtual-dom/h')
var hyperx = require('hyperx') var hyperx = require('hyperx')
var hx = hyperx(h) var hx = hyperx(h)
var WebChimeraPlayer = require('wcjs-player')
var prettyBytes = require('prettier-bytes') var prettyBytes = require('prettier-bytes')
var Bitfield = require('bitfield') var Bitfield = require('bitfield')
@@ -19,6 +18,7 @@ function Player (state) {
return hx` return hx`
<div <div
class='player' class='player'
onwheel=${handleVolumeWheel}
onmousemove=${dispatcher('mediaMouseMoved')}> onmousemove=${dispatcher('mediaMouseMoved')}>
${showVideo ? renderMedia(state) : renderCastScreen(state)} ${showVideo ? renderMedia(state) : renderCastScreen(state)}
${renderPlayerControls(state)} ${renderPlayerControls(state)}
@@ -26,16 +26,14 @@ function Player (state) {
` `
} }
function renderMedia (state) { // Handles volume change by wheel
if (!state.server) return function handleVolumeWheel (e) {
if (false) return renderMediaTag(state) dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
else return renderMediaVLC(state)
} }
// Renders using a <video> or <audio> tag function renderMedia (state) {
// Handles only a subset of codecs, but it's cleaner and more efficient if (!state.server) return
// See renderMediaVLC()
function renderMediaTag (state) {
// Unfortunately, play/pause can't be done just by modifying HTML. // Unfortunately, play/pause can't be done just by modifying HTML.
// Instead, grab the DOM node and play/pause it if necessary // Instead, grab the DOM node and play/pause it if necessary
var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */ var mediaElement = document.querySelector(state.playing.type) /* get the <video> or <audio> tag */
@@ -56,14 +54,10 @@ function renderMediaTag (state) {
state.playing.setVolume = null state.playing.setVolume = null
} }
// fix textTrack cues not been removed <track> rerender // Switch to the newly added subtitle track, if available
if (state.playing.subtitles.change) { var tracks = mediaElement.textTracks
var tracks = mediaElement.textTracks for (var j = 0; j < tracks.length; j++) {
for (var j = 0; j < tracks.length; j++) { tracks[j].mode = (j === state.playing.subtitles.selectedIndex) ? 'showing' : 'hidden'
// mode is not an <track> attribute, only available on DOM
tracks[j].mode = (tracks[j].label === state.playing.subtitles.change) ? 'showing' : 'hidden'
}
state.playing.subtitles.change = null
} }
state.playing.currentTime = mediaElement.currentTime state.playing.currentTime = mediaElement.currentTime
@@ -73,12 +67,13 @@ function renderMediaTag (state) {
// Add subtitles to the <video> tag // Add subtitles to the <video> tag
var trackTags = [] var trackTags = []
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) { if (state.playing.subtitles.selectedIndex >= 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) { for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i] var track = state.playing.subtitles.tracks[i]
var isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push(hx` trackTags.push(hx`
<track <track
${track.selected ? 'default' : ''} ${isSelected ? 'default' : ''}
label=${track.label} label=${track.label}
type='subtitles' type='subtitles'
src=${track.buffer}> src=${track.buffer}>
@@ -96,7 +91,8 @@ function renderMediaTag (state) {
onstalling=${dispatcher('mediaStalled')} onstalling=${dispatcher('mediaStalled')}
onerror=${dispatcher('mediaError')} onerror=${dispatcher('mediaError')}
ontimeupdate=${dispatcher('mediaTimeUpdate')} ontimeupdate=${dispatcher('mediaTimeUpdate')}
autoplay> onencrypted=${dispatcher('mediaEncrypted')}
oncanplay=${onCanPlay}>
${trackTags} ${trackTags}
</div> </div>
` `
@@ -127,101 +123,15 @@ function renderMediaTag (state) {
function onEnded (e) { function onEnded (e) {
state.playing.isPaused = true state.playing.isPaused = true
} }
}
// Renders using WebChimera.js to render using VLC function onCanPlay (e) {
// That lets us play media that the <video> tag can't play var video = e.target
function renderMediaVLC (state) { if (video.webkitVideoDecodedByteCount > 0 &&
// Unfortunately, WebChimera can't be done just by modifying HTML. video.webkitAudioDecodedByteCount === 0) {
// Instead, grab the DOM node dispatch('mediaError', 'Audio codec unsupported')
if (document.querySelector('#media-player')) {
if (!state.playing.chimera) {
state.playing.chimera = new WebChimeraPlayer('#media-player')
.addPlayer({
autoplay: true,
vlcArgs: ['-vvv'],
wcjsRendererOptions: {'fallbackRenderer': true}
})
.onPlaying(dispatcher('mediaPlaying'))
.onPaused(dispatcher('mediaPaused'))
.onBuffering(dispatcher('mediaStalled'))
.onTime(dispatcher('mediaTimeUpdate'))
.onEnded(onEnded)
.onFrameSetup(onLoadedMetadata)
.addPlaylist(state.server.localURL)
state.playing.chimera.ui(false)
} else { } else {
var player = state.playing.chimera video.play()
if (state.playing.isPaused && player.playing()) {
player.pause()
} else if (!state.playing.isPaused && !player.playing()) {
player.play()
}
// When the user clicks or drags on the progress bar, jump to that position
if (state.playing.jumpToTime) {
player.time(state.playing.jumpToTime * 1000) // WebChimera expects milliseconds
state.playing.jumpToTime = null
}
// Set volume
if (state.playing.setVolume !== null && isFinite(state.playing.setVolume)) {
player.volume(Math.round(state.playing.setVolume * 100)) // WebChimera expects integer percent
state.playing.setVolume = null
}
state.playing.currentTime = player.time() / 1000
state.playing.duration = player.length() / 1000
state.playing.volume = player.volume() / 100
} }
} else {
state.playing.chimera = null
}
// Add subtitles to the <video> tag
var trackTags = []
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i]
trackTags.push(hx`
<track
default=${track.selected ? 'default' : ''}
label=${track.language}
type='subtitles'
src=${track.buffer}>
`)
}
}
// Create the <audio> or <video> tag
var mediaType = state.playing.type /* 'video' or 'audio' */
var mediaTag = hx`
<div id='media-player' class='${mediaType}-player'>
${trackTags}
</div>
`
// Show the media.
return hx`
<div
class='letterbox'
onmousemove=${dispatcher('mediaMouseMoved')}>
${mediaTag}
${renderOverlay(state)}
</div>
`
// As soon as the video loads enough to know the video dimensions, resize the window
function onLoadedMetadata (e) {
if (mediaType !== 'video') return
var dimensions = {
width: player.width(),
height: player.height()
}
dispatch('setDimensions', dimensions)
}
// When the video completes, pause the video instead of looping
function onEnded (e) {
state.playing.isPaused = true
} }
} }
@@ -251,7 +161,7 @@ function renderOverlay (state) {
} }
function renderAudioMetadata (state) { function renderAudioMetadata (state) {
var torrentSummary = getPlayingTorrentSummary(state) var torrentSummary = state.getPlayingTorrentSummary()
var fileSummary = torrentSummary.files[state.playing.fileIndex] var fileSummary = torrentSummary.files[state.playing.fileIndex]
if (!fileSummary.audioInfo) return if (!fileSummary.audioInfo) return
var info = fileSummary.audioInfo var info = fileSummary.audioInfo
@@ -290,7 +200,7 @@ function renderLoadingSpinner (state) {
(new Date().getTime() - state.playing.lastTimeUpdate > 2000) (new Date().getTime() - state.playing.lastTimeUpdate > 2000)
if (!isProbablyStalled) return if (!isProbablyStalled) return
var prog = getPlayingTorrentSummary(state).progress || {} var prog = state.getPlayingTorrentSummary().progress || {}
var fileProgress = 0 var fileProgress = 0
if (prog.files) { if (prog.files) {
var file = prog.files[state.playing.fileIndex] var file = prog.files[state.playing.fileIndex]
@@ -310,20 +220,33 @@ function renderLoadingSpinner (state) {
} }
function renderCastScreen (state) { function renderCastScreen (state) {
var castIcon, castType var castIcon, castType, isCast
if (state.playing.location.startsWith('chromecast')) { if (state.playing.location.startsWith('chromecast')) {
castIcon = 'cast_connected' castIcon = 'cast_connected'
castType = 'Chromecast' castType = 'Chromecast'
isCast = true
} else if (state.playing.location.startsWith('airplay')) { } else if (state.playing.location.startsWith('airplay')) {
castIcon = 'airplay' castIcon = 'airplay'
castType = 'AirPlay' castType = 'AirPlay'
isCast = true
} else if (state.playing.location.startsWith('dlna')) { } else if (state.playing.location.startsWith('dlna')) {
castIcon = 'tv' castIcon = 'tv'
castType = 'DLNA' castType = 'DLNA'
isCast = true
} else if (state.playing.location === 'vlc') {
castIcon = 'tv'
castType = 'VLC'
isCast = false
} else if (state.playing.location === 'error') {
castIcon = 'error_outline'
castType = 'Error'
isCast = false
} }
var isStarting = state.playing.location.endsWith('-pending') var isStarting = state.playing.location.endsWith('-pending')
var castStatus = isStarting ? 'Connecting...' : 'Connected' var castStatus
if (isCast) castStatus = isStarting ? 'Connecting...' : 'Connected'
else castStatus = ''
// Show a nice title image, if possible // Show a nice title image, if possible
var style = { var style = {
@@ -343,15 +266,28 @@ function renderCastScreen (state) {
function renderSubtitlesOptions (state) { function renderSubtitlesOptions (state) {
var subtitles = state.playing.subtitles var subtitles = state.playing.subtitles
if (subtitles.tracks.length && subtitles.show) { if (!subtitles.tracks.length || !subtitles.showMenu) return
return hx`<ul.subtitles-list>
${subtitles.tracks.map(function (w, i) { var items = subtitles.tracks.map(function (track, ix) {
return hx`<li onclick=${dispatcher('selectSubtitle', w.label)}><i.icon>${w.selected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>${w.label}</li>` var isSelected = state.playing.subtitles.selectedIndex === ix
})} return hx`
<li onclick=${dispatcher('selectSubtitle', '')}><i.icon>${!subtitles.enabled ? 'radio_button_checked' : 'radio_button_unchecked'}</i>None</li> <li onclick=${dispatcher('selectSubtitle', ix)}>
</ul> <i.icon>${isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
${track.label}
</li>
` `
} })
var noneSelected = state.playing.subtitles.selectedIndex === -1
return hx`
<ul.subtitles-list>
${items}
<li onclick=${dispatcher('selectSubtitle', -1)}>
<i.icon>${noneSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
None
</li>
</ul>
`
} }
function renderPlayerControls (state) { function renderPlayerControls (state) {
@@ -359,7 +295,7 @@ function renderPlayerControls (state) {
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' } var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
var captionsClass = state.playing.subtitles.tracks.length === 0 var captionsClass = state.playing.subtitles.tracks.length === 0
? 'disabled' ? 'disabled'
: state.playing.subtitles.enabled : state.playing.subtitles.selectedIndex >= 0
? 'active' ? 'active'
: '' : ''
@@ -476,8 +412,7 @@ function renderPlayerControls (state) {
} }
elements.push(hx` elements.push(hx`
<div.volume <div.volume>
onwheel=${handleVolumeWheel}>
<i.icon.volume-icon onmousedown=${handleVolumeMute}> <i.icon.volume-icon onmousedown=${handleVolumeMute}>
${volumeIcon} ${volumeIcon}
</i> </i>
@@ -486,7 +421,6 @@ function renderPlayerControls (state) {
onmousedown=${handleVolumeScrub} onmousedown=${handleVolumeScrub}
onmouseup=${handleVolumeScrub} onmouseup=${handleVolumeScrub}
onmousemove=${handleVolumeScrub} onmousemove=${handleVolumeScrub}
onwheel=${handleVolumeWheel}
style=${volumeStyle} style=${volumeStyle}
/> />
</div> </div>
@@ -515,11 +449,6 @@ function renderPlayerControls (state) {
dispatch('playbackJump', position) dispatch('playbackJump', position)
} }
// Handles volume change by wheel
function handleVolumeWheel (e) {
dispatch('changeVolume', (-e.deltaY | e.deltaX) / 500)
}
// Handles volume muting and Unmuting // Handles volume muting and Unmuting
function handleVolumeMute (e) { function handleVolumeMute (e) {
if (state.playing.volume === 0.0) { if (state.playing.volume === 0.0) {
@@ -553,7 +482,7 @@ function renderPlayerControls (state) {
// if no subtitles available select it // if no subtitles available select it
dispatch('openSubtitles') dispatch('openSubtitles')
} else { } else {
dispatch('showSubtitles') dispatch('toggleSubtitlesMenu')
} }
} }
} }
@@ -564,7 +493,7 @@ var volumeChanging = false
// Renders the loading bar. Shows which parts of the torrent are loaded, which // Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be "spongey" / non-contiguous // can be "spongey" / non-contiguous
function renderLoadingBar (state) { function renderLoadingBar (state) {
var torrentSummary = getPlayingTorrentSummary(state) var torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary.progress) { if (!torrentSummary.progress) {
return [] return []
} }
@@ -601,7 +530,7 @@ function renderLoadingBar (state) {
// Returns the CSS background-image string for a poster image + dark vignette // Returns the CSS background-image string for a poster image + dark vignette
function cssBackgroundImagePoster (state) { function cssBackgroundImagePoster (state) {
var torrentSummary = getPlayingTorrentSummary(state) var torrentSummary = state.getPlayingTorrentSummary()
var posterPath = TorrentSummary.getPosterPath(torrentSummary) var posterPath = TorrentSummary.getPosterPath(torrentSummary)
if (!posterPath) return '' if (!posterPath) return ''
return cssBackgroundImageDarkGradient() + `, url(${posterPath})` return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
@@ -611,8 +540,3 @@ function cssBackgroundImageDarkGradient () {
return 'radial-gradient(circle at center, ' + return 'radial-gradient(circle at center, ' +
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)' 'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
} }
function getPlayingTorrentSummary (state) {
var infoHash = state.playing.infoHash
return state.saved.torrents.find((x) => x.infoHash === infoHash)
}

View File

@@ -67,39 +67,49 @@ function TorrentList (state) {
// If it's downloading/seeding then show progress info // If it's downloading/seeding then show progress info
var prog = torrentSummary.progress var prog = torrentSummary.progress
if (torrentSummary.status !== 'paused' && prog) { if (torrentSummary.status !== 'paused' && prog) {
var progress = Math.floor(100 * prog.progress)
var downloaded = prettyBytes(prog.downloaded)
var total = prettyBytes(prog.length || 0)
if (downloaded !== total) downloaded += ` / ${total}`
elements.push(hx` elements.push(hx`
<div class='status ellipsis'> <div class='ellipsis'>
${getFilesLength()} ${renderPercentProgress()}
<span>${getPeers()}</span> ${renderTotalProgress()}
<span>↓ ${prettyBytes(prog.downloadSpeed || 0)}/s</span> ${renderPeers()}
<span>↑ ${prettyBytes(prog.uploadSpeed || 0)}/s</span> ${renderDownloadSpeed()}
</div> ${renderUploadSpeed()}
`)
elements.push(hx`
<div class='status2 ellipsis'>
<span class='progress'>${progress}%</span>
<span>${downloaded}</span>
</div> </div>
`) `)
} }
return hx`<div class='metadata'>${elements}</div>` return hx`<div class='metadata'>${elements}</div>`
function getPeers () { function renderPercentProgress () {
var count = prog.numPeers === 1 ? 'peer' : 'peers' var progress = Math.floor(100 * prog.progress)
return `${prog.numPeers} ${count}` return hx`<span>${progress}%</span>`
} }
function getFilesLength () { function renderTotalProgress () {
if (torrentSummary.files && torrentSummary.files.length > 1) { var downloaded = prettyBytes(prog.downloaded)
return hx`<span class='files'>${torrentSummary.files.length} files</span>` var total = prettyBytes(prog.length || 0)
if (downloaded === total) {
return hx`<span>${downloaded}</span>`
} else {
return hx`<span>${downloaded} / ${total}</span>`
} }
} }
function renderPeers () {
if (prog.numPeers === 0) return
var count = prog.numPeers === 1 ? 'peer' : 'peers'
return hx`<span>${prog.numPeers} ${count}</span>`
}
function renderDownloadSpeed () {
if (prog.downloadSpeed === 0) return
return hx`<span>↓ ${prettyBytes(prog.downloadSpeed)}/s</span>`
}
function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return
return hx`<span>↑ ${prettyBytes(prog.uploadSpeed)}/s</span>`
}
} }
// Download button toggles between torrenting (DL/seed) and paused // Download button toggles between torrenting (DL/seed) and paused
@@ -167,7 +177,6 @@ function TorrentList (state) {
// Show files, per-file download status and play buttons, and so on // Show files, per-file download status and play buttons, and so on
function renderTorrentDetails (torrentSummary) { function renderTorrentDetails (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
@@ -182,10 +191,6 @@ function TorrentList (state) {
filesElement = hx` filesElement = hx`
<div class='files'> <div class='files'>
<strong>Files</strong> <strong>Files</strong>
<span class='open-folder'
onclick=${dispatcher('openFolder', infoHash)}>
Open folder
</span>
<table> <table>
${fileRows} ${fileRows}
</table> </table>
@@ -203,7 +208,8 @@ function TorrentList (state) {
// Show a single torrentSummary file in the details view for a single torrent // Show a single torrentSummary file in the details view for a single torrent
function renderFileRow (torrentSummary, file, index) { function renderFileRow (torrentSummary, file, index) {
// First, find out how much of the file we've downloaded // First, find out how much of the file we've downloaded
var isDone = false var isSelected = torrentSummary.selections[index] // Are we even torrenting it?
var isDone = false // Are we finished torrenting it?
var progress = '' var progress = ''
if (torrentSummary.progress && torrentSummary.progress.files) { if (torrentSummary.progress && torrentSummary.progress.files) {
var fileProg = torrentSummary.progress.files[index] var fileProg = torrentSummary.progress.files[index]
@@ -212,26 +218,38 @@ function TorrentList (state) {
} }
// Second, render the file as a table row // Second, render the file as a table row
var isPlayable = TorrentPlayer.isPlayable(file)
var infoHash = torrentSummary.infoHash var infoHash = torrentSummary.infoHash
var icon var icon
var rowClass = ''
var handleClick var handleClick
if (TorrentPlayer.isPlayable(file)) { if (isPlayable) {
icon = 'play_arrow' /* playable? add option to play */ icon = 'play_arrow' /* playable? add option to play */
handleClick = dispatcher('play', infoHash, index) handleClick = dispatcher('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'
handleClick = dispatcher('openFile', infoHash, index) handleClick = dispatcher('openFile', infoHash, index)
} }
var rowClass = ''
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
return hx` return hx`
<tr onclick=${handleClick} class='${rowClass}'> <tr>
<td class='col-icon'> <td class='col-icon ${rowClass}' onclick=${handleClick}>
<i class='icon'>${icon}</i> <i class='icon'>${icon}</i>
</td> </td>
<td class='col-name'>${file.name}</td> <td class='col-name ${rowClass}' onclick=${handleClick}>
<td class='col-progress'>${progress}</td> ${file.name}
<td class='col-size'>${prettyBytes(file.length)}</td> </td>
<td class='col-progress ${rowClass}' onclick=${handleClick}>
${isSelected ? progress : ''}
</td>
<td class='col-size ${rowClass}' onclick=${handleClick}>
${prettyBytes(file.length)}
</td>
<td class='col-select'
onclick=${dispatcher('toggleTorrentFile', infoHash, index)}>
<i class='icon'>${isSelected ? 'close' : 'add'}</i>
</td>
</tr> </tr>
` `
} }

View File

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

View File

@@ -28,7 +28,14 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid // Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq // client, as explained here: https://webtorrent.io/faq
var client = window.client = new WebTorrent() var client = window.client = new WebTorrent({
tracker: {
// HACK: OS X: Disable WebRTC peers to fix 100% CPU issue caused by Chrome bug.
// Fixed in Chrome 51, so we can remove this hack once Electron updates Chrome.
// Issue: https://github.com/feross/webtorrent-desktop/issues/353
wrtc: process.platform !== 'darwin'
}
})
// WebTorrent-to-HTTP streaming sever // WebTorrent-to-HTTP streaming sever
var server = window.server = null var server = window.server = null
@@ -42,8 +49,8 @@ function init () {
client.on('warning', (err) => ipc.send('wt-warning', null, err.message)) client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message)) client.on('error', (err) => ipc.send('wt-error', null, err.message))
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes) => ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes)) startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
ipc.on('wt-stop-torrenting', (e, infoHash) => ipc.on('wt-stop-torrenting', (e, infoHash) =>
stopTorrenting(infoHash)) stopTorrenting(infoHash))
ipc.on('wt-create-torrent', (e, torrentKey, options) => ipc.on('wt-create-torrent', (e, torrentKey, options) =>
@@ -58,6 +65,8 @@ function init () {
startServer(infoHash, index)) startServer(infoHash, index))
ipc.on('wt-stop-server', (e) => ipc.on('wt-stop-server', (e) =>
stopServer()) stopServer())
ipc.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections))
ipc.send('ipcReadyWebTorrent') ipc.send('ipcReadyWebTorrent')
@@ -66,31 +75,27 @@ function init () {
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object // Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent- // See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
function startTorrenting (torrentKey, torrentID, path, fileModtimes) { function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
console.log('starting torrent %s: %s', torrentKey, torrentID) console.log('starting torrent %s: %s', torrentKey, torrentID)
var torrent
try { var torrent = client.add(torrentID, {
torrent = client.add(torrentID, { path: path,
path: path, fileModtimes: fileModtimes
fileModtimes: fileModtimes })
})
} catch (err) {
return ipc.send('wt-error', torrentKey, err.message)
}
// If we add a duplicate magnet URI or infohash, WebTorrent returns the
// existing torrent object! (If we add a duplicate torrent file, it creates a
// new torrent object and raises an error later.) Workaround:
if (torrent.key) {
return ipc.send('wt-error', torrentKey, 'Can\'t add duplicate torrent')
}
torrent.key = torrentKey torrent.key = torrentKey
// Listen for ready event, progress notifications, etc
addTorrentEvents(torrent) addTorrentEvents(torrent)
// Only download the files the user wants, not necessarily all files
torrent.once('ready', () => selectFiles(torrent, selections))
return torrent return torrent
} }
function stopTorrenting (infoHash) { function stopTorrenting (infoHash) {
var torrent = client.get(infoHash) var torrent = client.get(infoHash)
torrent.destroy() if (torrent) torrent.destroy()
} }
// Create a new torrent, start seeding // Create a new torrent, start seeding
@@ -159,9 +164,7 @@ function getTorrentFileInfo (file) {
return { return {
name: file.name, name: file.name,
length: file.length, length: file.length,
path: file.path, path: file.path
numPiecesPresent: 0,
numPieces: null
} }
} }
@@ -312,6 +315,48 @@ function getAudioMetadata (infoHash, index) {
}) })
} }
function selectFiles (torrentOrInfoHash, selections) {
// Get the torrent object
var torrent
if (typeof torrentOrInfoHash === 'string') {
torrent = client.get(torrentOrInfoHash)
} else {
torrent = torrentOrInfoHash
}
// Selections not specified?
// Load all files. We still need to replace the default whole-torrent
// selection with individual selections for each file, so we can
// select/deselect files later on
if (!selections) {
selections = torrent.files.map((x) => true)
}
// Selections specified incorrectly?
if (selections.length !== torrent.files.length) {
throw new Error('got ' + selections.length + ' file selections, ' +
'but the torrent contains ' + torrent.files.length + ' files')
}
// Remove default selection (whole torrent)
torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files)
for (var i = 0; i < selections.length; i++) {
var file = torrent.files[i]
if (selections[i]) {
file.select()
} else {
console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
file.deselect()
// If we deselected a file, try to nuke it to save disk space
var filePath = path.join(torrent.path, file.path)
fs.unlink(filePath) // Ignore errors for now
}
}
}
// Gets a WebTorrent handle by torrentKey // Gets a WebTorrent handle by torrentKey
// Throws an Error if we're not currently torrenting anything w/ that key // Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) { function getTorrent (torrentKey) {