Compare commits

..

177 Commits
v0.24.0 ... esm

Author SHA1 Message Date
Diego Rodríguez Baquero
f9e858d21a Merge branch 'master' into esm 2021-10-18 19:52:42 -05:00
Renovate Bot
2cc62e26b3 chore(deps): update dependency electron to v15.2.0 2021-10-15 22:43:17 +00:00
Diego Rodríguez Baquero
7e8078ed92 esm 2021-10-11 18:27:41 -05:00
Diego Rodríguez Baquero
e42a515199 fix: modernify code (#2068)
* fix: modernify code

* standard fix

* fixes
2021-10-11 18:23:09 -05:00
Diego Rodríguez Baquero
c36e43eaa3 fix: electron upgrade fixes (#2058)
* fix: electron upgrade fixes

* fix: electron remote

* fixes

* fixes

* pin

* actually
2021-10-10 13:21:45 -05:00
Renovate Bot
0732bd93d2 chore(deps): update dependency electron to v15.1.2 2021-10-09 19:34:19 +00:00
Renovate Bot
f12027f836 chore(deps): update babel monorepo to v7.15.8 2021-10-07 21:33:39 +00:00
Renovate Bot
d4dfaa6047 chore(deps): update dependency electron to v15.1.1 2021-10-05 23:46:19 +00:00
Renovate Bot
809d122d98 fix(deps): update dependency webtorrent to v1.5.6 2021-10-05 17:07:29 +00:00
Renovate Bot
6dbbf8b85b fix(deps): update dependency chromecasts to v1.10.2 2021-10-05 15:11:48 +00:00
Renovate Bot
3150c04ef4 chore(deps): update dependency electron to v15.1.0 2021-10-02 16:55:35 +00:00
Alex
770c641f1b refactor: ETA calculation (#2055)
* Refactor ETA calculation

* Update src/renderer/pages/player-page.js

Co-authored-by: Alex <alxmorais8@msn.com>

* Fix lint issue

Co-authored-by: Alex <alxmorais8@msn.com>
2021-10-02 15:11:45 +02:00
Renovate Bot
9c0ad29db9 chore(deps): update dependency nodemon to v2.0.13 2021-09-24 21:10:00 +00:00
renovate[bot]
add6b2e900 chore(deps): update dependency electron to v15 (#2051)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-09-23 10:41:07 -05:00
Renovate Bot
83c2e7d731 fix(deps): update dependency music-metadata to v7.11.4 2021-09-23 03:38:47 +00:00
Renovate Bot
29393b1803 chore(deps): update dependency electron-packager to v15.4.0 2021-09-23 03:33:12 +00:00
Renovate Bot
0ba4d3b831 chore(deps): update dependency electron-notarize to v1.1.1 2021-09-23 03:26:29 +00:00
Renovate Bot
872f60e161 chore(deps): update babel monorepo 2021-09-23 03:17:49 +00:00
Renovate Bot
65886933ee fix(deps): update dependency chromecasts to v1.10.1 2021-09-23 03:07:39 +00:00
Renovate Bot
f300d8af21 chore(deps): update dependency plist to v3.0.4 2021-09-23 02:57:06 +00:00
Renovate Bot
f87068a288 chore(deps): update dependency gh-release to v6.0.1 2021-09-23 02:45:16 +00:00
renovate[bot]
1916f4f1a3 chore(deps): update dependency electron to v13.4.0 (#1997)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-09-22 20:53:54 -05:00
renovate[bot]
b0d9b89451 chore(deps): update actions/setup-node action to v2 (#2052)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-09-22 20:51:12 -05:00
Benjamin Tan
0d512db6e2 Ensure that default file/protocol handlers are re-installed after updating. (#997)
Previously, they were only installed when the preference was changed.
This caused the handlers to point to non-existing files after updates
occurred and older versions were removed by Squirrel.

Closes #791, #911.
2021-09-22 20:11:53 -05:00
Vincenzo De Petris
24969c87f3 Fix ghost image showing on media player drag (#1922)
* fix: ghost image showing on media player drag

* fix: add missing space

* fix: access Image from window object
2021-09-22 20:10:30 -05:00
Adam Gotlib
5619f1805b Release media keys when opening external player (#1237) 2021-09-22 20:07:00 -05:00
Alex
e9d72ebb17 fix: resume torrents after deletion (#1974)
* fix: resume torrents after deletion

* Update torrent-list-controller.js

Co-authored-by: Diego Rodríguez Baquero <github@diegorbaquero.com>
2021-09-22 19:56:00 -05:00
Julen Garcia Leunda
c7647440ba build: add package Github Action (#1888)
* Add package Github Action

* Update package.yml

Co-authored-by: Diego Rodríguez Baquero <github@diegorbaquero.com>
2021-09-22 19:52:06 -05:00
Renovate Bot
86ba117e04 fix(deps): update dependency webtorrent to v1.5.5 2021-09-02 13:01:29 +00:00
Renovate Bot
27cf60d5dc fix(deps): update dependency webtorrent to v1.5.4 2021-08-25 23:15:05 +00:00
Renovate Bot
d06cddd94a fix(deps): update dependency webtorrent to v1.5.3 2021-08-20 19:17:40 +00:00
Renovate Bot
21ad84daf8 fix(deps): update dependency webtorrent to v1.5.1 2021-08-20 02:28:03 +00:00
Renovate Bot
c882e13889 fix(deps): update dependency webtorrent to v1.4.0 2021-08-17 20:38:57 +00:00
Renovate Bot
9a68910e31 fix(deps): update dependency webtorrent to v1.3.10 2021-08-16 14:21:14 +00:00
Agyant Tiwari
610abea1c3 Play/Pause video on click (#1878)
* Play/Pause video on click

* Fixing lint

* fix: use dispatcher

Co-authored-by: Alex <alxmorais8@msn.com>
2021-08-16 13:47:53 +02:00
Renovate Bot
b132fb73df chore(deps): update dependency tape to v5.3.1 2021-08-08 07:07:12 +00:00
Renovate Bot
eff52911c2 fix(deps): update dependency webtorrent to v1.3.9 2021-08-07 00:35:35 +00:00
Renovate Bot
3ff41a965f fix(deps): update dependency create-torrent to v5.0.1 2021-08-06 22:42:34 +00:00
Renovate Bot
8373fd17b8 fix(deps): update dependency webtorrent to v1.3.8 2021-08-06 05:07:39 +00:00
renovate[bot]
c531169a37 fix(deps): update dependency create-torrent to v5 (#2034)
* fix(deps): update dependency create-torrent to v5

* Update webtorrent.js

Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Diego Rodríguez Baquero <github@diegorbaquero.com>
2021-08-05 20:54:37 -05:00
renovate[bot]
d00b9ba081 fix(deps): update dependency webtorrent to v1.3.7 (#2033)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-05 07:49:04 +00:00
renovate[bot]
1d644c82de fix(deps): update webtorrent (#2032)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-05 02:22:15 +00:00
renovate[bot]
d6308734ea fix(deps): update dependency chromecasts to v1.10.0 (#1991)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-04 15:09:59 -05:00
renovate[bot]
534c6719d1 chore(deps): update dependency electron-notarize to v1.0.1 (#2027)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-03 19:25:10 -05:00
renovate[bot]
4e4315160e fix(deps): update dependency chokidar to v3.5.2 (#1993)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-03 19:23:40 -05:00
renovate[bot]
25398030fa chore(deps): update babel monorepo (#1992)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-03 19:22:50 -05:00
renovate[bot]
b261a24bda chore(deps): update dependency open to v8.2.1 (#1996)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-03 19:21:56 -05:00
renovate[bot]
dcbe5851c2 fix(deps): update dependency drag-drop to v7.2.0 (#2004)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-03 19:21:28 -05:00
renovate[bot]
24ef81387e chore(deps): update dependency tape to v5.3.0 (#2023)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-03 19:16:45 -05:00
renovate[bot]
6a761c9a10 chore(deps): update dependency electron-packager to v15.3.0 (#2016)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-08-03 19:16:33 -05:00
Renovate Bot
8ac6e9effe chore(deps): update dependency nodemon to v2.0.12 2021-08-03 23:51:42 +00:00
Renovate Bot
fec063bd1d fix(deps): update dependency webtorrent to v1.3.4 2021-07-30 17:56:47 +00:00
Renovate Bot
8b76535ce2 fix(deps): update dependency webtorrent to v1.3.3 2021-07-25 02:21:47 +00:00
Renovate Bot
7072caf3ce fix(deps): update dependency webtorrent to v1.3.2 2021-07-24 20:29:48 +00:00
Renovate Bot
2ba8fd20ee fix(deps): update dependency webtorrent to v1.3.0 2021-07-24 00:22:23 +00:00
Renovate Bot
7f962109e9 fix(deps): update dependency webtorrent to v1.2.6 2021-07-23 20:45:48 +00:00
Renovate Bot
33e6b6565c fix(deps): update dependency create-torrent to v4.7.1 2021-07-22 16:38:52 +00:00
Diego Rodríguez Baquero
04cdc64a18 chore: add stale 2021-07-21 19:20:42 -05:00
Renovate Bot
452bc0e235 fix(deps): update dependency webtorrent to v1.2.5 2021-07-19 17:26:26 +00:00
Renovate Bot
607c5b8c11 fix(deps): update dependency webtorrent to v1.2.4 2021-07-13 22:53:36 +00:00
Renovate Bot
7ef697ed72 fix(deps): update dependency webtorrent to v1.2.3 2021-07-13 20:32:00 +00:00
Renovate Bot
86182417cc fix(deps): update dependency webtorrent to v1.2.2 2021-07-11 04:09:25 +00:00
Renovate Bot
200f385d23 fix(deps): update dependency webtorrent to v1.2.0 2021-07-10 00:09:01 +00:00
Renovate Bot
8a5140d93d fix(deps): update dependency webtorrent to v1.1.6 2021-07-08 22:26:41 +00:00
Renovate Bot
a5c2fa08a5 fix(deps): update dependency webtorrent to v1.1.5 2021-07-06 18:43:20 +00:00
Renovate Bot
d0198a4419 fix(deps): update dependency webtorrent to v1.1.4 2021-07-03 06:48:43 +00:00
Renovate Bot
de1b38e660 fix(deps): update dependency webtorrent to v1.1.3 2021-07-03 00:49:17 +00:00
Renovate Bot
50bf04c73a fix(deps): update dependency webtorrent to v1.1.0 2021-06-30 22:08:43 +00:00
Renovate Bot
1cfa101b90 fix(deps): update dependency webtorrent to v1.0.4 2021-06-30 07:13:21 +00:00
Renovate Bot
14e2775980 fix(deps): update dependency webtorrent to v1.0.2 2021-06-23 20:32:19 +00:00
Julen Garcia Leunda
8e86c855d3 Add armv7l arch support (#1901) 2021-06-12 12:50:48 -05:00
renovate[bot]
01b3772138 fix(deps): update dependency music-metadata to v7.8.6 (#1958)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-12 12:50:16 -05:00
renovate[bot]
1f84a81d97 chore(deps): update dependency electron to v13.1.0 (#1983)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-12 12:49:19 -05:00
renovate[bot]
12ab547bbc chore(deps): update babel monorepo to v7.14.5 (#1990)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-12 12:49:02 -05:00
Alex
ef4fc2d28d feat: paste torrent address with right click (#1989)
* feat: paste torrent address with right click

* Change right click to directly pasting
2021-06-09 23:55:40 +02:00
Feross Aboukhadijeh
d12fd7768a fix: Link to VLC using HTTPS (#1987)
May help with users who are man-in-the-middled and redirected to malicious websites.

See: https://github.com/webtorrent/webtorrent-desktop/issues/1908
2021-06-02 16:46:17 -07:00
Feross Aboukhadijeh
756480a69f chore: Update to Electron 13; Bump all deps (#1986)
* chore: Update to Electron 12; Bump all deps

* Fixes for Electron 12

* chore: Update to Electron 13
2021-06-02 08:21:35 -07:00
renovate[bot]
19a8e9aa3e chore(deps): update dependency gh-release to v6 (#1961)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Feross Aboukhadijeh <feross@feross.org>
2021-06-02 08:04:27 -07:00
renovate[bot]
23f20546f7 chore(deps): pin dependency electron to v11.1.0 (#1976)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 08:00:12 -07:00
renovate[bot]
7a2d1d1807 fix(deps): update dependency webtorrent to v0.118.0 (#1981)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:59:43 -07:00
renovate[bot]
0138a178c3 fix(deps): update dependency iso-639-1 to v2.1.9 (#1948)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:56:22 -07:00
renovate[bot]
ff6900d8f1 fix(deps): update dependency semver to v7.3.5 (#1949)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:54:49 -07:00
renovate[bot]
7217d45ff9 fix(deps): update react monorepo to v17.0.2 (#1950)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:53:00 -07:00
renovate[bot]
ba86131e4a chore(deps): update dependency electron-winstaller to v5 (#1960)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:52:21 -07:00
renovate[bot]
b57278f7f1 chore(deps): update dependency open to v8 (#1962)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:52:03 -07:00
renovate[bot]
c9136422b9 fix(deps): update dependency drag-drop to v7 (#1964)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:51:57 -07:00
renovate[bot]
34692dfd38 chore(deps): update babel monorepo (#1951)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:51:49 -07:00
renovate[bot]
a990b00b21 chore(deps): update dependency ws to 7.4.6 [security] (#1972)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:41:44 -07:00
Alberto Miranda
5f5e4e0ee2 Fix cannot set property 'enabled' of undefined (#1938) 2021-06-02 07:41:09 -07:00
Feross Aboukhadijeh
e9fa721ca8 Switch from Travis to GitHub Actions (#1978) 2021-06-02 07:33:48 -07:00
Feross Aboukhadijeh
7ba1510131 chore: Set node 16 and npm 7 as minimum versions (#1977) 2021-06-02 07:25:48 -07:00
renovate[bot]
057c192dc4 chore(deps): update dependency electron to 11.1.0 [security] (#1939)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-06-02 07:18:43 -07:00
renovate[bot]
b78966ae2e chore(deps): pin dependencies (#1965)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-05-31 22:02:24 -05:00
renovate[bot]
2c34f7af1c chore(deps): update dependency lodash to 4.17.21 [security] (#1941)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-05-24 08:09:49 -05:00
renovate[bot]
6885d23bde fix(deps): update dependency debounce to ^1.2.1 (#1947)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-05-24 08:09:16 -05:00
Diego Rodríguez Baquero
277010a1b8 Merge pull request #1956 from webtorrent/renovate_tape-5.x
chore(deps): update dependency tape to ^5.2.2
2021-05-24 08:08:00 -05:00
Diego Rodríguez Baquero
d0849accbb Merge pull request #1957 from webtorrent/renovate_chokidar-3.x
fix(deps): update dependency chokidar to ^3.5.1
2021-05-24 08:06:23 -05:00
Renovate Bot
080e8da591 fix(deps): update dependency chokidar to ^3.5.1 2021-05-24 04:34:36 +00:00
Renovate Bot
bf0b5f8cdb chore(deps): update dependency tape to ^5.2.2 2021-05-24 04:33:03 +00:00
Diego Rodríguez Baquero
6e151cd2a1 chore: add renovate config 2021-05-23 23:00:09 -05:00
Alex
62273fa6b2 Merge pull request #1932 from webtorrent/dependabot/npm_and_yarn/ua-parser-js-0.7.28
Bump ua-parser-js from 0.7.22 to 0.7.28
2021-05-07 11:31:34 +02:00
dependabot[bot]
00d0c3d828 Bump ua-parser-js from 0.7.22 to 0.7.28
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.22 to 0.7.28.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.22...0.7.28)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-06 21:11:35 +00:00
Feross Aboukhadijeh
61efbd6823 Merge pull request #1928 from RayBB/patch-1
update brew install command
2021-04-26 16:17:48 -07:00
Alex
e6a33fb53a Merge pull request #1905 from getsnoopy/fix-units
Fix formatting of measurement units
2021-04-14 09:57:26 +02:00
Alex
228d733539 Merge pull request #1927 from webtorrent/dependabot/npm_and_yarn/y18n-4.0.1
Bump y18n from 4.0.0 to 4.0.1
2021-04-14 09:53:16 +02:00
Raymond Berger
83eb584422 update brew install command 2021-04-10 19:24:07 -10:00
dependabot[bot]
47e89813a0 Bump y18n from 4.0.0 to 4.0.1
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-30 09:23:23 +00:00
Alex
f0894ced54 Merge pull request #1904 from webtorrent/dependabot/npm_and_yarn/ini-1.3.7
Bump ini from 1.3.5 to 1.3.7
2021-02-01 18:37:45 +01:00
Alex
5372887d46 Merge pull request #1920 from BlueManCZ/tray-fix
Improve libappindicator checking
2021-02-01 18:37:13 +01:00
BlueManCZ
f8d9567422 Change command 2021-01-31 22:21:13 +01:00
BlueManCZ
97b54744df Update comment 2021-01-31 11:19:44 +01:00
BlueManCZ
4a974a1d3d Look for libappindicator libraries instead of package 2021-01-31 11:12:18 +01:00
BlueManCZ
192560755c Merge branch 'master' of github.com:webtorrent/webtorrent-desktop 2021-01-31 10:40:05 +01:00
Varun Varada
d299704f57 Fix formatting of measurement units
There were some incorrectly formatted unit symbols and units names in
the app, so this commit fixes them per SI and ISO/IEC specifications.
2020-12-22 19:00:05 -06:00
dependabot[bot]
314e74d1ed Bump ini from 1.3.5 to 1.3.7
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-10 23:07:23 +00:00
Feross Aboukhadijeh
919af5bf15 Merge pull request #1217 from cezar-carneiro/show-progress-on-cast-screen 2020-11-26 01:44:32 +01:00
Feross Aboukhadijeh
d819acedd5 Merge pull request #1865 from BlueManCZ/patch-1 2020-11-25 22:33:25 +01:00
Feross Aboukhadijeh
54fcc5057b recursively update all deps 2020-11-24 12:02:08 -10:00
Feross Aboukhadijeh
cd5da2fe9b cross-zip@4 2020-11-24 12:00:00 -10:00
Feross Aboukhadijeh
1c79ffc720 bump deps; including electron ~11.0.3
Fixes: https://github.com/webtorrent/webtorrent-desktop/issues/1896
2020-11-24 11:56:17 -10:00
Feross Aboukhadijeh
a1a590fbba travis: disable cache
the cache keeps caching outdated versions which cause test failures
2020-11-22 16:32:36 -10:00
Feross Aboukhadijeh
606c02aef4 Remove extra-lint
- standard enforces 'let' and 'const' now
- I've never been a fan of the <100 chars per line rule since it violates the standard philosophy. it should be possible to write conformant code without needing a tool, so 'magic numbers' like 'no more than X lines/chars/statements' are not ideal
2020-11-22 16:29:59 -10:00
Feross Aboukhadijeh
4f68a8ad0b parse-torrent@9 2020-11-22 16:26:42 -10:00
Feross Aboukhadijeh
37d962fcf3 bump deps 2020-11-22 16:24:46 -10:00
Feross Aboukhadijeh
baf06461df Remove unused 'buble' dependency 2020-11-22 16:20:09 -10:00
Feross Aboukhadijeh
464e63fd92 standard (Fixes for PR #1818) 2020-11-22 16:12:04 -10:00
Feross Aboukhadijeh
2761e9c768 Merge pull request #1818 from surajrathod/master 2020-11-23 03:11:34 +01:00
suraj rathod
0efb5fa7ad Merge branch 'master' into master 2020-11-20 11:27:33 +05:30
Feross Aboukhadijeh
34947c355d standard@16 2020-11-18 12:38:20 -10:00
Feross Aboukhadijeh
958814f2ea use @babel/eslint-parser 2020-11-18 12:37:03 -10:00
Feross Aboukhadijeh
ec0c0c7870 Update package-lock.json 2020-11-18 12:30:13 -10:00
Feross Aboukhadijeh
ec0a1d1bb4 Merge pull request #1895 from webtorrent/electron11 2020-11-18 03:41:45 +01:00
Feross Aboukhadijeh
969d71afec Merge pull request #1698 from webtorrent/babel
Use babel instead of buble
2020-11-18 03:41:19 +01:00
Feross Aboukhadijeh
c1db01b936 Use babel instead of buble
This way we can target just the JSX tranformation. We don't use any other cutting edge JS features that require compilation, nor should we in the future.
2020-11-17 16:40:14 -10:00
Feross Aboukhadijeh
90670a4645 changelog: add link to security issue explanation 2020-11-17 15:42:11 -10:00
Feross Aboukhadijeh
b5fad54f4a Upgrade to Electron 11 2020-11-16 18:03:35 -10:00
Feross Aboukhadijeh
50718d3791 Merge pull request #1887 from webtorrent/fix_lintian_warnings
Fix lintian warnings
2020-11-09 22:58:14 +01:00
Julen Garcia Leunda
bd54a8c214 Fix lintian warnings 2020-11-06 17:13:22 +01:00
Feross Aboukhadijeh
0cd0edf4cc Merge pull request #1886 from webtorrent/throttling
Disable background throttling
2020-11-05 22:30:22 +01:00
Feross Aboukhadijeh
9e0d91f144 Disable background throttling
Reference: https://github.com/webtorrent/webtorrent-desktop/pull/1720#issuecomment-722011487

I actually think we want backgroundThrottling: false so our timers and intervals aren't throttled when the app is backgrounded.
2020-11-04 12:34:48 -10:00
Feross Aboukhadijeh
38d9c4c504 Merge pull request #1885 from webtorrent/bitfield
bitfield@4
2020-11-04 10:15:40 +01:00
Feross Aboukhadijeh
9e1721990d bitfield@4 2020-11-03 13:03:40 -10:00
Feross Aboukhadijeh
a20f82cc61 standard 2020-10-29 11:30:07 -10:00
Feross Aboukhadijeh
630ed76b8c standard 2020-10-28 09:52:07 -10:00
Feross Aboukhadijeh
1155cba4dd Merge pull request #1883 from webtorrent/deps 2020-10-28 19:18:16 +01:00
Feross Aboukhadijeh
2343181e1b Update package-lock.json 2020-10-28 08:15:53 -10:00
Feross Aboukhadijeh
9bb7330caa spectron@12 2020-10-28 08:14:10 -10:00
Feross Aboukhadijeh
3aae1614ea pngjs@6 2020-10-28 08:13:13 -10:00
Feross Aboukhadijeh
cdd308fc4a gh-release@4 2020-10-28 08:12:11 -10:00
Feross Aboukhadijeh
594d18e76e react 17 2020-10-28 08:11:10 -10:00
Feross Aboukhadijeh
50d772eb54 parse-torrent@8 2020-10-28 08:10:43 -10:00
Feross Aboukhadijeh
57ffb5e0c4 bump deps 2020-10-28 08:10:26 -10:00
Feross Aboukhadijeh
49ab4bd201 Merge pull request #1882 from webtorrent/render-deprecated 2020-10-28 19:07:03 +01:00
Feross Aboukhadijeh
7b06acb972 don't rely on deprecated return value from ReactDOM.render 2020-10-27 18:18:30 -10:00
Feross Aboukhadijeh
80b86b5693 standard 2020-10-21 13:50:42 -10:00
Diego Rodríguez Baquero
748b996522 Merge pull request #1875 from webtorrent/dependabot/npm_and_yarn/electron-10.1.2
Bump electron from 10.1.1 to 10.1.2
2020-10-20 10:51:26 -05:00
Julen Garcia Leunda
77877d4201 Merge pull request #1812 from dsernst/scrub-preview
Adds Youtube-style Scrubbing preview
2020-10-20 17:31:43 +02:00
dependabot[bot]
696fa4479c Bump electron from 10.1.1 to 10.1.2
Bumps [electron](https://github.com/electron/electron) from 10.1.1 to 10.1.2.
- [Release notes](https://github.com/electron/electron/releases)
- [Changelog](https://github.com/electron/electron/blob/master/docs/breaking-changes.md)
- [Commits](https://github.com/electron/electron/compare/v10.1.1...v10.1.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-06 17:49:29 +00:00
Feross Aboukhadijeh
e0bb7562be Update package-lock.json 2020-09-03 11:36:21 -07:00
Feross Aboukhadijeh
8f20d9676b electron 10.1.1 2020-09-03 11:35:08 -07:00
BlueManCZ
b61a4e84f5 Remove Version entry 2020-08-29 14:19:36 +02:00
BlueManCZ
9c59157a98 Remove VERSION entry 2020-08-29 14:03:27 +02:00
Ivo Šmerek
5def2d3947 Fix StartupWMClass 2020-08-29 13:53:02 +02:00
Feross Aboukhadijeh
ce6326a869 Update RELEASE_PROCESS.md 2020-08-28 17:58:07 -07:00
Feross Aboukhadijeh
7e56550887 Update RELEASE_PROCESS.md 2020-08-28 17:51:12 -07:00
David Ernst
d3bfccaa6b Check for type === video in clearPreview() 2020-07-30 19:10:14 -07:00
SurajRathod
ab9f86d6c9 added .vscode ignore 2020-06-06 17:47:38 +05:30
SurajRathod
583cecc661 selective import 2020-06-06 17:46:30 +05:30
David Ernst
c4b318bd5f Clean up code and add better comments 2020-05-02 11:40:00 -07:00
David Ernst
d55acf9c12 Fix 'extra-lint' errors 2020-05-01 22:41:19 -07:00
David Ernst
24bc40c95e Fix preview border rendering 2020-05-01 22:06:05 -07:00
David Ernst
0716444be5 Working video scrubbing preview 2020-05-01 21:57:42 -07:00
David Ernst
85c0e99e16 Only show previews for video 2020-05-01 21:05:29 -07:00
David Ernst
0b57961ff7 Basic placeholder UI for scrubbing preview 2020-05-01 19:43:47 -07:00
Cezar Carneiro
ab41d55e4b fix ETA and reorganize 2017-08-02 22:52:45 -03:00
Cezar Carneiro
739e7a50f7 improve infos 2017-08-02 00:03:52 -03:00
Cezar Carneiro
64ff5ebfd5 show percentage, size, speed and peers 2017-07-31 22:47:53 -03:00
83 changed files with 20690 additions and 5113 deletions

21
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: ci
on: [push,pull_request]
jobs:
test:
name: Node ${{ matrix.node }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
node:
- '16'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: npm install
- run: npm run build --if-present
- run: npm test

69
.github/workflows/package.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: package
on:
workflow_dispatch:
jobs:
package_linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm install
- run: npm run package -- linux
- uses: actions/upload-artifact@v2
with:
name: linux
path: |
dist/*.deb
dist/*.rpm
dist/*.zip
package_macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm install
- run: npm run package -- darwin
- uses: actions/upload-artifact@v2
with:
name: macos
path: |
dist/*.dmg
dist/*.zip
package_windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- run: npm install
- run: npm run package -- win32
- uses: actions/upload-artifact@v2
with:
name: windows
path: |
dist/*.exe
dist/*.nupkg
dist/*.zip

23
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: '0 12 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v4
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?'
stale-pr-message: 'Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?'
exempt-issue-labels: accepted,blocked,bug,dependency,enhancement,'help wanted',question,security,meta
exempt-pr-labels: accepted,blocked,bug,dependency,enhancement,'help wanted',question,security,meta
stale-issue-label: 'stale'
stale-pr-label: 'stale'

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@ node_modules/
build/ build/
dist/ dist/
npm-debug.log* npm-debug.log*
.vscode/
# JetBrains IntelliJ IDEA project files # JetBrains IntelliJ IDEA project files
.idea .idea

View File

@@ -1,3 +0,0 @@
language: node_js
node_js:
- lts/*

View File

@@ -19,7 +19,7 @@
## v0.23.0 - 2020-07-15 ## v0.23.0 - 2020-07-15
🔒 This release contains a critical security fix. Please update as soon as possible. 🔒 This release contains a critical security fix. Please update as soon as possible. [\#1837](https://github.com/webtorrent/webtorrent-desktop/issues/1837#issuecomment-729320901)
### Added ### Added

View File

@@ -35,7 +35,7 @@ Download the latest version of WebTorrent Desktop from
- Use [Homebrew-Cask](https://github.com/caskroom/homebrew-cask) to install from the command line: - Use [Homebrew-Cask](https://github.com/caskroom/homebrew-cask) to install from the command line:
``` ```
$ brew cask install webtorrent $ brew install --cask webtorrent
``` ```
- Try the (unstable) development version by cloning the Git repository. See the - Try the (unstable) development version by cloning the Git repository. See the

View File

@@ -74,17 +74,29 @@
**This is the most important part.** **This is the most important part.**
- Manually download the binaries for each platform from Github. - Manually download the binaries for each platform from Github.
**Do not use your locally built binaries.** Modern OSs treat executables differently if they've **Do not use your locally built binaries.** Modern OSs treat executables differently if they've
been downloaded, even though the files are byte for byte identical. This ensures that the been downloaded, even though the files are byte for byte identical. This ensures that the
codesigning worked and is valid. codesigning worked and is valid.
- Smoke test WebTorrent Desktop on each platform. - Smoke test WebTorrent Desktop on each platform. Before a release, check that the following basic use cases work correctly:
See Smoke Tests below for details. Open DevTools 1. Click "Play" to stream a built-in torrent (e.g. Sintel)
on Windows and Mac, and ensure that the auto updater is running. If the auto updater does not - Ensure that seeking to undownloaded region works and plays immediately.
run, users will successfully auto update to this new version, and then be stuck there forever. - Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
2. Check that the auto-updater works
- Open the console and check for the line "No update available" to indicate that the auto-updater is working. (If the auto updater does not run, users will successfully auto update to this new version, and then be stuck there forever.)
3. Add a new .torrent file via drag-and-drop.
- Ensure that it gets added to the list and starts downloading.
4. Remove a torrent from the client
- Ensure that the file is removed from `~/Downloads`
5. Create and seed a new a torrent via drag-and-drop.
- Ensure that the torrent gets created and seeding begins.
### 4. Ship it ### 4. Ship it
@@ -93,25 +105,5 @@
Create a pull request in [webtorrent.io](https://github.com/webtorrent/webtorrent.io). Update Create a pull request in [webtorrent.io](https://github.com/webtorrent/webtorrent.io). Update
`config.js`, updating the desktop app version. `config.js`, updating the desktop app version.
As soon as this PR is merged, Jenkins will automatically redeploy the WebTorrent website, and Once this PR is merged and Feross redeploys the WebTorrent website,
hundreds of thousands of users around the world will start auto updating. **Merge with care.** hundreds of thousands of users around the world will start auto updating. **Merge with care.**
## Smoke Tests
Before a release, check that the following basic use cases work correctly:
1. Click "Play" to stream a built-in torrent (e.g. Sintel)
- Ensure that seeking to undownloaded region works and plays immediately.
- Ensure that sintel.mp4 gets downloaded to `~/Downloads`.
2. Check that the auto-updater works
- Open the console and check for the line "No update available" to indicate that the auto-updater is working.
3. Add a new .torrent file via drag-and-drop.
- Ensure that it gets added to the list and starts downloading.
4. Remove a torrent from the client
- Ensure that the file is removed from `~/Downloads`
5. Create and seed a new a torrent via drag-and-drop.
- Ensure that the torrent gets created and seeding begins.

View File

@@ -1,39 +0,0 @@
#!/usr/bin/env node
const walkSync = require('walk-sync')
const fs = require('fs')
const path = require('path')
let hasErrors = false
// Find all Javascript source files
const files = walkSync('src', { globs: ['**/*.js'] })
console.log('Running extra-lint on ' + files.length + ' files...')
// Read each file, line by line
files.forEach(function (file) {
const filepath = path.join('src', file)
const lines = fs.readFileSync(filepath, 'utf8').split('\n')
lines.forEach(function (line, i) {
let error
// No lines over 100 characters
if (line.length > 100) {
error = 'Line >100 chars'
}
if (line.match(/^var /) || line.match(/ var /)) {
error = 'Use const or let'
}
if (error) {
const name = path.basename(file)
console.log('%s:%d - %s:\n%s', name, i + 1, error, line)
hasErrors = true
}
})
})
if (hasErrors) process.exit(1)
else console.log('Looks good!')

View File

@@ -172,8 +172,8 @@ const linux = {
// Build for Linux. // Build for Linux.
platform: 'linux', platform: 'linux',
// Build x64 and arm64 binaries. // Build x64, armv7l, and arm64 binaries.
arch: ['x64', 'arm64'] arch: ['x64', 'armv7l', 'arm64']
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option. // Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
} }
@@ -545,7 +545,17 @@ function buildLinux (cb) {
}, },
categories: ['Network', 'FileTransfer', 'P2P'], categories: ['Network', 'FileTransfer', 'P2P'],
mimeType: ['application/x-bittorrent', 'x-scheme-handler/magnet', 'x-scheme-handler/stream-magnet'], mimeType: ['application/x-bittorrent', 'x-scheme-handler/magnet', 'x-scheme-handler/stream-magnet'],
desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs') desktopTemplate: path.join(config.STATIC_PATH, 'linux/webtorrent-desktop.ejs'),
lintianOverrides: [
'unstripped-binary-or-object',
'embedded-library',
'missing-dependency-on-libc',
'changelog-file-missing-in-native-package',
'description-synopsis-is-duplicated',
'setuid-binary',
'binary-without-manpage',
'shlib-with-executable-bit'
]
} }
installer(options).then( installer(options).then(

1
electron.cjs Normal file
View File

@@ -0,0 +1 @@
module.exports = require('electron')

1
index.cjs Normal file
View File

@@ -0,0 +1 @@
import('./build/main/index.js')

View File

@@ -1 +0,0 @@
require('./build/main')

22675
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
{ {
"name": "webtorrent-desktop", "name": "webtorrent-desktop",
"type": "module",
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.", "description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
"version": "0.24.0", "version": "0.24.0",
"author": { "author": {
@@ -7,69 +8,82 @@
"email": "feross@webtorrent.io", "email": "feross@webtorrent.io",
"url": "https://webtorrent.io" "url": "https://webtorrent.io"
}, },
"babel": {
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"useBuiltIns": true
}
]
]
},
"bugs": { "bugs": {
"url": "https://github.com/webtorrent/webtorrent-desktop/issues" "url": "https://github.com/webtorrent/webtorrent-desktop/issues"
}, },
"dependencies": { "dependencies": {
"@electron/remote": "2.0.1",
"airplayer": "github:webtorrent/airplayer#fix-security", "airplayer": "github:webtorrent/airplayer#fix-security",
"application-config": "^2.0.0", "application-config": "2.0.0",
"arch": "^2.1.2", "arch": "2.2.0",
"auto-launch": "^5.0.5", "auto-launch": "5.0.5",
"bitfield": "^3.0.0", "bitfield": "4.0.0",
"capture-frame": "^4.0.0", "capture-frame": "4.0.0",
"chokidar": "^3.4.2", "chokidar": "3.5.2",
"chromecasts": "^1.9.1", "chromecasts": "1.10.2",
"create-torrent": "^4.4.2", "create-torrent": "5.0.1",
"debounce": "^1.2.0", "debounce": "1.2.1",
"dlnacasts": "^0.1.0", "dlnacasts": "0.1.0",
"drag-drop": "^6.0.2", "drag-drop": "7.2.0",
"es6-error": "^4.1.1", "es6-error": "4.1.1",
"fn-getter": "^1.0.0", "fn-getter": "1.0.0",
"iso-639-1": "^2.1.4", "iso-639-1": "2.1.9",
"languagedetect": "^2.0.0", "languagedetect": "2.0.0",
"location-history": "^1.1.2", "location-history": "1.1.2",
"material-ui": "^0.20.2", "material-ui": "0.20.2",
"music-metadata": "^7.0.2", "music-metadata": "7.11.4",
"network-address": "^1.1.2", "network-address": "1.1.2",
"parse-torrent": "^7.1.3", "parse-torrent": "9.1.4",
"prettier-bytes": "^1.0.4", "prettier-bytes": "1.0.4",
"prop-types": "^15.7.2", "prop-types": "15.7.2",
"react": "^16.13.1", "react": "17.0.2",
"react-dom": "^16.13.1", "react-dom": "17.0.2",
"rimraf": "^3.0.2", "rimraf": "3.0.2",
"run-parallel": "^1.1.9", "run-parallel": "1.2.0",
"semver": "^7.3.2", "semver": "7.3.5",
"simple-concat": "^1.0.1", "simple-concat": "1.0.1",
"simple-get": "^4.0.0", "simple-get": "4.0.0",
"srt-to-vtt": "^1.1.3", "srt-to-vtt": "1.1.3",
"vlc-command": "^1.2.0", "vlc-command": "1.2.0",
"webtorrent": ">=0.108.6", "webtorrent": "1.5.6",
"winreg": "^1.2.4" "winreg": "1.2.4"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "@babel/cli": "7.15.7",
"buble": "^0.20.0", "@babel/core": "7.15.8",
"cross-zip": "^3.1.0", "@babel/eslint-parser": "7.15.8",
"depcheck": "^1.2.0", "@babel/plugin-transform-react-jsx": "7.14.9",
"electron": "~10.1.0", "cross-zip": "4.0.0",
"electron-notarize": "^1.0.0", "depcheck": "1.4.2",
"electron-osx-sign": "^0.4.17", "electron": "15.2.0",
"electron-packager": "^15.1.0", "electron-notarize": "1.1.1",
"electron-winstaller": "^4.0.1", "electron-osx-sign": "0.5.0",
"gh-release": "^3.5.0", "electron-packager": "15.4.0",
"minimist": "^1.2.5", "electron-winstaller": "5.0.0",
"nodemon": "^2.0.4", "gh-release": "6.0.1",
"open": "^7.2.1", "minimist": "1.2.5",
"plist": "^3.0.1", "nodemon": "2.0.13",
"pngjs": "^5.0.0", "open": "8.3.0",
"run-series": "^1.1.8", "plist": "3.0.4",
"spectron": "~11.1.0", "pngjs": "6.0.0",
"standard": "*", "run-series": "1.1.9",
"tape": "^5.0.1", "spectron": "15.0.0",
"walk-sync": "^2.2.0" "standard": "16.0.4",
"tape": "5.3.1"
}, },
"engines": { "engines": {
"node": ">=4.0.0" "node": "^16.0.0",
"npm": "^7.10.0 || ^8.0.0"
}, },
"homepage": "https://webtorrent.io", "homepage": "https://webtorrent.io",
"keywords": [ "keywords": [
@@ -83,11 +97,11 @@
"webtorrent" "webtorrent"
], ],
"license": "MIT", "license": "MIT",
"main": "index.js", "main": "index.cjs",
"optionalDependencies": { "optionalDependencies": {
"appdmg": "^0.6.0", "appdmg": "^0.6.0",
"electron-installer-debian": "^3.1.0", "electron-installer-debian": "^3.1.0",
"electron-installer-redhat": "^3.2.0" "electron-installer-redhat": "^3.3.0"
}, },
"private": true, "private": true,
"productName": "WebTorrent", "productName": "WebTorrent",
@@ -96,19 +110,24 @@
"url": "git://github.com/webtorrent/webtorrent-desktop.git" "url": "git://github.com/webtorrent/webtorrent-desktop.git"
}, },
"scripts": { "scripts": {
"build": "buble src --target chrome:71 --output build", "build": "babel src --out-dir build",
"clean": "node ./bin/clean.js", "clean": "node ./bin/clean.js",
"gh-release": "gh-release", "gh-release": "gh-release",
"install-system-deps": "brew install fakeroot dpkg rpm", "install-system-deps": "brew install fakeroot dpkg rpm",
"open-config": "node ./bin/open-config.js", "open-config": "node ./bin/open-config.js",
"package": "node ./bin/package.js", "package": "node ./bin/package.js",
"start": "npm run build && electron --no-sandbox .", "start": "npm run build && electron --no-sandbox .",
"test": "standard && depcheck --ignores=standard,babel-eslint --ignore-dirs=build,dist && node ./bin/extra-lint.js", "test": "standard && depcheck --ignores=standard,@babel/eslint-parser --ignore-dirs=build,dist",
"test-integration": "npm run build && node ./test", "test-integration": "npm run build && node ./test",
"update-authors": "./bin/update-authors.sh", "update-authors": "./bin/update-authors.sh",
"watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/" "watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/"
}, },
"standard": { "standard": {
"parser": "babel-eslint" "parser": "@babel/eslint-parser"
},
"renovate": {
"extends": [
"github>webtorrent/renovate-config"
]
} }
} }

View File

@@ -1,12 +1,13 @@
const appConfig = require('application-config')('WebTorrent') import applicationConfig from 'application-config'
const path = require('path') import path from 'path'
const electron = require('electron') import arch from 'arch'
const arch = require('arch') import fs from 'fs'
import electron from '../electron.cjs'
const appConfig = applicationConfig('WebTorrent')
const APP_NAME = 'WebTorrent' const APP_NAME = 'WebTorrent'
const APP_TEAM = 'WebTorrent, LLC' const APP_TEAM = 'WebTorrent, LLC'
const APP_VERSION = require('../package.json').version const APP_VERSION = JSON.parse(fs.readFileSync('package.json').toString()).version
const IS_TEST = isTest() const IS_TEST = isTest()
const PORTABLE_PATH = IS_TEST const PORTABLE_PATH = IS_TEST
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest') ? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
@@ -17,93 +18,6 @@ const IS_PORTABLE = isPortable()
const UI_HEADER_HEIGHT = 38 const UI_HEADER_HEIGHT = 38
const UI_TORRENT_HEIGHT = 100 const UI_TORRENT_HEIGHT = 100
module.exports = {
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
AUTO_UPDATE_URL: 'https://webtorrent.io/desktop/update',
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
APP_COPYRIGHT: `Copyright © 2014-${new Date().getFullYear()} ${APP_TEAM}`,
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
APP_NAME,
APP_TEAM,
APP_VERSION,
APP_WINDOW_TITLE: APP_NAME,
CONFIG_PATH: getConfigPath(),
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'
}
],
DELAYED_INIT: 3000 /* 3 seconds */,
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
GITHUB_URL: 'https://github.com/webtorrent/webtorrent-desktop',
GITHUB_URL_ISSUES: 'https://github.com/webtorrent/webtorrent-desktop/issues',
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/webtorrent/webtorrent-desktop/master',
GITHUB_URL_RELEASES: 'https://github.com/webtorrent/webtorrent-desktop/releases',
HOME_PAGE_URL: 'https://webtorrent.io',
TWITTER_PAGE_URL: 'https://twitter.com/WebTorrentApp',
IS_PORTABLE,
IS_PRODUCTION,
IS_TEST,
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
ROOT_PATH: path.join(__dirname, '..'),
STATIC_PATH: path.join(__dirname, '..', 'static'),
TORRENT_PATH: path.join(getConfigPath(), 'Torrents'),
WINDOW_ABOUT: 'file://' + path.join(__dirname, '..', 'static', 'about.html'),
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
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_TORRENT_HEIGHT
}
function getConfigPath () { function getConfigPath () {
if (IS_PORTABLE) { if (IS_PORTABLE) {
return PORTABLE_PATH return PORTABLE_PATH
@@ -121,16 +35,12 @@ function getDefaultDownloadPath () {
} }
function getPath (key) { function getPath (key) {
if (!process.versions.electron) { if (!process.versions.electron || process.type !== 'browser') {
// Node.js process // Node.js process
return '' return ''
} else if (process.type === 'renderer') {
// Electron renderer process
return electron.remote.app.getPath(key)
} else {
// Electron main process
return electron.app.getPath(key)
} }
// Electron main process
return electron.app.getPath(key)
} }
function isTest () { function isTest () {
@@ -147,8 +57,6 @@ function isPortable () {
return false return false
} }
const fs = require('fs')
try { try {
// This line throws if the "Portable Settings" folder does not exist, and does // This line throws if the "Portable Settings" folder does not exist, and does
// nothing otherwise. // nothing otherwise.
@@ -174,3 +82,117 @@ function isProduction () {
return !/\/electron$/.test(process.execPath) return !/\/electron$/.test(process.execPath)
} }
} }
export const ANNOUNCEMENT_URL = 'https://webtorrent.io/desktop/announcement'
export const AUTO_UPDATE_URL = 'https://webtorrent.io/desktop/update'
export const CRASH_REPORT_URL = 'https://webtorrent.io/desktop/crash-report'
export const TELEMETRY_URL = 'https://webtorrent.io/desktop/telemetry'
export const APP_COPYRIGHT = `Copyright © 2014-${new Date().getFullYear()} ${APP_TEAM}`
export const APP_FILE_ICON = new URL('../static/WebTorrentFile', import.meta.url).pathname // path.join(__dirname, '..',
// 'static', 'WebTorrentFile')
export const APP_ICON = new URL('../static/WebTorrent', import.meta.url).pathname // path.join(__dirname, '..',
// 'static',
// 'WebTorrent')
export const CONFIG_PATH = getConfigPath()
export const 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'
}
]
export const DELAYED_INIT = 3000 /* 3 seconds */
export const DEFAULT_DOWNLOAD_PATH = getDefaultDownloadPath()
export const GITHUB_URL = 'https://github.com/webtorrent/webtorrent-desktop'
export const GITHUB_URL_ISSUES = 'https://github.com/webtorrent/webtorrent-desktop/issues'
export const GITHUB_URL_RAW = 'https://raw.githubusercontent.com/webtorrent/webtorrent-desktop/master'
export const GITHUB_URL_RELEASES = 'https://github.com/webtorrent/webtorrent-desktop/releases'
export const HOME_PAGE_URL = 'https://webtorrent.io'
export const TWITTER_PAGE_URL = 'https://twitter.com/WebTorrentApp'
export const OS_SYSARCH = arch() === 'x64' ? 'x64' : 'ia32'
export const POSTER_PATH = path.join(getConfigPath(), 'Posters')
export const ROOT_PATH = new URL('../', import.meta.url).pathname
export const STATIC_PATH = new URL('../static', import.meta.url).pathname
export const TORRENT_PATH = path.join(getConfigPath(), 'Torrents')
export const WINDOW_ABOUT = 'file://' + new URL('../static/about.html', import.meta.url).pathname
export const WINDOW_MAIN = 'file://' + new URL('../static/main.html', import.meta.url).pathname
export const WINDOW_WEBTORRENT = 'file://' + new URL('../static/webtorrent.html', import.meta.url).pathname
export const WINDOW_INITIAL_BOUNDS = {
width: 500,
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
}
export const WINDOW_MIN_HEIGHT = UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2)
export const WINDOW_MIN_WIDTH = 425
export { APP_NAME }
export { APP_TEAM }
export { APP_VERSION }
export { APP_NAME as APP_WINDOW_TITLE }
export { IS_PORTABLE }
export { IS_PRODUCTION }
export { IS_TEST }
export { UI_HEADER_HEIGHT }
export { UI_TORRENT_HEIGHT }
export default {
ANNOUNCEMENT_URL,
AUTO_UPDATE_URL,
CRASH_REPORT_URL,
TELEMETRY_URL,
APP_COPYRIGHT,
APP_FILE_ICON,
APP_ICON,
APP_NAME,
APP_TEAM,
APP_VERSION,
APP_WINDOW_TITLE: APP_NAME,
CONFIG_PATH,
DEFAULT_TORRENTS,
DELAYED_INIT,
DEFAULT_DOWNLOAD_PATH,
GITHUB_URL,
GITHUB_URL_ISSUES,
GITHUB_URL_RAW,
GITHUB_URL_RELEASES,
HOME_PAGE_URL,
TWITTER_PAGE_URL,
IS_PORTABLE,
IS_PRODUCTION,
IS_TEST,
OS_SYSARCH,
POSTER_PATH,
ROOT_PATH,
STATIC_PATH,
TORRENT_PATH,
WINDOW_ABOUT,
WINDOW_MAIN,
WINDOW_WEBTORRENT,
WINDOW_INITIAL_BOUNDS,
WINDOW_MIN_HEIGHT,
WINDOW_MIN_WIDTH,
UI_HEADER_HEIGHT,
UI_TORRENT_HEIGHT
}

View File

@@ -1,11 +1,7 @@
module.exports = { import electron from '../electron.cjs'
init
}
function init () {
const config = require('./config')
const electron = require('electron')
async function init () {
const config = await import('./config.js')
electron.crashReporter.start({ electron.crashReporter.start({
productName: config.APP_NAME, productName: config.APP_NAME,
submitURL: config.CRASH_REPORT_URL, submitURL: config.CRASH_REPORT_URL,
@@ -13,3 +9,5 @@ function init () {
compress: true compress: true
}) })
} }
export default { init }

View File

@@ -1,11 +1,6 @@
module.exports = { import electron from '../../electron.cjs'
init import config from '../config.js'
} import log from './log.js'
const electron = require('electron')
const config = require('../config')
const log = require('./log')
const ANNOUNCEMENT_URL = const ANNOUNCEMENT_URL =
`${config.ANNOUNCEMENT_URL}?version=${config.APP_VERSION}&platform=${process.platform}` `${config.ANNOUNCEMENT_URL}?version=${config.APP_VERSION}&platform=${process.platform}`
@@ -24,8 +19,8 @@ const ANNOUNCEMENT_URL =
* "detail": "Please update to v0.xx as soon as possible..." * "detail": "Please update to v0.xx as soon as possible..."
* } * }
*/ */
function init () { async function init () {
const get = require('simple-get') const { default: get } = await import('simple-get')
get.concat(ANNOUNCEMENT_URL, onResponse) get.concat(ANNOUNCEMENT_URL, onResponse)
} }
@@ -52,3 +47,5 @@ function onResponse (err, res, data) {
detail: data.detail detail: data.detail
}) })
} }
export default { init }

View File

@@ -1,20 +1,13 @@
module.exports = { import electron from '../../electron.cjs'
openSeedFile, import log from './log.js'
openSeedDirectory, import * as windows from './windows/index.js'
openTorrentFile,
openTorrentAddress,
openFiles
}
const electron = require('electron') const { dialog } = electron
const log = require('./log')
const windows = require('./windows')
/** /**
* Show open dialog to create a single-file torrent. * Show open dialog to create a single-file torrent.
*/ */
function openSeedFile () { export function openSeedFile () {
if (!windows.main.win) return if (!windows.main.win) return
log('openSeedFile') log('openSeedFile')
const opts = { const opts = {
@@ -29,18 +22,18 @@ function openSeedFile () {
* Windows and Linux, open dialogs are for files *or* directories only, not both, * Windows and Linux, open dialogs are for files *or* directories only, not both,
* so this function shows a directory dialog on those platforms. * so this function shows a directory dialog on those platforms.
*/ */
function openSeedDirectory () { export function openSeedDirectory () {
if (!windows.main.win) return if (!windows.main.win) return
log('openSeedDirectory') log('openSeedDirectory')
const opts = process.platform === 'darwin' const opts = process.platform === 'darwin'
? { ? {
title: 'Select a file or folder for the torrent.', title: 'Select a file or folder for the torrent.',
properties: ['openFile', 'openDirectory'] properties: ['openFile', 'openDirectory']
} }
: { : {
title: 'Select a folder for the torrent.', title: 'Select a folder for the torrent.',
properties: ['openDirectory'] properties: ['openDirectory']
} }
showOpenSeed(opts) showOpenSeed(opts)
} }
@@ -48,20 +41,20 @@ function openSeedDirectory () {
* Show flexible open dialog that supports selecting .torrent files to add, or * Show flexible open dialog that supports selecting .torrent files to add, or
* a file or folder to create a single-file or single-directory torrent. * a file or folder to create a single-file or single-directory torrent.
*/ */
function openFiles () { export function openFiles () {
if (!windows.main.win) return if (!windows.main.win) return
log('openFiles') log('openFiles')
const opts = process.platform === 'darwin' const opts = process.platform === 'darwin'
? { ? {
title: 'Select a file or folder to add.', title: 'Select a file or folder to add.',
properties: ['openFile', 'openDirectory'] properties: ['openFile', 'openDirectory']
} }
: { : {
title: 'Select a file to add.', title: 'Select a file to add.',
properties: ['openFile'] properties: ['openFile']
} }
setTitle(opts.title) setTitle(opts.title)
const selectedPaths = electron.dialog.showOpenDialogSync(windows.main.win, opts) const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle() resetTitle()
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('onOpen', selectedPaths) windows.main.dispatch('onOpen', selectedPaths)
@@ -70,7 +63,7 @@ function openFiles () {
/* /*
* Show open dialog to open a .torrent file. * Show open dialog to open a .torrent file.
*/ */
function openTorrentFile () { export function openTorrentFile () {
if (!windows.main.win) return if (!windows.main.win) return
log('openTorrentFile') log('openTorrentFile')
const opts = { const opts = {
@@ -79,10 +72,10 @@ function openTorrentFile () {
properties: ['openFile', 'multiSelections'] properties: ['openFile', 'multiSelections']
} }
setTitle(opts.title) setTitle(opts.title)
const selectedPaths = electron.dialog.showOpenDialogSync(windows.main.win, opts) const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle() resetTitle()
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(selectedPaths)) return
selectedPaths.forEach(function (selectedPath) { selectedPaths.forEach(selectedPath => {
windows.main.dispatch('addTorrent', selectedPath) windows.main.dispatch('addTorrent', selectedPath)
}) })
} }
@@ -90,7 +83,7 @@ function openTorrentFile () {
/* /*
* Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.) * Show modal dialog to open a torrent URL (magnet uri, http torrent link, etc.)
*/ */
function openTorrentAddress () { export function openTorrentAddress () {
log('openTorrentAddress') log('openTorrentAddress')
windows.main.dispatch('openTorrentAddress') windows.main.dispatch('openTorrentAddress')
} }
@@ -114,7 +107,7 @@ function resetTitle () {
*/ */
function showOpenSeed (opts) { function showOpenSeed (opts) {
setTitle(opts.title) setTitle(opts.title)
const selectedPaths = electron.dialog.showOpenDialogSync(windows.main.win, opts) const selectedPaths = dialog.showOpenDialogSync(windows.main.win, opts)
resetTitle() resetTitle()
if (!Array.isArray(selectedPaths)) return if (!Array.isArray(selectedPaths)) return
windows.main.dispatch('showCreateTorrent', selectedPaths) windows.main.dispatch('showCreateTorrent', selectedPaths)

View File

@@ -1,13 +1,8 @@
module.exports = { import electron from '../../electron.cjs'
downloadFinished, import * as dialog from './dialog.js'
init, import log from './log.js'
setBadge
}
const { app, Menu } = require('electron') const { app, Menu } = electron
const dialog = require('./dialog')
const log = require('./log')
/** /**
* Add a right-click menu to the dock icon. (Mac) * Add a right-click menu to the dock icon. (Mac)
@@ -57,3 +52,5 @@ function getMenuTemplate () {
} }
] ]
} }
export default { downloadFinished, init, setBadge }

View File

@@ -1,27 +1,19 @@
module.exports = { import cp from 'child_process'
spawn, import path from 'path'
kill, import vlcCommand from 'vlc-command'
checkInstall import log from './log.js'
} import * as windows from './windows'
const cp = require('child_process')
const path = require('path')
const vlcCommand = require('vlc-command')
const log = require('./log')
const windows = require('./windows')
// holds a ChildProcess while we're playing a video in an external player, null otherwise // holds a ChildProcess while we're playing a video in an external player, null otherwise
let proc = null let proc = null
function checkInstall (playerPath, cb) { export function checkInstall (playerPath, cb) {
// check for VLC if external player has not been specified by the user // check for VLC if external player has not been specified by the user
// otherwise assume the player is installed // otherwise assume the player is installed
if (!playerPath) return vlcCommand(cb) if (!playerPath) return vlcCommand(cb)
process.nextTick(() => cb(null)) process.nextTick(() => cb(null))
} }
function spawn (playerPath, url, title) { export function spawn (playerPath, url, title) {
if (playerPath) return spawnExternal(playerPath, [url]) if (playerPath) return spawnExternal(playerPath, [url])
// Try to find and use VLC if external player is not specified // Try to find and use VLC if external player is not specified
@@ -37,7 +29,7 @@ function spawn (playerPath, url, title) {
}) })
} }
function kill () { export function kill () {
if (!proc) return if (!proc) return
log(`Killing external player, pid ${proc.pid}`) log(`Killing external player, pid ${proc.pid}`)
proc.kill('SIGKILL') // kill -9 proc.kill('SIGKILL') // kill -9

View File

@@ -1,7 +1,7 @@
const chokidar = require('chokidar') import * as chokidar from 'chokidar'
const log = require('./log') import log from './log.js'
class FolderWatcher { export class FolderWatcher {
constructor ({ window, state }) { constructor ({ window, state }) {
this.window = window this.window = window
this.state = state this.state = state
@@ -46,5 +46,3 @@ class FolderWatcher {
this.watching = false this.watching = false
} }
} }
module.exports = FolderWatcher

View File

@@ -1,12 +1,7 @@
module.exports = { import config from '../config.js'
install, import path from 'path'
uninstall
}
const config = require('../config') export function install () {
const path = require('path')
function install () {
switch (process.platform) { switch (process.platform) {
case 'darwin': installDarwin() case 'darwin': installDarwin()
break break
@@ -15,7 +10,7 @@ function install () {
} }
} }
function uninstall () { export function uninstall () {
switch (process.platform) { switch (process.platform) {
case 'darwin': uninstallDarwin() case 'darwin': uninstallDarwin()
break break
@@ -24,10 +19,8 @@ function uninstall () {
} }
} }
function installDarwin () { async function installDarwin () {
const electron = require('electron') const { app } = await import('electron')
const app = electron.app
// On Mac, only protocols that are listed in `Info.plist` can be set as the // On Mac, only protocols that are listed in `Info.plist` can be set as the
// default handler at runtime. // default handler at runtime.
app.setAsDefaultProtocolClient('magnet') app.setAsDefaultProtocolClient('magnet')
@@ -44,33 +37,14 @@ if (!config.IS_PRODUCTION) {
EXEC_COMMAND.push(config.ROOT_PATH) EXEC_COMMAND.push(config.ROOT_PATH)
} }
function installWin32 () { async function installWin32 () {
const Registry = require('winreg') const Registry = await import('wingreg')
const log = await import('./log')
const log = require('./log') const iconPath = path.join(process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico')
registerProtocolHandlerWin32('magnet', 'URL:BitTorrent Magnet URL', iconPath, EXEC_COMMAND)
const iconPath = path.join( registerProtocolHandlerWin32('stream-magnet', 'URL:BitTorrent Stream-Magnet URL', iconPath, EXEC_COMMAND)
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico' registerFileHandlerWin32('.torrent', 'io.webtorrent.torrent', 'BitTorrent Document', iconPath, EXEC_COMMAND)
)
registerProtocolHandlerWin32(
'magnet',
'URL:BitTorrent Magnet URL',
iconPath,
EXEC_COMMAND
)
registerProtocolHandlerWin32(
'stream-magnet',
'URL:BitTorrent Stream-Magnet URL',
iconPath,
EXEC_COMMAND
)
registerFileHandlerWin32(
'.torrent',
'io.webtorrent.torrent',
'BitTorrent Document',
iconPath,
EXEC_COMMAND
)
/** /**
* To add a protocol handler, the following keys must be added to the Windows registry: * To add a protocol handler, the following keys must be added to the Windows registry:
@@ -198,9 +172,8 @@ function installWin32 () {
} }
} }
function uninstallWin32 () { async function uninstallWin32 () {
const Registry = require('winreg') const Registry = await import('winreg')
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND) unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND) unregisterFileHandlerWin32('.torrent', 'io.webtorrent.torrent', EXEC_COMMAND)

View File

@@ -1,22 +1,25 @@
/* eslint-disable import/first */
console.time('init') console.time('init')
const electron = require('electron') import * as RemoteMain from '@electron/remote/main/index.js'
const app = electron.app RemoteMain.initialize()
import electron from '../../electron.cjs'
const { app, ipcMain } = electron
// Start crash reporter early, so it takes effect for child processes // Start crash reporter early, so it takes effect for child processes
const crashReporter = require('../crash-reporter') import crashReporter from '../crash-reporter.js'
crashReporter.init() crashReporter.init()
const parallel = require('run-parallel') import fs from 'fs'
import parallel from 'run-parallel'
import config from '../config.js'
import ipc from './ipc.js'
import log from './log.js'
import menu from './menu.js'
import State from '../renderer/lib/state.js'
import * as windows from './windows/index.js'
const config = require('../config') const WEBTORRENT_VERSION = JSON.parse(fs.readFileSync('node_modules/webtorrent/package.json').toString()).version
const ipc = require('./ipc')
const log = require('./log')
const menu = require('./menu')
const State = require('../renderer/lib/state')
const windows = require('./windows')
const WEBTORRENT_VERSION = require('webtorrent/package.json').version
let shouldQuit = false let shouldQuit = false
let argv = sliceArgv(process.argv) let argv = sliceArgv(process.argv)
@@ -54,27 +57,30 @@ if (!shouldQuit && !config.IS_PORTABLE) {
if (shouldQuit) { if (shouldQuit) {
app.quit() app.quit()
} else { } else {
init() init().catch(console.error)
} }
function init () { async function init () {
console.log('index init')
app.whenReady().then(() => {
console.log('readyyyy')
})
app.on('second-instance', (event, commandLine, workingDirectory) => onAppOpen(commandLine)) app.on('second-instance', (event, commandLine, workingDirectory) => onAppOpen(commandLine))
if (config.IS_PORTABLE) { if (config.IS_PORTABLE) {
const path = require('path') console.log('is portable')
const path = await import('path')
// Put all user data into the "Portable Settings" folder // Put all user data into the "Portable Settings" folder
app.setPath('userData', config.CONFIG_PATH) app.setPath('userData', config.CONFIG_PATH)
// Put Electron crash files, etc. into the "Portable Settings\Temp" folder // Put Electron crash files, etc. into the "Portable Settings\Temp" folder
app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp')) app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp'))
} }
const ipcMain = electron.ipcMain
let isReady = false // app ready, windows can be created let isReady = false // app ready, windows can be created
app.ipcReady = false // main window has finished loading and IPC is ready app.ipcReady = false // main window has finished loading and IPC is ready
app.isQuitting = false app.isQuitting = false
parallel({ parallel({
appReady: (cb) => app.on('ready', () => cb(null)), appReady: (cb) => app.whenReady().then(cb),
state: (cb) => State.load(cb) state: (cb) => State.load(cb)
}, onReady) }, onReady)
@@ -84,9 +90,9 @@ function init () {
isReady = true isReady = true
const state = results.state const state = results.state
menu.init()
windows.main.init(state, { hidden }) windows.main.init(state, { hidden })
windows.webtorrent.init() windows.webtorrent.init()
menu.init()
// To keep app startup fast, some code is delayed. // To keep app startup fast, some code is delayed.
setTimeout(() => { setTimeout(() => {
@@ -112,13 +118,13 @@ function init () {
ipc.init() ipc.init()
app.once('ipcReady', function () { app.once('ipcReady', () => {
log('Command line args:', argv) log('Command line args:', argv)
processArgv(argv) processArgv(argv)
console.timeEnd('init') console.timeEnd('init')
}) })
app.on('before-quit', function (e) { app.on('before-quit', e => {
if (app.isQuitting) return if (app.isQuitting) return
app.isQuitting = true app.isQuitting = true
@@ -131,18 +137,19 @@ function init () {
}, 4000) // quit after 4 secs, at most }, 4000) // quit after 4 secs, at most
}) })
app.on('activate', function () { app.on('activate', () => {
console.log('activate')
if (isReady) windows.main.show() if (isReady) windows.main.show()
}) })
} }
function delayedInit (state) { async function delayedInit (state) {
if (app.isQuitting) return if (app.isQuitting) return
const announcement = require('./announcement') const { default: announcement } = await import('./announcement.js')
const dock = require('./dock') const { default: dock } = await import('./dock.js')
const updater = require('./updater') const { default: updater } = await import('./updater.js')
const FolderWatcher = require('./folder-watcher') const { FolderWatcher } = await import('./folder-watcher.js')
const folderWatcher = new FolderWatcher({ window: windows.main, state }) const folderWatcher = new FolderWatcher({ window: windows.main, state })
announcement.init() announcement.init()
@@ -155,13 +162,13 @@ function delayedInit (state) {
} }
if (process.platform === 'win32') { if (process.platform === 'win32') {
const userTasks = require('./user-tasks') const userTasks = await import('./user-tasks.js')
userTasks.init() userTasks.init()
} }
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
const tray = require('./tray') const { init: trayInit } = await import('./tray.js')
tray.init() trayInit()
} }
} }
@@ -198,17 +205,21 @@ function onAppOpen (newArgv) {
// Development: 2 args, eg: electron . // Development: 2 args, eg: electron .
// Test: 4 args, eg: electron -r .../mocks.js . // Test: 4 args, eg: electron -r .../mocks.js .
function sliceArgv (argv) { function sliceArgv (argv) {
return argv.slice(config.IS_PRODUCTION ? 1 return argv.slice(
: config.IS_TEST ? 4 config.IS_PRODUCTION
: 2) ? 1
: config.IS_TEST
? 4
: 2
)
} }
function processArgv (argv) { async function processArgv (argv) {
const torrentIds = [] const torrentIds = []
argv.forEach(function (arg) { await Promise.all(argv.forEach(async arg => {
if (arg === '-n' || arg === '-o' || arg === '-u') { if (arg === '-n' || arg === '-o' || arg === '-u') {
// Critical path: Only load the 'dialog' package if it is needed // Critical path: Only load the 'dialog' package if it is needed
const dialog = require('./dialog') const dialog = await import('./dialog')
if (arg === '-n') { if (arg === '-n') {
dialog.openSeedDirectory() dialog.openSeedDirectory()
} else if (arg === '-o') { } else if (arg === '-o') {
@@ -231,7 +242,7 @@ function processArgv (argv) {
// running. // running.
torrentIds.push(arg) torrentIds.push(arg)
} }
}) }))
if (torrentIds.length > 0) { if (torrentIds.length > 0) {
windows.main.dispatch('onOpen', torrentIds) windows.main.dispatch('onOpen', torrentIds)
} }

View File

@@ -1,15 +1,10 @@
module.exports = { import electron from '../../electron.cjs'
init,
setModule
}
const electron = require('electron') import log from './log.js'
import menu from './menu.js'
import * as windows from './windows/index.js'
const app = electron.app const { app, ipcMain } = electron
const log = require('./log')
const menu = require('./menu')
const windows = require('./windows')
// Messages from the main process, to be sent once the WebTorrent process starts // Messages from the main process, to be sent once the WebTorrent process starts
const messageQueueMainToWebTorrent = [] const messageQueueMainToWebTorrent = []
@@ -23,18 +18,16 @@ function setModule (name, module) {
} }
function init () { function init () {
const ipc = electron.ipcMain ipcMain.once('ipcReady', e => {
ipc.once('ipcReady', function (e) {
app.ipcReady = true app.ipcReady = true
app.emit('ipcReady') app.emit('ipcReady')
}) })
ipc.once('ipcReadyWebTorrent', function (e) { ipcMain.once('ipcReadyWebTorrent', e => {
app.ipcReadyWebTorrent = true app.ipcReadyWebTorrent = true
log('sending %d queued messages from the main win to the webtorrent window', log('sending %d queued messages from the main win to the webtorrent window',
messageQueueMainToWebTorrent.length) messageQueueMainToWebTorrent.length)
messageQueueMainToWebTorrent.forEach(function (message) { messageQueueMainToWebTorrent.forEach(message => {
windows.webtorrent.send(message.name, ...message.args) windows.webtorrent.send(message.name, ...message.args)
log('webtorrent: sent queued %s', message.name) log('webtorrent: sent queued %s', message.name)
}) })
@@ -44,12 +37,12 @@ function init () {
* Dialog * Dialog
*/ */
ipc.on('openTorrentFile', () => { ipcMain.on('openTorrentFile', async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openTorrentFile() dialog.openTorrentFile()
}) })
ipc.on('openFiles', () => { ipcMain.on('openFiles', async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openFiles() dialog.openFiles()
}) })
@@ -57,12 +50,12 @@ function init () {
* Dock * Dock
*/ */
ipc.on('setBadge', (e, ...args) => { ipcMain.on('setBadge', async (e, ...args) => {
const dock = require('./dock') const dock = await import('./dock')
dock.setBadge(...args) dock.setBadge(...args)
}) })
ipc.on('downloadFinished', (e, ...args) => { ipcMain.on('downloadFinished', async (e, ...args) => {
const dock = require('./dock') const dock = await import('./dock')
dock.downloadFinished(...args) dock.downloadFinished(...args)
}) })
@@ -70,10 +63,10 @@ function init () {
* Player Events * Player Events
*/ */
ipc.on('onPlayerOpen', function () { ipcMain.on('onPlayerOpen', async () => {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = await import('./power-save-blocker')
const shortcuts = require('./shortcuts') const shortcuts = await import('./shortcuts')
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
menu.togglePlaybackControls(true) menu.togglePlaybackControls(true)
powerSaveBlocker.enable() powerSaveBlocker.enable()
@@ -81,17 +74,17 @@ function init () {
thumbar.enable() thumbar.enable()
}) })
ipc.on('onPlayerUpdate', function (e, ...args) { ipcMain.on('onPlayerUpdate', async (e, ...args) => {
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
menu.onPlayerUpdate(...args) menu.onPlayerUpdate(...args)
thumbar.onPlayerUpdate(...args) thumbar.onPlayerUpdate(...args)
}) })
ipc.on('onPlayerClose', function () { ipcMain.on('onPlayerClose', async () => {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = await import('./power-save-blocker')
const shortcuts = require('./shortcuts') const shortcuts = await import('./shortcuts')
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
menu.togglePlaybackControls(false) menu.togglePlaybackControls(false)
powerSaveBlocker.disable() powerSaveBlocker.disable()
@@ -99,17 +92,17 @@ function init () {
thumbar.disable() thumbar.disable()
}) })
ipc.on('onPlayerPlay', function () { ipcMain.on('onPlayerPlay', async () => {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = await import('./power-save-blocker')
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
powerSaveBlocker.enable() powerSaveBlocker.enable()
thumbar.onPlayerPlay() thumbar.onPlayerPlay()
}) })
ipc.on('onPlayerPause', function () { ipcMain.on('onPlayerPause', async () => {
const powerSaveBlocker = require('./power-save-blocker') const powerSaveBlocker = await import('./power-save-blocker')
const thumbar = require('./thumbar') const thumbar = await import('./thumbar')
powerSaveBlocker.disable() powerSaveBlocker.disable()
thumbar.onPlayerPause() thumbar.onPlayerPause()
@@ -119,7 +112,7 @@ function init () {
* Folder Watcher Events * Folder Watcher Events
*/ */
ipc.on('startFolderWatcher', function () { ipcMain.on('startFolderWatcher', () => {
if (!modules.folderWatcher) { if (!modules.folderWatcher) {
log('IPC ERR: folderWatcher module is not defined.') log('IPC ERR: folderWatcher module is not defined.')
return return
@@ -128,7 +121,7 @@ function init () {
modules.folderWatcher.start() modules.folderWatcher.start()
}) })
ipc.on('stopFolderWatcher', function () { ipcMain.on('stopFolderWatcher', () => {
if (!modules.folderWatcher) { if (!modules.folderWatcher) {
log('IPC ERR: folderWatcher module is not defined.') log('IPC ERR: folderWatcher module is not defined.')
return return
@@ -141,16 +134,16 @@ function init () {
* Shell * Shell
*/ */
ipc.on('openPath', (e, ...args) => { ipcMain.on('openPath', async (e, ...args) => {
const shell = require('./shell') const shell = await import('./shell')
shell.openPath(...args) shell.openPath(...args)
}) })
ipc.on('showItemInFolder', (e, ...args) => { ipcMain.on('showItemInFolder', async (e, ...args) => {
const shell = require('./shell') const shell = await import('./shell')
shell.showItemInFolder(...args) shell.showItemInFolder(...args)
}) })
ipc.on('moveItemToTrash', (e, ...args) => { ipcMain.on('moveItemToTrash', async (e, ...args) => {
const shell = require('./shell') const shell = await import('./shell')
shell.moveItemToTrash(...args) shell.moveItemToTrash(...args)
}) })
@@ -158,8 +151,8 @@ function init () {
* File handlers * File handlers
*/ */
ipc.on('setDefaultFileHandler', (e, flag) => { ipcMain.on('setDefaultFileHandler', async (e, flag) => {
const handlers = require('./handlers') const handlers = await import('./handlers')
if (flag) handlers.install() if (flag) handlers.install()
else handlers.uninstall() else handlers.uninstall()
@@ -169,8 +162,8 @@ function init () {
* Auto start on login * Auto start on login
*/ */
ipc.on('setStartup', (e, flag) => { ipcMain.on('setStartup', async (e, flag) => {
const startup = require('./startup') const startup = await import('./startup')
if (flag) startup.install() if (flag) startup.install()
else startup.uninstall() else startup.uninstall()
@@ -182,37 +175,39 @@ function init () {
const main = windows.main const main = windows.main
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args)) ipcMain.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
ipc.on('setBounds', (e, ...args) => main.setBounds(...args)) ipcMain.on('setBounds', (e, ...args) => main.setBounds(...args))
ipc.on('setProgress', (e, ...args) => main.setProgress(...args)) ipcMain.on('setProgress', (e, ...args) => main.setProgress(...args))
ipc.on('setTitle', (e, ...args) => main.setTitle(...args)) ipcMain.on('setTitle', (e, ...args) => main.setTitle(...args))
ipc.on('show', () => main.show()) ipcMain.on('show', () => main.show())
ipc.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args)) ipcMain.on('toggleFullScreen', (e, ...args) => main.toggleFullScreen(...args))
ipc.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args)) ipcMain.on('setAllowNav', (e, ...args) => menu.setAllowNav(...args))
/** /**
* External Media Player * External Media Player
*/ */
ipc.on('checkForExternalPlayer', function (e, path) { ipcMain.on('checkForExternalPlayer', async (e, path) => {
const externalPlayer = require('./external-player') const externalPlayer = await import('./external-player')
externalPlayer.checkInstall(path, function (err) { externalPlayer.checkInstall(path, err => {
windows.main.send('checkForExternalPlayer', !err) windows.main.send('checkForExternalPlayer', !err)
}) })
}) })
ipc.on('openExternalPlayer', (e, ...args) => { ipcMain.on('openExternalPlayer', async (e, ...args) => {
const externalPlayer = require('./external-player') const externalPlayer = await import('./external-player')
const thumbar = require('./thumbar') const shortcuts = await import('./shortcuts')
const thumbar = await import('./thumbar')
menu.togglePlaybackControls(false) menu.togglePlaybackControls(false)
shortcuts.disable()
thumbar.disable() thumbar.disable()
externalPlayer.spawn(...args) externalPlayer.spawn(...args)
}) })
ipc.on('quitExternalPlayer', () => { ipcMain.on('quitExternalPlayer', async () => {
const externalPlayer = require('./external-player') const externalPlayer = await import('./external-player')
externalPlayer.kill() externalPlayer.kill()
}) })
@@ -220,11 +215,12 @@ function init () {
* Message passing * Message passing
*/ */
const oldEmit = ipc.emit const oldEmit = ipcMain.emit
ipc.emit = function (name, e, ...args) { ipcMain.emit = (name, e, ...args) => {
// Relay messages between the main window and the WebTorrent hidden window // Relay messages between the main window and the WebTorrent hidden window
if (name.startsWith('wt-') && !app.isQuitting) { if (name.startsWith('wt-') && !app.isQuitting) {
if (e.sender.browserWindowOptions.title === 'webtorrent-hidden-window') { console.dir(e.sender.getTitle())
if (e.sender.getTitle() === 'WebTorrent Hidden Window') {
// Send message to main window // Send message to main window
windows.main.send(name, ...args) windows.main.send(name, ...args)
log('webtorrent: got %s', name) log('webtorrent: got %s', name)
@@ -244,6 +240,13 @@ function init () {
} }
// Emit all other events normally // Emit all other events normally
oldEmit.call(ipc, name, e, ...args) oldEmit.call(ipcMain, name, e, ...args)
} }
} }
export { init }
export { setModule }
export default {
init,
setModule
}

View File

@@ -1,5 +1,7 @@
module.exports = log import electron from '../../electron.cjs'
module.exports.error = error import * as windows from './windows/index.js'
const { app } = electron
/** /**
* In the main electron process, we do not use console.log() statements because they do * In the main electron process, we do not use console.log() statements because they do
@@ -7,13 +9,7 @@ module.exports.error = error
* version of the app. Instead use this module, which sends the logs to the main window * version of the app. Instead use this module, which sends the logs to the main window
* where they can be viewed in Developer Tools. * where they can be viewed in Developer Tools.
*/ */
export default function log (...args) {
const electron = require('electron')
const windows = require('./windows')
const app = electron.app
function log (...args) {
if (app.ipcReady) { if (app.ipcReady) {
windows.main.send('log', ...args) windows.main.send('log', ...args)
} else { } else {
@@ -21,7 +17,7 @@ function log (...args) {
} }
} }
function error (...args) { export function error (...args) {
if (app.ipcReady) { if (app.ipcReady) {
windows.main.send('error', ...args) windows.main.send('error', ...args)
} else { } else {

View File

@@ -1,23 +1,12 @@
module.exports = { import electron from '../../electron.cjs'
init, import config from '../config.js'
togglePlaybackControls, import * as windows from './windows/index.js'
setWindowFocus, import shell from './shell.js'
setAllowNav,
onPlayerUpdate,
onToggleAlwaysOnTop,
onToggleFullScreen
}
const electron = require('electron')
const app = electron.app
const config = require('../config')
const windows = require('./windows')
let menu = null let menu = null
function init () { function init () {
console.log('menu init')
menu = electron.Menu.buildFromTemplate(getMenuTemplate()) menu = electron.Menu.buildFromTemplate(getMenuTemplate())
electron.Menu.setApplicationMenu(menu) electron.Menu.setApplicationMenu(menu)
} }
@@ -70,12 +59,11 @@ function onToggleFullScreen (flag) {
} }
function getMenuItem (label) { function getMenuItem (label) {
for (let i = 0; i < menu.items.length; i++) { for (const menuItem of menu.items) {
const menuItem = menu.items[i].submenu.items.find(function (item) { const submenuItem = menuItem.submenu.items.find(item => item.label === label)
return item.label === label if (submenuItem) return submenuItem
})
if (menuItem) return menuItem
} }
return {}
} }
function getMenuTemplate () { function getMenuTemplate () {
@@ -88,24 +76,24 @@ function getMenuTemplate () {
? 'Create New Torrent...' ? 'Create New Torrent...'
: 'Create New Torrent from Folder...', : 'Create New Torrent from Folder...',
accelerator: 'CmdOrCtrl+N', accelerator: 'CmdOrCtrl+N',
click: () => { click: async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openSeedDirectory() dialog.openSeedDirectory()
} }
}, },
{ {
label: 'Open Torrent File...', label: 'Open Torrent File...',
accelerator: 'CmdOrCtrl+O', accelerator: 'CmdOrCtrl+O',
click: () => { click: async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openTorrentFile() dialog.openTorrentFile()
} }
}, },
{ {
label: 'Open Torrent Address...', label: 'Open Torrent Address...',
accelerator: 'CmdOrCtrl+U', accelerator: 'CmdOrCtrl+U',
click: () => { click: async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openTorrentAddress() dialog.openTorrentAddress()
} }
}, },
@@ -306,21 +294,18 @@ function getMenuTemplate () {
{ {
label: 'Learn more about ' + config.APP_NAME, label: 'Learn more about ' + config.APP_NAME,
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.HOME_PAGE_URL) shell.openExternal(config.HOME_PAGE_URL)
} }
}, },
{ {
label: 'Release Notes', label: 'Release Notes',
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_RELEASES) shell.openExternal(config.GITHUB_URL_RELEASES)
} }
}, },
{ {
label: 'Contribute on GitHub', label: 'Contribute on GitHub',
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL) shell.openExternal(config.GITHUB_URL)
} }
}, },
@@ -330,21 +315,18 @@ function getMenuTemplate () {
{ {
label: 'Report an Issue...', label: 'Report an Issue...',
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.GITHUB_URL_ISSUES) shell.openExternal(config.GITHUB_URL_ISSUES)
} }
}, },
{ {
label: 'Follow us on Twitter', label: 'Follow us on Twitter',
click: () => { click: () => {
const shell = require('./shell')
shell.openExternal(config.TWITTER_PAGE_URL) shell.openExternal(config.TWITTER_PAGE_URL)
} }
} }
] ]
} }
] ]
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// WebTorrent menu (Mac) // WebTorrent menu (Mac)
template.unshift({ template.unshift({
@@ -429,8 +411,8 @@ function getMenuTemplate () {
// File menu (Windows, Linux) // File menu (Windows, Linux)
template[0].submenu.unshift({ template[0].submenu.unshift({
label: 'Create New Torrent from File...', label: 'Create New Torrent from File...',
click: () => { click: async () => {
const dialog = require('./dialog') const dialog = await import('./dialog')
dialog.openSeedFile() dialog.openSeedFile()
} }
}) })
@@ -463,9 +445,26 @@ function getMenuTemplate () {
// File menu (Linux) // File menu (Linux)
template[0].submenu.push({ template[0].submenu.push({
label: 'Quit', label: 'Quit',
click: () => app.quit() click: () => electron.app.quit()
}) })
} }
return template return template
} }
export { init }
export { togglePlaybackControls }
export { setWindowFocus }
export { setAllowNav }
export { onPlayerUpdate }
export { onToggleAlwaysOnTop }
export { onToggleFullScreen }
export default {
init,
togglePlaybackControls,
setWindowFocus,
setAllowNav,
onPlayerUpdate,
onToggleAlwaysOnTop,
onToggleFullScreen
}

View File

@@ -1,10 +1,5 @@
module.exports = { import { powerSaveBlocker } from 'electron'
enable, import log from './log.js'
disable
}
const electron = require('electron')
const log = require('./log')
let blockId = 0 let blockId = 0
@@ -12,23 +7,23 @@ let blockId = 0
* Block the system from entering low-power (sleep) mode or turning off the * Block the system from entering low-power (sleep) mode or turning off the
* display. * display.
*/ */
function enable () { export function enable () {
if (electron.powerSaveBlocker.isStarted(blockId)) { if (powerSaveBlocker.isStarted(blockId)) {
// If a power saver block already exists, do nothing. // If a power saver block already exists, do nothing.
return return
} }
blockId = electron.powerSaveBlocker.start('prevent-display-sleep') blockId = powerSaveBlocker.start('prevent-display-sleep')
log(`powerSaveBlocker.enable: ${blockId}`) log(`powerSaveBlocker.enable: ${blockId}`)
} }
/** /**
* Stop blocking the system from entering low-power mode. * Stop blocking the system from entering low-power mode.
*/ */
function disable () { export function disable () {
if (!electron.powerSaveBlocker.isStarted(blockId)) { if (!powerSaveBlocker.isStarted(blockId)) {
// If a power saver block does not exist, do nothing. // If a power saver block does not exist, do nothing.
return return
} }
electron.powerSaveBlocker.stop(blockId) powerSaveBlocker.stop(blockId)
log(`powerSaveBlocker.disable: ${blockId}`) log(`powerSaveBlocker.disable: ${blockId}`)
} }

View File

@@ -1,19 +1,14 @@
module.exports = { import electron from '../../electron.cjs'
openExternal, import log from './log.js'
openPath,
showItemInFolder,
moveItemToTrash
}
const electron = require('electron') const { shell } = electron
const log = require('./log')
/** /**
* Open the given external protocol URL in the desktops default manner. * Open the given external protocol URL in the desktops default manner.
*/ */
function openExternal (url) { function openExternal (url) {
log(`openExternal: ${url}`) log(`openExternal: ${url}`)
electron.shell.openExternal(url) shell.openExternal(url)
} }
/** /**
@@ -21,7 +16,7 @@ function openExternal (url) {
*/ */
function openPath (path) { function openPath (path) {
log(`openPath: ${path}`) log(`openPath: ${path}`)
electron.shell.openPath(path) shell.openPath(path)
} }
/** /**
@@ -29,7 +24,7 @@ function openPath (path) {
*/ */
function showItemInFolder (path) { function showItemInFolder (path) {
log(`showItemInFolder: ${path}`) log(`showItemInFolder: ${path}`)
electron.shell.showItemInFolder(path) shell.showItemInFolder(path)
} }
/** /**
@@ -37,5 +32,12 @@ function showItemInFolder (path) {
*/ */
function moveItemToTrash (path) { function moveItemToTrash (path) {
log(`moveItemToTrash: ${path}`) log(`moveItemToTrash: ${path}`)
electron.shell.moveItemToTrash(path) shell.trashItem(path)
}
export default {
openExternal,
openPath,
showItemInFolder,
moveItemToTrash
} }

View File

@@ -1,30 +1,25 @@
module.exports = { import { globalShortcut } from 'electron'
disable, import * as windows from './windows'
enable
}
const electron = require('electron') export function enable () {
const windows = require('./windows')
function enable () {
// Register play/pause media key, available on some keyboards. // Register play/pause media key, available on some keyboards.
electron.globalShortcut.register( globalShortcut.register(
'MediaPlayPause', 'MediaPlayPause',
() => windows.main.dispatch('playPause') () => windows.main.dispatch('playPause')
) )
electron.globalShortcut.register( globalShortcut.register(
'MediaNextTrack', 'MediaNextTrack',
() => windows.main.dispatch('nextTrack') () => windows.main.dispatch('nextTrack')
) )
electron.globalShortcut.register( globalShortcut.register(
'MediaPreviousTrack', 'MediaPreviousTrack',
() => windows.main.dispatch('previousTrack') () => windows.main.dispatch('previousTrack')
) )
} }
function disable () { export function disable () {
// Return the media key to the OS, so other apps can use it. // Return the media key to the OS, so other apps can use it.
electron.globalShortcut.unregister('MediaPlayPause') globalShortcut.unregister('MediaPlayPause')
electron.globalShortcut.unregister('MediaNextTrack') globalShortcut.unregister('MediaNextTrack')
electron.globalShortcut.unregister('MediaPreviousTrack') globalShortcut.unregister('MediaPreviousTrack')
} }

View File

@@ -1,24 +1,16 @@
module.exports = { import { app } from 'electron'
handleEvent import path from 'path'
} import { spawn } from 'child_process'
import handlers from './handlers.js'
const path = require('path')
const spawn = require('child_process').spawn
const electron = require('electron')
const app = electron.app
const handlers = require('./handlers')
const EXE_NAME = path.basename(process.execPath) const EXE_NAME = path.basename(process.execPath)
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe') const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
const run = function (args, done) { const run = (args, done) => {
spawn(UPDATE_EXE, args, { detached: true }) spawn(UPDATE_EXE, args, { detached: true }).on('close', done)
.on('close', done)
} }
function handleEvent (cmd) { export function handleEvent (cmd) {
if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') { if (cmd === '--squirrel-install' || cmd === '--squirrel-updated') {
run([`--createShortcut=${EXE_NAME}`], app.quit) run([`--createShortcut=${EXE_NAME}`], app.quit)
return true return true

View File

@@ -1,17 +1,12 @@
module.exports = { import { APP_NAME } from '../config.js'
install, import AutoLaunch from 'auto-launch'
uninstall
}
const { APP_NAME } = require('../config')
const AutoLaunch = require('auto-launch')
const appLauncher = new AutoLaunch({ const appLauncher = new AutoLaunch({
name: APP_NAME, name: APP_NAME,
isHidden: true isHidden: true
}) })
function install () { export function install () {
return appLauncher return appLauncher
.isEnabled() .isEnabled()
.then(enabled => { .then(enabled => {
@@ -19,7 +14,7 @@ function install () {
}) })
} }
function uninstall () { export function uninstall () {
return appLauncher return appLauncher
.isEnabled() .isEnabled()
.then(enabled => { .then(enabled => {

View File

@@ -1,21 +1,6 @@
module.exports = { import path from 'path'
disable, import config from '../config.js'
enable, import * as windows from './windows'
onPlayerPause,
onPlayerPlay,
onPlayerUpdate
}
/**
* On Windows, add a "thumbnail toolbar" with a play/pause button in the taskbar.
* This provides users a way to access play/pause functionality without restoring
* or activating the window.
*/
const path = require('path')
const config = require('../config')
const windows = require('./windows')
const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png') const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png')
const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png') const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
@@ -32,7 +17,7 @@ let buttons = []
/** /**
* Show the Windows thumbnail toolbar buttons. * Show the Windows thumbnail toolbar buttons.
*/ */
function enable () { export function enable () {
buttons = [ buttons = [
{ {
tooltip: 'Previous Track', tooltip: 'Previous Track',
@@ -56,26 +41,26 @@ function enable () {
/** /**
* Hide the Windows thumbnail toolbar buttons. * Hide the Windows thumbnail toolbar buttons.
*/ */
function disable () { export function disable () {
buttons = [] buttons = []
update() update()
} }
function onPlayerPause () { export function onPlayerPause () {
if (!isEnabled()) return if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Play' buttons[PLAY_PAUSE].tooltip = 'Play'
buttons[PLAY_PAUSE].icon = PLAY_ICON buttons[PLAY_PAUSE].icon = PLAY_ICON
update() update()
} }
function onPlayerPlay () { export function onPlayerPlay () {
if (!isEnabled()) return if (!isEnabled()) return
buttons[PLAY_PAUSE].tooltip = 'Pause' buttons[PLAY_PAUSE].tooltip = 'Pause'
buttons[PLAY_PAUSE].icon = PAUSE_ICON buttons[PLAY_PAUSE].icon = PAUSE_ICON
update() update()
} }
function onPlayerUpdate (state) { export function onPlayerUpdate (state) {
if (!isEnabled()) return if (!isEnabled()) return
buttons[PREV].flags = [state.hasPrevious ? 'enabled' : 'disabled'] buttons[PREV].flags = [state.hasPrevious ? 'enabled' : 'disabled']
buttons[NEXT].flags = [state.hasNext ? 'enabled' : 'disabled'] buttons[NEXT].flags = [state.hasNext ? 'enabled' : 'disabled']

View File

@@ -1,19 +1,10 @@
module.exports = { import { app, Tray, Menu } from 'electron'
hasTray, import config from '../config.js'
init, import * as windows from './windows'
setWindowFocus
}
const electron = require('electron')
const app = electron.app
const config = require('../config')
const windows = require('./windows')
let tray let tray
function init () { export function init () {
if (process.platform === 'linux') { if (process.platform === 'linux') {
initLinux() initLinux()
} }
@@ -26,17 +17,17 @@ function init () {
/** /**
* Returns true if there a tray icon is active. * Returns true if there a tray icon is active.
*/ */
function hasTray () { export function hasTray () {
return !!tray return !!tray
} }
function setWindowFocus (flag) { export function setWindowFocus (flag) {
if (!tray) return if (!tray) return
updateTrayMenu() updateTrayMenu()
} }
function initLinux () { function initLinux () {
checkLinuxTraySupport(function (err) { checkLinuxTraySupport(err => {
if (!err) createTray() if (!err) createTray()
}) })
} }
@@ -46,28 +37,20 @@ function initWin32 () {
} }
/** /**
* Check for libappindicator1 support before creating tray icon * Check for libappindicator support before creating tray icon.
*/ */
function checkLinuxTraySupport (cb) { async function checkLinuxTraySupport (cb) {
const cp = require('child_process') const cp = await import('child_process')
// Check that we're on Ubuntu (or another debian system) and that we have // Check that libappindicator libraries are installed in system.
// libappindicator1. If WebTorrent was installed from the deb file, we should cp.exec('ldconfig -p | grep libappindicator', (err, stdout) => {
// always have it. If it was installed from the zip file, we might not.
cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) {
if (err) return cb(err) if (err) return cb(err)
// Unfortunately there's no cleaner way, as far as I can tell, to check cb(null)
// whether a debian package is installed:
if (stdout.endsWith('\tinstall\n')) {
cb(null)
} else {
cb(new Error('debian package not installed'))
}
}) })
} }
function createTray () { function createTray () {
tray = new electron.Tray(getIconPath()) tray = new Tray(getIconPath())
// On Windows, left click opens the app, right click opens the context menu. // On Windows, left click opens the app, right click opens the context menu.
// On Linux, any click (left or right) opens the context menu. // On Linux, any click (left or right) opens the context menu.
@@ -78,7 +61,7 @@ function createTray () {
} }
function updateTrayMenu () { function updateTrayMenu () {
const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate()) const contextMenu = Menu.buildFromTemplate(getMenuTemplate())
tray.setContextMenu(contextMenu) tray.setContextMenu(contextMenu)
} }

View File

@@ -1,13 +1,10 @@
module.exports = { import electron from '../../electron.cjs'
init import get from 'simple-get'
} import config from '../config.js'
import log from './log.js'
import * as windows from './windows/index.js'
const electron = require('electron') const { autoUpdater } = electron
const get = require('simple-get')
const config = require('../config')
const log = require('./log')
const windows = require('./windows')
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL + const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
'?version=' + config.APP_VERSION + '?version=' + config.APP_VERSION +
@@ -22,6 +19,8 @@ function init () {
} }
} }
export default { init }
// The Electron auto-updater does not support Linux yet, so manually check for // The Electron auto-updater does not support Linux yet, so manually check for
// updates and show the user a modal notification. // updates and show the user a modal notification.
function initLinux () { function initLinux () {
@@ -47,31 +46,31 @@ function onResponse (err, res, data) {
} }
function initDarwinWin32 () { function initDarwinWin32 () {
electron.autoUpdater.on( autoUpdater.on(
'error', 'error',
(err) => log.error(`Update error: ${err.message}`) (err) => log(`Update error: ${err.message}`)
) )
electron.autoUpdater.on( autoUpdater.on(
'checking-for-update', 'checking-for-update',
() => log('Checking for update') () => log('Checking for update')
) )
electron.autoUpdater.on( autoUpdater.on(
'update-available', 'update-available',
() => log('Update available') () => log('Update available')
) )
electron.autoUpdater.on( autoUpdater.on(
'update-not-available', 'update-not-available',
() => log('No update available') () => log('No update available')
) )
electron.autoUpdater.on( autoUpdater.on(
'update-downloaded', 'update-downloaded',
(e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`) (e, notes, name, date, url) => log(`Update downloaded: ${name}: ${url}`)
) )
electron.autoUpdater.setFeedURL({ url: AUTO_UPDATE_URL }) autoUpdater.setFeedURL({ url: AUTO_UPDATE_URL })
electron.autoUpdater.checkForUpdates() autoUpdater.checkForUpdates()
} }

View File

@@ -1,15 +1,9 @@
module.exports = { import { app } from 'electron'
init
}
const electron = require('electron')
const app = electron.app
/** /**
* Add a user task menu to the app icon on right-click. (Windows) * Add a user task menu to the app icon on right-click. (Windows)
*/ */
function init () { export function init () {
if (process.platform !== 'win32') return if (process.platform !== 'win32') return
app.setUserTasks(getUserTasks()) app.setUserTasks(getUserTasks())
} }

View File

@@ -1,11 +1,12 @@
const about = module.exports = { import config from '../../config.js'
import electron from '../../../electron.cjs'
import * as RemoteMain from '@electron/remote/main/index.js'
export const about = {
init, init,
win: null win: null
} }
const config = require('../../config')
const electron = require('electron')
function init () { function init () {
if (about.win) { if (about.win) {
return about.win.show() return about.win.show()
@@ -26,15 +27,17 @@ function init () {
useContentSize: true, useContentSize: true,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks', enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true enableRemoteModule: true,
backgroundThrottling: false
}, },
width: 300 width: 300
}) })
RemoteMain.enable(win.webContents)
win.loadURL(config.WINDOW_ABOUT) win.loadURL(config.WINDOW_ABOUT)
win.once('ready-to-show', function () { win.once('ready-to-show', () => {
win.show() win.show()
// No menu on the About window // No menu on the About window
// Hack: BrowserWindow removeMenu method not working on electron@7 // Hack: BrowserWindow removeMenu method not working on electron@7
@@ -42,7 +45,7 @@ function init () {
win.setMenuBarVisibility(false) win.setMenuBarVisibility(false)
}) })
win.once('closed', function () { win.once('closed', () => {
about.win = null about.win = null
}) })
} }

View File

@@ -1,3 +1,3 @@
exports.about = require('./about') export * from './about.js'
exports.main = require('./main') export * from './main.js'
exports.webtorrent = require('./webtorrent') export * from './webtorrent.js'

View File

@@ -1,4 +1,13 @@
const main = module.exports = { import debounce from 'debounce'
import electron from '../../../electron.cjs'
import config from '../../config.js'
import log from '../log.js'
import menu from '../menu.js'
import * as RemoteMain from '@electron/remote/main/index.js'
const { app, BrowserWindow, screen } = electron
export const main = {
dispatch, dispatch,
hide, hide,
init, init,
@@ -14,15 +23,6 @@ const main = module.exports = {
win: null win: null
} }
const electron = require('electron')
const debounce = require('debounce')
const app = electron.app
const config = require('../../config')
const log = require('../log')
const menu = require('../menu')
function init (state, options) { function init (state, options) {
if (main.win) { if (main.win) {
return main.win.show() return main.win.show()
@@ -30,7 +30,7 @@ function init (state, options) {
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds) const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
const win = main.win = new electron.BrowserWindow({ const win = main.win = new BrowserWindow({
backgroundColor: '#282828', backgroundColor: '#282828',
darkTheme: true, // Forces dark theme (GTK+3) darkTheme: true, // Forces dark theme (GTK+3)
height: initialBounds.height, height: initialBounds.height,
@@ -44,13 +44,16 @@ function init (state, options) {
width: initialBounds.width, width: initialBounds.width,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks', enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true enableRemoteModule: true,
backgroundThrottling: false
}, },
x: initialBounds.x, x: initialBounds.x,
y: initialBounds.y y: initialBounds.y
}) })
RemoteMain.enable(win.webContents)
console.log(config.WINDOW_MAIN)
win.loadURL(config.WINDOW_MAIN) win.loadURL(config.WINDOW_MAIN)
win.once('ready-to-show', () => { win.once('ready-to-show', () => {
@@ -65,7 +68,7 @@ function init (state, options) {
menu.onToggleFullScreen(main.win.isFullScreen()) menu.onToggleFullScreen(main.win.isFullScreen())
}) })
win.webContents.on('will-navigate', (e, url) => { win.webContents.on('will-navigate', (e) => {
// Prevent drag-and-drop from navigating the Electron window, which can happen // Prevent drag-and-drop from navigating the Electron window, which can happen
// before our drag-and-drop handlers have been initialized. // before our drag-and-drop handlers have been initialized.
e.preventDefault() e.preventDefault()
@@ -97,10 +100,10 @@ function init (state, options) {
send('windowBoundsChanged', e.sender.getBounds()) send('windowBoundsChanged', e.sender.getBounds())
}, 1000)) }, 1000))
win.on('close', e => { win.on('close', async e => {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
const tray = require('../tray') const { hasTray } = await import('../tray.js')
if (!tray.hasTray()) { if (!hasTray()) {
app.quit() app.quit()
return return
} }
@@ -151,7 +154,7 @@ function setBounds (bounds, maximize) {
log('setBounds: maximizing') log('setBounds: maximizing')
main.win.maximize() main.win.maximize()
} else if (maximize === false && main.win.isMaximized()) { } else if (maximize === false && main.win.isMaximized()) {
log('setBounds: unmaximizing') log('setBounds: minimizing')
main.win.unmaximize() main.win.unmaximize()
} }
@@ -161,7 +164,7 @@ function setBounds (bounds, maximize) {
log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`) log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`)
if (bounds.x === null && bounds.y === null) { if (bounds.x === null && bounds.y === null) {
// X and Y not specified? By default, center on current screen // X and Y not specified? By default, center on current screen
const scr = electron.screen.getDisplayMatching(main.win.getBounds()) const scr = screen.getDisplayMatching(main.win.getBounds())
bounds.x = Math.round(scr.bounds.x + (scr.bounds.width / 2) - (bounds.width / 2)) bounds.x = Math.round(scr.bounds.x + (scr.bounds.width / 2) - (bounds.width / 2))
bounds.y = Math.round(scr.bounds.y + (scr.bounds.height / 2) - (bounds.height / 2)) bounds.y = Math.round(scr.bounds.y + (scr.bounds.height / 2) - (bounds.height / 2))
log(`setBounds: centered to ${JSON.stringify(bounds)}`) log(`setBounds: centered to ${JSON.stringify(bounds)}`)

View File

@@ -1,4 +1,8 @@
const webtorrent = module.exports = { import electron from '../../../electron.cjs'
import config from '../../config.js'
import * as RemoteMain from '@electron/remote/main/index.js'
export const webtorrent = {
init, init,
send, send,
show, show,
@@ -6,10 +10,6 @@ const webtorrent = module.exports = {
win: null win: null
} }
const electron = require('electron')
const config = require('../../config')
function init () { function init () {
const win = webtorrent.win = new electron.BrowserWindow({ const win = webtorrent.win = new electron.BrowserWindow({
backgroundColor: '#1E1E1E', backgroundColor: '#1E1E1E',
@@ -26,16 +26,17 @@ function init () {
useContentSize: true, useContentSize: true,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks', enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true enableRemoteModule: true,
backgroundThrottling: false
}, },
width: 150 width: 150
}) })
RemoteMain.enable(win.webContents)
win.loadURL(config.WINDOW_WEBTORRENT) win.loadURL(config.WINDOW_WEBTORRENT)
// Prevent killing the WebTorrent process // Prevent killing the WebTorrent process
win.on('close', function (e) { win.on('close', e => {
if (electron.app.isQuitting) { if (electron.app.isQuitting) {
return return
} }

View File

@@ -1,8 +1,7 @@
const React = require('react') import * as React from 'react'
import { dispatcher } from '../lib/dispatcher.js'
const { dispatcher } = require('../lib/dispatcher') export default class CreateTorrentErrorPage extends React.Component {
module.exports = class CreateTorrentErrorPage extends React.Component {
render () { render () {
return ( return (
<div className='create-torrent'> <div className='create-torrent'>

View File

@@ -1,9 +1,8 @@
const React = require('react') import * as React from 'react'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch, dispatcher } from '../lib/dispatcher.js'
const ModalOKCancel = require('./modal-ok-cancel') export default class DeleteAllTorrentsModal extends React.Component {
const { dispatch, dispatcher } = require('../lib/dispatcher')
module.exports = class DeleteAllTorrentsModal extends React.Component {
render () { render () {
const { state: { modal: { deleteData } } } = this.props const { state: { modal: { deleteData } } } = this.props
const message = deleteData const message = deleteData

View File

@@ -1,8 +1,7 @@
const React = require('react') import * as React from 'react'
import { dispatcher } from '../lib/dispatcher.js'
const { dispatcher } = require('../lib/dispatcher') export default class Header extends React.Component {
class Header extends React.Component {
render () { render () {
const loc = this.props.state.location const loc = this.props.state.location
return ( return (
@@ -56,5 +55,3 @@ class Header extends React.Component {
) )
} }
} }
module.exports = Header

View File

@@ -1,9 +1,8 @@
const React = require('react') import * as React from 'react'
const PropTypes = require('prop-types') import PropTypes from 'prop-types'
import * as colors from 'material-ui/styles/colors'
const colors = require('material-ui/styles/colors') export default class Heading extends React.Component {
class Heading extends React.Component {
static get propTypes () { static get propTypes () {
return { return {
level: PropTypes.number level: PropTypes.number
@@ -31,5 +30,3 @@ class Heading extends React.Component {
) )
} }
} }
module.exports = Heading

View File

@@ -1,8 +1,10 @@
const React = require('react') import * as React from 'react'
const FlatButton = require('material-ui/FlatButton').default import flatButton from 'material-ui/FlatButton'
const RaisedButton = require('material-ui/RaisedButton').default import raisedButton from 'material-ui/RaisedButton'
module.exports = class ModalOKCancel extends React.Component { const FlatButton = flatButton.default
const RaisedButton = raisedButton.default
export default class ModalOKCancel extends React.Component {
render () { render () {
const cancelStyle = { marginRight: 10, color: 'black' } const cancelStyle = { marginRight: 10, color: 'black' }
const { cancelText, onCancel, okText, onOK } = this.props const { cancelText, onCancel, okText, onOK } = this.props

View File

@@ -1,12 +1,14 @@
const React = require('react') import * as React from 'react'
const TextField = require('material-ui/TextField').default import textField from 'material-ui/TextField'
const { clipboard } = require('electron') import electron from 'electron'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch, dispatcher } from '../lib/dispatcher.js'
import { isMagnetLink } from '../lib/torrent-player.js'
const ModalOKCancel = require('./modal-ok-cancel') const TextField = textField.default
const { dispatch, dispatcher } = require('../lib/dispatcher') const { clipboard } = electron
const { isMagnetLink } = require('../lib/torrent-player')
module.exports = class OpenTorrentAddressModal extends React.Component { export default class OpenTorrentAddressModal extends React.Component {
render () { render () {
return ( return (
<div className='open-torrent-address-modal'> <div className='open-torrent-address-modal'>

View File

@@ -1,19 +1,17 @@
const path = require('path') import path from 'path'
import * as colors from 'material-ui/styles/colors'
const colors = require('material-ui/styles/colors') import remote from '@electron/remote'
const electron = require('electron') import * as React from 'react'
const React = require('react') import PropTypes from 'prop-types'
const PropTypes = require('prop-types') import raisedButton from 'material-ui/RaisedButton'
import textField from 'material-ui/TextField'
const remote = electron.remote
const RaisedButton = require('material-ui/RaisedButton').default
const TextField = require('material-ui/TextField').default
const RaisedButton = raisedButton.default
const TextField = textField.default
// Lets you pick a file or directory. // Lets you pick a file or directory.
// Uses the system Open File dialog. // Uses the system Open File dialog.
// You can't edit the text field directly. // You can't edit the text field directly.
class PathSelector extends React.Component { export default class PathSelector extends React.Component {
static propTypes () { static propTypes () {
return { return {
className: PropTypes.string, className: PropTypes.string,
@@ -83,5 +81,3 @@ class PathSelector extends React.Component {
) )
} }
} }
module.exports = PathSelector

View File

@@ -1,9 +1,8 @@
const React = require('react') import * as React from 'react'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch, dispatcher } from '../lib/dispatcher.js'
const ModalOKCancel = require('./modal-ok-cancel') export default class RemoveTorrentModal extends React.Component {
const { dispatch, dispatcher } = require('../lib/dispatcher')
module.exports = class RemoveTorrentModal extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
const message = state.modal.deleteData const message = state.modal.deleteData

View File

@@ -1,9 +1,10 @@
const React = require('react') import * as React from 'react'
const PropTypes = require('prop-types') import PropTypes from 'prop-types'
import raisedButton from 'material-ui/RaisedButton'
const RaisedButton = require('material-ui/RaisedButton').default const RaisedButton = raisedButton.default
class ShowMore extends React.Component { export default class ShowMore extends React.Component {
static get propTypes () { static get propTypes () {
return { return {
defaultExpanded: PropTypes.bool, defaultExpanded: PropTypes.bool,
@@ -21,11 +22,9 @@ class ShowMore extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
this.state = { this.state = {
expanded: !!this.props.defaultExpanded expanded: !!this.props.defaultExpanded
} }
this.handleClick = this.handleClick.bind(this) this.handleClick = this.handleClick.bind(this)
} }
@@ -51,5 +50,3 @@ class ShowMore extends React.Component {
) )
} }
} }
module.exports = ShowMore

View File

@@ -1,10 +1,9 @@
const React = require('react') import * as React from 'react'
const electron = require('electron') import { shell } from 'electron'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatcher } from '../lib/dispatcher.js'
const ModalOKCancel = require('./modal-ok-cancel') export default class UnsupportedMediaModal extends React.Component {
const { dispatcher } = require('../lib/dispatcher')
module.exports = class UnsupportedMediaModal extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
const err = state.modal.error const err = state.modal.error
@@ -36,7 +35,7 @@ module.exports = class UnsupportedMediaModal extends React.Component {
} }
onInstall () { onInstall () {
electron.shell.openExternal('http://www.videolan.org/vlc/') shell.openExternal('https://www.videolan.org/vlc/')
// TODO: dcposch send a dispatch rather than modifying state directly // TODO: dcposch send a dispatch rather than modifying state directly
const state = this.props.state const state = this.props.state

View File

@@ -1,10 +1,9 @@
const React = require('react') import * as React from 'react'
const electron = require('electron') import { shell } from 'electron'
import ModalOKCancel from './modal-ok-cancel.js'
import { dispatch } from '../lib/dispatcher.js'
const ModalOKCancel = require('./modal-ok-cancel') export default class UpdateAvailableModal extends React.Component {
const { dispatch } = require('../lib/dispatcher')
module.exports = class UpdateAvailableModal extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
return ( return (
@@ -25,7 +24,7 @@ module.exports = class UpdateAvailableModal extends React.Component {
function handleShow () { function handleShow () {
// TODO: use the GitHub urls from config.js // TODO: use the GitHub urls from config.js
electron.shell.openExternal('https://github.com/webtorrent/webtorrent-desktop/releases') shell.openExternal('https://github.com/webtorrent/webtorrent-desktop/releases')
dispatch('exitModal') dispatch('exitModal')
} }

View File

@@ -1,6 +1,6 @@
const { dispatch } = require('../lib/dispatcher') import { dispatch } from '../lib/dispatcher.js'
module.exports = class AudioTracksController { export default class AudioTracksController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }

View File

@@ -1,6 +1,6 @@
const { ipcRenderer } = require('electron') import { ipcRenderer } from 'electron'
module.exports = class FolderWatcherController { export default class FolderWatcherController {
start () { start () {
console.log('-- IPC: start folder watcher') console.log('-- IPC: start folder watcher')
ipcRenderer.send('startFolderWatcher') ipcRenderer.send('startFolderWatcher')

View File

@@ -1,10 +1,8 @@
const { ipcRenderer } = require('electron') import { ipcRenderer } from 'electron'
const telemetry = require('../lib/telemetry') import telemetry from '../lib/telemetry.js'
const Playlist = require('../lib/playlist') import Playlist from '../lib/playlist.js'
// Controls local play back: the <video>/<audio> tag and VLC export default class MediaController {
// Does not control remote casting (Chromecast etc)
module.exports = class MediaController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }
@@ -23,7 +21,7 @@ module.exports = class MediaController {
telemetry.logPlayAttempt('error') telemetry.logPlayAttempt('error')
state.playing.location = 'error' state.playing.location = 'error'
ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath) ipcRenderer.send('checkForExternalPlayer', state.saved.prefs.externalPlayerPath)
ipcRenderer.once('checkForExternalPlayer', function (e, isInstalled) { ipcRenderer.once('checkForExternalPlayer', (e, isInstalled) => {
state.modal = { state.modal = {
id: 'unsupported-media-modal', id: 'unsupported-media-modal',
error, error,
@@ -56,7 +54,7 @@ module.exports = class MediaController {
const state = this.state const state = this.state
state.playing.location = 'external' state.playing.location = 'external'
const onServerRunning = function () { const onServerRunning = () => {
state.playing.isReady = true state.playing.isReady = true
telemetry.logPlayAttempt('external') telemetry.logPlayAttempt('external')

View File

@@ -1,21 +1,29 @@
const electron = require('electron') import { ipcRenderer } from 'electron'
const path = require('path') import path from 'path'
import Cast from '../lib/cast.js'
const Cast = require('../lib/cast') import { dispatch } from '../lib/dispatcher.js'
const { dispatch } = require('../lib/dispatcher') import telemetry from '../lib/telemetry.js'
const telemetry = require('../lib/telemetry') import { UnplayableFileError, UnplayableTorrentError } from '../lib/errors.js'
const { UnplayableFileError, UnplayableTorrentError } = require('../lib/errors') import sound from '../lib/sound.js'
const sound = require('../lib/sound') import TorrentPlayer from '../lib/torrent-player.js'
const TorrentPlayer = require('../lib/torrent-player') import TorrentSummary from '../lib/torrent-summary.js'
const TorrentSummary = require('../lib/torrent-summary') import Playlist from '../lib/playlist.js'
const Playlist = require('../lib/playlist') import State from '../lib/state.js'
const State = require('../lib/state') // Checks whether we are connected and already casting
// Returns false if we not casting (state.playing.location === 'local')
const ipcRenderer = electron.ipcRenderer // or if we're trying to connect but haven't yet ('chromecast-pending', etc)
function isCasting (state) {
// Controls playback of torrents and files within torrents return state.playing.location === 'chromecast' ||
// both local (<video>,<audio>,external player) and remote (cast) state.playing.location === 'airplay' ||
module.exports = class PlaybackController { state.playing.location === 'dlna'
}
function restoreBounds (state) {
ipcRenderer.send('setAspectRatio', 0)
if (state.window.bounds) {
ipcRenderer.send('setBounds', state.window.bounds, false)
}
}
export default class PlaybackController {
constructor (state, config, update) { constructor (state, config, update) {
this.state = state this.state = state
this.config = config this.config = config
@@ -28,12 +36,10 @@ module.exports = class PlaybackController {
// * If no file index is provided, restore the most recently viewed file or autoplay the first // * If no file index is provided, restore the most recently viewed file or autoplay the first
playFile (infoHash, index /* optional */) { playFile (infoHash, index /* optional */) {
this.pauseActiveTorrents(infoHash) this.pauseActiveTorrents(infoHash)
const state = this.state const state = this.state
if (state.location.url() === 'player') { if (state.location.url() === 'player') {
this.updatePlayer(infoHash, index, false, (err) => { this.updatePlayer(infoHash, index, false, (err) => {
if (err) dispatch('error', err) if (err) { dispatch('error', err) } else { this.play() }
else this.play()
}) })
} else { } else {
let initialized = false let initialized = false
@@ -41,13 +47,10 @@ module.exports = class PlaybackController {
url: 'player', url: 'player',
setup: (cb) => { setup: (cb) => {
const torrentSummary = TorrentSummary.getByKey(state, infoHash) const torrentSummary = TorrentSummary.getByKey(state, infoHash)
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable) if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
if (index === undefined) return cb(new UnplayableTorrentError()) if (index === undefined) return cb(new UnplayableTorrentError())
initialized = true initialized = true
this.openPlayer(infoHash, index, (err) => { this.openPlayer(infoHash, index, (err) => {
if (!err) this.play() if (!err) this.play()
cb(err) cb(err)
@@ -55,7 +58,7 @@ module.exports = class PlaybackController {
}, },
destroy: () => this.closePlayer() destroy: () => this.closePlayer()
}, (err) => { }, (err) => {
if (err) dispatch('error', err) if (err) { dispatch('error', err) }
}) })
} }
} }
@@ -63,9 +66,7 @@ module.exports = class PlaybackController {
// Open a file in OS default app. // Open a file in OS default app.
openPath (infoHash, index) { openPath (infoHash, index) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const filePath = path.join( const filePath = path.join(torrentSummary.path, torrentSummary.files[index].path)
torrentSummary.path,
torrentSummary.files[index].path)
ipcRenderer.send('openPath', filePath) ipcRenderer.send('openPath', filePath)
} }
@@ -73,27 +74,21 @@ module.exports = class PlaybackController {
playPause () { playPause () {
const state = this.state const state = this.state
if (state.location.url() !== 'player') return if (state.location.url() !== 'player') return
// force rerendering if window is hidden, // force rerendering if window is hidden,
// in order to bypass `raf` and play/pause media immediately // in order to bypass `raf` and play/pause media immediately
const mediaTag = document.querySelector('video,audio') const mediaTag = document.querySelector('video,audio')
if (!state.window.isVisible && mediaTag) { if (!state.window.isVisible && mediaTag) {
if (state.playing.isPaused) mediaTag.play() if (state.playing.isPaused) { mediaTag.play() } else { mediaTag.pause() }
else mediaTag.pause()
} }
if (state.playing.isPaused) { this.play() } else { this.pause() }
if (state.playing.isPaused) this.play()
else this.pause()
} }
pauseActiveTorrents (infoHash) { pauseActiveTorrents (infoHash) {
// Playback Priority: pause all active torrents if needed. // Playback Priority: pause all active torrents if needed.
if (!this.state.saved.prefs.highestPlaybackPriority) return if (!this.state.saved.prefs.highestPlaybackPriority) return
// Do not pause active torrents if playing a fully downloaded torrent. // Do not pause active torrents if playing a fully downloaded torrent.
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
if (torrentSummary.status === 'seeding') return if (torrentSummary.status === 'seeding') return
dispatch('prioritizeTorrent', infoHash) dispatch('prioritizeTorrent', infoHash)
} }
@@ -101,11 +96,9 @@ module.exports = class PlaybackController {
nextTrack () { nextTrack () {
const state = this.state const state = this.state
if (Playlist.hasNext(state) && state.playing.location !== 'external') { if (Playlist.hasNext(state) && state.playing.location !== 'external') {
this.updatePlayer( this.updatePlayer(state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => { if (err) { dispatch('error', err) } else { this.play() }
if (err) dispatch('error', err) })
else this.play()
})
} }
} }
@@ -113,11 +106,9 @@ module.exports = class PlaybackController {
previousTrack () { previousTrack () {
const state = this.state const state = this.state
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') { if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
this.updatePlayer( this.updatePlayer(state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => { if (err) { dispatch('error', err) } else { this.play() }
if (err) dispatch('error', err) })
else this.play()
})
} }
} }
@@ -154,8 +145,21 @@ module.exports = class PlaybackController {
console.error('Tried to skip to a non-finite time ' + time) console.error('Tried to skip to a non-finite time ' + time)
return console.trace() return console.trace()
} }
if (isCasting(this.state)) Cast.seek(time) if (isCasting(this.state)) { Cast.seek(time) } else { this.state.playing.jumpToTime = time }
else this.state.playing.jumpToTime = time }
// Show video preview
preview (x) {
if (!Number.isFinite(x)) {
console.error('Tried to preview a non-finite position ' + x)
return console.trace()
}
this.state.playing.previewXCoord = x
}
// Hide video preview
clearPreview () {
this.state.playing.previewXCoord = null
} }
// Change playback speed. 1 = faster, -1 = slower // Change playback speed. 1 = faster, -1 = slower
@@ -193,7 +197,6 @@ module.exports = class PlaybackController {
setVolume (volume) { setVolume (volume) {
// check if its in [0.0 - 1.0] range // check if its in [0.0 - 1.0] range
volume = Math.max(0, Math.min(1, volume)) volume = Math.max(0, Math.min(1, volume))
const state = this.state const state = this.state
if (isCasting(state)) { if (isCasting(state)) {
Cast.setVolume(volume) Cast.setVolume(volume)
@@ -210,7 +213,6 @@ module.exports = class PlaybackController {
showOrHidePlayerControls () { showOrHidePlayerControls () {
const state = this.state const state = this.state
const hideControls = state.shouldHidePlayerControls() const hideControls = state.shouldHidePlayerControls()
if (hideControls !== state.playing.hideControls) { if (hideControls !== state.playing.hideControls) {
state.playing.hideControls = hideControls state.playing.hideControls = hideControls
return true return true
@@ -222,13 +224,10 @@ module.exports = class PlaybackController {
openPlayer (infoHash, index, cb) { openPlayer (infoHash, index, cb) {
const state = this.state const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash) const torrentSummary = TorrentSummary.getByKey(state, infoHash)
state.playing.infoHash = torrentSummary.infoHash state.playing.infoHash = torrentSummary.infoHash
state.playing.isReady = false state.playing.isReady = false
// update UI to show pending playback // update UI to show pending playback
sound.play('PLAY') sound.play('PLAY')
this.startServer(torrentSummary) this.startServer(torrentSummary)
ipcRenderer.send('onPlayerOpen') ipcRenderer.send('onPlayerOpen')
this.updatePlayer(infoHash, index, true, cb) this.updatePlayer(infoHash, index, true, cb)
@@ -237,15 +236,12 @@ module.exports = class PlaybackController {
// Starts WebTorrent server for media streaming // Starts WebTorrent server for media streaming
startServer (torrentSummary) { startServer (torrentSummary) {
const state = this.state const state = this.state
if (torrentSummary.status === 'paused') { if (torrentSummary.status === 'paused') {
dispatch('startTorrentingSummary', torrentSummary.torrentKey) dispatch('startTorrentingSummary', torrentSummary.torrentKey)
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, () => onTorrentReady())
() => onTorrentReady())
} else { } else {
onTorrentReady() onTorrentReady()
} }
function onTorrentReady () { function onTorrentReady () {
ipcRenderer.send('wt-start-server', torrentSummary.infoHash) ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
ipcRenderer.once('wt-server-running', () => { state.playing.isReady = true }) ipcRenderer.once('wt-server-running', () => { state.playing.isReady = true })
@@ -255,26 +251,23 @@ module.exports = class PlaybackController {
// Called each time the current file changes // Called each time the current file changes
updatePlayer (infoHash, index, resume, cb) { updatePlayer (infoHash, index, resume, cb) {
const state = this.state const state = this.state
const torrentSummary = TorrentSummary.getByKey(state, infoHash) const torrentSummary = TorrentSummary.getByKey(state, infoHash)
const fileSummary = torrentSummary.files[index] const fileSummary = torrentSummary.files[index]
if (!TorrentPlayer.isPlayable(fileSummary)) { if (!TorrentPlayer.isPlayable(fileSummary)) {
torrentSummary.mostRecentFileIndex = undefined torrentSummary.mostRecentFileIndex = undefined
return cb(new UnplayableFileError()) return cb(new UnplayableFileError())
} }
torrentSummary.mostRecentFileIndex = index torrentSummary.mostRecentFileIndex = index
// update state // update state
state.playing.infoHash = infoHash state.playing.infoHash = infoHash
state.playing.fileIndex = index state.playing.fileIndex = index
state.playing.fileName = fileSummary.name state.playing.fileName = fileSummary.name
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video' state.playing.type = TorrentPlayer.isVideo(fileSummary)
: TorrentPlayer.isAudio(fileSummary) ? 'audio' ? 'video'
: TorrentPlayer.isAudio(fileSummary)
? 'audio'
: 'other' : 'other'
// pick up where we left off
// pick up where we left off
let jumpToTime = 0 let jumpToTime = 0
if (resume && fileSummary.currentTime) { if (resume && fileSummary.currentTime) {
const fraction = fileSummary.currentTime / fileSummary.duration const fraction = fileSummary.currentTime / fileSummary.duration
@@ -284,30 +277,24 @@ module.exports = class PlaybackController {
} }
} }
state.playing.jumpToTime = jumpToTime state.playing.jumpToTime = jumpToTime
// if it's audio, parse out the metadata (artist, title, etc) // if it's audio, parse out the metadata (artist, title, etc)
if (torrentSummary.status === 'paused') { if (torrentSummary.status === 'paused') {
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata) ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata)
} else { } else {
getAudioMetadata() getAudioMetadata()
} }
function getAudioMetadata () { function getAudioMetadata () {
if (state.playing.type === 'audio') { if (state.playing.type === 'audio') {
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index) ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
} }
} }
// if it's video, check for subtitles files that are done downloading // if it's video, check for subtitles files that are done downloading
dispatch('checkForSubtitles') dispatch('checkForSubtitles')
// enable previously selected subtitle track // enable previously selected subtitle track
if (fileSummary.selectedSubtitle) { if (fileSummary.selectedSubtitle) {
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true) dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
} }
state.window.title = fileSummary.name state.window.title = fileSummary.name
// play in VLC if set as default player (Preferences / Playback / Play in VLC) // play in VLC if set as default player (Preferences / Playback / Play in VLC)
if (this.state.saved.prefs.openExternalPlayer) { if (this.state.saved.prefs.openExternalPlayer) {
dispatch('openExternalPlayer') dispatch('openExternalPlayer')
@@ -315,10 +302,8 @@ module.exports = class PlaybackController {
cb() cb()
return return
} }
// otherwise, play the video // otherwise, play the video
this.update() this.update()
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state)) ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
cb() cb()
} }
@@ -349,33 +334,13 @@ module.exports = class PlaybackController {
dispatch('toggleFullScreen', false) dispatch('toggleFullScreen', false)
} }
restoreBounds(state) restoreBounds(state)
// Tell the WebTorrent process to kill the torrent-to-HTTP server // Tell the WebTorrent process to kill the torrent-to-HTTP server
ipcRenderer.send('wt-stop-server') ipcRenderer.send('wt-stop-server')
ipcRenderer.send('onPlayerClose') ipcRenderer.send('onPlayerClose')
// Playback Priority: resume previously paused downloads. // Playback Priority: resume previously paused downloads.
if (this.state.saved.prefs.highestPlaybackPriority) { if (this.state.saved.prefs.highestPlaybackPriority) {
dispatch('resumePausedTorrents') dispatch('resumePausedTorrents')
} }
this.update() this.update()
} }
} }
// Checks whether we are connected and already casting
// Returns false if we not casting (state.playing.location === 'local')
// or if we're trying to connect but haven't yet ('chromecast-pending', etc)
function isCasting (state) {
return state.playing.location === 'chromecast' ||
state.playing.location === 'airplay' ||
state.playing.location === 'dlna'
}
function restoreBounds (state) {
ipcRenderer.send('setAspectRatio', 0)
if (state.window.bounds) {
ipcRenderer.send('setBounds', state.window.bounds, false)
}
}

View File

@@ -1,8 +1,7 @@
const { dispatch } = require('../lib/dispatcher') import { dispatch } from '../lib/dispatcher.js'
const ipcRenderer = require('electron').ipcRenderer import { ipcRenderer } from 'electron'
// Controls the Preferences screen export default class PrefsController {
module.exports = class PrefsController {
constructor (state, config) { constructor (state, config) {
this.state = state this.state = state
this.config = config this.config = config
@@ -13,7 +12,7 @@ module.exports = class PrefsController {
const state = this.state const state = this.state
state.location.go({ state.location.go({
url: 'preferences', url: 'preferences',
setup: function (cb) { setup (cb) {
// initialize preferences // initialize preferences
state.window.title = 'Preferences' state.window.title = 'Preferences'
ipcRenderer.send('setAllowNav', false) ipcRenderer.send('setAllowNav', false)

View File

@@ -1,13 +1,10 @@
const electron = require('electron') import remote from '@electron/remote'
const fs = require('fs') import fs from 'fs'
const path = require('path') import path from 'path'
const parallel = require('run-parallel') import parallel from 'run-parallel'
import { dispatch } from '../lib/dispatcher.js'
const remote = electron.remote export default class SubtitlesController {
const { dispatch } = require('../lib/dispatcher')
module.exports = class SubtitlesController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }
@@ -39,12 +36,11 @@ module.exports = class SubtitlesController {
// Read the files concurrently, then add all resulting subtitle tracks // Read the files concurrently, then add all resulting subtitle tracks
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb)) const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
parallel(tasks, function (err, tracks) { parallel(tasks, (err, tracks) => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
for (let i = 0; i < tracks.length; i++) { // No dupes allowed
// No dupes allowed tracks.forEach((track, i) => {
const track = tracks[i]
let trackIndex = subtitles.tracks.findIndex((t) => let trackIndex = subtitles.tracks.findIndex((t) =>
track.filePath === t.filePath) track.filePath === t.filePath)
@@ -57,7 +53,7 @@ module.exports = class SubtitlesController {
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) { if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
subtitles.selectedIndex = trackIndex subtitles.selectedIndex = trackIndex
} }
} })
// Finally, make sure no two tracks have the same label // Finally, make sure no two tracks have the same label
relabelSubtitles(subtitles) relabelSubtitles(subtitles)
@@ -85,18 +81,18 @@ module.exports = class SubtitlesController {
} }
} }
function loadSubtitle (file, cb) { async function loadSubtitle (file, cb) {
// Lazy load to keep startup fast // Lazy load to keep startup fast
const concat = require('simple-concat') const concat = await import('simple-concat')
const LanguageDetect = require('languagedetect') const LanguageDetect = await import('languagedetect')
const srtToVtt = require('srt-to-vtt') const srtToVtt = await import('srt-to-vtt')
// Read the .SRT or .VTT file, parse it, add subtitle track // Read the .SRT or .VTT file, parse it, add subtitle track
const filePath = file.path || file const filePath = file.path || file
const vttStream = fs.createReadStream(filePath).pipe(srtToVtt()) const vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
concat(vttStream, function (err, buf) { concat(vttStream, (err, buf) => {
if (err) return dispatch('error', 'Can\'t parse subtitles file.') if (err) return dispatch('error', 'Can\'t parse subtitles file.')
// Detect what language the subtitles are in // Detect what language the subtitles are in
@@ -118,8 +114,8 @@ function loadSubtitle (file, cb) {
// Checks whether a language name like 'English' or 'German' matches the system // Checks whether a language name like 'English' or 'German' matches the system
// language, aka the current locale // language, aka the current locale
function isSystemLanguage (language) { async function isSystemLanguage (language) {
const iso639 = require('iso-639-1') const iso639 = await import('iso-639-1')
const osLangISO = window.navigator.language.split('-')[0] // eg 'en' const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
const langIso = iso639.getCode(language) // eg 'de' if language is 'German' const langIso = iso639.getCode(language) // eg 'de' if language is 'German'
return langIso === osLangISO return langIso === osLangISO
@@ -129,7 +125,7 @@ function isSystemLanguage (language) {
// Labels each track by language, eg 'German', 'English', 'English 2', ... // Labels each track by language, eg 'German', 'English', 'English 2', ...
function relabelSubtitles (subtitles) { function relabelSubtitles (subtitles) {
const counts = {} const counts = {}
subtitles.tracks.forEach(function (track) { subtitles.tracks.forEach(track => {
const lang = track.language const lang = track.language
counts[lang] = (counts[lang] || 0) + 1 counts[lang] = (counts[lang] || 0) + 1
track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang track.label = counts[lang] > 1 ? (lang + ' ' + counts[lang]) : lang

View File

@@ -1,11 +1,10 @@
const path = require('path') import path from 'path'
const ipcRenderer = require('electron').ipcRenderer import { ipcRenderer } from 'electron'
import TorrentSummary from '../lib/torrent-summary.js'
import sound from '../lib/sound.js'
import { dispatch } from '../lib/dispatcher.js'
const TorrentSummary = require('../lib/torrent-summary') export default class TorrentController {
const sound = require('../lib/sound')
const { dispatch } = require('../lib/dispatcher')
module.exports = class TorrentController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }
@@ -177,7 +176,7 @@ function showDoneNotification (torrent) {
silent: true silent: true
}) })
notif.onclick = function () { notif.onclick = () => {
ipcRenderer.send('show') ipcRenderer.send('show')
} }

View File

@@ -1,18 +1,18 @@
const fs = require('fs') import fs from 'fs'
const path = require('path') import path from 'path'
const electron = require('electron') import electron from 'electron'
import remote from '@electron/remote'
const { dispatch } = require('../lib/dispatcher') import { dispatch } from '../lib/dispatcher.js'
const { TorrentKeyNotFoundError } = require('../lib/errors') import { TorrentKeyNotFoundError } from '../lib/errors.js'
const sound = require('../lib/sound') import sound from '../lib/sound.js'
const TorrentSummary = require('../lib/torrent-summary') import TorrentSummary from '../lib/torrent-summary.js'
const ipcRenderer = electron.ipcRenderer const { ipcRenderer, clipboard } = electron
const instantIoRegex = /^(https:\/\/)?instant\.io\/#/ const instantIoRegex = /^(https:\/\/)?instant\.io\/#/
// Controls the torrent list: creating, adding, deleting, & manipulating torrents export default class TorrentListController {
module.exports = class TorrentListController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }
@@ -95,7 +95,7 @@ module.exports = class TorrentListController {
if (!fileOrFolder) return start() if (!fileOrFolder) return start()
// Existing torrent: check that the path is still there // Existing torrent: check that the path is still there
fs.stat(fileOrFolder, function (err) { fs.stat(fileOrFolder, err => {
if (err) { if (err) {
s.error = 'path-missing' s.error = 'path-missing'
dispatch('backToList') dispatch('backToList')
@@ -157,10 +157,8 @@ module.exports = class TorrentListController {
prioritizeTorrent (infoHash) { prioritizeTorrent (infoHash) {
this.state.saved.torrents this.state.saved.torrents
.filter((torrent) => { // We're interested in active torrents only. .filter(torrent => ['downloading', 'seeding'].includes(torrent.status)) // Active torrents only.
return (['downloading', 'seeding'].indexOf(torrent.status) !== -1) .forEach((torrent) => { // Pause all active torrents except the one that started playing.
})
.map((torrent) => { // Pause all active torrents except the one that started playing.
if (infoHash === torrent.infoHash) return if (infoHash === torrent.infoHash) return
// Pause torrent without playing sounds. // Pause torrent without playing sounds.
@@ -175,7 +173,7 @@ module.exports = class TorrentListController {
resumePausedTorrents () { resumePausedTorrents () {
console.log('Playback Priority: resuming paused torrents') console.log('Playback Priority: resuming paused torrents')
if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return
this.state.saved.torrentsToResume.map((infoHash) => { this.state.saved.torrentsToResume.forEach((infoHash) => {
this.toggleTorrent(infoHash) this.toggleTorrent(infoHash)
}) })
@@ -229,6 +227,11 @@ module.exports = class TorrentListController {
} }
deleteAllTorrents (deleteData) { deleteAllTorrents (deleteData) {
// Go back to list before the current playing torrent is deleted
if (this.state.location.url() === 'player') {
dispatch('backToList')
}
this.state.saved.torrents.forEach((summary) => deleteTorrentFile(summary, deleteData)) this.state.saved.torrents.forEach((summary) => deleteTorrentFile(summary, deleteData))
this.state.saved.torrents = [] this.state.saved.torrents = []
@@ -249,59 +252,59 @@ module.exports = class TorrentListController {
openTorrentContextMenu (infoHash) { openTorrentContextMenu (infoHash) {
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash) const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
const menu = new electron.remote.Menu() const menu = new remote.Menu()
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
label: 'Remove From List', label: 'Remove From List',
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false) click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, false)
})) }))
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
label: 'Remove Data File', label: 'Remove Data File',
click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true) click: () => dispatch('confirmDeleteTorrent', torrentSummary.infoHash, true)
})) }))
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
type: 'separator' type: 'separator'
})) }))
if (torrentSummary.files) { if (torrentSummary.files) {
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder', label: process.platform === 'darwin' ? 'Show in Finder' : 'Show in Folder',
click: () => showItemInFolder(torrentSummary) click: () => showItemInFolder(torrentSummary)
})) }))
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
type: 'separator' type: 'separator'
})) }))
} }
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
label: 'Copy Magnet Link to Clipboard', label: 'Copy Magnet Link to Clipboard',
click: () => electron.clipboard.writeText(torrentSummary.magnetURI) click: () => clipboard.writeText(torrentSummary.magnetURI)
})) }))
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
label: 'Copy Instant.io Link to Clipboard', label: 'Copy Instant.io Link to Clipboard',
click: () => electron.clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`) click: () => clipboard.writeText(`https://instant.io/#${torrentSummary.infoHash}`)
})) }))
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
label: 'Save Torrent File As...', label: 'Save Torrent File As...',
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey), click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey),
enabled: torrentSummary.torrentFileName != null enabled: torrentSummary.torrentFileName != null
})) }))
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
type: 'separator' type: 'separator'
})) }))
const sortedByName = this.state.saved.prefs.sortByName const sortedByName = this.state.saved.prefs.sortByName
menu.append(new electron.remote.MenuItem({ menu.append(new remote.MenuItem({
label: `${sortedByName ? '✓ ' : ''}Sort by Name`, label: `${sortedByName ? '✓ ' : ''}Sort by Name`,
click: () => dispatch('updatePreferences', 'sortByName', !sortedByName) click: () => dispatch('updatePreferences', 'sortByName', !sortedByName)
})) }))
menu.popup({ window: electron.remote.getCurrentWindow() }) menu.popup({ window: remote.getCurrentWindow() })
} }
// Takes a torrentSummary or torrentKey // Takes a torrentSummary or torrentKey
@@ -311,7 +314,7 @@ module.exports = class TorrentListController {
if (!torrentSummary) throw new TorrentKeyNotFoundError(torrentKey) if (!torrentSummary) throw new TorrentKeyNotFoundError(torrentKey)
const downloadPath = this.state.saved.prefs.downloadPath const downloadPath = this.state.saved.prefs.downloadPath
const newFileName = path.parse(torrentSummary.name).name + '.torrent' const newFileName = path.parse(torrentSummary.name).name + '.torrent'
const win = electron.remote.getCurrentWindow() const win = remote.getCurrentWindow()
const opts = { const opts = {
title: 'Save Torrent File', title: 'Save Torrent File',
defaultPath: path.join(downloadPath, newFileName), defaultPath: path.join(downloadPath, newFileName),
@@ -322,14 +325,14 @@ module.exports = class TorrentListController {
buttonLabel: 'Save' buttonLabel: 'Save'
} }
const savePath = electron.remote.dialog.showSaveDialogSync(win, opts) const savePath = remote.dialog.showSaveDialogSync(win, opts)
if (!savePath) return // They clicked Cancel if (!savePath) return // They clicked Cancel
console.log('Saving torrent ' + torrentKey + ' to ' + savePath) console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary) const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
fs.readFile(torrentPath, function (err, torrentFile) { fs.readFile(torrentPath, (err, torrentFile) => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
fs.writeFile(savePath, torrentFile, function (err) { fs.writeFile(savePath, torrentFile, err => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
}) })
}) })
@@ -342,8 +345,8 @@ function findFilesRecursive (paths, cb_) {
if (paths.length > 1) { if (paths.length > 1) {
let numComplete = 0 let numComplete = 0
const ret = [] const ret = []
paths.forEach(function (path) { paths.forEach(path => {
findFilesRecursive([path], function (fileObjs) { findFilesRecursive([path], fileObjs => {
ret.push(...fileObjs) ret.push(...fileObjs)
if (++numComplete === paths.length) { if (++numComplete === paths.length) {
ret.sort((a, b) => a.path < b.path ? -1 : Number(a.path > b.path)) ret.sort((a, b) => a.path < b.path ? -1 : Number(a.path > b.path))
@@ -355,7 +358,7 @@ function findFilesRecursive (paths, cb_) {
} }
const fileOrFolder = paths[0] const fileOrFolder = paths[0]
fs.stat(fileOrFolder, function (err, stat) { fs.stat(fileOrFolder, (err, stat) => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
// Files: return name, path, and size // Files: return name, path, and size
@@ -370,7 +373,7 @@ function findFilesRecursive (paths, cb_) {
// Folders: recurse, make a list of all the files // Folders: recurse, make a list of all the files
const folderPath = fileOrFolder const folderPath = fileOrFolder
fs.readdir(folderPath, function (err, fileNames) { fs.readdir(folderPath, (err, fileNames) => {
if (err) return dispatch('error', err) if (err) return dispatch('error', err)
const paths = fileNames.map((fileName) => path.join(folderPath, fileName)) const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
findFilesRecursive(paths, cb_) findFilesRecursive(paths, cb_)
@@ -380,7 +383,7 @@ function findFilesRecursive (paths, cb_) {
function deleteFile (path) { function deleteFile (path) {
if (!path) return if (!path) return
fs.unlink(path, function (err) { fs.unlink(path, err => {
if (err) dispatch('error', err) if (err) dispatch('error', err)
}) })
} }

View File

@@ -1,7 +1,6 @@
const { dispatch } = require('../lib/dispatcher') import { dispatch } from '../lib/dispatcher.js'
// Controls the UI checking for new versions of the app, prompting install export default class UpdateController {
module.exports = class UpdateController {
constructor (state) { constructor (state) {
this.state = state this.state = state
} }

View File

@@ -1,40 +1,18 @@
// The Cast module talks to Airplay and Chromecast import http from 'http'
// * Modifies state when things change import config from '../src/config.js'
// * Starts and stops casting, provides remote video controls import { CastingError } from './errors.js'
module.exports = {
init,
toggleMenu,
selectDevice,
stop,
play,
pause,
seek,
setVolume,
setRate
}
const http = require('http')
const config = require('../../config')
const { CastingError } = require('./errors')
// Lazy load these for a ~300ms improvement in startup time // Lazy load these for a ~300ms improvement in startup time
let airplayer, chromecasts, dlnacasts let airplayer, chromecasts, dlnacasts
// App state. Cast modifies state.playing and state.errors in response to events // App state. Cast modifies state.playing and state.errors in response to events
let state let state
// Callback to notify module users when state has changed // Callback to notify module users when state has changed
let update let update
// setInterval() for updating cast status // setInterval() for updating cast status
let statusInterval = null let statusInterval = null
// Start looking for cast devices on the local network // Start looking for cast devices on the local network
function init (appState, callback) { function init (appState, callback) {
state = appState state = appState
update = callback update = callback
// Don't actually cast during integration tests // Don't actually cast during integration tests
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.) // (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
if (config.IS_TEST) { if (config.IS_TEST) {
@@ -43,32 +21,26 @@ function init (appState, callback) {
state.devices.dlna = testPlayer('dlna') state.devices.dlna = testPlayer('dlna')
return return
} }
// Load modules, scan the network for devices // Load modules, scan the network for devices
airplayer = require('airplayer')() airplayer = require('airplayer')()
chromecasts = require('chromecasts')() chromecasts = require('chromecasts')()
dlnacasts = require('dlnacasts')() dlnacasts = require('dlnacasts')()
state.devices.chromecast = chromecastPlayer() state.devices.chromecast = chromecastPlayer()
state.devices.dlna = dlnaPlayer() state.devices.dlna = dlnaPlayer()
state.devices.airplay = airplayPlayer() state.devices.airplay = airplayPlayer()
// Listen for devices: Chromecast, DLNA and Airplay // Listen for devices: Chromecast, DLNA and Airplay
chromecasts.on('update', function (device) { chromecasts.on('update', device => {
// TODO: how do we tell if there are *no longer* any Chromecasts available? // TODO: how do we tell if there are *no longer* any Chromecasts available?
// From looking at the code, chromecasts.players only grows, never shrinks // From looking at the code, chromecasts.players only grows, never shrinks
state.devices.chromecast.addDevice(device) state.devices.chromecast.addDevice(device)
}) })
dlnacasts.on('update', device => {
dlnacasts.on('update', function (device) {
state.devices.dlna.addDevice(device) state.devices.dlna.addDevice(device)
}) })
airplayer.on('update', device => {
airplayer.on('update', function (device) {
state.devices.airplay.addDevice(device) state.devices.airplay.addDevice(device)
}) })
} }
// integration test player implementation // integration test player implementation
function testPlayer (type) { function testPlayer (type) {
return { return {
@@ -81,20 +53,17 @@ function testPlayer (type) {
seek, seek,
volume volume
} }
function getDevices () { function getDevices () {
return [{ name: type + '-1' }, { name: type + '-2' }] return [{ name: type + '-1' }, { name: type + '-2' }]
} }
function open () { }
function open () {} function play () { }
function play () {} function pause () { }
function pause () {} function stop () { }
function stop () {} function status () { }
function status () {} function seek () { }
function seek () {} function volume () { }
function volume () {}
} }
// chromecast player implementation // chromecast player implementation
function chromecastPlayer () { function chromecastPlayer () {
const ret = { const ret = {
@@ -110,13 +79,11 @@ function chromecastPlayer () {
volume volume
} }
return ret return ret
function getDevices () { function getDevices () {
return chromecasts.players return chromecasts.players
} }
function addDevice (device) { function addDevice (device) {
device.on('error', function (err) { device.on('error', err => {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -125,13 +92,12 @@ function chromecastPlayer () {
}) })
update() update()
}) })
device.on('disconnect', function () { device.on('disconnect', () => {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
}) })
} }
function serveSubtitles (callback) { function serveSubtitles (callback) {
const subtitles = state.playing.subtitles const subtitles = state.playing.subtitles
const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex] const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex]
@@ -145,23 +111,22 @@ function chromecastPlayer () {
'Transfer-Encoding': 'chunked' 'Transfer-Encoding': 'chunked'
}) })
res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64')) res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64'))
}).listen(0, function () { }).listen(0, () => {
const port = ret.subServer.address().port const port = ret.subServer.address().port
const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/' const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/'
callback(subtitlesUrl) callback(subtitlesUrl)
}) })
} }
} }
function open () { function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
serveSubtitles(function (subtitlesUrl) { serveSubtitles(subtitlesUrl => {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name,
subtitles: subtitlesUrl ? [subtitlesUrl] : [], subtitles: subtitlesUrl ? [subtitlesUrl] : [],
autoSubtitles: !!subtitlesUrl autoSubtitles: !!subtitlesUrl
}, function (err) { }, err => {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -175,35 +140,28 @@ function chromecastPlayer () {
}) })
}) })
} }
function play (callback) { function play (callback) {
ret.device.play(null, null, callback) ret.device.play(null, null, callback)
} }
function pause (callback) { function pause (callback) {
ret.device.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
ret.device.stop(callback) ret.device.stop(callback)
if (ret.subServer) { if (ret.subServer) {
ret.subServer.close() ret.subServer.close()
} }
} }
function status () { function status () {
ret.device.status(handleStatus) ret.device.status(handleStatus)
} }
function seek (time, callback) { function seek (time, callback) {
ret.device.seek(time, callback) ret.device.seek(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
ret.device.volume(volume, callback) ret.device.volume(volume, callback)
} }
} }
// airplay player implementation // airplay player implementation
function airplayPlayer () { function airplayPlayer () {
const ret = { const ret = {
@@ -219,9 +177,8 @@ function airplayPlayer () {
volume volume
} }
return ret return ret
function addDevice (player) { function addDevice (player) {
player.on('event', function (event) { player.on('event', event => {
switch (event.state) { switch (event.state) {
case 'loading': case 'loading':
break break
@@ -237,13 +194,11 @@ function airplayPlayer () {
update() update()
}) })
} }
function getDevices () { function getDevices () {
return airplayer.players return airplayer.players
} }
function open () { function open () {
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, (err, res) => {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -256,21 +211,17 @@ function airplayPlayer () {
update() update()
}) })
} }
function play (callback) { function play (callback) {
ret.device.resume(callback) ret.device.resume(callback)
} }
function pause (callback) { function pause (callback) {
ret.device.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
ret.device.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
ret.device.playbackInfo(function (err, res, status) { ret.device.playbackInfo((err, res, status) => {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -284,18 +235,15 @@ function airplayPlayer () {
} }
}) })
} }
function seek (time, callback) { function seek (time, callback) {
ret.device.scrub(time, callback) ret.device.scrub(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
// AirPlay doesn't support volume // AirPlay doesn't support volume
// TODO: We should just disable the volume slider // TODO: We should just disable the volume slider
state.playing.volume = volume state.playing.volume = volume
} }
} }
// DLNA player implementation // DLNA player implementation
function dlnaPlayer (player) { function dlnaPlayer (player) {
const ret = { const ret = {
@@ -311,13 +259,11 @@ function dlnaPlayer (player) {
volume volume
} }
return ret return ret
function getDevices () { function getDevices () {
return dlnacasts.players return dlnacasts.players
} }
function addDevice (device) { function addDevice (device) {
device.on('error', function (err) { device.on('error', err => {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -326,20 +272,19 @@ function dlnaPlayer (player) {
}) })
update() update()
}) })
device.on('disconnect', function () { device.on('disconnect', () => {
if (device !== ret.device) return if (device !== ret.device) return
state.playing.location = 'local' state.playing.location = 'local'
update() update()
}) })
} }
function open () { function open () {
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash) const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, { ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
type: 'video/mp4', type: 'video/mp4',
title: config.APP_NAME + ' - ' + torrentSummary.name, title: config.APP_NAME + ' - ' + torrentSummary.name,
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0 seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
}, function (err) { }, err => {
if (err) { if (err) {
state.playing.location = 'local' state.playing.location = 'local'
state.errors.push({ state.errors.push({
@@ -352,56 +297,45 @@ function dlnaPlayer (player) {
update() update()
}) })
} }
function play (callback) { function play (callback) {
ret.device.play(null, null, callback) ret.device.play(null, null, callback)
} }
function pause (callback) { function pause (callback) {
ret.device.pause(callback) ret.device.pause(callback)
} }
function stop (callback) { function stop (callback) {
ret.device.stop(callback) ret.device.stop(callback)
} }
function status () { function status () {
ret.device.status(handleStatus) ret.device.status(handleStatus)
} }
function seek (time, callback) { function seek (time, callback) {
ret.device.seek(time, callback) ret.device.seek(time, callback)
} }
function volume (volume, callback) { function volume (volume, callback) {
ret.device.volume(volume, function (err) { ret.device.volume(volume, err => {
// quick volume update // quick volume update
state.playing.volume = volume state.playing.volume = volume
callback(err) callback(err)
}) })
} }
} }
function handleStatus (err, status) { function handleStatus (err, status) {
if (err || !status) { if (err || !status) {
return console.log('error getting %s status: %o', return console.log('error getting %s status: %o', state.playing.location, err || 'missing response')
state.playing.location,
err || 'missing response')
} }
state.playing.isPaused = status.playerState === 'PAUSED' state.playing.isPaused = status.playerState === 'PAUSED'
state.playing.currentTime = status.currentTime state.playing.currentTime = status.currentTime
state.playing.volume = status.volume.muted ? 0 : status.volume.level state.playing.volume = status.volume.muted ? 0 : status.volume.level
update() update()
} }
// Start polling cast device state, whenever we're connected // Start polling cast device state, whenever we're connected
function startStatusInterval () { function startStatusInterval () {
statusInterval = setInterval(function () { statusInterval = setInterval(() => {
const player = getPlayer() const player = getPlayer()
if (player) player.status() if (player) player.status()
}, 1000) }, 1000)
} }
/* /*
* Shows the device menu for a given cast type ('chromecast', 'airplay', etc) * Shows the device menu for a given cast type ('chromecast', 'airplay', etc)
* The menu lists eg. all Chromecasts detected; the user can click one to cast. * The menu lists eg. all Chromecasts detected; the user can click one to cast.
@@ -413,48 +347,38 @@ function toggleMenu (location) {
state.devices.castMenu = null state.devices.castMenu = null
return return
} }
// Never cast to two devices at the same time // Never cast to two devices at the same time
if (state.playing.location !== 'local') { if (state.playing.location !== 'local') {
throw new CastingError( throw new CastingError(`You can't connect to ${location} when already connected to another device`)
`You can't connect to ${location} when already connected to another device`
)
} }
// Find all cast devices of the given type // Find all cast devices of the given type
const player = getPlayer(location) const player = getPlayer(location)
const devices = player ? player.getDevices() : [] const devices = player ? player.getDevices() : []
if (devices.length === 0) { if (devices.length === 0) {
throw new CastingError(`No ${location} devices available`) throw new CastingError(`No ${location} devices available`)
} }
// Show a menu // Show a menu
state.devices.castMenu = { location, devices } state.devices.castMenu = { location, devices }
} }
function selectDevice (index) { function selectDevice (index) {
const { location, devices } = state.devices.castMenu const { location, devices } = state.devices.castMenu
// Start casting // Start casting
const player = getPlayer(location) const player = getPlayer(location)
player.device = devices[index] player.device = devices[index]
player.open() player.open()
// Poll the casting device's status every few seconds // Poll the casting device's status every few seconds
startStatusInterval() startStatusInterval()
// Show the Connecting... screen // Show the Connecting... screen
state.devices.castMenu = null state.devices.castMenu = null
state.playing.castName = devices[index].name state.playing.castName = devices[index].name
state.playing.location = location + '-pending' state.playing.location = location + '-pending'
update() update()
} }
// Stops casting, move video back to local screen // Stops casting, move video back to local screen
function stop () { function stop () {
const player = getPlayer() const player = getPlayer()
if (player) { if (player) {
player.stop(function () { player.stop(() => {
player.device = null player.device = null
stoppedCasting() stoppedCasting()
}) })
@@ -463,7 +387,6 @@ function stop () {
stoppedCasting() stoppedCasting()
} }
} }
function stoppedCasting () { function stoppedCasting () {
state.playing.location = 'local' state.playing.location = 'local'
state.playing.jumpToTime = Number.isFinite(state.playing.currentTime) state.playing.jumpToTime = Number.isFinite(state.playing.currentTime)
@@ -471,7 +394,6 @@ function stoppedCasting () {
: 0 : 0
update() update()
} }
function getPlayer (location) { function getPlayer (location) {
if (location) { if (location) {
return state.devices[location] return state.devices[location]
@@ -485,17 +407,14 @@ function getPlayer (location) {
return null return null
} }
} }
function play () { function play () {
const player = getPlayer() const player = getPlayer()
if (player) player.play(castCallback) if (player) player.play(castCallback)
} }
function pause () { function pause () {
const player = getPlayer() const player = getPlayer()
if (player) player.pause(castCallback) if (player) player.pause(castCallback)
} }
function setRate (rate) { function setRate (rate) {
let player let player
let result = true let result = true
@@ -511,17 +430,34 @@ function setRate (rate) {
} }
return result return result
} }
function seek (time) { function seek (time) {
const player = getPlayer() const player = getPlayer()
if (player) player.seek(time, castCallback) if (player) { player.seek(time, castCallback) }
} }
function setVolume (volume) { function setVolume (volume) {
const player = getPlayer() const player = getPlayer()
if (player) player.volume(volume, castCallback) if (player) { player.volume(volume, castCallback) }
} }
function castCallback (...args) {
function castCallback () { console.log('%s callback: %o', state.playing.location, args)
console.log('%s callback: %o', state.playing.location, arguments) }
export { init }
export { toggleMenu }
export { selectDevice }
export { stop }
export { play }
export { pause }
export { seek }
export { setVolume }
export { setRate }
export default {
init,
toggleMenu,
selectDevice,
stop,
play,
pause,
seek,
setVolume,
setRate
} }

View File

@@ -1,20 +1,11 @@
module.exports = {
dispatch,
dispatcher,
setDispatch
}
const dispatchers = {} const dispatchers = {}
let _dispatch = function () {} let _dispatch = () => { }
function setDispatch (dispatch) { function setDispatch (dispatch) {
_dispatch = dispatch _dispatch = dispatch
} }
function dispatch (...args) { function dispatch (...args) {
_dispatch(...args) _dispatch(...args)
} }
// Most DOM event handlers are trivial functions like `() => dispatch(<args>)`. // Most DOM event handlers are trivial functions like `() => dispatch(<args>)`.
// For these, `dispatcher(<args>)` is preferred because it memoizes the handler // For these, `dispatcher(<args>)` is preferred because it memoizes the handler
// function. This prevents React from updating the listener functions on // function. This prevents React from updating the listener functions on
@@ -23,17 +14,23 @@ function dispatcher (...args) {
const str = JSON.stringify(args) const str = JSON.stringify(args)
let handler = dispatchers[str] let handler = dispatchers[str]
if (!handler) { if (!handler) {
handler = dispatchers[str] = function (e) { handler = dispatchers[str] = e => {
// Do not propagate click to elements below the button // Do not propagate click to elements below the button
e.stopPropagation() e.stopPropagation()
if (e.currentTarget.classList.contains('disabled')) { if (e.currentTarget.classList.contains('disabled')) {
// Ignore clicks on disabled elements // Ignore clicks on disabled elements
return return
} }
dispatch(...args) dispatch(...args)
} }
} }
return handler return handler
} }
export { dispatch }
export { dispatcher }
export { setDispatch }
export default {
dispatch,
dispatcher,
setDispatch
}

View File

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

View File

@@ -2,11 +2,12 @@ const mediaExtensions = {
audio: [ audio: [
'.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a', '.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a',
'.m2a', '.m4a', '.mpc', '.m4b', '.mka', '.mp2', '.mp3', '.mpc', '.oga', '.m2a', '.m4a', '.mpc', '.m4b', '.mka', '.mp2', '.mp3', '.mpc', '.oga',
'.ogg', '.opus', '.spx', '.wma', '.wav', '.wv', '.wvp'], '.ogg', '.opus', '.spx', '.wma', '.wav', '.wv', '.wvp'
],
video: [ video: [
'.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg', '.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg',
'.ogv', '.webm', '.wmv', '.m2ts'], '.ogv', '.webm', '.wmv', '.m2ts'
],
image: ['.gif', '.jpg', '.jpeg', '.png'] image: ['.gif', '.jpg', '.jpeg', '.png']
} }
export default mediaExtensions
module.exports = mediaExtensions

View File

@@ -1,14 +1,8 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import fs, { copyFileSync } from 'fs'
module.exports = { import path from 'path'
run import semver from 'semver'
} import config from '../../config.js'
const fs = require('fs')
const path = require('path')
const semver = require('semver')
const config = require('../../config')
// Change `state.saved` (which will be saved back to config.json on exit) as // 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 // needed, for example to deal with config.json format changes across versions
@@ -31,15 +25,25 @@ function run (state) {
if (semver.lt(version, '0.21.0')) migrate_0_21_0(saved) if (semver.lt(version, '0.21.0')) migrate_0_21_0(saved)
if (semver.lt(version, '0.22.0')) migrate_0_22_0(saved) if (semver.lt(version, '0.22.0')) migrate_0_22_0(saved)
if (semver.lt(version, config.APP_VERSION)) {
installHandlers(state.saved)
}
// Config is now on the new version // Config is now on the new version
state.saved.version = config.APP_VERSION state.saved.version = config.APP_VERSION
} }
function migrate_0_7_0 (saved) { // Whenever the app is updated, re-install default handlers if the user has
const { copyFileSync } = require('fs') // enabled them.
const path = require('path') async function installHandlers (saved) {
if (saved.prefs.isFileHandler) {
const { ipcRenderer } = await import('electron')
ipcRenderer.send('setDefaultFileHandler', true)
}
}
saved.torrents.forEach(function (ts) { function migrate_0_7_0 (saved) {
saved.torrents.forEach(ts => {
const infoHash = ts.infoHash const infoHash = ts.infoHash
// Replace torrentPath with torrentFileName // Replace torrentPath with torrentFileName
@@ -78,7 +82,6 @@ function migrate_0_7_0 (saved) {
delete ts.posterURL delete ts.posterURL
ts.posterFileName = infoHash + extension ts.posterFileName = infoHash + extension
} }
// Fix exception caused by incorrect file ordering. // Fix exception caused by incorrect file ordering.
// https://github.com/webtorrent/webtorrent-desktop/pull/604#issuecomment-222805214 // https://github.com/webtorrent/webtorrent-desktop/pull/604#issuecomment-222805214
delete ts.defaultPlayFileIndex delete ts.defaultPlayFileIndex
@@ -103,8 +106,8 @@ function migrate_0_11_0 (saved) {
} }
} }
function migrate_0_12_0 (saved) { async function migrate_0_12_0 (saved) {
const TorrentSummary = require('./torrent-summary') const TorrentSummary = await import('./torrent-summary')
if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) { if (saved.prefs.openExternalPlayer == null && saved.prefs.playInVlc != null) {
saved.prefs.openExternalPlayer = saved.prefs.playInVlc saved.prefs.openExternalPlayer = saved.prefs.playInVlc
@@ -121,7 +124,7 @@ function migrate_0_12_0 (saved) {
'02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent', '02767050e0be2fd4db9a2ad6c12416ac806ed6ed.torrent',
'3ba219a8634bf7bae3d848192b2da75ae995589d.torrent' '3ba219a8634bf7bae3d848192b2da75ae995589d.torrent'
] ]
saved.torrents.forEach(function (torrentSummary) { saved.torrents.forEach(torrentSummary => {
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary) const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
if (!fileOrFolder) return if (!fileOrFolder) return
@@ -135,16 +138,16 @@ function migrate_0_12_0 (saved) {
} }
function migrate_0_14_0 (saved) { function migrate_0_14_0 (saved) {
saved.torrents.forEach(function (ts) { saved.torrents.forEach(ts => {
delete ts.defaultPlayFileIndex delete ts.defaultPlayFileIndex
}) })
} }
function migrate_0_17_0 (saved) { function migrate_0_17_0 (saved) {
// Fix a sad, sad bug that resulted in 100MB+ config.json files // Fix a sad, sad bug that resulted in 100MB+ config.json files
saved.torrents.forEach(function (ts) { saved.torrents.forEach(ts => {
if (!ts.files) return if (!ts.files) return
ts.files.forEach(function (file) { ts.files.forEach(file => {
if (!file.audioInfo || !file.audioInfo.picture) return if (!file.audioInfo || !file.audioInfo.picture) return
// This contained a Buffer, which 30x'd in size when serialized to JSON // This contained a Buffer, which 30x'd in size when serialized to JSON
delete file.audioInfo.picture delete file.audioInfo.picture
@@ -156,19 +159,13 @@ function migrate_0_17_2 (saved) {
// Remove the trailing dot (.) from the Wired CD torrent name, since // Remove the trailing dot (.) from the Wired CD torrent name, since
// folders/files that end in a trailing dot (.) or space are not deletable from // folders/files that end in a trailing dot (.) or space are not deletable from
// Windows Explorer. See: https://github.com/webtorrent/webtorrent-desktop/issues/905 // Windows Explorer. See: https://github.com/webtorrent/webtorrent-desktop/issues/905
const { copyFileSync } = require('fs')
const rimraf = require('rimraf')
const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.' const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.'
const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share' const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share'
const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d' const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d'
const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3' const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3'
const ts = saved.torrents.find((ts) => { const ts = saved.torrents.find(ts => ts.infoHash === OLD_HASH)
return ts.infoHash === OLD_HASH
})
if (!ts) return // Wired CD torrent does not exist if (!ts) return // Wired CD torrent does not exist
@@ -191,7 +188,7 @@ function migrate_0_17_2 (saved) {
} catch (err) {} } catch (err) {}
ts.posterFileName = NEW_HASH + '.jpg' ts.posterFileName = NEW_HASH + '.jpg'
rimraf.sync(path.join(config.TORRENT_PATH, ts.torrentFileName)) fs.rmdirSync(path.join(config.TORRENT_PATH, ts.torrentFileName))
copyFileSync( copyFileSync(
path.join(config.STATIC_PATH, 'wiredCd.torrent'), path.join(config.STATIC_PATH, 'wiredCd.torrent'),
path.join(config.TORRENT_PATH, NEW_HASH + '.torrent') path.join(config.TORRENT_PATH, NEW_HASH + '.torrent')
@@ -221,3 +218,7 @@ function migrate_0_22_0 (saved) {
saved.prefs.externalPlayerPath = '' saved.prefs.externalPlayerPath = ''
} }
} }
export default {
run
}

View File

@@ -1,52 +1,36 @@
module.exports = { import TorrentSummary from './torrent-summary.js'
hasNext, import TorrentPlayer from './torrent-player.js'
getNextIndex,
hasPrevious,
getPreviousIndex,
getCurrentLocalURL
}
const TorrentSummary = require('./torrent-summary')
const TorrentPlayer = require('./torrent-player')
const cache = { const cache = {
infoHash: null, infoHash: null,
previousIndex: null, previousIndex: null,
currentIndex: null, currentIndex: null,
nextIndex: null nextIndex: null
} }
function hasNext (state) { function hasNext (state) {
updateCache(state) updateCache(state)
return cache.nextIndex !== null return cache.nextIndex !== null
} }
function getNextIndex (state) { function getNextIndex (state) {
updateCache(state) updateCache(state)
return cache.nextIndex return cache.nextIndex
} }
function hasPrevious (state) { function hasPrevious (state) {
updateCache(state) updateCache(state)
return cache.previousIndex !== null return cache.previousIndex !== null
} }
function getPreviousIndex (state) { function getPreviousIndex (state) {
updateCache(state) updateCache(state)
return cache.previousIndex return cache.previousIndex
} }
function getCurrentLocalURL (state) { function getCurrentLocalURL (state) {
return state.server return state.server
? state.server.localURL + '/' + state.playing.fileIndex + '/' + ? state.server.localURL + '/' + state.playing.fileIndex + '/' +
encodeURIComponent(state.playing.fileName) encodeURIComponent(state.playing.fileName)
: '' : ''
} }
function updateCache (state) { function updateCache (state) {
const infoHash = state.playing.infoHash const infoHash = state.playing.infoHash
const fileIndex = state.playing.fileIndex const fileIndex = state.playing.fileIndex
if (infoHash === cache.infoHash) { if (infoHash === cache.infoHash) {
switch (fileIndex) { switch (fileIndex) {
case cache.currentIndex: case cache.currentIndex:
@@ -65,12 +49,10 @@ function updateCache (state) {
} else { } else {
cache.infoHash = infoHash cache.infoHash = infoHash
} }
cache.previousIndex = findPreviousIndex(state) cache.previousIndex = findPreviousIndex(state)
cache.currentIndex = fileIndex cache.currentIndex = fileIndex
cache.nextIndex = findNextIndex(state) cache.nextIndex = findNextIndex(state)
} }
function findPreviousIndex (state) { function findPreviousIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (let i = state.playing.fileIndex - 1; i >= 0; i--) { for (let i = state.playing.fileIndex - 1; i >= 0; i--) {
@@ -78,7 +60,6 @@ function findPreviousIndex (state) {
} }
return null return null
} }
function findNextIndex (state) { function findNextIndex (state) {
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
for (let i = state.playing.fileIndex + 1; i < files.length; i++) { for (let i = state.playing.fileIndex + 1; i < files.length; i++) {
@@ -86,3 +67,15 @@ function findNextIndex (state) {
} }
return null return null
} }
export { hasNext }
export { getNextIndex }
export { hasPrevious }
export { getPreviousIndex }
export { getCurrentLocalURL }
export default {
hasNext,
getNextIndex,
hasPrevious,
getPreviousIndex,
getCurrentLocalURL
}

View File

@@ -1,20 +1,11 @@
module.exports = { import config from '../src/config.js'
init, import { InvalidSoundNameError } from './errors.js'
play import path from 'path'
}
const config = require('../../config')
const { InvalidSoundNameError } = require('./errors')
const path = require('path')
const VOLUME = 0.25 const VOLUME = 0.25
// App state to access the soundNotifications preference // App state to access the soundNotifications preference
let state let state
/* Cache of Audio elements, for instant playback */ /* Cache of Audio elements, for instant playback */
const cache = {} const cache = {}
const sounds = { const sounds = {
ADD: { ADD: {
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'), url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'add.wav'),
@@ -49,20 +40,16 @@ const sounds = {
volume: VOLUME volume: VOLUME
} }
} }
function init (appState) { function init (appState) {
state = appState state = appState
} }
function play (name) { function play (name) {
if (state == null) { if (state == null) {
return return
} }
if (!state.saved.prefs.soundNotifications) { if (!state.saved.prefs.soundNotifications) {
return return
} }
let audio = cache[name] let audio = cache[name]
if (!audio) { if (!audio) {
const sound = sounds[name] const sound = sounds[name]
@@ -76,3 +63,9 @@ function play (name) {
audio.currentTime = 0 audio.currentTime = 0
audio.play() audio.play()
} }
export { init }
export { play }
export default {
init,
play
}

View File

@@ -1,30 +1,33 @@
const appConfig = require('application-config')('WebTorrent') import applicationConfig from 'application-config'
const path = require('path') import path from 'path'
const { EventEmitter } = require('events') import { EventEmitter } from 'events'
const config = require('../../config') import config from '../../config.js'
const appConfig = applicationConfig('WebTorrent')
const SAVE_DEBOUNCE_INTERVAL = 1000 const SAVE_DEBOUNCE_INTERVAL = 1000
appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json') appConfig.filePath = path.join(config.CONFIG_PATH, 'config.json')
const State = module.exports = Object.assign(new EventEmitter(), { const State = Object.assign(new EventEmitter(), {
getDefaultPlayState, getDefaultPlayState,
load, load,
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit. // state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
save: function () { async save (...args) {
// Perf optimization: Lazy-require debounce (and it's dependencies) // Perf optimization: Lazy-require debounce (and it's dependencies)
const debounce = require('debounce') const debounce = await import('debounce')
// After first State.save() invokation, future calls go straight to the // After first State.save() invokation, future calls go straight to the
// debounced function // debounced function
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL) State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
State.save(...arguments) State.save(...args)
}, },
saveImmediate saveImmediate
}) })
function getDefaultState () { export default State
const LocationHistory = require('location-history')
async function getDefaultState () {
const { default: LocationHistory } = await import('location-history')
return { return {
/* /*
@@ -115,9 +118,9 @@ function getDefaultPlayState () {
} }
/* If the saved state file doesn't exist yet, here's what we use instead */ /* If the saved state file doesn't exist yet, here's what we use instead */
function setupStateSaved () { async function setupStateSaved () {
const { copyFileSync, mkdirSync, readFileSync } = require('fs') const { copyFileSync, mkdirSync, readFileSync } = await import('fs')
const parseTorrent = require('parse-torrent') const { default: parseTorrent } = await import('parse-torrent')
const saved = { const saved = {
prefs: { prefs: {
@@ -203,12 +206,13 @@ function shouldHidePlayerControls () {
} }
async function load (cb) { async function load (cb) {
console.log('state load')
let saved = await appConfig.read() let saved = await appConfig.read()
if (!saved || !saved.version) { if (!saved || !saved.version) {
console.log('Missing config file: Creating new one') console.log('Missing config file: Creating new one')
try { try {
saved = setupStateSaved() saved = await setupStateSaved()
} catch (err) { } catch (err) {
onSavedState(err) onSavedState(err)
return return
@@ -217,9 +221,9 @@ async function load (cb) {
onSavedState(null, saved) onSavedState(null, saved)
function onSavedState (err, saved) { async function onSavedState (err, saved) {
if (err) return cb(err) if (err) return cb(err)
const state = getDefaultState() const state = await getDefaultState()
state.saved = saved state.saved = saved
if (process.type === 'renderer') { if (process.type === 'renderer') {
@@ -228,6 +232,7 @@ async function load (cb) {
migrations.run(state) migrations.run(state)
} }
console.log('calling cb')
cb(null, state) cb(null, state)
} }
} }
@@ -242,7 +247,7 @@ async function saveImmediate (state, cb) {
// reading the torrent file or file(s) to seed & don't have an infohash // reading the torrent file or file(s) to seed & don't have an infohash
copy.torrents = copy.torrents copy.torrents = copy.torrents
.filter((x) => x.infoHash) .filter((x) => x.infoHash)
.map(function (x) { .map(x => {
const torrent = {} const torrent = {}
for (const key in x) { for (const key in x) {
if (key === 'progress' || key === 'torrentKey') { if (key === 'progress' || key === 'torrentKey') {

View File

@@ -1,24 +1,14 @@
// Collects anonymous usage stats and uncaught errors import remote from '@electron/remote'
// Reports back so that we can improve WebTorrent Desktop import config from '../../config.js'
module.exports = {
init,
send,
logUncaughtError,
logPlayAttempt
}
const electron = require('electron')
const config = require('../../config')
let telemetry let telemetry
function init (state) { async function init (state) {
telemetry = state.saved.telemetry telemetry = state.saved.telemetry
// First app run // First app run
if (!telemetry) { if (!telemetry) {
const crypto = require('crypto') const crypto = await import('crypto')
telemetry = state.saved.telemetry = { telemetry = state.saved.telemetry = {
userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID
} }
@@ -26,7 +16,7 @@ function init (state) {
} }
} }
function send (state) { async function send (state) {
const now = new Date() const now = new Date()
telemetry.version = config.APP_VERSION telemetry.version = config.APP_VERSION
telemetry.timestamp = now.toISOString() telemetry.timestamp = now.toISOString()
@@ -42,7 +32,7 @@ function send (state) {
return reset() return reset()
} }
const get = require('simple-get') const get = await import('simple-get')
const opts = { const opts = {
url: config.TELEMETRY_URL, url: config.TELEMETRY_URL,
@@ -50,7 +40,7 @@ function send (state) {
json: true json: true
} }
get.post(opts, function (err, res) { get.post(opts, (err, res) => {
if (err) return console.error('Error sending telemetry', err) if (err) return console.error('Error sending telemetry', err)
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
return console.error(`Error sending telemetry, status code: ${res.statusCode}`) return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
@@ -74,7 +64,7 @@ function reset () {
// Track screen resolution // Track screen resolution
function getScreenInfo () { function getScreenInfo () {
return electron.remote.screen.getAllDisplays().map((screen) => ({ return remote.screen.getAllDisplays().map((screen) => ({
width: screen.size.width, width: screen.size.width,
height: screen.size.height, height: screen.size.height,
scaleFactor: screen.scaleFactor scaleFactor: screen.scaleFactor
@@ -82,8 +72,8 @@ function getScreenInfo () {
} }
// Track basic system info like OS version and amount of RAM // Track basic system info like OS version and amount of RAM
function getSystemInfo () { async function getSystemInfo () {
const os = require('os') const os = await import('os')
return { return {
osPlatform: process.platform, osPlatform: process.platform,
osRelease: os.type() + ' ' + os.release(), osRelease: os.type() + ' ' + os.release(),
@@ -106,15 +96,14 @@ function getTorrentStats (state) {
} }
// First, count torrents & total file size // First, count torrents & total file size
for (let i = 0; i < count; i++) { for (const torrent of state.saved.torrents) {
const t = state.saved.torrents[i] const stat = byStatus[torrent.status]
const stat = byStatus[t.status] if (!torrent || !torrent.files || !stat) continue
if (!t || !t.files || !stat) continue
stat.count++ stat.count++
for (let j = 0; j < t.files.length; j++) {
const f = t.files[j] for (const file of torrent.files) {
if (!f || !f.length) continue if (!file || !file.length) continue
const fileSizeMB = f.length / (1 << 20) const fileSizeMB = file.length / (1 << 20)
sizeMB += fileSizeMB sizeMB += fileSizeMB
stat.sizeMB += fileSizeMB stat.sizeMB += fileSizeMB
} }
@@ -145,7 +134,7 @@ function roundPow2 (n) {
if (n <= 0) return 0 if (n <= 0) return 0
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space // Otherwise, return 1, 2, 4, 8, etc by rounding in log space
const log2 = Math.log(n) / Math.log(2) const log2 = Math.log(n) / Math.log(2)
return Math.pow(2, Math.round(log2)) return 2 ** Math.round(log2)
} }
// An uncaught error happened in the main process or in one of the windows // An uncaught error happened in the main process or in one of the windows
@@ -221,3 +210,14 @@ function logPlayAttempt (result) {
attempts.total = (attempts.total || 0) + 1 attempts.total = (attempts.total || 0) + 1
attempts[result] = (attempts[result] || 0) + 1 attempts[result] = (attempts[result] || 0) + 1
} }
export { init }
export { send }
export { logUncaughtError }
export { logPlayAttempt }
export default {
init,
send,
logUncaughtError,
logPlayAttempt
}

17
src/renderer/lib/time.js Normal file
View File

@@ -0,0 +1,17 @@
function calculateEta (missing, downloadSpeed) {
if (downloadSpeed === 0 || missing === 0) return
const rawEta = missing / downloadSpeed
const hours = Math.floor(rawEta / 3600) % 24
const minutes = Math.floor(rawEta / 60) % 60
const seconds = Math.floor(rawEta % 60)
// Only display hours and minutes if they are greater than 0 but always
// display minutes if hours is being displayed
const hoursStr = hours ? hours + ' h' : ''
const minutesStr = (hours || minutes) ? minutes + ' min' : ''
const secondsStr = seconds + ' s'
return `${hoursStr} ${minutesStr} ${secondsStr} remaining`
}
export { calculateEta }
export default {
calculateEta
}

View File

@@ -1,32 +1,18 @@
module.exports = { import path from 'path'
isPlayable, import mediaExtensions from './media-extensions.js'
isVideo,
isAudio,
isTorrent,
isMagnetLink,
isPlayableTorrentSummary
}
const path = require('path')
const mediaExtensions = require('./media-extensions')
// Checks whether a fileSummary or file path is audio/video that we can play, // Checks whether a fileSummary or file path is audio/video that we can play,
// based on the file extension // based on the file extension
function isPlayable (file) { function isPlayable (file) {
return isVideo(file) || isAudio(file) return isVideo(file) || isAudio(file)
} }
// Checks whether a fileSummary or file path is playable video // Checks whether a fileSummary or file path is playable video
function isVideo (file) { function isVideo (file) {
return mediaExtensions.video.includes(getFileExtension(file)) return mediaExtensions.video.includes(getFileExtension(file))
} }
// Checks whether a fileSummary or file path is playable audio // Checks whether a fileSummary or file path is playable audio
function isAudio (file) { function isAudio (file) {
return mediaExtensions.audio.includes(getFileExtension(file)) return mediaExtensions.audio.includes(getFileExtension(file))
} }
// Checks if the argument is either: // Checks if the argument is either:
// - a string that's a valid filename ending in .torrent // - a string that's a valid filename ending in .torrent
// - a file object where obj.name is ends in .torrent // - a file object where obj.name is ends in .torrent
@@ -34,20 +20,30 @@ function isAudio (file) {
function isTorrent (file) { function isTorrent (file) {
return isTorrentFile(file) || isMagnetLink(file) return isTorrentFile(file) || isMagnetLink(file)
} }
function isTorrentFile (file) { function isTorrentFile (file) {
return getFileExtension(file) === '.torrent' return getFileExtension(file) === '.torrent'
} }
function isMagnetLink (link) { function isMagnetLink (link) {
return typeof link === 'string' && /^(stream-)?magnet:/.test(link) return typeof link === 'string' && /^(stream-)?magnet:/.test(link)
} }
function getFileExtension (file) { function getFileExtension (file) {
const name = typeof file === 'string' ? file : file.name const name = typeof file === 'string' ? file : file.name
return path.extname(name).toLowerCase() return path.extname(name).toLowerCase()
} }
function isPlayableTorrentSummary (torrentSummary) { function isPlayableTorrentSummary (torrentSummary) {
return torrentSummary.files && torrentSummary.files.some(isPlayable) return torrentSummary.files && torrentSummary.files.some(isPlayable)
} }
export { isPlayable }
export { isVideo }
export { isAudio }
export { isTorrent }
export { isMagnetLink }
export { isPlayableTorrentSummary }
export default {
isPlayable,
isVideo,
isAudio,
isTorrent,
isMagnetLink,
isPlayableTorrentSummary
}

View File

@@ -1,34 +1,20 @@
module.exports = torrentPoster import captureFrame from 'capture-frame'
import path from 'path'
const captureFrame = require('capture-frame') import mediaExtensions from './media-extensions.js'
const path = require('path')
const mediaExtensions = require('./media-extensions')
const msgNoSuitablePoster = 'Cannot generate a poster from any files in the torrent' const msgNoSuitablePoster = 'Cannot generate a poster from any files in the torrent'
function torrentPoster (torrent, cb) { function torrentPoster (torrent, cb) {
// First, try to use a poster image if available // First, try to use a poster image if available
const posterFile = torrent.files.filter(function (file) { const posterFile = torrent.files.filter(file => /^poster\.(jpg|png|gif)$/.test(file.name))[0]
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return extractPoster(posterFile, cb) if (posterFile) return extractPoster(posterFile, cb)
// 'score' each media type based on total size present in torrent // 'score' each media type based on total size present in torrent
const bestScore = ['audio', 'video', 'image'].map(mediaType => { const bestScore = ['audio', 'video', 'image'].map(mediaType => ({
return { type: mediaType,
type: mediaType, size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType]) })).sort((a, b) => b.size - a.size)[0] // sort descending on size
}
}).sort((a, b) => { // sort descending on size
return b.size - a.size
})[0]
if (bestScore.size === 0) { if (bestScore.size === 0) {
// Admit defeat, no video, audio or image had a significant presence // Admit defeat, no video, audio or image had a significant presence
return cb(new Error(msgNoSuitablePoster)) return cb(new Error(msgNoSuitablePoster))
} }
// Based on which media type is dominant we select the corresponding poster function // Based on which media type is dominant we select the corresponding poster function
switch (bestScore.type) { switch (bestScore.type) {
case 'audio': case 'audio':
@@ -39,7 +25,6 @@ function torrentPoster (torrent, cb) {
return torrentPosterFromVideo(torrent, cb) return torrentPosterFromVideo(torrent, cb)
} }
} }
/** /**
* Calculate the total data size of file matching one of the provided extensions * Calculate the total data size of file matching one of the provided extensions
* @param torrent * @param torrent
@@ -51,11 +36,8 @@ function calculateDataLengthByExtension (torrent, extensions) {
if (files.length === 0) return 0 if (files.length === 0) return 0
return files return files
.map(file => file.length) .map(file => file.length)
.reduce((a, b) => { .reduce((a, b) => a + b)
return a + b
})
} }
/** /**
* Get the largest file of a given torrent, filtered by provided extension * Get the largest file of a given torrent, filtered by provided extension
* @param torrent Torrent to search in * @param torrent Torrent to search in
@@ -65,11 +47,8 @@ function calculateDataLengthByExtension (torrent, extensions) {
function getLargestFileByExtension (torrent, extensions) { function getLargestFileByExtension (torrent, extensions) {
const files = filterOnExtension(torrent, extensions) const files = filterOnExtension(torrent, extensions)
if (files.length === 0) return undefined if (files.length === 0) return undefined
return files.reduce((a, b) => { return files.reduce((a, b) => a.length > b.length ? a : b)
return a.length > b.length ? a : b
})
} }
/** /**
* Filter file on a list extension, can be used to find al image files * Filter file on a list extension, can be used to find al image files
* @param torrent Torrent to filter files from * @param torrent Torrent to filter files from
@@ -82,7 +61,6 @@ function filterOnExtension (torrent, extensions) {
return extensions.indexOf(extname) !== -1 return extensions.indexOf(extname) !== -1
}) })
} }
/** /**
* Returns a score how likely the file is suitable as a poster * Returns a score how likely the file is suitable as a poster
* @param imgFile File object of an image * @param imgFile File object of an image
@@ -98,7 +76,6 @@ function scoreAudioCoverFile (imgFile) {
back: 20, back: 20,
spectrogram: -80 spectrogram: -80
} }
for (const keyword in relevanceScore) { for (const keyword in relevanceScore) {
if (fileName === keyword) { if (fileName === keyword) {
return relevanceScore[keyword] return relevanceScore[keyword]
@@ -109,18 +86,13 @@ function scoreAudioCoverFile (imgFile) {
} }
return 0 return 0
} }
function torrentPosterFromAudio (torrent, cb) { function torrentPosterFromAudio (torrent, cb) {
const imageFiles = filterOnExtension(torrent, mediaExtensions.image) const imageFiles = filterOnExtension(torrent, mediaExtensions.image)
if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster)) if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster))
const bestCover = imageFiles.map(file => ({
const bestCover = imageFiles.map(file => { file,
return { score: scoreAudioCoverFile(file)
file, })).reduce((a, b) => {
score: scoreAudioCoverFile(file)
}
}).reduce((a, b) => {
if (a.score > b.score) { if (a.score > b.score) {
return a return a
} }
@@ -133,62 +105,47 @@ function torrentPosterFromAudio (torrent, cb) {
} }
return b return b
}) })
const extname = path.extname(bestCover.file.name) const extname = path.extname(bestCover.file.name)
bestCover.file.getBuffer((err, buf) => cb(err, buf, extname)) bestCover.file.getBuffer((err, buf) => cb(err, buf, extname))
} }
function torrentPosterFromVideo (torrent, cb) { function torrentPosterFromVideo (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.video) const file = getLargestFileByExtension(torrent, mediaExtensions.video)
const index = torrent.files.indexOf(file) const index = torrent.files.indexOf(file)
const server = torrent.createServer(0) const server = torrent.createServer(0)
server.listen(0, onListening) server.listen(0, onListening)
function onListening () { function onListening () {
const port = server.address().port const port = server.address().port
const url = 'http://localhost:' + port + '/' + index const url = 'http://localhost:' + port + '/' + index
const video = document.createElement('video') const video = document.createElement('video')
video.addEventListener('canplay', onCanPlay) video.addEventListener('canplay', onCanPlay)
video.volume = 0 video.volume = 0
video.src = url video.src = url
video.play() video.play()
function onCanPlay () { function onCanPlay () {
video.removeEventListener('canplay', onCanPlay) video.removeEventListener('canplay', onCanPlay)
video.addEventListener('seeked', onSeeked) video.addEventListener('seeked', onSeeked)
video.currentTime = Math.min((video.duration || 600) * 0.03, 60) video.currentTime = Math.min((video.duration || 600) * 0.03, 60)
} }
function onSeeked () { function onSeeked () {
video.removeEventListener('seeked', onSeeked) video.removeEventListener('seeked', onSeeked)
const frame = captureFrame(video) const frame = captureFrame(video)
const buf = frame && frame.image const buf = frame && frame.image
// unload video element // unload video element
video.pause() video.pause()
video.src = '' video.src = ''
video.load() video.load()
server.destroy() server.destroy()
if (buf.length === 0) return cb(new Error(msgNoSuitablePoster)) if (buf.length === 0) return cb(new Error(msgNoSuitablePoster))
cb(null, buf, '.jpg') cb(null, buf, '.jpg')
} }
} }
} }
function torrentPosterFromImage (torrent, cb) { function torrentPosterFromImage (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.image) const file = getLargestFileByExtension(torrent, mediaExtensions.image)
extractPoster(file, cb) extractPoster(file, cb)
} }
function extractPoster (file, cb) { function extractPoster (file, cb) {
const extname = path.extname(file.name) const extname = path.extname(file.name)
file.getBuffer((err, buf) => { return cb(err, buf, extname) }) file.getBuffer((err, buf) => cb(err, buf, extname))
} }
export default torrentPoster

View File

@@ -1,21 +1,11 @@
module.exports = { import path from 'path'
getPosterPath, import config from '../src/config.js'
getTorrentPath,
getByKey,
getTorrentId,
getFileOrFolder
}
const path = require('path')
const config = require('../../config')
// Expects a torrentSummary // Expects a torrentSummary
// Returns an absolute path to the torrent file, or null if unavailable // Returns an absolute path to the torrent file, or null if unavailable
function getTorrentPath (torrentSummary) { function getTorrentPath (torrentSummary) {
if (!torrentSummary || !torrentSummary.torrentFileName) return null if (!torrentSummary || !torrentSummary.torrentFileName) return null
return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName) return path.join(config.TORRENT_PATH, torrentSummary.torrentFileName)
} }
// Expects a torrentSummary // Expects a torrentSummary
// Returns an absolute path to the poster image, or null if unavailable // Returns an absolute path to the poster image, or null if unavailable
function getPosterPath (torrentSummary) { function getPosterPath (torrentSummary) {
@@ -25,7 +15,6 @@ function getPosterPath (torrentSummary) {
// Backslashes in URLS in CSS cause bizarre string encoding issues // Backslashes in URLS in CSS cause bizarre string encoding issues
return posterPath.replace(/\\/g, '/') return posterPath.replace(/\\/g, '/')
} }
// Expects a torrentSummary // Expects a torrentSummary
// Returns a torrentID: filename, magnet URI, or infohash // Returns a torrentID: filename, magnet URI, or infohash
function getTorrentId (torrentSummary) { function getTorrentId (torrentSummary) {
@@ -36,15 +25,12 @@ function getTorrentId (torrentSummary) {
return s.magnetURI || s.infoHash return s.magnetURI || s.infoHash
} }
} }
// Expects a torrentKey or infoHash // Expects a torrentKey or infoHash
// Returns the corresponding torrentSummary, or undefined // Returns the corresponding torrentSummary, or undefined
function getByKey (state, torrentKey) { function getByKey (state, torrentKey) {
if (!torrentKey) return undefined if (!torrentKey) return undefined
return state.saved.torrents.find((x) => return state.saved.torrents.find((x) => x.torrentKey === torrentKey || x.infoHash === torrentKey)
x.torrentKey === torrentKey || x.infoHash === torrentKey)
} }
// Returns the path to either the file (in a single-file torrent) or the root // Returns the path to either the file (in a single-file torrent) or the root
// folder (in multi-file torrent) // folder (in multi-file torrent)
// WARNING: assumes that multi-file torrents consist of a SINGLE folder. // WARNING: assumes that multi-file torrents consist of a SINGLE folder.
@@ -56,3 +42,15 @@ function getFileOrFolder (torrentSummary) {
const dirname = ts.files[0].path.split(path.sep)[0] const dirname = ts.files[0].path.split(path.sep)[0]
return path.join(ts.path, dirname) return path.join(ts.path, dirname)
} }
export { getPosterPath }
export { getTorrentPath }
export { getByKey }
export { getTorrentId }
export { getFileOrFolder }
export default {
getPosterPath,
getTorrentPath,
getByKey,
getTorrentId,
getFileOrFolder
}

View File

@@ -1,51 +1,43 @@
/** /* eslint-disable import/first */
* Perf optimization: Hook into require() to modify how certain modules load:
*
* - `inline-style-prefixer` (used by `material-ui`) takes ~40ms. It is not
* actually used because auto-prefixing is disabled with
* `darkBaseTheme.userAgent = false`. Return a fake object.
*/
const Module = require('module')
const _require = Module.prototype.require
Module.prototype.require = function (id) {
if (id === 'inline-style-prefixer') return {}
return _require.apply(this, arguments)
}
console.time('init') console.time('init')
// Perf optimization: Start asynchronously read on config file before all the // Perf optimization: Start asynchronously read on config file before all the
// blocking require() calls below. // blocking import() calls below.
const State = require('./lib/state') import State from './lib/state'
State.load(onState) State.load(onState)
const createGetter = require('fn-getter') import createGetter from 'fn-getter'
const debounce = require('debounce') import remote from '@electron/remote'
const dragDrop = require('drag-drop') import debounce from 'debounce'
const electron = require('electron') import dragDrop from 'drag-drop'
const fs = require('fs') import electron from 'electron'
const React = require('react') import fs from 'fs'
const ReactDOM = require('react-dom') import * as React from 'react'
import * as ReactDOM from 'react-dom'
const config = require('../config') import config from '../config.js'
const telemetry = require('./lib/telemetry') import telemetry from './lib/telemetry.js'
const sound = require('./lib/sound') import sound from './lib/sound.js'
const TorrentPlayer = require('./lib/torrent-player') import TorrentPlayer from './lib/torrent-player.js'
import App from './pages/app.js'
import { setDispatch } from './lib/dispatcher.js'
// Perf optimization: Needed immediately, so do not lazy load it below import TorrentController from './controllers/torrent-controller.js'
const TorrentListController = require('./controllers/torrent-list-controller') import AudioTracksController from './controllers/audio-tracks-controller.js'
import SubtitlesController from './controllers/subtitles-controller.js'
import PrefsController from './controllers/prefs-controller.js'
import PlaybackController from './controllers/playback-controller.js'
import MediaController from './controllers/media-controller.js'
import TorrentListController from './controllers/torrent-list-controller.js'
import UpdateController from './controllers/update-controller.js'
import FolderWatcherController from './controllers/folder-watcher-controller.js'
const App = require('./pages/app') const { clipboard, ipcRenderer } = electron
// Electron apps have two processes: a main process (node) runs first and starts
// a renderer process (essentially a Chrome window). We're in the renderer process,
// and this IPC channel receives from and sends messages to the main process
const ipcRenderer = electron.ipcRenderer
// Yo-yo pattern: state object lives here and percolates down thru all the views. // Yo-yo pattern: state object lives here and percolates down thru all the views.
// Events come back up from the views via dispatch(...) // Events come back up from the views via dispatch(...)
require('./lib/dispatcher').setDispatch(dispatch) setDispatch(dispatch)
// From dispatch(...), events are sent to one of the controllers // From dispatch(...), events are sent to one of the controllers
let controllers = null let controllers = null
@@ -80,41 +72,15 @@ function onState (err, _state) {
// Create controllers // Create controllers
controllers = { controllers = {
media: createGetter(() => { media: createGetter(() => new MediaController(state)),
const MediaController = require('./controllers/media-controller') playback: createGetter(() => new PlaybackController(state, config, update)),
return new MediaController(state) prefs: createGetter(() => new PrefsController(state, config)),
}), subtitles: createGetter(() => new SubtitlesController(state)),
playback: createGetter(() => { audioTracks: createGetter(() => new AudioTracksController(state)),
const PlaybackController = require('./controllers/playback-controller') torrent: createGetter(() => new TorrentController(state)),
return new PlaybackController(state, config, update) torrentList: createGetter(() => new TorrentListController(state)),
}), update: createGetter(() => new UpdateController(state)),
prefs: createGetter(() => { folderWatcher: createGetter(() => new FolderWatcherController())
const PrefsController = require('./controllers/prefs-controller')
return new PrefsController(state, config)
}),
subtitles: createGetter(() => {
const SubtitlesController = require('./controllers/subtitles-controller')
return new SubtitlesController(state)
}),
audioTracks: createGetter(() => {
const AudioTracksController = require('./controllers/audio-tracks-controller')
return new AudioTracksController(state)
}),
torrent: createGetter(() => {
const TorrentController = require('./controllers/torrent-controller')
return new TorrentController(state)
}),
torrentList: createGetter(() => {
return new TorrentListController(state)
}),
update: createGetter(() => {
const UpdateController = require('./controllers/update-controller')
return new UpdateController(state)
}),
folderWatcher: createGetter(() => {
const FolderWatcherController = require('./controllers/folder-watcher-controller')
return new FolderWatcherController()
})
} }
// Add first page to location history // Add first page to location history
@@ -130,7 +96,10 @@ function onState (err, _state) {
resumeTorrents() resumeTorrents()
// Initialize ReactDOM // Initialize ReactDOM
app = ReactDOM.render(<App state={state} />, document.querySelector('#body')) ReactDOM.render(
<App state={state} ref={elem => { app = elem }} />,
document.querySelector('#body')
)
// Calling update() updates the UI given the current state // Calling update() updates the UI given the current state
// Do this at least once a second to give every file in every torrentSummary // Do this at least once a second to give every file in every torrentSummary
@@ -152,11 +121,11 @@ function onState (err, _state) {
// Add YouTube style hotkey shortcuts // Add YouTube style hotkey shortcuts
window.addEventListener('keydown', onKeydown) window.addEventListener('keydown', onKeydown)
const debouncedFullscreenToggle = debounce(function () { const debouncedFullscreenToggle = debounce(() => {
dispatch('toggleFullScreen') dispatch('toggleFullScreen')
}, 1000, true) }, 1000, true)
document.addEventListener('wheel', function (event) { document.addEventListener('wheel', event => {
// ctrlKey detects pinch to zoom, http://crbug.com/289887 // ctrlKey detects pinch to zoom, http://crbug.com/289887
if (event.ctrlKey) { if (event.ctrlKey) {
event.preventDefault() event.preventDefault()
@@ -168,7 +137,7 @@ function onState (err, _state) {
window.addEventListener('focus', onFocus) window.addEventListener('focus', onFocus)
window.addEventListener('blur', onBlur) window.addEventListener('blur', onBlur)
if (electron.remote.getCurrentWindow().isVisible()) { if (remote.getCurrentWindow().isVisible()) {
sound.play('STARTUP') sound.play('STARTUP')
} }
@@ -198,9 +167,9 @@ function delayedInit () {
} }
// Lazily loads Chromecast and Airplay support // Lazily loads Chromecast and Airplay support
function lazyLoadCast () { async function lazyLoadCast () {
if (!Cast) { if (!Cast) {
Cast = require('./lib/cast') Cast = (await import('./lib/cast')).default
Cast.init(state, update) // Search the local network for Chromecast and Airplays Cast.init(state, update) // Search the local network for Chromecast and Airplays
} }
return Cast return Cast
@@ -252,6 +221,7 @@ const dispatchHandlers = {
controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData), controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
deleteTorrent: (infoHash, deleteData) => deleteTorrent: (infoHash, deleteData) =>
controllers.torrentList().deleteTorrent(infoHash, deleteData), controllers.torrentList().deleteTorrent(infoHash, deleteData),
openTorrentListContextMenu: () => onPaste(),
confirmDeleteAllTorrents: (deleteData) => confirmDeleteAllTorrents: (deleteData) =>
controllers.torrentList().confirmDeleteAllTorrents(deleteData), controllers.torrentList().confirmDeleteAllTorrents(deleteData),
deleteAllTorrents: (deleteData) => deleteAllTorrents: (deleteData) =>
@@ -274,6 +244,8 @@ const dispatchHandlers = {
previousTrack: () => controllers.playback().previousTrack(), previousTrack: () => controllers.playback().previousTrack(),
skip: (time) => controllers.playback().skip(time), skip: (time) => controllers.playback().skip(time),
skipTo: (time) => controllers.playback().skipTo(time), skipTo: (time) => controllers.playback().skipTo(time),
preview: (x) => controllers.playback().preview(x),
clearPreview: () => controllers.playback().clearPreview(),
changePlaybackRate: (dir) => controllers.playback().changePlaybackRate(dir), changePlaybackRate: (dir) => controllers.playback().changePlaybackRate(dir),
changeVolume: (delta) => controllers.playback().changeVolume(delta), changeVolume: (delta) => controllers.playback().changeVolume(delta),
setVolume: (vol) => controllers.playback().setVolume(vol), setVolume: (vol) => controllers.playback().setVolume(vol),
@@ -394,7 +366,7 @@ function setupIpc () {
function backToList () { function backToList () {
// Exit any modals and screens with a back button // Exit any modals and screens with a back button
state.modal = null state.modal = null
state.location.backToFirst(function () { state.location.backToFirst(() => {
// If we were already on the torrent list, scroll to the top // If we were already on the torrent list, scroll to the top
const contentTag = document.querySelector('.content') const contentTag = document.querySelector('.content')
if (contentTag) contentTag.scrollTop = 0 if (contentTag) contentTag.scrollTop = 0
@@ -428,7 +400,7 @@ function resumeTorrents () {
// Set window dimensions to match video dimensions or fill the screen // Set window dimensions to match video dimensions or fill the screen
function setDimensions (dimensions) { function setDimensions (dimensions) {
// Don't modify the window size if it's already maximized // Don't modify the window size if it's already maximized
if (electron.remote.getCurrentWindow().isMaximized()) { if (remote.getCurrentWindow().isMaximized()) {
state.window.bounds = null state.window.bounds = null
return return
} }
@@ -440,7 +412,7 @@ function setDimensions (dimensions) {
width: window.outerWidth, width: window.outerWidth,
height: window.outerHeight height: window.outerHeight
} }
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized state.window.wasMaximized = remote.getCurrentWindow().isMaximized
// Limit window size to screen size // Limit window size to screen size
const screenWidth = window.screen.width const screenWidth = window.screen.width
@@ -514,8 +486,8 @@ function onError (err) {
const editableHtmlTags = new Set(['input', 'textarea']) const editableHtmlTags = new Set(['input', 'textarea'])
function onPaste (e) { function onPaste (e) {
if (editableHtmlTags.has(e.target.tagName.toLowerCase())) return if (e && editableHtmlTags.has(e.target.tagName.toLowerCase())) return
controllers.torrentList().addTorrent(electron.clipboard.readText()) controllers.torrentList().addTorrent(clipboard.readText())
update() update()
} }
@@ -584,7 +556,7 @@ function onWindowBoundsChanged (e, newBounds) {
} }
function checkDownloadPath () { function checkDownloadPath () {
fs.stat(state.saved.prefs.downloadPath, function (err, stat) { fs.stat(state.saved.prefs.downloadPath, (err, stat) => {
if (err) { if (err) {
state.downloadPathStatus = 'missing' state.downloadPathStatus = 'missing'
return console.error(err) return console.error(err)

View File

@@ -1,32 +1,35 @@
const colors = require('material-ui/styles/colors') import * as colors from 'material-ui/styles/colors'
const createGetter = require('fn-getter') import createGetter from 'fn-getter'
const React = require('react') import * as React from 'react'
import darkBaseTheme$0 from 'material-ui/styles/baseThemes/darkBaseTheme'
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default import getMuiTheme$0 from 'material-ui/styles/getMuiTheme'
const getMuiTheme = require('material-ui/styles/getMuiTheme').default import muiThemeProvider from 'material-ui/styles/MuiThemeProvider'
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default import Header from '../components/header.js'
import TorrentListPage from './torrent-list-page.js'
const Header = require('../components/header') import playerPage from './player-page.js'
import createTorrentPage from './create-torrent-page.js'
// Perf optimization: Needed immediately, so do not lazy load it below import preferencesPage from './preferences-page.js'
const TorrentListPage = require('./torrent-list-page') import openTorrentAddressModal from '../components/open-torrent-address-modal.js'
import removeTorrentModal from '../components/remove-torrent-modal.js'
import updateAvailableModal from '../components/update-available-modal.js'
import unsupportedMediaModal from '../components/unsupported-media-modal.js'
import deleteAllTorrentsModal from '../components/delete-all-torrents-modal.js'
const darkBaseTheme = { default: darkBaseTheme$0 }.default
const getMuiTheme = getMuiTheme$0.default
const MuiThemeProvider = muiThemeProvider.default
const Views = { const Views = {
home: createGetter(() => TorrentListPage), home: createGetter(() => TorrentListPage),
player: createGetter(() => require('./player-page')), player: createGetter(() => playerPage),
'create-torrent': createGetter(() => require('./create-torrent-page')), 'create-torrent': createGetter(() => createTorrentPage),
preferences: createGetter(() => require('./preferences-page')) preferences: createGetter(() => preferencesPage)
} }
const Modals = { const Modals = {
'open-torrent-address-modal': createGetter( 'open-torrent-address-modal': createGetter(() => openTorrentAddressModal),
() => require('../components/open-torrent-address-modal') 'remove-torrent-modal': createGetter(() => removeTorrentModal),
), 'update-available-modal': createGetter(() => updateAvailableModal),
'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')), 'unsupported-media-modal': createGetter(() => unsupportedMediaModal),
'update-available-modal': createGetter(() => require('../components/update-available-modal')), 'delete-all-torrents-modal': createGetter(() => deleteAllTorrentsModal)
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal')),
'delete-all-torrents-modal':
createGetter(() => require('../components/delete-all-torrents-modal'))
} }
const fontFamily = process.platform === 'win32' const fontFamily = process.platform === 'win32'
@@ -45,7 +48,7 @@ darkBaseTheme.palette.accent3Color = colors.redA100
let darkMuiTheme let darkMuiTheme
let lightMuiTheme let lightMuiTheme
class App extends React.Component { export default class App extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
@@ -86,9 +89,7 @@ class App extends React.Component {
const recentErrors = state.errors.filter((x) => now - x.time < 5000) const recentErrors = state.errors.filter((x) => now - x.time < 5000)
const hasErrors = recentErrors.length > 0 const hasErrors = recentErrors.length > 0
const errorElems = recentErrors.map(function (error, i) { const errorElems = recentErrors.map((error, i) => <div key={i} className='error'>{error.message}</div>)
return (<div key={i} className='error'>{error.message}</div>)
})
return ( return (
<div <div
key='errors' key='errors'
@@ -100,12 +101,12 @@ class App extends React.Component {
) )
} }
getModal () { async getModal () {
const state = this.props.state const state = this.props.state
if (!state.modal) return if (!state.modal) return
if (!lightMuiTheme) { if (!lightMuiTheme) {
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default const lightBaseTheme = (await import('material-ui/styles/baseThemes/lightBaseTheme')).default
lightBaseTheme.fontFamily = fontFamily lightBaseTheme.fontFamily = fontFamily
lightBaseTheme.userAgent = false lightBaseTheme.userAgent = false
lightMuiTheme = getMuiTheme(lightBaseTheme) lightMuiTheme = getMuiTheme(lightBaseTheme)
@@ -130,5 +131,3 @@ class App extends React.Component {
return (<View state={state} />) return (<View state={state} />)
} }
} }
module.exports = App

View File

@@ -1,22 +1,25 @@
const createTorrent = require('create-torrent') import createTorrent from 'create-torrent'
const path = require('path') import path from 'path'
const prettyBytes = require('prettier-bytes') import prettyBytes from 'prettier-bytes'
const React = require('react') import * as React from 'react'
import flatButton from 'material-ui/FlatButton'
import raisedButton from 'material-ui/RaisedButton'
import textField from 'material-ui/TextField'
import checkbox from 'material-ui/Checkbox'
const { dispatch, dispatcher } = require('../lib/dispatcher') import { dispatch, dispatcher } from '../lib/dispatcher.js'
import CreateTorrentErrorPage from '../components/create-torrent-error-page.js'
import Heading from '../components/heading.js'
import ShowMore from '../components/show-more.js'
const FlatButton = require('material-ui/FlatButton').default const FlatButton = flatButton.default
const RaisedButton = require('material-ui/RaisedButton').default const RaisedButton = raisedButton.default
const TextField = require('material-ui/TextField').default const TextField = textField.default
const Checkbox = require('material-ui/Checkbox').default const Checkbox = checkbox.default
const CreateTorrentErrorPage = require('../components/create-torrent-error-page')
const Heading = require('../components/heading')
const ShowMore = require('../components/show-more')
// Shows a basic UI to create a torrent, from an already-selected file or folder. // Shows a basic UI to create a torrent, from an already-selected file or folder.
// Includes a "Show Advanced..." button and more advanced UI. // Includes a "Show Advanced..." button and more advanced UI.
class CreateTorrentPage extends React.Component { export default class CreateTorrentPage extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -223,5 +226,3 @@ function containsDots (path, pathPrefix) {
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/') const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
return ('/' + suffix).includes('/.') return ('/' + suffix).includes('/.')
} }
module.exports = CreateTorrentPage

View File

@@ -1,14 +1,16 @@
const React = require('react') import * as React from 'react'
const Bitfield = require('bitfield') import bitfield from 'bitfield'
const prettyBytes = require('prettier-bytes') import prettyBytes from 'prettier-bytes'
const TorrentSummary = require('../lib/torrent-summary') import TorrentSummary from '../lib/torrent-summary.js'
const Playlist = require('../lib/playlist') import Playlist from '../lib/playlist.js'
const { dispatch, dispatcher } = require('../lib/dispatcher') import { dispatch, dispatcher } from '../lib/dispatcher.js'
const config = require('../../config') import config from '../../config.js'
import { calculateEta } from '../lib/time.js'
// Shows a streaming video player. Standard features + Chromecast + Airplay const BitField = bitfield.default
module.exports = class Player extends React.Component {
export default class Player extends React.Component {
render () { render () {
// Show the video as large as will fit in the window, play immediately // Show the video as large as will fit in the window, play immediately
// If the video is on Chromecast or Airplay, show a title screen instead // If the video is on Chromecast or Airplay, show a title screen instead
@@ -108,8 +110,7 @@ function renderMedia (state) {
// Add subtitles to the <video> tag // Add subtitles to the <video> tag
const trackTags = [] const trackTags = []
if (state.playing.subtitles.selectedIndex >= 0) { if (state.playing.subtitles.selectedIndex >= 0) {
for (let i = 0; i < state.playing.subtitles.tracks.length; i++) { state.playing.subtitles.tracks.forEach((track, i) => {
const track = state.playing.subtitles.tracks[i]
const isSelected = state.playing.subtitles.selectedIndex === i const isSelected = state.playing.subtitles.selectedIndex === i
trackTags.push( trackTags.push(
<track <track
@@ -120,7 +121,7 @@ function renderMedia (state) {
src={track.buffer} src={track.buffer}
/> />
) )
} })
} }
// Create the <audio> or <video> tag // Create the <audio> or <video> tag
@@ -129,6 +130,7 @@ function renderMedia (state) {
<MediaTagName <MediaTagName
src={Playlist.getCurrentLocalURL(state)} src={Playlist.getCurrentLocalURL(state)}
onDoubleClick={dispatcher('toggleFullScreen')} onDoubleClick={dispatcher('toggleFullScreen')}
onClick={dispatcher('playPause')}
onLoadedMetadata={onLoadedMetadata} onLoadedMetadata={onLoadedMetadata}
onEnded={onEnded} onEnded={onEnded}
onStalled={dispatcher('mediaStalled')} onStalled={dispatcher('mediaStalled')}
@@ -198,7 +200,7 @@ function renderMedia (state) {
} }
} }
function onEnded (e) { function onEnded () {
if (Playlist.hasNext(state)) { if (Playlist.hasNext(state)) {
dispatch('nextTrack') dispatch('nextTrack')
} else { } else {
@@ -340,13 +342,13 @@ function renderAudioMetadata (state) {
format.push(fileSummary.audioInfo.format.codec) format.push(fileSummary.audioInfo.format.codec)
} }
if (fileSummary.audioInfo.format.bitrate) { if (fileSummary.audioInfo.format.bitrate) {
format.push(Math.round(fileSummary.audioInfo.format.bitrate / 1000) + ' kbps') // 128 kbps format.push(Math.round(fileSummary.audioInfo.format.bitrate / 1000) + ' kbit/s') // 128 kbit/s
} }
if (fileSummary.audioInfo.format.sampleRate) { if (fileSummary.audioInfo.format.sampleRate) {
format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz') format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz')
} }
if (fileSummary.audioInfo.format.bitsPerSample) { if (fileSummary.audioInfo.format.bitsPerSample) {
format.push(fileSummary.audioInfo.format.bitsPerSample + ' bit') format.push(fileSummary.audioInfo.format.bitsPerSample + '-bit')
} }
if (format.length > 0) { if (format.length > 0) {
elems.push(( elems.push((
@@ -427,22 +429,70 @@ function renderCastScreen (state) {
const isStarting = state.playing.location.endsWith('-pending') const isStarting = state.playing.location.endsWith('-pending')
const castName = state.playing.castName const castName = state.playing.castName
const fileName = state.getPlayingFileSummary().name || ''
let castStatus let castStatus
if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...' if (isCast && isStarting) castStatus = 'Connecting to ' + castName + '...'
else if (isCast && !isStarting) castStatus = 'Connected to ' + castName else if (isCast && !isStarting) castStatus = 'Connected to ' + castName
else castStatus = '' else castStatus = ''
const prog = state.getPlayingTorrentSummary().progress || {}
// Show a nice title image, if possible // Show a nice title image, if possible
const style = { const style = {
backgroundImage: cssBackgroundImagePoster(state) backgroundImage: cssBackgroundImagePoster(state)
} }
function renderEta (total, downloaded) {
const missing = (total || 0) - (downloaded || 0)
const downloadSpeed = prog.downloadSpeed || 0
if (downloadSpeed === 0 || missing === 0) return
const etaStr = calculateEta(missing, downloadSpeed)
return (<span>{etaStr}</span>)
}
function renderDownloadProgress () {
if (!prog.files) return
const fileProg = prog.files[state.playing.fileIndex]
const fileProgress = fileProg.numPiecesPresent / fileProg.numPieces
const fileLength = state.getPlayingFileSummary().length
const fileDownloaded = fileProgress * fileLength
const progress = Math.round(100 * fileProgress)
const total = prettyBytes(fileLength)
const completed = prettyBytes(fileDownloaded)
const downloadSpeed = prettyBytes(prog.downloadSpeed || 0)
const uploadSpeed = prettyBytes(prog.uploadSpeed || 0)
let sizes
if (fileProgress < 1) {
sizes = <span> | {completed} / {total}</span>
} else {
sizes = <span> | {completed}</span>
}
return (
<div key='download-progress'>
<span className='progress'>{progress}% downloaded {sizes}</span>
<br />
<span>↓ {downloadSpeed}/s ↑ {uploadSpeed}/s | {prog.numPeers || 0} peer(s)</span>
<br />
{renderEta(fileLength, fileDownloaded)}
</div>
)
}
return ( return (
<div key='cast' className='letterbox' style={style}> <div key='cast' className='letterbox' style={style}>
<div className='cast-screen'> <div className='cast-screen'>
<i className='icon'>{castIcon}</i> <i className='icon'>{castIcon}</i>
<div key='type' className='cast-type'>{castType}</div> <div key='type' className='cast-type'>{castType}</div>
<div key='status' className='cast-status'>{castStatus}</div> <div key='status' className='cast-status'>{castStatus}</div>
<div key='name' className='name'>{fileName}</div>
{renderDownloadProgress()}
</div> </div>
</div> </div>
) )
@@ -454,7 +504,7 @@ function renderCastOptions (state) {
const { location, devices } = state.devices.castMenu const { location, devices } = state.devices.castMenu
const player = state.devices[location] const player = state.devices[location]
const items = devices.map(function (device, ix) { const items = devices.map((device, ix) => {
const isSelected = player.device === device const isSelected = player.device === device
const name = device.name const name = device.name
return ( return (
@@ -477,7 +527,7 @@ function renderSubtitleOptions (state) {
const subtitles = state.playing.subtitles const subtitles = state.playing.subtitles
if (!subtitles.tracks.length || !subtitles.showMenu) return if (!subtitles.tracks.length || !subtitles.showMenu) return
const items = subtitles.tracks.map(function (track, ix) { const items = subtitles.tracks.map((track, ix) => {
const isSelected = state.playing.subtitles.selectedIndex === ix const isSelected = state.playing.subtitles.selectedIndex === ix
return ( return (
<li key={ix} onClick={dispatcher('selectSubtitle', ix)}> <li key={ix} onClick={dispatcher('selectSubtitle', ix)}>
@@ -504,7 +554,7 @@ function renderAudioTrackOptions (state) {
const audioTracks = state.playing.audioTracks const audioTracks = state.playing.audioTracks
if (!audioTracks.tracks.length || !audioTracks.showMenu) return if (!audioTracks.tracks.length || !audioTracks.showMenu) return
const items = audioTracks.tracks.map(function (track, ix) { const items = audioTracks.tracks.map((track, ix) => {
const isSelected = state.playing.audioTracks.selectedIndex === ix const isSelected = state.playing.audioTracks.selectedIndex === ix
return ( return (
<li key={ix} onClick={dispatcher('selectAudioTrack', ix)}> <li key={ix} onClick={dispatcher('selectAudioTrack', ix)}>
@@ -536,6 +586,8 @@ function renderPlayerControls (state) {
const nextClass = Playlist.hasNext(state) ? '' : 'disabled' const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
const elements = [ const elements = [
renderPreview(state),
<div key='playback-bar' className='playback-bar'> <div key='playback-bar' className='playback-bar'>
{renderLoadingBar(state)} {renderLoadingBar(state)}
<div <div
@@ -546,7 +598,9 @@ function renderPlayerControls (state) {
<div <div
key='scrub-bar' key='scrub-bar'
className='scrub-bar' className='scrub-bar'
draggable draggable='true'
onMouseMove={handleScrubPreview}
onMouseOut={clearPreview}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onClick={handleScrub} onClick={handleScrub}
onDrag={handleScrub} onDrag={handleScrub}
@@ -618,7 +672,7 @@ function renderPlayerControls (state) {
airplay: { true: 'airplay', false: 'airplay' }, airplay: { true: 'airplay', false: 'airplay' },
dlna: { true: 'tv', false: 'tv' } dlna: { true: 'tv', false: 'tv' }
} }
castTypes.forEach(function (castType) { castTypes.forEach(castType => {
// Do we show this button (eg. the Chromecast button) at all? // Do we show this button (eg. the Chromecast button) at all?
const isCasting = state.playing.location.startsWith(castType) const isCasting = state.playing.location.startsWith(castType)
const player = state.devices[castType] const player = state.devices[castType]
@@ -655,10 +709,14 @@ function renderPlayerControls (state) {
// Render volume slider // Render volume slider
const volume = state.playing.volume const volume = state.playing.volume
const volumeIcon = 'volume_' + ( const volumeIcon = 'volume_' + (
volume === 0 ? 'off' volume === 0
: volume < 0.3 ? 'mute' ? 'off'
: volume < 0.6 ? 'down' : volume < 0.3
: 'up') ? 'mute'
: volume < 0.6
? 'down'
: 'up'
)
const volumeStyle = { const volumeStyle = {
background: '-webkit-gradient(linear, left top, right top, ' + background: '-webkit-gradient(linear, left top, right top, ' +
'color-stop(' + (volume * 100) + '%, #eee), ' + 'color-stop(' + (volume * 100) + '%, #eee), ' +
@@ -701,27 +759,31 @@ function renderPlayerControls (state) {
)) ))
} }
return ( const emptyImage = new window.Image(0, 0)
<div emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs%3D'
key='controls' className='controls'
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
>
{elements}
{renderCastOptions(state)}
{renderSubtitleOptions(state)}
{renderAudioTrackOptions(state)}
</div>
)
function handleDragStart (e) { function handleDragStart (e) {
// Prevent the cursor from changing, eg to a green + icon on Mac
if (e.dataTransfer) { if (e.dataTransfer) {
const dt = e.dataTransfer const dt = e.dataTransfer
// Prevent the cursor from changing, eg to a green + icon on Mac
dt.effectAllowed = 'none' dt.effectAllowed = 'none'
// Prevent ghost image
dt.setDragImage(emptyImage, 0, 0)
} }
} }
// Handles a scrub hover (preview another position in the video)
function handleScrubPreview (e) {
// Only show for videos
if (!e.clientX || state.playing.type !== 'video') return
dispatch('mediaMouseMoved')
dispatch('preview', e.clientX)
}
function clearPreview () {
if (state.playing.type !== 'video') return
dispatch('clearPreview')
}
// Handles a click or drag to scrub (jump to another position in the video) // Handles a click or drag to scrub (jump to another position in the video)
function handleScrub (e) { function handleScrub (e) {
if (!e.clientX) return if (!e.clientX) return
@@ -733,7 +795,7 @@ function renderPlayerControls (state) {
} }
// Handles volume muting and Unmuting // Handles volume muting and Unmuting
function handleVolumeMute (e) { function handleVolumeMute () {
if (state.playing.volume === 0.0) { if (state.playing.volume === 0.0) {
dispatch('setVolume', 1.0) dispatch('setVolume', 1.0)
} else { } else {
@@ -755,9 +817,72 @@ function renderPlayerControls (state) {
} }
} }
function handleAudioTracks (e) { function handleAudioTracks () {
dispatch('toggleAudioTracksMenu') dispatch('toggleAudioTracksMenu')
} }
return (
<div
key='controls' className='controls'
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
>
{elements}
{renderCastOptions(state)}
{renderSubtitleOptions(state)}
{renderAudioTrackOptions(state)}
</div>
)
}
function renderPreview (state) {
const { previewXCoord = null } = state.playing
// Calculate time from x-coord as fraction of track width
const windowWidth = document.querySelector('body').clientWidth
const fraction = previewXCoord / windowWidth
const time = fraction * state.playing.duration /* seconds */
const height = 70
let width = 0
const previewEl = document.querySelector('video#preview')
if (previewEl !== null && previewXCoord !== null) {
previewEl.currentTime = time
// Auto adjust width to maintain video aspect ratio
width = Math.floor((previewEl.videoWidth / previewEl.videoHeight) * height)
}
// Center preview window on mouse cursor,
// while avoiding falling off the left or right edges
const xPos = Math.min(Math.max(previewXCoord - (width / 2), 5), windowWidth - width - 5)
return (
<div
key='preview' style={{
position: 'absolute',
bottom: 50,
left: xPos,
display: previewXCoord == null && 'none' // Hide preview when XCoord unset
}}
>
<div style={{ width, height, backgroundColor: 'black' }}>
<video
src={Playlist.getCurrentLocalURL(state)}
id='preview'
style={{ border: '1px solid lightgrey', borderRadius: 2 }}
/>
</div>
<p
style={{
textAlign: 'center', margin: 5, textShadow: '0 0 2px rgba(0,0,0,.5)', color: '#eee'
}}
>
{formatTime(time, state.playing.duration)}
</p>
</div>
)
} }
// Renders the loading bar. Shows which parts of the torrent are loaded, which // Renders the loading bar. Shows which parts of the torrent are loaded, which
@@ -779,7 +904,7 @@ function renderLoadingBar (state) {
const parts = [] const parts = []
let lastPiecePresent = false let lastPiecePresent = false
for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) { for (let i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
const partPresent = Bitfield.prototype.get.call(prog.bitfield, i) const partPresent = BitField.prototype.get.call(prog.bitfield, i)
if (partPresent && !lastPiecePresent) { if (partPresent && !lastPiecePresent) {
parts.push({ start: i - fileProg.startPiece, count: 1 }) parts.push({ start: i - fileProg.startPiece, count: 1 })
} else if (partPresent) { } else if (partPresent) {
@@ -789,7 +914,7 @@ function renderLoadingBar (state) {
} }
// Output some bars to show which parts of the file are loaded // Output some bars to show which parts of the file are loaded
const loadingBarElems = parts.map(function (part, i) { const loadingBarElems = parts.map((part, i) => {
const style = { const style = {
left: (100 * part.start / fileProg.numPieces) + '%', left: (100 * part.start / fileProg.numPieces) + '%',
width: (100 * part.count / fileProg.numPieces) + '%' width: (100 * part.count / fileProg.numPieces) + '%'

View File

@@ -1,16 +1,18 @@
const React = require('react') import * as React from 'react'
const PropTypes = require('prop-types') import PropTypes from 'prop-types'
import * as colors from 'material-ui/styles/colors'
import checkbox from 'material-ui/Checkbox'
import raisedButton from 'material-ui/RaisedButton'
const colors = require('material-ui/styles/colors') import Heading from '../components/heading.js'
const Checkbox = require('material-ui/Checkbox').default import PathSelector from '../components/path-selector.js'
const RaisedButton = require('material-ui/RaisedButton').default import { dispatch } from '../lib/dispatcher.js'
const Heading = require('../components/heading') import config from '../../config.js'
const PathSelector = require('../components/path-selector')
const { dispatch } = require('../lib/dispatcher') const Checkbox = checkbox.default
const config = require('../../config') const RaisedButton = raisedButton.default
class PreferencesPage extends React.Component { export default class PreferencesPage extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -286,5 +288,3 @@ class Preference extends React.Component {
return (<div style={style}>{this.props.children}</div>) return (<div style={style}>{this.props.children}</div>)
} }
} }
module.exports = PreferencesPage

View File

@@ -1,14 +1,17 @@
const React = require('react') import * as React from 'react'
const prettyBytes = require('prettier-bytes') import prettyBytes from 'prettier-bytes'
import checkbox from 'material-ui/Checkbox'
import linearProgress from 'material-ui/LinearProgress'
const Checkbox = require('material-ui/Checkbox').default import TorrentSummary from '../lib/torrent-summary.js'
const LinearProgress = require('material-ui/LinearProgress').default import TorrentPlayer from '../lib/torrent-player.js'
import { dispatcher } from '../lib/dispatcher.js'
import { calculateEta } from '../lib/time.js'
const TorrentSummary = require('../lib/torrent-summary') const Checkbox = checkbox.default
const TorrentPlayer = require('../lib/torrent-player') const LinearProgress = linearProgress.default
const { dispatcher } = require('../lib/dispatcher')
module.exports = class TorrentList extends React.Component { export default class TorrentList extends React.Component {
render () { render () {
const state = this.props.state const state = this.props.state
@@ -35,7 +38,11 @@ module.exports = class TorrentList extends React.Component {
) )
return ( return (
<div key='torrent-list' className='torrent-list'> <div
key='torrent-list'
className='torrent-list'
onContextMenu={dispatcher('openTorrentListContextMenu')}
>
{contents} {contents}
</div> </div>
) )
@@ -191,18 +198,9 @@ module.exports = class TorrentList extends React.Component {
const downloadSpeed = prog.downloadSpeed const downloadSpeed = prog.downloadSpeed
if (downloadSpeed === 0 || missing === 0) return if (downloadSpeed === 0 || missing === 0) return
const rawEta = missing / downloadSpeed const etaStr = calculateEta(missing, downloadSpeed)
const hours = Math.floor(rawEta / 3600) % 24
const minutes = Math.floor(rawEta / 60) % 60
const seconds = Math.floor(rawEta % 60)
// Only display hours and minutes if they are greater than 0 but always return (<span key='eta'>{etaStr}</span>)
// display minutes if hours is being displayed
const hoursStr = hours ? hours + 'h' : ''
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
const secondsStr = seconds + 's'
return (<span key='eta'>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
} }
function renderTorrentStatus () { function renderTorrentStatus () {

View File

@@ -1,33 +1,33 @@
/* eslint-disable import/first */
// To keep the UI snappy, we run WebTorrent in its own hidden window, a separate // To keep the UI snappy, we run WebTorrent in its own hidden window, a separate
// process from the main window. // process from the main window.
console.time('init') console.time('init')
const crypto = require('crypto') import crypto from 'crypto'
const util = require('util') import util from 'util'
const defaultAnnounceList = require('create-torrent').announceList import { announceList as defaultAnnounceList } from 'create-torrent'
const electron = require('electron') import electron from 'electron'
const fs = require('fs') import fs from 'fs'
const mm = require('music-metadata') import * as mm from 'music-metadata'
const networkAddress = require('network-address') import networkAddress from 'network-address'
const path = require('path') import path from 'path'
const WebTorrent = require('webtorrent') import WebTorrent from 'webtorrent'
const config = require('../config') import config from '../config.js'
const { TorrentKeyNotFoundError } = require('./lib/errors') import { TorrentKeyNotFoundError } from './lib/errors.js'
const torrentPoster = require('./lib/torrent-poster') import torrentPoster from './lib/torrent-poster.js'
// Send & receive messages from the main window const { ipcRenderer } = electron
const ipc = electron.ipcRenderer
// Force use of webtorrent trackers on all torrents // Force use of webtorrent trackers on all torrents
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList globalThis.WEBTORRENT_ANNOUNCE = defaultAnnounceList
.map((arr) => arr[0]) .map((arr) => arr[0])
.filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0) .filter((url) => url.indexOf('wss://') === 0 || url.indexOf('ws://') === 0)
/** /**
* WebTorrent version. * WebTorrent version.
*/ */
const VERSION = require('../../package.json').version const VERSION = JSON.parse(fs.readFileSync('package.json').toString()).version
/** /**
* Version number in Azureus-style. Generated from major and minor semver version. * Version number in Azureus-style. Generated from major and minor semver version.
@@ -68,29 +68,29 @@ init()
function init () { function init () {
listenToClientEvents() listenToClientEvents()
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) => ipcRenderer.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections)) startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
ipc.on('wt-stop-torrenting', (e, infoHash) => ipcRenderer.on('wt-stop-torrenting', (e, infoHash) =>
stopTorrenting(infoHash)) stopTorrenting(infoHash))
ipc.on('wt-create-torrent', (e, torrentKey, options) => ipcRenderer.on('wt-create-torrent', (e, torrentKey, options) =>
createTorrent(torrentKey, options)) createTorrent(torrentKey, options))
ipc.on('wt-save-torrent-file', (e, torrentKey) => ipcRenderer.on('wt-save-torrent-file', (e, torrentKey) =>
saveTorrentFile(torrentKey)) saveTorrentFile(torrentKey))
ipc.on('wt-generate-torrent-poster', (e, torrentKey) => ipcRenderer.on('wt-generate-torrent-poster', (e, torrentKey) =>
generateTorrentPoster(torrentKey)) generateTorrentPoster(torrentKey))
ipc.on('wt-get-audio-metadata', (e, infoHash, index) => ipcRenderer.on('wt-get-audio-metadata', (e, infoHash, index) =>
getAudioMetadata(infoHash, index)) getAudioMetadata(infoHash, index))
ipc.on('wt-start-server', (e, infoHash) => ipcRenderer.on('wt-start-server', (e, infoHash) =>
startServer(infoHash)) startServer(infoHash))
ipc.on('wt-stop-server', (e) => ipcRenderer.on('wt-stop-server', () =>
stopServer()) stopServer())
ipc.on('wt-select-files', (e, infoHash, selections) => ipcRenderer.on('wt-select-files', (e, infoHash, selections) =>
selectFiles(infoHash, selections)) selectFiles(infoHash, selections))
ipc.send('ipcReadyWebTorrent') ipcRenderer.send('ipcReadyWebTorrent')
window.addEventListener('error', (e) => window.addEventListener('error', (e) =>
ipc.send('wt-uncaught-error', { message: e.error.message, stack: e.error.stack }), ipcRenderer.send('wt-uncaught-error', { message: e.error.message, stack: e.error.stack }),
true) true)
setInterval(updateTorrentProgress, 1000) setInterval(updateTorrentProgress, 1000)
@@ -98,8 +98,8 @@ function init () {
} }
function listenToClientEvents () { function listenToClientEvents () {
client.on('warning', (err) => ipc.send('wt-warning', null, err.message)) client.on('warning', (err) => ipcRenderer.send('wt-warning', null, err.message))
client.on('error', (err) => ipc.send('wt-error', null, err.message)) client.on('error', (err) => ipcRenderer.send('wt-error', null, err.message))
} }
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. // Starts a given TorrentID, which can be an infohash, magnet URI, etc.
@@ -133,44 +133,44 @@ function createTorrent (torrentKey, options) {
const torrent = client.seed(paths, options) const torrent = client.seed(paths, options)
torrent.key = torrentKey torrent.key = torrentKey
addTorrentEvents(torrent) addTorrentEvents(torrent)
ipc.send('wt-new-torrent') ipcRenderer.send('wt-new-torrent')
} }
function addTorrentEvents (torrent) { function addTorrentEvents (torrent) {
torrent.on('warning', (err) => torrent.on('warning', (err) =>
ipc.send('wt-warning', torrent.key, err.message)) ipcRenderer.send('wt-warning', torrent.key, err.message))
torrent.on('error', (err) => torrent.on('error', (err) =>
ipc.send('wt-error', torrent.key, err.message)) ipcRenderer.send('wt-error', torrent.key, err.message))
torrent.on('infoHash', () => torrent.on('infoHash', () =>
ipc.send('wt-parsed', torrent.key, torrent.infoHash, torrent.magnetURI)) ipcRenderer.send('wt-parsed', torrent.key, torrent.infoHash, torrent.magnetURI))
torrent.on('metadata', torrentMetadata) torrent.on('metadata', torrentMetadata)
torrent.on('ready', torrentReady) torrent.on('ready', torrentReady)
torrent.on('done', torrentDone) torrent.on('done', torrentDone)
function torrentMetadata () { function torrentMetadata () {
const info = getTorrentInfo(torrent) const info = getTorrentInfo(torrent)
ipc.send('wt-metadata', torrent.key, info) ipcRenderer.send('wt-metadata', torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
} }
function torrentReady () { function torrentReady () {
const info = getTorrentInfo(torrent) const info = getTorrentInfo(torrent)
ipc.send('wt-ready', torrent.key, info) ipcRenderer.send('wt-ready', torrent.key, info)
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) ipcRenderer.send('wt-ready-' + torrent.infoHash, torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
} }
function torrentDone () { function torrentDone () {
const info = getTorrentInfo(torrent) const info = getTorrentInfo(torrent)
ipc.send('wt-done', torrent.key, info) ipcRenderer.send('wt-done', torrent.key, info)
updateTorrentProgress() updateTorrentProgress()
torrent.getFileModtimes(function (err, fileModtimes) { torrent.getFileModtimes((err, fileModtimes) => {
if (err) return onError(err) if (err) return onError(err)
ipc.send('wt-file-modtimes', torrent.key, fileModtimes) ipcRenderer.send('wt-file-modtimes', torrent.key, fileModtimes)
}) })
} }
} }
@@ -203,19 +203,19 @@ function saveTorrentFile (torrentKey) {
const torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent') const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
fs.access(torrentPath, fs.constants.R_OK, function (err) { fs.access(torrentPath, fs.constants.R_OK, err => {
const fileName = torrent.infoHash + '.torrent' const fileName = torrent.infoHash + '.torrent'
if (!err) { if (!err) {
// We've already saved the file // We've already saved the file
return ipc.send('wt-file-saved', torrentKey, fileName) return ipcRenderer.send('wt-file-saved', torrentKey, fileName)
} }
// Otherwise, save the .torrent file, under the app config folder // Otherwise, save the .torrent file, under the app config folder
fs.mkdir(config.TORRENT_PATH, { recursive: true }, function (_) { fs.mkdir(config.TORRENT_PATH, { recursive: true }, _ => {
fs.writeFile(torrentPath, torrent.torrentFile, function (err) { fs.writeFile(torrentPath, torrent.torrentFile, err => {
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err) if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
console.log('saved torrent file %s', torrentPath) console.log('saved torrent file %s', torrentPath)
return ipc.send('wt-file-saved', torrentKey, fileName) return ipcRenderer.send('wt-file-saved', torrentKey, fileName)
}) })
}) })
}) })
@@ -225,17 +225,17 @@ function saveTorrentFile (torrentKey) {
// Auto chooses either a frame from a video file, an image, etc // Auto chooses either a frame from a video file, an image, etc
function generateTorrentPoster (torrentKey) { function generateTorrentPoster (torrentKey) {
const torrent = getTorrent(torrentKey) const torrent = getTorrent(torrentKey)
torrentPoster(torrent, function (err, buf, extension) { torrentPoster(torrent, (err, buf, extension) => {
if (err) return console.log('error generating poster: %o', err) if (err) return console.log('error generating poster: %o', err)
// save it for next time // save it for next time
fs.mkdir(config.POSTER_PATH, { recursive: true }, function (err) { fs.mkdir(config.POSTER_PATH, { recursive: true }, err => {
if (err) return console.log('error creating poster dir: %o', err) if (err) return console.log('error creating poster dir: %o', err)
const posterFileName = torrent.infoHash + extension const posterFileName = torrent.infoHash + extension
const posterFilePath = path.join(config.POSTER_PATH, posterFileName) const posterFilePath = path.join(config.POSTER_PATH, posterFileName)
fs.writeFile(posterFilePath, buf, function (err) { fs.writeFile(posterFilePath, buf, err => {
if (err) return console.log('error saving poster: %o', err) if (err) return console.log('error saving poster: %o', err)
// show the poster // show the poster
ipc.send('wt-poster', torrentKey, posterFileName) ipcRenderer.send('wt-poster', torrentKey, posterFileName)
}) })
}) })
}) })
@@ -247,22 +247,20 @@ function updateTorrentProgress () {
if (prevProgress && util.isDeepStrictEqual(progress, prevProgress)) { if (prevProgress && util.isDeepStrictEqual(progress, prevProgress)) {
return /* don't send heavy object if it hasn't changed */ return /* don't send heavy object if it hasn't changed */
} }
ipc.send('wt-progress', progress) ipcRenderer.send('wt-progress', progress)
prevProgress = progress prevProgress = progress
} }
function getTorrentProgress () { function getTorrentProgress () {
// First, track overall progress // First, track overall progress
const progress = client.progress const progress = client.progress
const hasActiveTorrents = client.torrents.some(function (torrent) { const hasActiveTorrents = client.torrents.some(torrent => torrent.progress !== 1)
return torrent.progress !== 1
})
// Track progress for every file in each torrent // Track progress for every file in each torrent
// TODO: ideally this would be tracked by WebTorrent, which could do it // TODO: ideally this would be tracked by WebTorrent, which could do it
// more efficiently than looping over torrent.bitfield // more efficiently than looping over torrent.bitfield
const torrentProg = client.torrents.map(function (torrent) { const torrentProg = client.torrents.map(torrent => {
const fileProg = torrent.files && torrent.files.map(function (file, index) { const fileProg = torrent.files && torrent.files.map(file => {
const numPieces = file._endPiece - file._startPiece + 1 const numPieces = file._endPiece - file._startPiece + 1
let numPiecesPresent = 0 let numPiecesPresent = 0
for (let piece = file._startPiece; piece <= file._endPiece; piece++) { for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
@@ -302,12 +300,12 @@ function startServer (infoHash) {
else torrent.once('ready', () => startServerFromReadyTorrent(torrent)) else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
} }
function startServerFromReadyTorrent (torrent, cb) { function startServerFromReadyTorrent (torrent) {
if (server) return if (server) return
// start the streaming torrent-to-http server // start the streaming torrent-to-http server
server = torrent.createServer() server = torrent.createServer()
server.listen(0, function () { server.listen(0, () => {
const port = server.address().port const port = server.address().port
const urlSuffix = ':' + port const urlSuffix = ':' + port
const info = { const info = {
@@ -317,8 +315,8 @@ function startServerFromReadyTorrent (torrent, cb) {
networkAddress: networkAddress() networkAddress: networkAddress()
} }
ipc.send('wt-server-running', info) ipcRenderer.send('wt-server-running', info)
ipc.send('wt-server-' + torrent.infoHash, info) ipcRenderer.send('wt-server-' + torrent.infoHash, info)
}) })
} }
@@ -336,14 +334,14 @@ function getAudioMetadata (infoHash, index) {
// Set initial matadata to display the filename first. // Set initial matadata to display the filename first.
const metadata = { title: file.name } const metadata = { title: file.name }
ipc.send('wt-audio-metadata', infoHash, index, metadata) ipcRenderer.send('wt-audio-metadata', infoHash, index, metadata)
const options = { const options = {
native: false, native: false,
skipCovers: true, skipCovers: true,
fileSize: file.length, fileSize: file.length,
observer: event => { observer: () => {
ipc.send('wt-audio-metadata', infoHash, index, { ipcRenderer.send('wt-audio-metadata', infoHash, index, {
common: metadata.common, common: metadata.common,
format: metadata.format format: metadata.format
}) })
@@ -358,7 +356,7 @@ function getAudioMetadata (infoHash, index) {
onMetadata onMetadata
.then( .then(
metadata => { metadata => {
ipc.send('wt-audio-metadata', infoHash, index, metadata) ipcRenderer.send('wt-audio-metadata', infoHash, index, metadata)
console.log(`metadata for file='${file.name}' completed.`) console.log(`metadata for file='${file.name}' completed.`)
}, },
err => { err => {
@@ -387,7 +385,7 @@ function selectFiles (torrentOrInfoHash, selections) {
// selection with individual selections for each file, so we can // selection with individual selections for each file, so we can
// select/deselect files later on // select/deselect files later on
if (!selections) { if (!selections) {
selections = torrent.files.map((x) => true) selections = new Array(torrent.files.length).fill(true)
} }
// Selections specified incorrectly? // Selections specified incorrectly?
@@ -400,15 +398,15 @@ function selectFiles (torrentOrInfoHash, selections) {
torrent.deselect(0, torrent.pieces.length - 1, false) torrent.deselect(0, torrent.pieces.length - 1, false)
// Add selections (individual files) // Add selections (individual files)
for (let i = 0; i < selections.length; i++) { selections.forEach((selection, i) => {
const file = torrent.files[i] const file = torrent.files[i]
if (selections[i]) { if (selection) {
file.select() file.select()
} else { } else {
console.log('deselecting file ' + i + ' of torrent ' + torrent.name) console.log('deselecting file ' + i + ' of torrent ' + torrent.name)
file.deselect() file.deselect()
} }
} })
} }
// Gets a WebTorrent handle by torrentKey // Gets a WebTorrent handle by torrentKey
@@ -426,7 +424,7 @@ function onError (err) {
// TODO: remove this once the following bugs are fixed: // TODO: remove this once the following bugs are fixed:
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143 // https://bugs.chromium.org/p/chromium/issues/detail?id=490143
// https://github.com/electron/electron/issues/7212 // https://github.com/electron/electron/issues/7212
window.testOfflineMode = function () { window.testOfflineMode = () => {
console.log('Test, going OFFLINE') console.log('Test, going OFFLINE')
client = window.client = new WebTorrent({ client = window.client = new WebTorrent({
peerId: PEER_ID, peerId: PEER_ID,

View File

@@ -1,6 +1,5 @@
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
<% if (version) { %>Version=<%= version %><% } %>
Name=<%= productName %> Name=<%= productName %>
<% if (genericName) { %>GenericName=<%= genericName %><% } %> <% if (genericName) { %>GenericName=<%= genericName %><% } %>
<% if (description) { %>Comment=<%= description %><% } %> <% if (description) { %>Comment=<%= description %><% } %>
@@ -11,7 +10,7 @@ Actions=CreateNewTorrent;OpenTorrentFile;OpenTorrentAddress;
<% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;<% } %> <% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;<% } %>
<% if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;<% } %> <% if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;<% } %>
StartupNotify=true StartupNotify=true
<% if (name) { %>StartupWMClass=<%= name %> <% } %> StartupWMClass=<%= productName %>
[Desktop Action CreateNewTorrent] [Desktop Action CreateNewTorrent]
Name=Create New Torrent... Name=Create New Torrent...

View File

@@ -322,6 +322,10 @@ textarea,
* TORRENT LIST * TORRENT LIST
*/ */
.torrent-list {
height: 100%;
}
.torrent { .torrent {
background: linear-gradient(to bottom right, #4B79A1, #283E51); background: linear-gradient(to bottom right, #4B79A1, #283E51);
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -558,6 +562,13 @@ body.drag .app::after {
width: 100%; width: 100%;
} }
.player .name {
font-size: 20px;
font-weight: bold;
line-height: 1.5em;
margin-bottom: 6px;
}
/* /*
* PLAYER CONTROLS * PLAYER CONTROLS
*/ */

View File

@@ -8,6 +8,6 @@
</head> </head>
<body> <body>
<div id='body'></div> <div id='body'></div>
<script>require('../build/renderer/main.js')</script> <script type="module" src="../build/renderer/main.js"></script>
</body> </body>
</html> </html>