Compare commits

...

115 Commits

Author SHA1 Message Date
DC
bf3b416662 changelog 2016-09-25 18:23:00 -07:00
DC
736832c4e3 authors 2016-09-25 18:23:00 -07:00
DC
f08c0995a2 0.17.0 2016-09-25 18:23:00 -07:00
DC
de8a4d1160 Design: smaller progress bar 2016-09-25 18:23:00 -07:00
DC
739c1f705e Design: torrent list expand and collapse handle 2016-09-25 18:22:53 -07:00
DC
a32889291f Design: don't show 'Paused' on new torrents 2016-09-25 17:57:15 -07:00
Noam Okman
1e7e4cafd4 Use the progress bar from Material UI (#967) 2016-09-25 15:42:07 -07:00
Feross Aboukhadijeh
d5bed6c50a Add more peer ID entropy (#960) 2016-09-23 12:14:50 -07:00
Dan Flettre
8f827c9aae avoid saving window bounds when on player (#964) 2016-09-23 12:09:06 -07:00
DC
24fe033e2f Design: bring back the old startup sound
Talked about it w @feross, the new one sounds kind of like an error or warning
2016-09-23 03:44:52 -07:00
DC
85d04f931b Design: update integration test screenshots 2016-09-23 03:42:13 -07:00
DC
41b111c8a8 Design: address PR comments 2016-09-23 03:33:16 -07:00
DC
72db60bb12 Player: handle case where torrent-to-HTTP server hasn't started yet 2016-09-23 02:59:57 -07:00
DC
076eb009b9 Fix bug where playback rate could go negative 2016-09-23 02:59:57 -07:00
DC
789bd0ce82 Don't hide header when moused over player controls 2016-09-23 02:59:52 -07:00
DC
5155fca0e4 Design: off white primary color 2016-09-23 02:59:48 -07:00
DC
3b6819f894 Unmount audio/video tag when exiting player 2016-09-23 02:59:48 -07:00
DC
0dd1683298 Remove play button spinner, go to player page immediately 2016-09-23 02:59:48 -07:00
DC
2788d7433b Design: torrent list 2016-09-23 02:59:48 -07:00
DC
504a2419f6 Design: remove extra CSS 2016-09-23 02:59:48 -07:00
DC
75e5316ba1 Design: no gradients, nicer default colors
Before, the gradient transparent black overlay made text hard to read in some cases. Torrents without a poster image showed up in blue-gray and didn't look good.
2016-09-23 02:59:47 -07:00
DC
3be018521a Torrent list redesign 2016-09-23 02:59:47 -07:00
Feross Aboukhadijeh
6d375d5b5b Wait for 'ready-to-show' event before showing window (#959)
This gets rid of the light gray to dark gray background color change on
the main window at startup. Makes the window show slightly later, but
it's gray for less time. Doesn't affect overall startup time. Feels
less jank IMO.

From the Electron docs:

> While loading the page, the 'ready-to-show' event will be emitted
when renderer process has done drawing for the first time, showing
window after this event will have no visual flash.
2016-09-23 02:59:22 -07:00
Feross Aboukhadijeh
5de39bd7e5 Merge pull request #957 from feross/f/perf
startup perf: Reduce require() calls
2016-09-23 04:38:12 +02:00
Feross Aboukhadijeh
a08d576851 startup perf: Reduce require() calls
Every require() that we do before the users sees UI reduces startup
time.

I used the following code (added to index.js) to log every require()
call in the main process:

```js
var Module = require('module')
var required = {}
Module.prototype.require = function (orig) {
  return function (id) {
    if (!required[id]) {
      required[id] = true
      console.log(`${id}   (from ${this.filename})`)
    }
    return orig.apply(this, arguments)
  }
}(Module.prototype.require)
```

From this I was able to learn that lots of modules were being required
that aren't actually used until later.

I also sent this related PR to eliminate another few require()s:
https://github.com/LinusU/node-application-config/pull/4

This increases startup time by 50ms.

We'll probably realize much bigger gains by following this same
procedure for the renderer process.
2016-09-22 16:33:26 -07:00
Feross Aboukhadijeh
b8bdf65514 Use subtler UI sounds (#945)
* Use subtler UI sounds

Fixes #858

* Second round of volume tweaks
2016-09-22 14:42:43 -07:00
Feross Aboukhadijeh
1c0c3d07ff Merge pull request #949 from feross/f/state
State cleanup, rename, & tweaks
2016-09-22 23:41:01 +02:00
Feross Aboukhadijeh
832980eb9a Update Electron to 1.4.1 (#955)
Changelog: https://github.com/electron/electron/releases/tag/v1.4.1

(Should fix the Spectron console issue that @dcposch reported.)
2016-09-22 14:39:32 -07:00
Feross Aboukhadijeh
7c158e9f2c Rename events to be consistent
- Make State.save() always throttle calls -- since that's the common
case.

- Immediate saves are now the exception, with State.saveImmediate().

- The function is called State.save(), so the dispatch event should be
'stateSave'.
2016-09-22 14:25:57 -07:00
Feross Aboukhadijeh
a98d22ed72 code style 2016-09-22 14:20:58 -07:00
Feross Aboukhadijeh
63b7d34e29 Merge pull request #954 from omrilitov/master
use arch to determine OS_SYSARCH
2016-09-22 23:07:20 +02:00
Omri Litov
67ae6061aa use arch to determine OS_SYSARCH 2016-09-22 23:29:02 +03:00
Feross Aboukhadijeh
a8a861260e main: Start loading state before app is ready (#952)
As mentioned in
https://github.com/feross/webtorrent-desktop/pull/827#discussion_r799219
59

We should load the state outside the app.on('ready') handler so there's
a chance it's ready by the time 'ready' fires.

This improves startup time by roughly 50ms on my Macbook 12".
2016-09-22 03:00:40 -07:00
Feross Aboukhadijeh
fc879d5801 Update electron-packager to v8 (#946)
* electron-packager@8

* package: updates for electron-packager 8
2016-09-22 02:42:43 -07:00
DC
2d2645e642 Integration test: announce list windows screenshot 2016-09-21 23:58:42 -07:00
DC
8e66f641ce Integration test: default announce list changed 2016-09-21 23:58:21 -07:00
DC
82c49b5fc5 Integration test: yet another round of fixes 2016-09-21 23:58:21 -07:00
DC
b57ee73035 Integration tests: extra wait in test-audio 2016-09-21 23:58:21 -07:00
Feross Aboukhadijeh
ed8f493b8b Merge pull request #950 from feross/f/config
Make config static, not dynamic
2016-09-21 22:46:30 +02:00
Feross Aboukhadijeh
9e853027da make config static, not dynamic
I don't think we should dynamically generate the config object -- it
makes it harder to figure out what's going on.
2016-09-21 13:08:32 -07:00
Feross Aboukhadijeh
1e05487acd state: Use dispatch instead of direct call 2016-09-21 11:48:23 -07:00
Feross Aboukhadijeh
167da9dfd5 Double wait time until quit
On my modern Macbook 12" I've run into "Saving state took too long.
Quitting.". We have users with spinning disk drives, so let's be a bit
more generous.
2016-09-21 11:46:41 -07:00
Feross Aboukhadijeh
46e138a376 state: Use debounce to throttle saves 2016-09-21 11:46:00 -07:00
DC
853db922f1 Integration test: windows screenshot fuzzy diff 2016-09-20 23:53:00 -07:00
DC
7bf51b11ee Integration test: screenshot timing 2016-09-20 23:51:38 -07:00
DC
3a286ae978 Integration test: wait for next song 2016-09-20 23:51:38 -07:00
DC
82245f0b5c Integration tests: README 2016-09-20 23:51:37 -07:00
DC
802a898394 Integration tests: offline by default 2016-09-20 23:51:37 -07:00
DC
2200fffa1e Fix integration tests on Windows 2016-09-20 23:51:37 -07:00
DC
f4b2e78e72 Fix Delete Data on Windows. Fixes #935 2016-09-20 23:51:37 -07:00
DC
ad1162c7de Integration tests on Windows 2016-09-20 23:51:33 -07:00
DC
ed4daeb560 Integration test reliability 2016-09-20 23:49:45 -07:00
Feross Aboukhadijeh
927ae16e4f Merge pull request #941 from feross/dc/perf
Fix a sad, sad bug that resulted in 100+MB config.json
2016-09-21 08:18:48 +02:00
Feross Aboukhadijeh
917c89542b Merge pull request #944 from feross/buble
Replace babel with bublé
2016-09-21 08:18:27 +02:00
Feross Aboukhadijeh
39570bd4d7 Replace babel with bublé
Pros of bubel over babel:

- No configuration (a la standard)
- Runs twice as fast, for quicker development
- Converts everything to ES5 (which is likely to be faster than ES6,
untested)
- Easy to swap Babel back in -- low commitment

Cons:
- Less battle-tested than Babel, but recommended by React core
developer so probably not too bad
- No babel plugin support, but we're not using that right now anyway.
Can switch back to babel if we need that later

BEFORE:

$ time npm run build

> webtorrent-desktop@0.16.0 build /Users/feross/code/webtorrent-desktop
> babel --quiet src --out-dir build

npm run build  3.07s user 0.27s system 115% cpu 2.902 total

AFTER:

$ time npm run build

> webtorrent-desktop@0.16.0 build /Users/feross/code/webtorrent-desktop
> buble src --output build

npm run build  1.38s user 0.16s system 114% cpu 1.343 total
2016-09-20 15:21:17 -07:00
DC
f368dfad81 Fix a sad, sad bug that resulted in 100+MB config.json 2016-09-19 22:31:26 -07:00
DC
205e17cc83 Merge pull request #937 from feross/dynamic-window-min-height
make WINDOW_MIN_HEIGHT use existing values
2016-09-19 19:30:05 -07:00
Dan Flettre
4e052c8059 make WINDOW_MIN_HEIGHT use existing values 2016-09-19 16:48:12 -05:00
Dan Flettre
0afecd6063 Merge pull request #827 from feross/window-position
remember window position
2016-09-19 16:04:46 -05:00
DC
41511c5615 Integration test: use /tmp 2016-09-18 19:17:07 -07:00
Feross Aboukhadijeh
b9d39e3c64 changelog 2016-09-18 03:03:45 -07:00
Feross Aboukhadijeh
c1f482a950 Fix app DMG image 2016-09-18 02:58:39 -07:00
Feross Aboukhadijeh
e424031ad9 Windows build path fix 2016-09-18 02:58:36 -07:00
Feross Aboukhadijeh
f43dc2fc98 Fix windows path rename 2016-09-18 02:11:23 -07:00
Feross Aboukhadijeh
9cdc73edce 0.16.0 2016-09-18 01:56:59 -07:00
Feross Aboukhadijeh
3d254fa075 changelog 2016-09-18 01:41:01 -07:00
Feross Aboukhadijeh
ed1e43015e Merge pull request #931 from feross/detect-arch
Add 64-bit Windows build
2016-09-18 10:37:05 +02:00
Feross Aboukhadijeh
e6cbbd73f0 Fix silly typo 2016-09-18 01:35:20 -07:00
Feross Aboukhadijeh
67dff7b38c Add sanity check 2016-09-18 01:33:51 -07:00
Feross Aboukhadijeh
ced67176a3 add changelog 2016-09-18 01:29:14 -07:00
Feross Aboukhadijeh
00ac8afe64 About window: Show 32-bit vs. 64-bit status 2016-09-18 01:22:16 -07:00
Feross Aboukhadijeh
a6964c4495 Change file name inside RELEASES-ia32 to match renamed file 2016-09-18 01:07:45 -07:00
Dan Flettre
aedbc3c32f remember window position 2016-09-18 00:29:37 -05:00
DC
6541291e0d Integration test: address PR comments 2016-09-17 20:35:54 -07:00
DC
711d274398 Integration test: mock cast, remove loading bar
This lets us use exact screenshots with no transparency.
2016-09-17 20:35:53 -07:00
DC
6c5861b9fc Test screenshots: Create Torrent raised button 2016-09-17 20:35:53 -07:00
DC
f7ab27f9fd Integration test: audio 2016-09-17 20:35:53 -07:00
DC
e4e789cc5b Integration test: screenshot compare ignoring transparency 2016-09-17 20:35:52 -07:00
DC
09b525fe58 Integration test: simplify offline mode 2016-09-17 20:35:52 -07:00
DC
9dabfc1367 Integration test: offline mode 2016-09-17 20:35:52 -07:00
DC
bdb733352a Integration test: video playback 2016-09-17 20:35:52 -07:00
DC
75a4655a0f Integration test: create torrents 2016-09-17 20:35:52 -07:00
DC
051c1516a0 Integration test: add existing torrent 2016-09-17 20:35:51 -07:00
DC
62c5b78358 Integration test: update README 2016-09-17 20:35:51 -07:00
DC
290913d07a Integration test: delete torrent + data 2016-09-17 20:35:51 -07:00
Feross Aboukhadijeh
77534d650a Add 64-bit Windows build
Right now all Windows users are running a 32-bit app, even if their OS
is 64-bit.

Here's the plan to improve things:

1. We release a 64-bit installer, in addition to the 32-bit installer.

2. We auto-detect in the browser when a visitor is on a 32-bit vs.
64-bit OS and try to offer them the right installer. When in doubt, we
give them the 32-bit installer since that's safest.

3. The auto-updater will return the right binaries for the architecture
the user is on. This means that all our existing users who have 64-bit
OSs but are running the 32-bit app will get updated to the 64-bit app
on the next update. Also, 64-bit users who accidentally download the
32-bit installer will also get the 64-bit app on the next update.

---

Other notes:

- We don't generate 32-bit delta files. See reasoning inline.
- The package script now deletes extraneous Squirrel files (i.e.
*.nupkg delta files for older versions of the app) which should make
uploading the right files to GitHub easier. :)

The binary file naming works like this:

- Most users are on 64-bit systems, so they get nice clean file names
that don't specify an architecture (WebTorrentSetup-v1.0.0.exe). The
32-bit build files have the same naming but contain the string "-ia32"
at the end. In a few years, we will be able to just stop producing the
32-bit build files entirely.

- This means that the "WebTorrent-v0.15.0-linux-x64.zip" linux build
file is changing to "WebTorrent-v0.15.0-linux.zip" to match the Windows
naming convention. The .deb installer files must contain to
architecture in order to install correctly, so those do not change.

- Mac is 100% 64-bit, so it does not change.
2016-09-17 19:37:50 -07:00
Feross Aboukhadijeh
a4c715e3f6 Merge pull request #928 from feross/detect-arch
Detect system architecture; send in update/telemetry
2016-09-17 16:21:46 -07:00
Feross Aboukhadijeh
7415d3cee5 Detect system architecture; send in update/telemetry
Detect the actual operating system CPU architecture. This is different
than `process.arch` which returns the architecture the binary was
compiled for.

This is just good info to have in the telemetry, but we're also sending
it in the update check so that eventually we can upgrade Windows 32-bit
apps to 64-bit, like Slack does.

Context:
https://github.com/feross/webtorrent-desktop/issues/873#issuecomment-247
722023
2016-09-16 19:24:21 -07:00
Feross Aboukhadijeh
e1ba9c89fe Merge pull request #922 from feross/raised
Use raised button for inline button
2016-09-16 15:45:20 -07:00
Feross Aboukhadijeh
0fcbe7369a Merge pull request #925 from feross/capture-frame
Use capture-frame package
2016-09-16 15:43:20 -07:00
Feross Aboukhadijeh
c8087b5b63 Merge pull request #926 from feross/electron-1.4.0
Electron 1.4.0
2016-09-16 15:42:44 -07:00
Feross Aboukhadijeh
065faca8eb Electron 1.4.0 2016-09-16 10:25:03 -07:00
Feross Aboukhadijeh
bcd6a38a05 Use capture-frame package
See: https://github.com/feross/capture-frame
Capture video screenshot from a `<video>` tag (at the current time)

Changes from our version:

- Added tests in Chrome/Firefox browsers.
- Use built-in TypeError (which is meant for bad arguments) instead of
custom IllegalArgumentError.
2016-09-16 10:14:39 -07:00
DC
fa67f9b82b changelog 2016-09-16 01:11:12 -07:00
DC
39acd0bd47 authors 2016-09-16 01:02:16 -07:00
DC
c549fcfc27 0.15.0 2016-09-16 01:01:32 -07:00
DC
45e838d4c3 Telemetry: add torrent stats 2016-09-16 00:54:07 -07:00
DC
64f49e4d4f Auto launch: start minimized on MacOS 2016-09-13 19:49:47 -07:00
DC
61caa90901 Auto launch: don't open a terminal on MacOS 2016-09-13 19:49:47 -07:00
Noam Okman
3e85289318 Pref: start automatically on login 2016-09-13 19:49:47 -07:00
Feross Aboukhadijeh
a629f287f0 Use raised button for inline button 2016-09-13 08:16:38 -07:00
Mathias Rasmussen
3a4906079b External player clean up (#914)
* minor `addSubtitles` clean up

* external player clean up
2016-09-12 17:46:48 -07:00
Kai Curtis
3edf21f457 Update mouse moved time on header hover (#919)
Previously, moving the mouse into the player window from the sides or
bottom would bring up the HUD, but moving the mouse in from the top
would not. With this commit, moving the mouse in from the top of the
window will also bring up the HUD.

Fixes feross/webtorrent-desktop#241
2016-09-12 17:46:23 -07:00
DC
785c44cd2a Integration test: torrent list 2016-09-08 23:55:37 -07:00
DC
1ad8a5313b Integration test: save failed screenshot comparisons 2016-09-08 19:13:14 -07:00
DC
967e161288 Integration test: screenshots 2016-09-08 19:10:28 -07:00
DC
fe8c3b190c Integration test: tape + spectron hello world 2016-09-08 19:10:28 -07:00
Benjamin Tan
993e7d77ad Fix notification click not working. (#912)
This was changed incorrectly in 2a1e987.
2016-09-08 16:16:43 -07:00
DC
e0be052df4 Fix Open Torrent Address modal
Fixes a bug introduced in 0.14.0: cicking OK works, but hitting Enter doesn't do anything
2016-09-07 13:28:49 -07:00
Adam Gotlib
d331bae548 Move error definitions to errors.js (#898) 2016-09-07 13:21:59 -07:00
Adam Gotlib
d88229694a Disable playback controls while in external player (#909) 2016-09-07 13:13:50 -07:00
Adam Gotlib
8da5b955d6 Make git ignore npm-debug.log (#896) 2016-09-05 20:04:21 -07:00
DC
54882679c1 Dedupe cast.js status handlers, fix #889 2016-09-04 15:10:39 -07:00
DC
f2007be1b0 Fix selectFiles error, fixes #891 2016-09-04 14:56:17 -07:00
DC
7a757f9e05 More info in torrentFileModtimes, fix #892 2016-09-04 14:27:22 -07:00
128 changed files with 1608 additions and 599 deletions

2
.gitignore vendored
View File

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

View File

@@ -32,5 +32,7 @@
- Vamsi Krishna Avula (vamsi_ism@outlook.com)
- Noam Okman (noamokman@gmail.com)
- PurgingPanda (t3ch0wn3r@gmail.com)
- Kai Curtis (morecode@kcurtis.com)
- Omri Litov (omrilitov@gmail.com)
#### Generated by bin/update-authors.sh.

View File

@@ -1,5 +1,48 @@
# WebTorrent Desktop Version History
## v0.17.0 - 2016-09-23
### Added
- Remember window size and position
### Changed
- Torrent list redesign
- Quieter, more subtle sounds
- Got rid of the play button spinner, now goes to the player immediately
- Faster startup
### Fixed
- Fix bug where playback rate could go negative
- Don't hide header when moused over player controls
- Fix Delete Data File on Windows
- Fix a sad, sad bug that resulted in 100+ MB config files
- Fix app DMG background image
## v0.16.0 - 2016-09-18
### Added
- **Windows 64-bit support!** ([#931](https://github.com/feross/webtorrent-desktop/pull/931))
- Existing 32-bit users will update to 64-bit automatically in next release
- 64-bit reduces likelihood of out-of-memory errors by increasing the address space
### Fixed
- Mac: Fix background image on .DMG
## v0.15.0 - 2016-09-16
### Added
- Option to start automatically on login
- Add integration tests
- Add more detailed telemetry to diagnose "buffer allocation failed"
### Changed
- Disable playback controls while in external player (#909)
### Fixed
- Fix several uncaught errors (#889, #891, #892)
- Update to the latest webtorrent.js, fixing some more uncaught errors
- Clicking on the "torrent finished" notification works again (#912)
## v0.14.0 - 2016-09-03
### Added

View File

@@ -56,6 +56,32 @@ Restart the app automatically every time code changes. Useful during development
$ npm run watch
```
### Run linters
```
$ npm test
```
### Run integration tests
```
$ npm run integration-test
```
The integration tests use Spectron and Tape. They click through the app, taking screenshots and comparing each one to a reference. Why screenshots?
* Ad-hoc checking makes the tests a lot more work to write
* Even diffing the whole HTML is not as thorough as screenshot diffing. For example, it wouldn't catch an bug where hitting ESC from a video doesn't correctly restore window size.
* Chrome's own integration tests use screenshot diffing iirc
* Small UI changes will break a few tests, but the fix is as easy as deleting the offending screenshots and running the tests, which will recreate them with the new look.
* The resulting Github PR will then show, pixel by pixel, the exact UI changes that were made! Ses https://github.com/blog/817-behold-image-view-modes
For MacOS, you'll need a Retina screen for the integration tests to pass. Your screen should have the same resolution as a 2016 12" Macbook.
For Windows, you'll need Windows 10 with a 1366x768 screen.
When running integration tests, keep the mouse on the edge of the screen and don't touch the mouse or keyboard while the tests are running.
### Package the app
Builds app binaries for Mac, Linux, and Windows.

View File

@@ -39,9 +39,9 @@ function build () {
rimraf.sync(DIST_PATH)
rimraf.sync(BUILD_PATH)
console.log('Babel: Building JSX...')
console.log('Build: Transpiling to ES5...')
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
console.log('Babel: Built JSX.')
console.log('Build: Transpiled to ES5.')
var platform = argv._[0]
if (platform === 'darwin') {
@@ -73,11 +73,11 @@ var all = {
// Package the application's source code into an archive, using Electron's archive
// format. Mitigates issues around long path names on Windows and slightly speeds up
// require().
asar: true,
// A glob expression, that unpacks the files with matching names to the
// "app.asar.unpacked" directory.
'asar-unpack': 'WebTorrent*',
asar: {
// A glob expression, that unpacks the files with matching names to the
// "app.asar.unpacked" directory.
unpack: 'WebTorrent*'
},
// The build version of the application. Maps to the FileVersion metadata property on
// Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to
@@ -112,7 +112,7 @@ var darwin = {
// Build for Mac
platform: 'darwin',
// Build 64 bit binaries only.
// Build x64 binaries only.
arch: 'x64',
// The bundle identifier to use in the application's plist (Mac only).
@@ -133,11 +133,11 @@ var win32 = {
// Build for Windows.
platform: 'win32',
// Build 32 bit binaries only.
arch: 'ia32',
// Build ia32 and x64 binaries.
arch: 'all',
// Object hash of application metadata to embed into the executable (Windows only)
'version-string': {
win32metadata: {
// Company that produced the file.
CompanyName: config.APP_NAME,
@@ -167,7 +167,7 @@ var linux = {
// Build for Linux.
platform: 'linux',
// Build 32 and 64 bit binaries.
// Build ia32 and x64 binaries.
arch: 'all'
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
@@ -388,19 +388,25 @@ function buildWin32 (cb) {
}
var tasks = []
if (argv.package === 'exe' || argv.package === 'all') {
tasks.push((cb) => packageInstaller(cb))
}
if (argv.package === 'portable' || argv.package === 'all') {
tasks.push((cb) => packagePortable(cb))
}
buildPath.forEach(function (filesPath) {
var destArch = filesPath.split('-').pop()
if (argv.package === 'exe' || argv.package === 'all') {
tasks.push((cb) => packageInstaller(filesPath, destArch, cb))
}
if (argv.package === 'portable' || argv.package === 'all') {
tasks.push((cb) => packagePortable(filesPath, destArch, cb))
}
})
series(tasks, cb)
function packageInstaller (cb) {
console.log('Windows: Creating installer...')
function packageInstaller (filesPath, destArch, cb) {
console.log(`Windows: Creating ${destArch} installer...`)
var archStr = destArch === 'ia32' ? '-ia32' : ''
installer.createWindowsInstaller({
appDirectory: buildPath[0],
appDirectory: filesPath,
authors: config.APP_TEAM,
description: config.APP_NAME,
exe: config.APP_NAME + '.exe',
@@ -410,8 +416,21 @@ function buildWin32 (cb) {
noMsi: true,
outputDirectory: DIST_PATH,
productName: config.APP_NAME,
remoteReleases: config.GITHUB_URL,
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
/**
* Only create delta updates for the Windows x64 build because 90% of our
* users have Windows x64 and the delta files take a *very* long time to
* generate. Also, the ia32 files on GitHub have non-standard Squirrel
* names (i.e. RELEASES-ia32 instead of RELEASES) and so Squirrel won't
* find them unless we proxy the requests.
*/
remoteReleases: destArch === 'x64'
? config.GITHUB_URL
: undefined,
/**
* If you hit a "GitHub API rate limit exceeded" error, set this token!
*/
remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + archStr + '.exe',
setupIcon: config.APP_ICON + '.ico',
signWithParams: signWithParams,
title: config.APP_NAME,
@@ -419,23 +438,65 @@ function buildWin32 (cb) {
version: pkg.version
})
.then(function () {
console.log('Windows: Created installer.')
console.log(`Windows: Created ${destArch} installer.`)
/**
* Delete extraneous Squirrel files (i.e. *.nupkg delta files for older
* versions of the app)
*/
fs.readdirSync(DIST_PATH)
.filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version))
.forEach((filename) => {
fs.unlinkSync(path.join(DIST_PATH, filename))
})
if (destArch === 'ia32') {
console.log('Windows: Renaming ia32 installer files...')
// RELEASES -> RELEASES-ia32
var relPath = path.join(DIST_PATH, 'RELEASES-ia32')
fs.renameSync(
path.join(DIST_PATH, 'RELEASES'),
relPath
)
// WebTorrent-vX.X.X-full.nupkg -> WebTorrent-vX.X.X-ia32-full.nupkg
fs.renameSync(
path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-full.nupkg`),
path.join(DIST_PATH, `${config.APP_NAME}-${config.APP_VERSION}-ia32-full.nupkg`)
)
// Change file name inside RELEASES-ia32 to match renamed file
var relContent = fs.readFileSync(relPath, 'utf8')
var relContent32 = relContent.replace('full.nupkg', 'ia32-full.nupkg')
fs.writeFileSync(relPath, relContent32)
if (relContent === relContent32) {
// Sanity check
throw new Error('Fixing RELEASES-ia32 failed. Replacement did not modify the file.')
}
console.log('Windows: Renamed ia32 installer files.')
}
cb(null)
})
.catch(cb)
}
function packagePortable (cb) {
console.log('Windows: Creating portable app...')
function packagePortable (filesPath, destArch, cb) {
console.log(`Windows: Creating ${destArch} portable app...`)
var portablePath = path.join(buildPath[0], 'Portable Settings')
var portablePath = path.join(filesPath, 'Portable Settings')
mkdirp.sync(portablePath)
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
var archStr = destArch === 'ia32' ? '-ia32' : ''
var inPath = path.join(DIST_PATH, path.basename(filesPath))
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win' + archStr + '.zip')
zip.zipSync(inPath, outPath)
console.log('Windows: Created portable app.')
console.log(`Windows: Created ${destArch} portable app.`)
cb(null)
}
})
@@ -500,8 +561,10 @@ function buildLinux (cb) {
// Create .zip file for Linux
console.log(`Linux: Creating ${destArch} zip...`)
var archStr = destArch === 'ia32' ? '-ia32' : ''
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' + archStr + '.zip')
zip.zipSync(inPath, outPath)
console.log(`Linux: Created ${destArch} zip.`)

View File

@@ -1,7 +1,7 @@
{
"name": "webtorrent-desktop",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.14.0",
"version": "0.17.0",
"author": {
"name": "WebTorrent, LLC",
"email": "feross@webtorrent.io",
@@ -16,13 +16,18 @@
"dependencies": {
"airplayer": "^2.0.0",
"application-config": "^1.0.0",
"arch": "^2.0.0",
"auto-launch": "^4.0.1",
"bitfield": "^1.0.2",
"capture-frame": "^1.0.0",
"chromecasts": "^1.8.0",
"create-torrent": "^3.24.5",
"debounce": "^1.0.0",
"deep-equal": "^1.0.1",
"dlnacasts": "^0.1.0",
"drag-drop": "^2.12.1",
"electron": "1.3.3",
"electron": "1.4.1",
"es6-error": "^3.0.1",
"fs-extra": "^0.30.0",
"iso-639-1": "^1.2.1",
"languagedetect": "^1.1.1",
@@ -46,15 +51,11 @@
"zero-fill": "^2.2.3"
},
"devDependencies": {
"babel-cli": "^6.11.4",
"babel-plugin-syntax-jsx": "^6.13.0",
"babel-plugin-transform-es2015-destructuring": "^6.9.0",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-plugin-transform-react-jsx": "^6.8.0",
"buble": "^0.14.0",
"cross-zip": "^2.0.1",
"depcheck": "^0.6.4",
"electron-osx-sign": "^0.3.0",
"electron-packager": "^7.0.0",
"electron-packager": "^8.0.0",
"electron-winstaller": "^2.3.0",
"gh-release": "^2.0.3",
"minimist": "^1.2.0",
@@ -63,9 +64,12 @@
"nodemon": "^1.10.2",
"open": "0.0.5",
"plist": "^2.0.1",
"pngjs": "^3.0.0",
"rimraf": "^2.5.2",
"run-series": "^1.1.4",
"spectron": "^3.3.0",
"standard": "*",
"tape": "^4.6.0",
"walk-sync": "^0.3.1"
},
"engines": {
@@ -93,13 +97,14 @@
"url": "git://github.com/feross/webtorrent-desktop.git"
},
"scripts": {
"build": "babel --quiet src --out-dir build",
"build": "buble src --output build",
"clean": "node ./bin/clean.js",
"open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js",
"prepublish": "npm run build",
"start": "npm run build && electron .",
"test": "standard && depcheck --ignores=babel-cli,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
"integration-test": "npm run build && node ./test",
"test": "standard && depcheck --ignores=buble,nodemon,gh-release --ignore-dirs=build,dist && node ./bin/extra-lint.js",
"gh-release": "gh-release",
"update-authors": "./bin/update-authors.sh",
"watch": "nodemon --exec \"npm run start\" --ext js,pug,css"

View File

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

View File

@@ -1,12 +1,26 @@
const appConfig = require('application-config')('WebTorrent')
const fs = require('fs')
const path = require('path')
const electron = require('electron')
const arch = require('arch')
const APP_NAME = 'WebTorrent'
const APP_TEAM = 'WebTorrent, LLC'
const APP_VERSION = require('../package.json').version
const PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
const IS_TEST = isTest()
const PORTABLE_PATH = IS_TEST
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
: path.join(path.dirname(process.execPath), 'Portable Settings')
const IS_PORTABLE = isPortable()
const IS_PRODUCTION = isProduction()
const UI_HEADER_HEIGHT = 38
const UI_TORRENT_HEIGHT = 100
console.log('Production: %s portable: %s test: %s',
IS_PRODUCTION, IS_PORTABLE, IS_TEST)
if (IS_PORTABLE) console.log('Portable path: %s', PORTABLE_PATH)
module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
@@ -26,26 +40,31 @@ module.exports = {
DEFAULT_TORRENTS: [
{
testID: 'bbb',
name: 'Big Buck Bunny',
posterFileName: 'bigBuckBunny.jpg',
torrentFileName: 'bigBuckBunny.torrent'
},
{
testID: 'cosmos',
name: 'Cosmos Laundromat (Preview)',
posterFileName: 'cosmosLaundromat.jpg',
torrentFileName: 'cosmosLaundromat.torrent'
},
{
testID: 'sintel',
name: 'Sintel',
posterFileName: 'sintel.jpg',
torrentFileName: 'sintel.torrent'
},
{
testID: 'tears',
name: 'Tears of Steel',
posterFileName: 'tearsOfSteel.jpg',
torrentFileName: 'tearsOfSteel.torrent'
},
{
testID: 'wired',
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
posterFileName: 'wiredCd.jpg',
torrentFileName: 'wiredCd.torrent'
@@ -62,8 +81,11 @@ module.exports = {
HOME_PAGE_URL: 'https://webtorrent.io',
IS_PORTABLE: isPortable(),
IS_PRODUCTION: isProduction(),
IS_PORTABLE: IS_PORTABLE,
IS_PRODUCTION: IS_PRODUCTION,
IS_TEST: IS_TEST,
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: path.join(__dirname, '..'),
@@ -74,12 +96,19 @@ module.exports = {
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents
WINDOW_MIN_WIDTH: 425
WINDOW_INITIAL_BOUNDS: {
width: 500,
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
},
WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents
WINDOW_MIN_WIDTH: 425,
UI_HEADER_HEIGHT: UI_HEADER_HEIGHT,
UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT
}
function getConfigPath () {
if (isPortable()) {
if (IS_PORTABLE) {
return PORTABLE_PATH
} else {
return path.dirname(appConfig.filePath)
@@ -89,22 +118,31 @@ function getConfigPath () {
function getDefaultDownloadPath () {
if (!process || !process.type) {
return ''
}
if (isPortable()) {
} else if (IS_PORTABLE) {
return path.join(getConfigPath(), 'Downloads')
} else {
return getPath('downloads')
}
}
const electron = require('electron')
function getPath (key) {
if (process.type === 'renderer') {
return electron.remote.app.getPath(key)
} else {
return electron.app.getPath(key)
}
}
return process.type === 'renderer'
? electron.remote.app.getPath('downloads')
: electron.app.getPath('downloads')
function isTest () {
return process.env.NODE_ENV === 'test'
}
function isPortable () {
if (IS_TEST) {
return true
}
try {
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
return process.platform === 'win32' && IS_PRODUCTION && !!fs.statSync(PORTABLE_PATH)
} catch (err) {
return false
}

View File

@@ -21,12 +21,7 @@ function openSeedFile () {
title: 'Select a file for the torrent.',
properties: [ 'openFile' ]
}
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
showOpenSeed(opts)
}
/*
@@ -46,12 +41,7 @@ function openSeedDirectory () {
title: 'Select a folder for the torrent.',
properties: [ 'openDirectory' ]
}
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
showOpenSeed(opts)
}
/*
@@ -119,3 +109,16 @@ function setTitle (title) {
function resetTitle () {
windows.main.dispatch('resetTitle')
}
/**
* Pops up an Open File dialog with the given options.
* After the user selects files / folders, shows the Create Torrent page.
*/
function showOpenSeed (opts) {
setTitle(opts.title)
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
resetTitle()
if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths)
})
}

View File

@@ -5,6 +5,7 @@ module.exports = {
}
const cp = require('child_process')
const path = require('path')
const vlcCommand = require('vlc-command')
const log = require('./log')
@@ -13,15 +14,15 @@ const windows = require('./windows')
// holds a ChildProcess while we're playing a video in an external player, null otherwise
let proc = null
function checkInstall (path, cb) {
function checkInstall (playerPath, cb) {
// check for VLC if external player has not been specified by the user
// otherwise assume the player is installed
if (path == null) return vlcCommand((err) => cb(!err))
if (playerPath == null) return vlcCommand((err) => cb(!err))
process.nextTick(() => cb(true))
}
function spawn (path, url, title) {
if (path != null) return spawnExternal(path, [url])
function spawn (playerPath, url, title) {
if (playerPath != null) return spawnExternal(playerPath, [url])
// Try to find and use VLC if external player is not specified
vlcCommand(function (err, vlcPath) {
@@ -44,10 +45,15 @@ function kill () {
proc = null
}
function spawnExternal (path, args) {
log('Running external media player:', path + ' ' + args.join(' '))
function spawnExternal (playerPath, args) {
log('Running external media player:', playerPath + ' ' + args.join(' '))
proc = cp.spawn(path, args, {stdio: 'ignore'})
if (path.extname(playerPath) === '.app') {
// Mac: Use executable in packaged .app bundle
playerPath += '/Contents/MacOS/' + path.basename(playerPath, '.app')
}
proc = cp.spawn(playerPath, args, {stdio: 'ignore'})
// If it works, close the modal after a second
const closeModalTimeout = setTimeout(() =>

View File

@@ -1,27 +1,26 @@
console.time('init')
const electron = require('electron')
const app = electron.app
const ipcMain = electron.ipcMain
const announcement = require('./announcement')
const parallel = require('run-parallel')
const config = require('../config')
const crashReporter = require('../crash-reporter')
const dialog = require('./dialog')
const dock = require('./dock')
const ipc = require('./ipc')
const log = require('./log')
const menu = require('./menu')
const squirrelWin32 = require('./squirrel-win32')
const tray = require('./tray')
const updater = require('./updater')
const userTasks = require('./user-tasks')
const State = require('../renderer/lib/state')
const windows = require('./windows')
let shouldQuit = false
let argv = sliceArgv(process.argv)
// Start the app without showing the main window when auto launching on login
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
const hidden = argv.includes('--hidden') ||
(process.platform === 'darwin' && app.getLoginItemSettings().wasOpenedAsHidden)
if (config.IS_PRODUCTION) {
// When Electron is running in production mode (packaged app), then run React
// in production mode too.
@@ -29,6 +28,7 @@ if (config.IS_PRODUCTION) {
}
if (process.platform === 'win32') {
const squirrelWin32 = require('./squirrel-win32')
shouldQuit = squirrelWin32.handleEvent(argv[0])
argv = argv.filter((arg) => !arg.includes('--squirrel'))
}
@@ -51,24 +51,23 @@ function init () {
app.setPath('userData', config.CONFIG_PATH)
}
const ipcMain = electron.ipcMain
let isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false
// Open handlers must be added as early as possible
app.on('open-file', onOpen)
app.on('open-url', onOpen)
parallel({
appReady: (cb) => app.on('ready', () => cb(null)),
state: (cb) => State.load(cb)
}, onReady)
ipc.init()
function onReady (err, results) {
if (err) throw err
app.once('will-finish-launching', function () {
crashReporter.init()
})
app.on('ready', function () {
isReady = true
windows.main.init()
windows.main.init(results.state, {hidden: hidden})
windows.webtorrent.init()
menu.init()
@@ -81,6 +80,15 @@ function init () {
const error = {message: err.message, stack: err.stack}
windows.main.dispatch('uncaughtError', 'main', error)
})
}
app.on('open-file', onOpen)
app.on('open-url', onOpen)
ipc.init()
app.once('will-finish-launching', function () {
crashReporter.init()
})
app.once('ipcReady', function () {
@@ -94,12 +102,12 @@ function init () {
app.isQuitting = true
e.preventDefault()
windows.main.dispatch('saveState') // try to save state on exit
ipcMain.once('savedState', () => app.quit())
windows.main.dispatch('stateSaveImmediate') // try to save state on exit
ipcMain.once('stateSaved', () => app.quit())
setTimeout(() => {
console.error('Saving state took too long. Quitting.')
app.quit()
}, 2000) // quit after 2 secs, at most
}, 4000) // quit after 4 secs, at most
})
app.on('activate', function () {
@@ -108,6 +116,12 @@ function init () {
}
function delayedInit () {
const announcement = require('./announcement')
const dock = require('./dock')
const tray = require('./tray')
const updater = require('./updater')
const userTasks = require('./user-tasks')
announcement.init()
dock.init()
tray.init()
@@ -143,22 +157,38 @@ function onAppOpen (newArgv) {
}
}
// Remove leading args.
// Production: 1 arg, eg: /Applications/WebTorrent.app/Contents/MacOS/WebTorrent
// Development: 2 args, eg: electron .
// Test: 4 args, eg: electron -r .../mocks.js .
function sliceArgv (argv) {
return argv.slice(config.IS_PRODUCTION ? 1 : 2)
return argv.slice(config.IS_PRODUCTION ? 1
: config.IS_TEST ? 4
: 2)
}
function processArgv (argv) {
let torrentIds = []
argv.forEach(function (arg) {
if (arg === '-n') {
dialog.openSeedDirectory()
} else if (arg === '-o') {
dialog.openTorrentFile()
} else if (arg === '-u') {
dialog.openTorrentAddress()
if (arg === '-n' || arg === '-o' || arg === '-u') {
// Critical path: Only load the 'dialog' package if it is needed
const dialog = require('./dialog')
if (arg === '-n') {
dialog.openSeedDirectory()
} else if (arg === '-o') {
dialog.openTorrentFile()
} else if (arg === '-u') {
dialog.openTorrentAddress()
}
} else if (arg === '--hidden') {
// Ignore hidden argument, already being handled
} else if (arg.startsWith('-psn')) {
// Ignore Mac launchd "process serial number" argument
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
} else if (arg.startsWith('--')) {
// Ignore Spectron flags
} else if (arg === 'data:,') {
// Ignore weird Spectron argument
} else if (arg !== '.') {
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
// development copy of WebTorrent is started while a production version is

View File

@@ -6,17 +6,9 @@ const electron = require('electron')
const app = electron.app
const dialog = require('./dialog')
const dock = require('./dock')
const handlers = require('./handlers')
const log = require('./log')
const menu = require('./menu')
const powerSaveBlocker = require('./power-save-blocker')
const shell = require('./shell')
const shortcuts = require('./shortcuts')
const externalPlayer = require('./external-player')
const windows = require('./windows')
const thumbar = require('./thumbar')
// Messages from the main process, to be sent once the WebTorrent process starts
const messageQueueMainToWebTorrent = []
@@ -43,45 +35,73 @@ function init () {
* Dialog
*/
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
ipc.on('openFiles', () => dialog.openFiles())
ipc.on('openTorrentFile', () => {
const dialog = require('./dialog')
dialog.openTorrentFile()
})
ipc.on('openFiles', () => {
const dialog = require('./dialog')
dialog.openFiles()
})
/**
* Dock
*/
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
ipc.on('setBadge', (e, ...args) => {
const dock = require('./dock')
dock.setBadge(...args)
})
ipc.on('downloadFinished', (e, ...args) => {
const dock = require('./dock')
dock.downloadFinished(...args)
})
/**
* Events
*/
ipc.on('onPlayerOpen', function () {
menu.setPlayerOpen(true)
const powerSaveBlocker = require('./power-save-blocker')
const shortcuts = require('./shortcuts')
const thumbar = require('./thumbar')
menu.togglePlaybackControls(true)
powerSaveBlocker.enable()
shortcuts.enable()
thumbar.enable()
})
ipc.on('onPlayerUpdate', function (e, ...args) {
const thumbar = require('./thumbar')
menu.onPlayerUpdate(...args)
thumbar.onPlayerUpdate(...args)
})
ipc.on('onPlayerClose', function () {
menu.setPlayerOpen(false)
const powerSaveBlocker = require('./power-save-blocker')
const shortcuts = require('./shortcuts')
const thumbar = require('./thumbar')
menu.togglePlaybackControls(false)
powerSaveBlocker.disable()
shortcuts.disable()
thumbar.disable()
})
ipc.on('onPlayerPlay', function () {
const powerSaveBlocker = require('./power-save-blocker')
const thumbar = require('./thumbar')
powerSaveBlocker.enable()
thumbar.onPlayerPlay()
})
ipc.on('onPlayerPause', function () {
const powerSaveBlocker = require('./power-save-blocker')
const thumbar = require('./thumbar')
powerSaveBlocker.disable()
thumbar.onPlayerPause()
})
@@ -90,18 +110,41 @@ function init () {
* Shell
*/
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
ipc.on('openItem', (e, ...args) => {
const shell = require('./shell')
shell.openItem(...args)
})
ipc.on('showItemInFolder', (e, ...args) => {
const shell = require('./shell')
shell.showItemInFolder(...args)
})
ipc.on('moveItemToTrash', (e, ...args) => {
const shell = require('./shell')
shell.moveItemToTrash(...args)
})
/**
* File handlers
*/
ipc.on('setDefaultFileHandler', (e, flag) => {
const handlers = require('./handlers')
if (flag) handlers.install()
else handlers.uninstall()
})
/**
* Auto start on login
*/
ipc.on('setStartup', (e, flag) => {
const startup = require('./startup')
if (flag) startup.install()
else startup.uninstall()
})
/**
* Windows: Main
*/
@@ -121,15 +164,31 @@ function init () {
*/
ipc.on('checkForExternalPlayer', function (e, path) {
const externalPlayer = require('./external-player')
externalPlayer.checkInstall(path, function (isInstalled) {
windows.main.send('checkForExternalPlayer', isInstalled)
})
})
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
ipc.on('openExternalPlayer', (e, ...args) => {
const externalPlayer = require('./external-player')
const thumbar = require('./thumbar')
menu.togglePlaybackControls(false)
thumbar.disable()
externalPlayer.spawn(...args)
})
ipc.on('quitExternalPlayer', () => {
const externalPlayer = require('./external-player')
externalPlayer.kill()
})
/**
* Message passing
*/
// Capture all events
const oldEmit = ipc.emit
ipc.emit = function (name, e, ...args) {
// Relay messages between the main window and the WebTorrent hidden window

View File

@@ -1,6 +1,6 @@
module.exports = {
init,
setPlayerOpen,
togglePlaybackControls,
setWindowFocus,
setAllowNav,
onPlayerUpdate,
@@ -13,8 +13,6 @@ const electron = require('electron')
const app = electron.app
const config = require('../config')
const dialog = require('./dialog')
const shell = require('./shell')
const windows = require('./windows')
let menu = null
@@ -24,7 +22,7 @@ function init () {
electron.Menu.setApplicationMenu(menu)
}
function setPlayerOpen (flag) {
function togglePlaybackControls (flag) {
getMenuItem('Play/Pause').enabled = flag
getMenuItem('Skip Next').enabled = flag
getMenuItem('Skip Previous').enabled = flag
@@ -90,17 +88,26 @@ function getMenuTemplate () {
? 'Create New Torrent...'
: 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N',
click: () => dialog.openSeedDirectory()
click: () => {
const dialog = require('./dialog')
dialog.openSeedDirectory()
}
},
{
label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O',
click: () => dialog.openTorrentFile()
click: () => {
const dialog = require('./dialog')
dialog.openTorrentFile()
}
},
{
label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U',
click: () => dialog.openTorrentAddress()
click: () => {
const dialog = require('./dialog')
dialog.openTorrentAddress()
}
},
{
type: 'separator'
@@ -277,18 +284,27 @@ function getMenuTemplate () {
submenu: [
{
label: 'Learn more about ' + config.APP_NAME,
click: () => shell.openExternal(config.HOME_PAGE_URL)
click: () => {
const shell = require('./shell')
shell.openExternal(config.HOME_PAGE_URL)
}
},
{
label: 'Contribute on GitHub',
click: () => shell.openExternal(config.GITHUB_URL)
click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL)
}
},
{
type: 'separator'
},
{
label: 'Report an Issue...',
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_ISSUES)
}
}
]
}
@@ -361,7 +377,10 @@ function getMenuTemplate () {
// File menu (Windows, Linux)
template[0].submenu.unshift({
label: 'Create New Torrent from File...',
click: () => dialog.openSeedFile()
click: () => {
const dialog = require('./dialog')
dialog.openSeedFile()
}
})
// Edit menu (Windows, Linux)

36
src/main/startup.js Normal file
View File

@@ -0,0 +1,36 @@
module.exports = {
install,
uninstall
}
const config = require('../config')
const AutoLaunch = require('auto-launch')
const { app } = require('electron')
// On Mac, work around a bug in auto-launch where it opens a Terminal window
// See https://github.com/Teamwork/node-auto-launch/issues/28#issuecomment-222194437
const appPath = process.platform === 'darwin'
? app.getPath('exe').replace(/\.app\/Content.*/, '.app')
: undefined // Use the default
const appLauncher = new AutoLaunch({
name: config.APP_NAME,
path: appPath,
isHidden: true
})
function install () {
return appLauncher
.isEnabled()
.then(enabled => {
if (!enabled) return appLauncher.enable()
})
}
function uninstall () {
return appLauncher
.isEnabled()
.then(enabled => {
if (enabled) return appLauncher.disable()
})
}

View File

@@ -11,7 +11,8 @@ const windows = require('./windows')
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
'?version=' + config.APP_VERSION +
'&platform=' + process.platform
'&platform=' + process.platform +
'&sysarch=' + config.OS_SYSARCH
function init () {
if (process.platform === 'linux') {

View File

@@ -32,7 +32,7 @@ function init () {
// No menu on the About window
win.setMenu(null)
win.webContents.once('did-finish-load', function () {
win.once('ready-to-show', function () {
win.show()
})

View File

@@ -21,15 +21,14 @@ const app = electron.app
const config = require('../../config')
const log = require('../log')
const menu = require('../menu')
const tray = require('../tray')
const HEADER_HEIGHT = 38
const TORRENT_HEIGHT = 100
function init () {
function init (state, options) {
if (main.win) {
return main.win.show()
}
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
const win = main.win = new electron.BrowserWindow({
backgroundColor: '#282828',
darkTheme: true, // Forces dark theme (GTK+3)
@@ -39,13 +38,20 @@ function init () {
title: config.APP_WINDOW_TITLE,
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
useContentSize: true, // Specify web page size without OS chrome
width: 500,
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
show: false,
width: initialBounds.width,
height: initialBounds.height,
x: initialBounds.x,
y: initialBounds.y
})
win.once('ready-to-show', function () {
if (!options.hidden) win.show()
})
win.loadURL(config.WINDOW_MAIN)
if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT)
if (win.setSheetOffset) win.setSheetOffset(config.UI_HEADER_HEIGHT)
win.webContents.on('dom-ready', function () {
menu.onToggleFullScreen(main.win.isFullScreen())
@@ -69,7 +75,17 @@ function init () {
win.setMenuBarVisibility(true)
})
win.on('move', function (e) {
send('windowBoundsChanged', e.sender.getBounds())
})
win.on('resize', function (e) {
send('windowBoundsChanged', e.sender.getBounds())
})
win.on('close', function (e) {
const tray = require('../tray')
if (process.platform !== 'darwin' && !tray.hasTray()) {
app.quit()
} else if (!app.isQuitting) {
@@ -210,11 +226,15 @@ function toggleFullScreen (flag) {
}
function onWindowBlur () {
const tray = require('../tray')
menu.setWindowFocus(false)
tray.setWindowFocus(false)
}
function onWindowFocus () {
const tray = require('../tray')
menu.setWindowFocus(true)
tray.setWindowFocus(true)
}

View File

@@ -6,7 +6,10 @@ class Header extends React.Component {
render () {
const loc = this.props.state.location
return (
<div className='header'>
<div className='header'
onMouseMove={dispatcher('mediaMouseMoved')}
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
{this.getTitle()}
<div className='nav left float-left'>
<i

View File

@@ -9,12 +9,12 @@ module.exports = class ModalOKCancel extends React.Component {
return (
<div className='float-right'>
<FlatButton
className='control'
className='control cancel'
style={cancelStyle}
label={cancelText}
onClick={onCancel} />
<RaisedButton
className='control'
className='control ok'
primary
label={okText}
onClick={onOK} />

View File

@@ -11,6 +11,7 @@ module.exports = class OpenTorrentAddressModal extends React.Component {
<p><label>Enter torrent address or magnet link</label></p>
<div>
<TextField
id='torrent-address-field'
className='control'
ref={(c) => { this.torrentURL = c }}
fullWidth
@@ -31,7 +32,7 @@ module.exports = class OpenTorrentAddressModal extends React.Component {
}
function handleKeyDown (e) {
if (e.which === 13) this.handleOK() /* hit Enter to submit */
if (e.which === 13) handleOK.call(this) /* hit Enter to submit */
}
function handleOK () {

View File

@@ -1,6 +1,6 @@
const React = require('react')
const FlatButton = require('material-ui/FlatButton').default
const RaisedButton = require('material-ui/RaisedButton').default
class ShowMore extends React.Component {
static get propTypes () {
@@ -39,9 +39,10 @@ class ShowMore extends React.Component {
? this.props.hideLabel
: this.props.showLabel
return (
<div style={this.props.style}>
<div className='show-more' style={this.props.style}>
{this.state.expanded ? this.props.children : null}
<FlatButton
<RaisedButton
className='control'
onClick={this.handleClick}
label={label} />
</div>

View File

@@ -1,6 +1,5 @@
const React = require('react')
const electron = require('electron')
const path = require('path')
const ModalOKCancel = require('./modal-ok-cancel')
const {dispatcher} = require('../lib/dispatcher')
@@ -12,15 +11,11 @@ module.exports = class UnsupportedMediaModal extends React.Component {
const message = (err && err.getMessage)
? err.getMessage()
: err
const playerPath = state.saved.prefs.externalPlayerPath
const playerName = playerPath
? path.basename(playerPath).split('.')[0]
: 'VLC'
const onAction = state.modal.externalPlayerInstalled
? dispatcher('openExternalPlayer')
: () => this.onInstall()
const actionText = state.modal.externalPlayerInstalled
? 'PLAY IN ' + playerName.toUpperCase()
? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase()
: 'INSTALL VLC'
const errorMessage = state.modal.externalPlayerNotFound
? 'Couldn\'t run external player. Please make sure it\'s installed.'

View File

@@ -44,6 +44,16 @@ module.exports = class MediaController {
this.state.playing.mouseStationarySince = new Date().getTime()
}
controlsMouseEnter () {
this.state.playing.mouseInControls = true
this.state.playing.mouseStationarySince = new Date().getTime()
}
controlsMouseLeave () {
this.state.playing.mouseInControls = false
this.state.playing.mouseStationarySince = new Date().getTime()
}
openExternalPlayer () {
const state = this.state
const mediaURL = Playlist.getCurrentLocalURL(this.state)

View File

@@ -4,7 +4,7 @@ const path = require('path')
const Cast = require('../lib/cast')
const {dispatch} = require('../lib/dispatcher')
const telemetry = require('../lib/telemetry')
const errors = require('../lib/errors')
const {UnplayableFileError, UnplayableTorrentError} = require('../lib/errors')
const sound = require('../lib/sound')
const TorrentPlayer = require('../lib/torrent-player')
const TorrentSummary = require('../lib/torrent-summary')
@@ -42,7 +42,7 @@ module.exports = class PlaybackController {
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
if (index === undefined) return cb(new errors.UnplayableError())
if (index === undefined) return cb(new UnplayableTorrentError())
initialized = true
@@ -87,7 +87,7 @@ module.exports = class PlaybackController {
// Play next file in list (if any)
nextTrack () {
const state = this.state
if (Playlist.hasNext(state)) {
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
this.updatePlayer(
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
if (err) dispatch('error', err)
@@ -99,7 +99,7 @@ module.exports = class PlaybackController {
// Play previous track in list (if any)
previousTrack () {
const state = this.state
if (Playlist.hasPrevious(state)) {
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
this.updatePlayer(
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
if (err) dispatch('error', err)
@@ -152,26 +152,21 @@ module.exports = class PlaybackController {
changePlaybackRate (direction) {
const state = this.state
let rate = state.playing.playbackRate
if (direction > 0 && rate >= 0.25 && rate < 2) {
if (direction > 0 && rate < 2) {
rate += 0.25
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
rate -= 0.25
} else if (direction < 0 && rate === 0.25) {
// When we set playback rate at 0 in html 5, playback hangs ;(
rate = -1
} else if (direction > 0 && rate === -1) {
rate = 0.25
} else if ((direction > 0 && rate >= 1 && rate < 16) ||
(direction < 0 && rate > -16 && rate <= -1)) {
} else if (direction > 0 && rate >= 1 && rate < 16) {
rate *= 2
} else if ((direction < 0 && rate > 1 && rate <= 16) ||
(direction > 0 && rate >= -16 && rate < -1)) {
} else if (direction < 0 && rate > 1 && rate <= 16) {
rate /= 2
}
state.playing.playbackRate = rate
if (isCasting(state) && !Cast.setRate(rate)) {
state.playing.playbackRate = 1
}
// Wait a bit before we hide the controls and header again
state.playing.mouseStationarySince = new Date().getTime()
}
// Change the volume, in range [0, 1], by some amount
@@ -201,11 +196,7 @@ module.exports = class PlaybackController {
// * The video is playing remotely on Chromecast or Airplay
showOrHidePlayerControls () {
const state = this.state
const hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local'
const hideControls = state.shouldHidePlayerControls()
if (hideControls !== state.playing.hideControls) {
state.playing.hideControls = hideControls
@@ -222,38 +213,15 @@ module.exports = class PlaybackController {
state.playing.infoHash = torrentSummary.infoHash
// update UI to show pending playback
if (torrentSummary.progress !== 1) sound.play('PLAY')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'requested'
this.update()
sound.play('PLAY')
const timeout = setTimeout(() => {
telemetry.logPlayAttempt('timeout')
// TODO: remove torrentSummary.playStatus
torrentSummary.playStatus = 'timeout' /* no seeders available? */
sound.play('ERROR')
cb(new Error('Playback timed out. Try again.'))
this.update()
}, 10000) /* give it a few seconds */
this.startServer(torrentSummary, () => {
clearTimeout(timeout)
// if we timed out (user clicked play a long time ago), don't autoplay
const timedOut = torrentSummary.playStatus === 'timeout'
delete torrentSummary.playStatus
if (timedOut) {
ipcRenderer.send('wt-stop-server')
return this.update()
}
ipcRenderer.send('onPlayerOpen')
this.updatePlayer(infoHash, index, true, cb)
})
this.startServer(torrentSummary)
ipcRenderer.send('onPlayerOpen')
this.updatePlayer(infoHash, index, true, cb)
}
// Starts WebTorrent server for media streaming
startServer (torrentSummary, cb) {
startServer (torrentSummary) {
if (torrentSummary.status === 'paused') {
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
@@ -264,7 +232,6 @@ module.exports = class PlaybackController {
function onTorrentReady () {
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, () => cb())
}
}
@@ -277,7 +244,7 @@ module.exports = class PlaybackController {
if (!TorrentPlayer.isPlayable(fileSummary)) {
torrentSummary.mostRecentFileIndex = undefined
return cb(new Error('Can\'t play that file'))
return cb(new UnplayableFileError())
}
torrentSummary.mostRecentFileIndex = index

View File

@@ -1,4 +1,3 @@
const State = require('../lib/state')
const {dispatch} = require('../lib/dispatcher')
const ipcRenderer = require('electron').ipcRenderer
@@ -17,7 +16,9 @@ module.exports = class PrefsController {
setup: function (cb) {
// initialize preferences
state.window.title = 'Preferences'
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
state.unsaved = Object.assign(state.unsaved || {}, {
prefs: Object.assign({}, state.saved.prefs)
})
ipcRenderer.send('setAllowNav', false)
cb()
},
@@ -50,8 +51,11 @@ module.exports = class PrefsController {
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
}
if (state.unsaved.prefs.startup !== state.saved.prefs.startup) {
ipcRenderer.send('setStartup', state.unsaved.prefs.startup)
}
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
State.save(state)
dispatch('stateSaveImmediate')
dispatch('checkDownloadPath')
}
}

View File

@@ -33,11 +33,10 @@ module.exports = class SubtitlesController {
}
addSubtitles (files, autoSelect) {
const state = this.state
// Subtitles are only supported when playing video files
if (state.playing.type !== 'video') return
if (this.state.playing.type !== 'video') return
if (files.length === 0) return
const subtitles = state.playing.subtitles
const subtitles = this.state.playing.subtitles
// Read the files concurrently, then add all resulting subtitle tracks
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
@@ -47,17 +46,17 @@ module.exports = class SubtitlesController {
for (let i = 0; i < tracks.length; i++) {
// No dupes allowed
const track = tracks[i]
let trackIndex = state.playing.subtitles.tracks
.findIndex((t) => track.filePath === t.filePath)
let trackIndex = subtitles.tracks.findIndex((t) =>
track.filePath === t.filePath)
// Add the track
if (trackIndex === -1) {
trackIndex = state.playing.subtitles.tracks.push(track) - 1
trackIndex = subtitles.tracks.push(track) - 1
}
// If we're auto-selecting a track, try to find one in the user's language
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
state.playing.subtitles.selectedIndex = trackIndex
subtitles.selectedIndex = trackIndex
}
}

View File

@@ -127,21 +127,22 @@ module.exports = class TorrentController {
torrentFileModtimes (torrentKey, fileModtimes) {
const torrentSummary = this.getTorrentSummary(torrentKey)
if (!torrentSummary) throw new Error('Not saving modtimes for deleted torrent ' + torrentKey)
torrentSummary.fileModtimes = fileModtimes
dispatch('saveStateThrottled')
dispatch('stateSave')
}
torrentFileSaved (torrentKey, torrentFileName) {
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
const torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.torrentFileName = torrentFileName
dispatch('saveStateThrottled')
dispatch('stateSave')
}
torrentPosterSaved (torrentKey, posterFileName) {
const torrentSummary = this.getTorrentSummary(torrentKey)
torrentSummary.posterFileName = posterFileName
dispatch('saveStateThrottled')
dispatch('stateSave')
}
torrentAudioMetadata (infoHash, index, info) {
@@ -176,7 +177,7 @@ function showDoneNotification (torrent) {
silent: true
})
notif.onClick = function () {
notif.onclick = function () {
ipcRenderer.send('show')
}

View File

@@ -3,7 +3,7 @@ const path = require('path')
const electron = require('electron')
const {dispatch} = require('../lib/dispatcher')
const State = require('../lib/state')
const {TorrentKeyNotFoundError} = require('../lib/errors')
const sound = require('../lib/sound')
const TorrentSummary = require('../lib/torrent-summary')
@@ -75,7 +75,7 @@ module.exports = class TorrentListController {
// Starts downloading and/or seeding a given torrentSummary.
startTorrentingSummary (torrentKey) {
const s = TorrentSummary.getByKey(this.state, torrentKey)
if (!s) throw new Error('Missing key: ' + torrentKey)
if (!s) throw new TorrentKeyNotFoundError(torrentKey)
// New torrent: give it a path
if (!s.path) {
@@ -127,7 +127,9 @@ module.exports = class TorrentListController {
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)
if (torrentSummary.status !== 'paused') {
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
}
}
confirmDeleteTorrent (infoHash, deleteData) {
@@ -156,7 +158,7 @@ module.exports = class TorrentListController {
// remove torrent from saved list
this.state.saved.torrents.splice(index, 1)
State.saveThrottled(this.state)
dispatch('stateSave')
}
// prevent user from going forward to a deleted torrent
@@ -212,11 +214,41 @@ module.exports = class TorrentListController {
menu.append(new electron.remote.MenuItem({
label: 'Save Torrent File As...',
click: () => saveTorrentFileAs(torrentSummary)
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey)
}))
menu.popup(electron.remote.getCurrentWindow())
}
// Takes a torrentSummary or torrentKey
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
saveTorrentFileAs (torrentKey) {
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
const downloadPath = this.state.saved.prefs.downloadPath
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
const win = electron.remote.getCurrentWindow()
const opts = {
title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
]
}
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
if (!savePath) return // They clicked Cancel
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return dispatch('error', err)
})
})
})
}
}
// Recursively finds {name, path, size} for all files in a folder
@@ -277,27 +309,3 @@ function moveItemToTrash (torrentSummary) {
function showItemInFolder (torrentSummary) {
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
}
function saveTorrentFileAs (torrentSummary) {
const downloadPath = this.state.saved.prefs.downloadPath
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
const opts = {
title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName),
filters: [
{ name: 'Torrent Files', extensions: ['torrent'] },
{ name: 'All Files', extensions: ['*'] }
]
}
const win = electron.remote.getCurrentWindow()
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
if (!savePath) return // They clicked Cancel
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) {
if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) {
if (err) return dispatch('error', err)
})
})
})
}

View File

@@ -1,4 +1,4 @@
const State = require('../lib/state')
const {dispatch} = require('../lib/dispatcher')
// Controls the UI checking for new versions of the app, prompting install
module.exports = class UpdateController {
@@ -21,6 +21,6 @@ module.exports = class UpdateController {
let skipped = this.state.saved.skippedVersions
if (!skipped) skipped = this.state.saved.skippedVersions = []
skipped.push(version)
State.saveThrottled(this.state)
dispatch('stateSave')
}
}

View File

@@ -1,30 +0,0 @@
module.exports = captureVideoFrame
function captureVideoFrame (video, format) {
if (typeof video === 'string') {
video = document.querySelector(video)
}
if (video == null || video.nodeName !== 'VIDEO') {
throw new Error('First argument must be a <video> element or selector')
}
if (format == null) {
format = 'png'
}
if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
throw new Error('Second argument must be one of "png", "jpg", or "webp"')
}
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
canvas.getContext('2d').drawImage(video, 0, 0)
const dataUri = canvas.toDataURL('image/' + format)
const data = dataUri.split(',')[1]
return new Buffer(data, 'base64')
}

View File

@@ -14,6 +14,7 @@ module.exports = {
}
const config = require('../../config')
const {CastingError} = require('./errors')
// Lazy load these for a ~300ms improvement in startup time
let airplayer, chromecasts, dlnacasts
@@ -32,6 +33,15 @@ function init (appState, callback) {
state = appState
update = callback
// Don't actually cast during integration tests
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
if (config.IS_TEST) {
state.devices.chromecast = testPlayer('chromecast')
state.devices.airplay = testPlayer('airplay')
state.devices.dlna = testPlayer('dlna')
return
}
// Load modules, scan the network for devices
airplayer = require('airplayer')()
chromecasts = require('chromecasts')()
@@ -57,6 +67,32 @@ function init (appState, callback) {
})
}
// integration test player implementation
function testPlayer (type) {
return {
getDevices,
open,
play,
pause,
stop,
status,
seek,
volume
}
function getDevices () {
return [{name: type + '-1'}, {name: type + '-2'}]
}
function open () {}
function play () {}
function pause () {}
function stop () {}
function status () {}
function seek () {}
function volume () {}
}
// chromecast player implementation
function chromecastPlayer () {
const ret = {
@@ -126,13 +162,7 @@ function chromecastPlayer () {
}
function status () {
ret.device.status(function (err, status) {
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level
update()
})
ret.device.status(handleStatus)
}
function seek (time, callback) {
@@ -306,13 +336,7 @@ function dlnaPlayer (player) {
}
function status () {
ret.device.status(function (err, status) {
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.level
update()
})
ret.device.status(handleStatus)
}
function seek (time, callback) {
@@ -328,6 +352,18 @@ function dlnaPlayer (player) {
}
}
function handleStatus (err, status) {
if (err || !status) {
return console.log('error getting %s status: %o',
state.playing.location,
err || 'missing response')
}
state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level
update()
}
// Start polling cast device state, whenever we're connected
function startStatusInterval () {
statusInterval = setInterval(function () {
@@ -350,14 +386,16 @@ function toggleMenu (location) {
// Never cast to two devices at the same time
if (state.playing.location !== 'local') {
throw new Error('You can\'t connect to ' + location +
' when already connected to another device')
}
throw new CastingError(
`You can't connect to ${location} when already connected to another device`
) }
// Find all cast devices of the given type
const player = getPlayer(location)
const devices = player ? player.getDevices() : []
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
if (devices.length === 0) {
throw new CastingError(`No ${location} devices available`)
}
// Show a menu
state.devices.castMenu = {location, devices}

View File

@@ -1,15 +1,44 @@
const ExtendableError = require('es6-error')
/* Generic errors */
class CastingError extends ExtendableError {}
class PlaybackError extends ExtendableError {}
class SoundError extends ExtendableError {}
class TorrentError extends ExtendableError {}
/* Playback */
class UnplayableTorrentError extends PlaybackError {
constructor () { super('Can\'t play any files in torrent') }
}
class UnplayableFileError extends PlaybackError {
constructor () { super('Can\'t play that file') }
}
/* Sound */
class InvalidSoundNameError extends SoundError {
constructor (name) { super(`Invalid sound name: ${name}`) }
}
/* Torrent */
class TorrentKeyNotFoundError extends TorrentError {
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
}
class InvalidTorrentError extends TorrentError {}
module.exports = {
CastingError,
PlaybackError,
SoundError,
TorrentError,
UnplayableTorrentError,
UnplayableFileError
UnplayableFileError,
InvalidSoundNameError,
TorrentKeyNotFoundError,
InvalidTorrentError
}
function UnplayableTorrentError () {
this.message = 'Can\'t play any files in torrent'
}
function UnplayableFileError () {
this.message = 'Can\'t play that file'
}
UnplayableTorrentError.prototype = Error
UnplayableFileError.prototype = Error

View File

@@ -4,10 +4,10 @@ module.exports = {
run
}
const semver = require('semver')
const config = require('../../config')
const TorrentSummary = require('./torrent-summary')
const fs = require('fs')
const semver = require('semver')
const config = require('../../config')
// Change `state.saved` (which will be saved back to config.json on exit) as
// needed, for example to deal with config.json format changes across versions
@@ -39,6 +39,10 @@ function run (state) {
migrate_0_14_0(state.saved)
}
if (semver.lt(version, '0.17.0')) {
migrate_0_17_0(state.saved)
}
// Config is now on the new version
state.saved.version = config.APP_VERSION
}
@@ -112,6 +116,8 @@ function migrate_0_11_0 (saved) {
}
function migrate_0_12_0 (saved) {
const TorrentSummary = require('./torrent-summary')
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
saved.prefs.openExternalPlayer = saved.prefs.playInVlc
}
@@ -145,3 +151,15 @@ function migrate_0_14_0 (saved) {
delete ts.defaultPlayFileIndex
})
}
function migrate_0_17_0 (saved) {
// Fix a sad, sad bug that resulted in 100MB+ config.json files
saved.torrents.forEach(function (ts) {
if (!ts.files) return
ts.files.forEach(function (file) {
if (!file.audioInfo || !file.audioInfo.picture) return
// This contained a Buffer, which 30x'd in size when serialized to JSON
delete file.audioInfo.picture
})
})
}

View File

@@ -37,7 +37,9 @@ function getPreviousIndex (state) {
}
function getCurrentLocalURL (state) {
return state.server.localURL + '/' + state.playing.fileIndex
return state.server
? state.server.localURL + '/' + state.playing.fileIndex
: ''
}
function updateCache (state) {

View File

@@ -4,9 +4,10 @@ module.exports = {
}
const config = require('../../config')
const {InvalidSoundNameError} = require('./errors')
const path = require('path')
const VOLUME = 0.15
const VOLUME = 0.3
/* Cache of Audio elements, for instant playback */
const cache = {}
@@ -18,7 +19,7 @@ const sounds = {
},
DELETE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
volume: VOLUME
volume: VOLUME * 0.5
},
DISABLE: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
@@ -38,11 +39,11 @@ const sounds = {
},
PLAY: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'play.wav'),
volume: VOLUME
volume: VOLUME * 1.25
},
STARTUP: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
volume: VOLUME * 2
volume: VOLUME
}
}
@@ -62,7 +63,7 @@ function play (name) {
if (!audio) {
const sound = sounds[name]
if (!sound) {
throw new Error('Invalid sound name')
throw new InvalidSoundNameError(name)
}
audio = cache[name] = new window.Audio()
audio.volume = sound.volume

View File

@@ -1,15 +1,19 @@
const appConfig = require('application-config')('WebTorrent')
const debounce = require('debounce')
const path = require('path')
const {EventEmitter} = require('events')
const config = require('../../config')
const migrations = require('./migrations')
const SAVE_DEBOUNCE_INTERVAL = 1000
const State = module.exports = Object.assign(new EventEmitter(), {
getDefaultPlayState,
load,
save,
saveThrottled
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
save: debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL),
saveImmediate
})
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
@@ -68,7 +72,9 @@ function getDefaultState () {
* Getters, for convenience
*/
getPlayingTorrentSummary,
getPlayingFileSummary
getPlayingFileSummary,
getExternalPlayerName,
shouldHidePlayerControls
}
}
@@ -97,7 +103,7 @@ function getDefaultPlayState () {
}
/* If the saved state file doesn't exist yet, here's what we use instead */
function setupSavedState (cb) {
function setupStateSaved (cb) {
const fs = require('fs-extra')
const parseTorrent = require('parse-torrent')
const parallel = require('run-parallel')
@@ -107,7 +113,8 @@ function setupSavedState (cb) {
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
isFileHandler: false,
openExternalPlayer: false,
externalPlayerPath: null
externalPlayerPath: null,
startup: false
},
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
@@ -151,7 +158,8 @@ function setupSavedState (cb) {
torrentFileName: parsedTorrent.infoHash + '.torrent',
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
files: parsedTorrent.files,
selections: parsedTorrent.files.map((x) => true)
selections: parsedTorrent.files.map((x) => true),
testID: t.testID
}
}
}
@@ -167,20 +175,35 @@ function getPlayingFileSummary () {
return torrentSummary.files[this.playing.fileIndex]
}
function load (cb) {
const state = getDefaultState()
function getExternalPlayerName () {
const playerPath = this.saved.prefs.externalPlayerPath
if (!playerPath) return 'VLC'
return path.basename(playerPath).split('.')[0]
}
function shouldHidePlayerControls () {
return this.location.url() === 'player' &&
this.playing.mouseStationarySince !== 0 &&
new Date().getTime() - this.playing.mouseStationarySince > 2000 &&
!this.playing.mouseInControls &&
!this.playing.isPaused &&
this.playing.location === 'local' &&
this.playing.playbackRate === 1
}
function load (cb) {
appConfig.read(function (err, saved) {
if (err || !saved.version) {
console.log('Missing config file: Creating new one')
setupSavedState(onSaved)
setupStateSaved(onSavedState)
} else {
onSaved(null, saved)
onSavedState(null, saved)
}
})
function onSaved (err, saved) {
function onSavedState (err, saved) {
if (err) return cb(err)
const state = getDefaultState()
state.saved = saved
migrations.run(state)
cb(null, state)
@@ -188,9 +211,8 @@ function load (cb) {
}
// Write state.saved to the JSON state file
function save (state, cb) {
function saveImmediate (state, cb) {
console.log('Saving state to ' + appConfig.filePath)
delete state.saveStateTimeout
// Clean up, so that we're not saving any pending state
const copy = Object.assign({}, state.saved)
@@ -204,9 +226,6 @@ function save (state, cb) {
if (key === 'progress' || key === 'torrentKey') {
continue // Don't save progress info or key for the webtorrent process
}
if (key === 'playStatus') {
continue // Don't save whether a torrent is playing / pending
}
if (key === 'error') {
continue // Don't save error states
}
@@ -217,15 +236,6 @@ function save (state, cb) {
appConfig.write(copy, (err) => {
if (err) console.error(err)
else State.emit('savedState')
else State.emit('stateSaved')
})
}
// Write, but no more than once a second
function saveThrottled (state) {
if (state.saveStateTimeout) return
state.saveStateTimeout = setTimeout(function () {
if (!state.saveStateTimeout) return
save(state)
}, 1000)
}

View File

@@ -29,7 +29,8 @@ function init (state) {
telemetry.localTime = now.toTimeString()
telemetry.screens = getScreenInfo()
telemetry.system = getSystemInfo()
telemetry.approxNumTorrents = getApproxNumTorrents(state)
telemetry.torrentStats = getTorrentStats(state)
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
if (config.IS_PRODUCTION) {
postToServer()
@@ -104,18 +105,64 @@ function getSystemInfo () {
osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(),
architecture: os.arch(),
totalMemoryMB: os.totalmem() / (1 << 20),
systemArchitecture: config.OS_SYSARCH,
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
numCores: os.cpus().length
}
}
// Get the number of torrents, rounded to the nearest power of two
function getApproxNumTorrents (state) {
const exactNum = state.saved.torrents.length
if (exactNum === 0) return 0
// Get stats like the # of torrents currently active, # in list, total size
function getTorrentStats (state) {
const count = state.saved.torrents.length
let sizeMB = 0
let byStatus = {
new: { count: 0, sizeMB: 0 },
downloading: { count: 0, sizeMB: 0 },
seeding: { count: 0, sizeMB: 0 },
paused: { count: 0, sizeMB: 0 }
}
// First, count torrents & total file size
for (let i = 0; i < count; i++) {
const t = state.saved.torrents[i]
const stat = byStatus[t.status]
if (!t || !t.files || !stat) continue
stat.count++
for (let j = 0; j < t.files.length; j++) {
const f = t.files[j]
if (!f || !f.length) continue
const fileSizeMB = f.length / (1 << 20)
sizeMB += fileSizeMB
stat.sizeMB += fileSizeMB
}
}
// Then, round all the counts and sums to the nearest power of 2
const ret = roundTorrentStats({count, sizeMB})
ret.byStatus = {
new: roundTorrentStats(byStatus.new),
downloading: roundTorrentStats(byStatus.downloading),
seeding: roundTorrentStats(byStatus.seeding),
paused: roundTorrentStats(byStatus.paused)
}
return ret
}
function roundTorrentStats (stats) {
return {
approxCount: roundPow2(stats.count),
approxSizeMB: roundPow2(stats.sizeMB)
}
}
// Rounds to the nearest power of 2, for privacy and easy bucketing.
// Rounds 35 to 32, 70 to 64, 5 to 4, 1 to 1, 0 to 0.
// Supports nonnegative numbers only.
function roundPow2 (n) {
if (n <= 0) return 0
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
const log2 = Math.log(exactNum) / Math.log(2)
return 1 << Math.round(log2)
const log2 = Math.log(n) / Math.log(2)
return Math.pow(2, Math.round(log2))
}
// An uncaught error happened in the main process or in one of the windows

View File

@@ -1,6 +1,6 @@
module.exports = torrentPoster
const captureVideoFrame = require('./capture-video-frame')
const captureFrame = require('capture-frame')
const path = require('path')
function torrentPoster (torrent, cb) {
@@ -61,7 +61,7 @@ function torrentPosterFromVideo (file, torrent, cb) {
function onSeeked () {
video.removeEventListener('seeked', onSeeked)
const buf = captureVideoFrame(video)
const buf = captureFrame(video)
// unload video element
video.pause()

View File

@@ -53,5 +53,6 @@ function getByKey (state, torrentKey) {
function getFileOrFolder (torrentSummary) {
const ts = torrentSummary
if (!ts.path || !ts.files || ts.files.length === 0) return null
return path.join(ts.path, ts.files[0].path.split('/')[0])
const dirname = ts.files[0].path.split(path.sep)[0]
return path.join(ts.path, dirname)
}

View File

@@ -58,6 +58,7 @@ State.load(onState)
function onState (err, _state) {
if (err) return onError(err)
state = window.state = _state // Make available for easier debugging
window.dispatch = dispatch
telemetry.init(state)
@@ -122,7 +123,9 @@ function onState (err, _state) {
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
// Done! Ideally we want to get here < 500ms after the user clicks the app
sound.play('STARTUP')
if (electron.remote.getCurrentWindow().isVisible()) {
sound.play('STARTUP')
}
console.timeEnd('init')
}
@@ -191,6 +194,8 @@ const dispatchHandlers = {
controllers.torrentList.openTorrentContextMenu(infoHash),
'startTorrentingSummary': (torrentKey) =>
controllers.torrentList.startTorrentingSummary(torrentKey),
'saveTorrentFileAs': (torrentKey) =>
controllers.torrentList.saveTorrentFileAs(torrentKey),
// Playback
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
@@ -217,6 +222,8 @@ const dispatchHandlers = {
'mediaSuccess': () => controllers.media.mediaSuccess(),
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
'mediaControlsMouseEnter': () => controllers.media.controlsMouseEnter(),
'mediaControlsMouseLeave': () => controllers.media.controlsMouseLeave(),
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
@@ -252,8 +259,8 @@ const dispatchHandlers = {
'onOpen': onOpen,
'error': onError,
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
'saveState': () => State.save(state),
'saveStateThrottled': () => State.saveThrottled(state),
'stateSave': () => State.save(state),
'stateSaveImmediate': () => State.saveImmediate(state),
'update': () => {} // No-op, just trigger an update
}
@@ -283,6 +290,7 @@ function setupIpc () {
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
ipcRenderer.on('windowBoundsChanged', onWindowBoundsChanged)
const tc = controllers.torrent
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
@@ -302,7 +310,7 @@ function setupIpc () {
ipcRenderer.send('ipcReady')
State.on('savedState', () => ipcRenderer.send('savedState'))
State.on('stateSaved', () => ipcRenderer.send('stateSaved'))
}
// Quits any modal popovers and returns to the torrent list screen
@@ -457,6 +465,13 @@ function onFullscreenChanged (e, isFullScreen) {
update()
}
function onWindowBoundsChanged (e, newBounds) {
if (state.location.url() !== 'player') {
state.saved.bounds = newBounds
dispatch('stateSave')
}
}
function checkDownloadPath () {
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
if (err) {

View File

@@ -27,8 +27,8 @@ const fontFamily = process.platform === 'win32'
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
lightBaseTheme.fontFamily = fontFamily
darkBaseTheme.fontFamily = fontFamily
darkBaseTheme.palette.primary1Color = colors.cyan500
darkBaseTheme.palette.primary2Color = colors.cyan500
darkBaseTheme.palette.primary1Color = colors.grey50
darkBaseTheme.palette.primary2Color = colors.grey50
darkBaseTheme.palette.primary3Color = colors.grey600
darkBaseTheme.palette.accent1Color = colors.redA200
darkBaseTheme.palette.accent2Color = colors.redA400
@@ -43,12 +43,7 @@ class App extends React.Component {
// * The mouse is over the controls or we're scrubbing (see CSS)
// * The video is paused
// * The video is playing remotely on Chromecast or Airplay
const hideControls = state.location.url() === 'player' &&
state.playing.mouseStationarySince !== 0 &&
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
!state.playing.isPaused &&
state.playing.location === 'local' &&
state.playing.playbackRate === 1
const hideControls = state.shouldHidePlayerControls()
const cls = [
'view-' + state.location.url(), /* e.g. view-home, view-player */

View File

@@ -99,14 +99,14 @@ class CreateTorrentPage extends React.Component {
</ShowMore>
<div className='float-right'>
<FlatButton
className='control'
className='control cancel'
label='Cancel'
style={{
marginRight: 10
}}
onClick={dispatcher('cancel')} />
<RaisedButton
className='control'
className='control create-torrent'
label='Create Torrent'
primary
onClick={this.handleSubmit} />

View File

@@ -2,11 +2,11 @@ const React = require('react')
const Bitfield = require('bitfield')
const prettyBytes = require('prettier-bytes')
const zeroFill = require('zero-fill')
const path = require('path')
const TorrentSummary = require('../lib/torrent-summary')
const Playlist = require('../lib/playlist')
const {dispatch, dispatcher} = require('../lib/dispatcher')
const config = require('../../config')
// Shows a streaming video player. Standard features + Chromecast + Airplay
module.exports = class Player extends React.Component {
@@ -26,6 +26,15 @@ module.exports = class Player extends React.Component {
</div>
)
}
onComponentWillUnmount () {
// Unload the media element so that Chromium stops trying to fetch data
const tag = document.querySelector('audio,video')
if (!tag) return
tag.pause()
tag.src = ''
tag.load()
}
}
// Handles volume change by wheel
@@ -289,11 +298,8 @@ function renderCastScreen (state) {
castType = 'DLNA'
isCast = true
} else if (state.playing.location === 'external') {
// TODO: get the player name in a more reliable way
const playerPath = state.saved.prefs.externalPlayerPath
const playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
castIcon = 'tv'
castType = playerName
castType = state.getExternalPlayerName()
isCast = false
} else if (state.playing.location === 'error') {
castIcon = 'error_outline'
@@ -432,7 +438,7 @@ function renderPlayerControls (state) {
]
if (state.playing.type === 'video') {
// show closed captions icon
// Show closed captions icon
elements.push((
<i
key='subtitles'
@@ -500,8 +506,6 @@ function renderPlayerControls (state) {
'color-stop(' + (volume * 100) + '%, #727272))'
}
// TODO: dcposch change the range input to use value / onChanged instead of
// "readonly" / onMouse[Down,Move,Up]
elements.push((
<div key='volume' className='volume float-left'>
<i
@@ -527,7 +531,7 @@ function renderPlayerControls (state) {
</span>
))
// render playback rate
// Render playback rate
if (state.playing.playbackRate !== 1) {
elements.push((
<span key='rate' className='rate float-left'>
@@ -537,7 +541,9 @@ function renderPlayerControls (state) {
}
return (
<div key='controls' className='controls'>
<div key='controls' className='controls'
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}>
{elements}
{renderCastOptions(state)}
{renderSubtitleOptions(state)}
@@ -589,6 +595,8 @@ function renderPlayerControls (state) {
// Renders the loading bar. Shows which parts of the torrent are loaded, which
// can be 'spongey' / non-contiguous
function renderLoadingBar (state) {
if (config.IS_TEST) return // Don't integration test the loading bar. Screenshots won't match.
const torrentSummary = state.getPlayingTorrentSummary()
if (!torrentSummary.progress) {
return []

View File

@@ -1,13 +1,14 @@
const colors = require('material-ui/styles/colors')
const path = require('path')
const React = require('react')
const colors = require('material-ui/styles/colors')
const Checkbox = require('material-ui/Checkbox').default
const RaisedButton = require('material-ui/RaisedButton').default
const Heading = require('../components/heading')
const PathSelector = require('../components/path-selector')
const RaisedButton = require('material-ui/RaisedButton').default
const {dispatch} = require('../lib/dispatcher')
const config = require('../../config')
class PreferencesPage extends React.Component {
constructor (props) {
@@ -21,6 +22,9 @@ class PreferencesPage extends React.Component {
this.handleExternalPlayerPathChange =
this.handleExternalPlayerPathChange.bind(this)
this.handleStartupChange =
this.handleStartupChange.bind(this)
}
downloadPathSelector () {
@@ -59,14 +63,12 @@ class PreferencesPage extends React.Component {
}
externalPlayerPathSelector () {
const playerName = path.basename(
this.props.state.unsaved.prefs.externalPlayerPath || 'VLC'
)
const playerPath = this.props.state.unsaved.prefs.externalPlayerPath
const playerName = this.props.state.getExternalPlayerName()
const description = this.props.state.unsaved.prefs.openExternalPlayer
? `Torrent media files will always play in ${playerName}.`
: `Torrent media files will play in ${playerName} if WebTorrent cannot ` +
'play them.'
: `Torrent media files will play in ${playerName} if WebTorrent cannot play them.`
return (
<Preference>
@@ -79,16 +81,12 @@ class PreferencesPage extends React.Component {
displayValue={playerName}
onChange={this.handleExternalPlayerPathChange}
title='External player'
value={this.props.state.unsaved.prefs.externalPlayerPath} />
value={playerPath ? path.dirname(playerPath) : null} />
</Preference>
)
}
handleExternalPlayerPathChange (filePath) {
if (path.extname(filePath) === '.app') {
// Mac: Use executable in packaged .app bundle
filePath += '/Contents/MacOS/' + path.basename(filePath, '.app')
}
dispatch('updatePreferences', 'externalPlayerPath', filePath)
}
@@ -112,6 +110,29 @@ class PreferencesPage extends React.Component {
)
}
handleStartupChange (e, isChecked) {
dispatch('updatePreferences', 'startup', isChecked)
}
setStartupSection () {
if (config.IS_PORTABLE) {
return
}
return (
<PreferencesSection title='Startup'>
<Preference>
<Checkbox
className='control'
checked={this.props.state.unsaved.prefs.startup}
label={'Open WebTorrent on startup.'}
onCheck={this.handleStartupChange}
/>
</Preference>
</PreferencesSection>
)
}
handleSetDefaultApp () {
dispatch('updatePreferences', 'isFileHandler', true)
}
@@ -134,6 +155,7 @@ class PreferencesPage extends React.Component {
<PreferencesSection title='Default torrent app'>
{this.setDefaultAppButton()}
</PreferencesSection>
{this.setStartupSection()}
</div>
)
}

View File

@@ -1,9 +1,12 @@
const React = require('react')
const prettyBytes = require('prettier-bytes')
const Checkbox = require('material-ui/Checkbox').default
const LinearProgress = require('material-ui/LinearProgress').default
const TorrentSummary = require('../lib/torrent-summary')
const TorrentPlayer = require('../lib/torrent-player')
const {dispatcher} = require('../lib/dispatcher')
const {InvalidTorrentError} = require('../lib/errors')
module.exports = class TorrentList extends React.Component {
render () {
@@ -46,7 +49,7 @@ module.exports = class TorrentList extends React.Component {
// Background image: show some nice visuals, like a frame from the movie, if possible
const style = {}
if (torrentSummary.posterFileName) {
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%)'
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
style.backgroundImage = `${gradient}, url('${posterPath}')`
}
@@ -54,13 +57,12 @@ module.exports = class TorrentList extends React.Component {
// Foreground: name of the torrent, basic info like size, play button,
// cast buttons if available, and delete
const classes = ['torrent']
// playStatus turns the play button into a loading spinner or error icon
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
if (isSelected) classes.push('selected')
if (!infoHash) classes.push('disabled')
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
if (!torrentSummary.torrentKey) throw new InvalidTorrentError('Missing torrentKey')
return (
<div
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
key={torrentSummary.torrentKey}
style={style}
className={classes.join(' ')}
@@ -69,6 +71,7 @@ module.exports = class TorrentList extends React.Component {
{this.renderTorrentMetadata(torrentSummary)}
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
<hr />
</div>
)
}
@@ -82,31 +85,66 @@ module.exports = class TorrentList extends React.Component {
// If it's downloading/seeding then show progress info
const prog = torrentSummary.progress
let progElems
if (torrentSummary.error) {
elements.push(
<div key='progress-info' className='ellipsis'>
{getErrorMessage(torrentSummary)}
</div>
)
progElems = [getErrorMessage(torrentSummary)]
} else if (torrentSummary.status !== 'paused' && prog) {
elements.push(
<div key='progress-info' className='ellipsis'>
{renderProgressBar()}
{renderPercentProgress()}
{renderTotalProgress()}
{renderPeers()}
{renderDownloadSpeed()}
{renderUploadSpeed()}
{renderEta()}
</div>
)
progElems = [
renderDownloadCheckbox(),
renderTorrentStatus(),
renderProgressBar(),
renderPercentProgress(),
renderTotalProgress(),
renderPeers(),
renderSpeeds(),
renderEta()
]
} else {
progElems = [
renderDownloadCheckbox(),
renderTorrentStatus()
]
}
elements.push(
<div key='progress-info' className='ellipsis'>
{progElems}
</div>
)
return (<div key='metadata' className='metadata'>{elements}</div>)
function renderDownloadCheckbox () {
const infoHash = torrentSummary.infoHash
const isActive = ['downloading', 'seeding'].includes(torrentSummary.status)
return (
<Checkbox
key='download-button'
className={'download ' + torrentSummary.status}
style={{display: 'inline-block', width: '32px'}}
iconStyle={{width: '20px', height: '20px'}}
checked={isActive}
onClick={stopPropagation}
onCheck={dispatcher('toggleTorrent', infoHash)} />
)
}
function renderProgressBar () {
const progress = Math.floor(100 * prog.progress)
return (<progress value={progress} max='100'>{progress}%</progress>)
const styles = {
wrapper: {
display: 'inline-block',
marginRight: '8px'
},
progress: {
height: '8px',
width: '30px'
}
}
return (
<div style={styles.wrapper}>
<LinearProgress style={styles.progress} mode='determinate' value={progress} />
</div>
)
}
function renderPercentProgress () {
@@ -130,14 +168,12 @@ module.exports = class TorrentList extends React.Component {
return (<span key='peers'>{prog.numPeers} {count}</span>)
}
function renderDownloadSpeed () {
if (prog.downloadSpeed === 0) return
return (<span key='download'> {prettyBytes(prog.downloadSpeed)}/s</span>)
}
function renderUploadSpeed () {
if (prog.uploadSpeed === 0) return
return (<span key='upload'> {prettyBytes(prog.uploadSpeed)}/s</span>)
function renderSpeeds () {
let str = ''
if (prog.downloadSpeed > 0) str += ' ↓ ' + prettyBytes(prog.downloadSpeed) + '/s'
if (prog.uploadSpeed > 0) str += ' ↑ ' + prettyBytes(prog.uploadSpeed) + '/s'
if (str === '') return
return (<span key='download'>{str}</span>)
}
function renderEta () {
@@ -158,7 +194,23 @@ module.exports = class TorrentList extends React.Component {
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
const secondsStr = seconds + 's'
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
return (<span>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
}
function renderTorrentStatus () {
let status
if (torrentSummary.status === 'paused') {
if (!torrentSummary.progress) status = ''
else if (torrentSummary.progress.progress === 1) status = 'Not seeding'
else status = 'Paused'
} else if (torrentSummary.status === 'downloading') {
status = 'Downloading'
} else if (torrentSummary.status === 'seeding') {
status = 'Seeding'
} else { // torrentSummary.status is 'new' or something unexpected
status = ''
}
return (<span>{status}</span>)
}
}
@@ -167,69 +219,23 @@ module.exports = class TorrentList extends React.Component {
renderTorrentButtons (torrentSummary) {
const infoHash = torrentSummary.infoHash
let playIcon, playTooltip, playClass
if (torrentSummary.playStatus === 'timeout') {
playIcon = 'warning'
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
} else {
playIcon = 'play_arrow'
playTooltip = 'Start streaming'
}
let downloadIcon, downloadTooltip
if (torrentSummary.status === 'seeding') {
downloadIcon = 'file_upload'
downloadTooltip = 'Seeding. Click to stop.'
} else if (torrentSummary.status === 'downloading') {
downloadIcon = 'file_download'
downloadTooltip = 'Torrenting. Click to stop.'
} else {
downloadIcon = 'file_download'
downloadTooltip = 'Click to start torrenting.'
}
// Only show the play/dowload buttons for torrents that contain playable media
let playButton, downloadButton, positionElem
if (!torrentSummary.error) {
downloadButton = (
let playButton
if (!torrentSummary.error && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
playButton = (
<i
key='download-button'
className={'button-round icon download ' + torrentSummary.status}
title={downloadTooltip}
onClick={dispatcher('toggleTorrent', infoHash)}>
{downloadIcon}
key='play-button'
title='Start streaming'
className={'icon play'}
onClick={dispatcher('playFile', infoHash)}>
play_circle_outline
</i>
)
// Do we have a saved position? Show it using a radial progress bar on top
// of the play button, unless already showing a spinner there:
const willShowSpinner = torrentSummary.playStatus === 'requested'
const mostRecentFile = torrentSummary.files &&
torrentSummary.files[torrentSummary.mostRecentFileIndex]
if (mostRecentFile && mostRecentFile.currentTime && !willShowSpinner) {
const fraction = mostRecentFile.currentTime / mostRecentFile.duration
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
playClass = 'resume-position'
}
if (TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
playButton = (
<i
key='play-button'
title={playTooltip}
className={'button-round icon play ' + playClass}
onClick={dispatcher('playFile', infoHash)}>
{playIcon}
</i>
)
}
}
return (
<div key='buttons' className='buttons'>
{positionElem}
<div className='torrent-controls'>
{playButton}
{downloadButton}
<i
key='delete-button'
className='icon delete'
@@ -345,7 +351,7 @@ module.exports = class TorrentList extends React.Component {
</td>
<td className='col-select'
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
<i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
</td>
</tr>
)
@@ -373,6 +379,10 @@ module.exports = class TorrentList extends React.Component {
}
}
function stopPropagation (e) {
e.stopPropagation()
}
function getErrorMessage (torrentSummary) {
const err = torrentSummary.error
if (err === 'path-missing') {

View File

@@ -15,6 +15,7 @@ const zeroFill = require('zero-fill')
const crashReporter = require('../crash-reporter')
const config = require('../config')
const {TorrentKeyNotFoundError} = require('./lib/errors')
const torrentPoster = require('./lib/torrent-poster')
// Report when the process crashes
@@ -53,11 +54,14 @@ const VERSION_STR = VERSION.match(/([0-9]+)/g)
*/
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'
/**
* Generate an ephemeral peer ID each time.
*/
const PEER_ID = Buffer.from(VERSION_PREFIX + crypto.randomBytes(9).toString('base64'))
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
// client, as explained here: https://webtorrent.io/faq
const client = window.client = new WebTorrent({
peerId: Buffer.from(VERSION_PREFIX + crypto.randomBytes(6).toString('hex'))
})
let client = window.client = new WebTorrent({ peerId: PEER_ID })
// WebTorrent-to-HTTP streaming sever
let server = null
@@ -68,8 +72,7 @@ let prevProgress = null
init()
function init () {
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message))
listenToClientEvents()
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
@@ -100,6 +103,11 @@ function init () {
console.timeEnd('init')
}
function listenToClientEvents () {
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message))
}
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
// Returns a WebTorrent object. See https://git.io/vik9M
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
@@ -156,7 +164,7 @@ function addTorrentEvents (torrent) {
function torrentReady () {
const info = getTorrentInfo(torrent)
ipc.send('wt-ready', torrent.key, info)
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
updateTorrentProgress()
}
@@ -336,9 +344,11 @@ function getAudioMetadata (infoHash, index) {
const torrent = client.get(infoHash)
const file = torrent.files[index]
musicmetadata(file.createReadStream(), function (err, info) {
if (err) return
console.log('got audio metadata for %s: %o', file.name, info)
ipc.send('wt-audio-metadata', infoHash, index, info)
if (err) return console.log('error getting audio metadata for ' + infoHash + ':' + index, err)
const { artist, album, albumartist, title, year, track, disk, genre } = info
const importantInfo = { artist, album, albumartist, title, year, track, disk, genre }
console.log('got audio metadata for %s: %o', file.name, importantInfo)
ipc.send('wt-audio-metadata', infoHash, index, importantInfo)
})
}
@@ -350,6 +360,9 @@ function selectFiles (torrentOrInfoHash, selections) {
} else {
torrent = torrentOrInfoHash
}
if (!torrent) {
throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
}
// Selections not specified?
// Load all files. We still need to replace the default whole-torrent
@@ -384,10 +397,24 @@ function selectFiles (torrentOrInfoHash, selections) {
// Throws an Error if we're not currently torrenting anything w/ that key
function getTorrent (torrentKey) {
const ret = client.torrents.find((x) => x.key === torrentKey)
if (!ret) throw new Error('missing torrent key ' + torrentKey)
if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
return ret
}
function onError (err) {
console.log(err)
}
// TODO: remove this once the following bugs are fixed:
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
// https://github.com/electron/electron/issues/7212
window.testOfflineMode = function () {
console.log('Test, going OFFLINE')
client = window.client = new WebTorrent({
peerId: PEER_ID,
tracker: false,
dht: false,
webSeeds: false
})
listenToClientEvents()
}

View File

@@ -32,6 +32,7 @@
<p>
Version <script>document.write(require('../package.json').version)</script>
(<script>document.write(require('webtorrent/package.json').version)</script>)
(<script>document.write(process.arch === 'x64' ? '64-bit' : '32-bit')</script>)
</p>
<p><script>document.write(require('../config').APP_COPYRIGHT)</script></p>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -309,15 +309,6 @@ i:not(.disabled):hover { /* Show they're clickable without pointer: cursor */
-webkit-filter: brightness(1.3);
}
.button-round { /* Circular icon buttons, used on <i> tags */
width: 40px;
height: 40px;
border-radius: 20px;
font-size: 22px;
transition: all 0.1s ease-out;
text-align: center;
}
/*
* INPUTS
*/
@@ -347,22 +338,18 @@ textarea,
}
.torrent:not(:last-child) {
border-bottom: 1px solid rgb(20, 20, 20);
border-bottom: 1px solid #282828;
}
.torrent .metadata {
position: absolute;
top: 25px;
top: 20px;
left: 15px;
right: 15px;
width: calc(100% - 40px);
width: calc(100% - 30px);
text-shadow: rgba(0, 0, 0, 0.5) 0 0 4px;
}
.torrent:hover .metadata {
width: calc(100% - 150px);
}
.torrent .metadata span:not(:last-child)::after {
content: ' • ';
opacity: 0.7;
@@ -370,79 +357,34 @@ textarea,
padding-right: 4px;
}
.torrent .buttons {
position: absolute;
top: 29px;
right: 10px;
align-items: center;
.torrent:hover .metadata {
width: calc(100% - 120px);
}
.torrent .torrent-controls {
display: none;
}
.torrent:hover .buttons {
display: flex;
.torrent:hover .torrent-controls {
display: block;
}
.torrent .buttons > * {
margin-left: 6px; /* space buttons apart */
.torrent .play {
position: absolute;
top: 23px;
right: 45px;
font-size: 55px;
}
.torrent .buttons .download {
background-color: #2233BB;
width: 28px;
height: 28px;
border-radius: 14px;
font-size: 18px;
padding-top: 6px;
.torrent .delete {
position: absolute;
top: 38px;
right: 12px;
}
.torrent .buttons .download.downloading {
color: #44dd44;
}
.torrent .buttons .download.seeding {
color: #44dd44;
}
.torrent .buttons .play {
padding-top: 10px;
background-color: #F44336;
}
.torrent.timeout .play {
padding-top: 8px;
}
.torrent.requested .play,
.loading-spinner {
border-top: 6px solid rgba(255, 255, 255, 0.2);
border-right: 6px solid rgba(255, 255, 255, 0.2);
border-bottom: 6px solid rgba(255, 255, 255, 0.2);
border-left: 6px solid #ffffff;
border-radius: 50%;
color: transparent;
animation: load8 1.1s infinite linear;
}
@keyframes load8 {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.torrent .buttons .play.resume-position {
position: relative;
-webkit-clip-path: circle(18px at center);
}
.torrent .buttons .delete {
opacity: 0.5;
}
.torrent .buttons .delete:hover {
opacity: 0.7;
.torrent .download {
vertical-align: -0.4em;
margin-left: -2px;
}
.torrent .buttons .radial-progress {
@@ -450,23 +392,27 @@ textarea,
}
.torrent .name {
font-size: 18px;
font-size: 20px;
font-weight: bold;
line-height: 1.5em;
margin-bottom: 6px;
}
progress {
width: 60px;
margin-right: 8px;
-webkit-appearance: none;
.torrent hr {
position: absolute;
bottom: 5px;
left: calc(50% - 20px);
width: 40px;
height: 1px;
background: #ccc;
display: none;
margin: 0;
padding: 0;
border: none;
}
progress::-webkit-progress-bar {
background-color: #888;
}
progress::-webkit-progress-value {
background-color: #eee;
.torrent:hover hr {
display: block;
}
/*
@@ -690,14 +636,6 @@ body.drag .app::after {
cursor: none;
}
/* TODO: find better way to handle this (that also
* keeps the header visible too).
*/
.app.hide-video-controls .player .controls:hover {
opacity: 1;
cursor: default;
}
/* invisible click target for scrubbing */
.player .controls .scrub-bar {
position: absolute;

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebTorrent Desktop</title>
<title>Main Window</title>
<link rel="stylesheet" href="main.css">
</head>
<body>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebTorrent Desktop</title>
<title>WebTorrent Hidden Window</title>
<style>
body {
background-color: #282828;

15
test/config.js Normal file
View File

@@ -0,0 +1,15 @@
const path = require('path')
const TEMP_DIR = process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp'
const TEST_DIR = path.join(TEMP_DIR, 'WebTorrentTest')
const TEST_DIR_DOWNLOAD = path.join(TEST_DIR, 'Downloads')
const TEST_DIR_DESKTOP = path.join(TEST_DIR, 'Desktop')
module.exports = {
TORRENT_FILES: [path.join(__dirname, 'resources', '1.torrent')],
SEED_FILES: [path.join(TEST_DIR_DESKTOP, 'tmp.jpg')],
SAVED_TORRENT_FILE: path.join(TEST_DIR_DESKTOP, 'saved.torrent'),
TEST_DIR,
TEST_DIR_DOWNLOAD,
TEST_DIR_DESKTOP
}

19
test/index.js Normal file
View File

@@ -0,0 +1,19 @@
const test = require('tape')
const setup = require('./setup')
test.onFinish(setup.deleteTestDataDir)
test('app runs', function (t) {
t.timeoutAfter(10e3)
setup.resetTestDataDir()
const app = setup.createApp()
setup.waitForLoad(app, t)
.then(() => setup.screenshotCreateOrCompare(app, t, 'app-basic'))
.then(() => setup.endTest(app, t),
(err) => setup.endTest(app, t, err || 'error'))
})
require('./test-torrent-list')
require('./test-add-torrent')
require('./test-video')
require('./test-audio')

15
test/mocks.js Normal file
View File

@@ -0,0 +1,15 @@
const electron = require('electron')
const config = require('./config')
console.log('Mocking electron.dialog.showOpenDialog...')
electron.dialog.showOpenDialog = function (win, opts, cb) {
const ret = /select.*torrent file/i.test(opts.title)
? config.TORRENT_FILES
: config.SEED_FILES
cb(ret)
}
console.log('Mocking electron.remote.dialog.showSaveDialog...')
electron.dialog.showSaveDialog = function (win, opts, cb) {
cb(config.SAVED_TORRENT_FILE)
}

BIN
test/resources/1.torrent Normal file

Binary file not shown.

Binary file not shown.

BIN
test/resources/m3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

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