Compare commits
729 Commits
v0.12.0
...
fix-startu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8835c3054 | ||
|
|
2834cca9fa | ||
|
|
744e720c3b | ||
|
|
a6dfd98717 | ||
|
|
4550f09bf1 | ||
|
|
7289e32bea | ||
|
|
0bdbe66d96 | ||
|
|
e58e701e2c | ||
|
|
ac9b79c8ca | ||
|
|
280fbc1e8d | ||
|
|
a1d4f706f7 | ||
|
|
6e2ff6887d | ||
|
|
fe297d4c56 | ||
|
|
b16971491f | ||
|
|
4b1866d521 | ||
|
|
c539375adc | ||
|
|
ec8beda1ed | ||
|
|
cd96986445 | ||
|
|
975e57d57b | ||
|
|
08ad4f73f1 | ||
|
|
c9f9c868df | ||
|
|
e7f8f4ccfc | ||
|
|
e1b0c85778 | ||
|
|
2148d98f3a | ||
|
|
d06508fda6 | ||
|
|
1f780fc307 | ||
|
|
0178e9dcb8 | ||
|
|
c170c247a9 | ||
|
|
4bb3edbf8d | ||
|
|
9dbae636ae | ||
|
|
b6234f7705 | ||
|
|
101dd356ae | ||
|
|
f8e9db25cd | ||
|
|
8e33507839 | ||
|
|
4c4c3508c2 | ||
|
|
a7ec8ba04e | ||
|
|
b5dce351b0 | ||
|
|
59fa19d7ad | ||
|
|
619e5249ac | ||
|
|
48340010b1 | ||
|
|
4599752d9d | ||
|
|
5eec89cd42 | ||
|
|
dcecab2d9f | ||
|
|
767d31ebd8 | ||
|
|
1ece5de579 | ||
|
|
f34a79df36 | ||
|
|
f231c54886 | ||
|
|
56f0826928 | ||
|
|
caceeee238 | ||
|
|
afd3b84b40 | ||
|
|
4b927832c2 | ||
|
|
c5679d546e | ||
|
|
097fa9cc7c | ||
|
|
6f1a94359f | ||
|
|
44e2f366ce | ||
|
|
cf6b46364c | ||
|
|
85c6816c23 | ||
|
|
0c1d7d6e0f | ||
|
|
3f073e083e | ||
|
|
6ce7e662c6 | ||
|
|
1e6c067139 | ||
|
|
f53038f6e9 | ||
|
|
c61234dd4c | ||
|
|
6336bbd6e3 | ||
|
|
649ac1900b | ||
|
|
27e3d33f7d | ||
|
|
c29deab728 | ||
|
|
3a4ba6af37 | ||
|
|
e49d80fb17 | ||
|
|
a37f00bc10 | ||
|
|
925d495b62 | ||
|
|
809beb4f81 | ||
|
|
0359451498 | ||
|
|
f6bd2cfcf4 | ||
|
|
9eecc2410c | ||
|
|
6cd8af91be | ||
|
|
7a3c1f0a10 | ||
|
|
f2f3558cc4 | ||
|
|
0dfaf6c1cf | ||
|
|
a6ecba8e13 | ||
|
|
1a2c507d47 | ||
|
|
95b61826e0 | ||
|
|
b1da72fde8 | ||
|
|
b5e64a390a | ||
|
|
da829e0d69 | ||
|
|
fd02908afa | ||
|
|
38092fa055 | ||
|
|
24a7e060f2 | ||
|
|
bd962d4d76 | ||
|
|
a54dbc42e9 | ||
|
|
4c2381db26 | ||
|
|
91400ae31c | ||
|
|
c8f1e23b1a | ||
|
|
188f1cac8f | ||
|
|
b216929dc4 | ||
|
|
08008bb25c | ||
|
|
c96f212726 | ||
|
|
80c1a61c33 | ||
|
|
7925e04834 | ||
|
|
4da9da3d9a | ||
|
|
9916073a98 | ||
|
|
820776427b | ||
|
|
da76ff53ce | ||
|
|
9a657bdc10 | ||
|
|
df73ba2512 | ||
|
|
450237052c | ||
|
|
ffde524c85 | ||
|
|
2618548fbe | ||
|
|
0471d141c4 | ||
|
|
8b31c0376c | ||
|
|
10c23104a1 | ||
|
|
9ed3533225 | ||
|
|
97342951d0 | ||
|
|
46d50ccda2 | ||
|
|
290027b5c5 | ||
|
|
f6129444f7 | ||
|
|
57189c0d06 | ||
|
|
0aaeea4d3c | ||
|
|
f59a54782a | ||
|
|
4156a3f546 | ||
|
|
13edc02440 | ||
|
|
cb4fa201f0 | ||
|
|
1905f8fec9 | ||
|
|
d45e3f3e7b | ||
|
|
a039cbf348 | ||
|
|
c5a21bddfd | ||
|
|
c204b7a82c | ||
|
|
f475975b40 | ||
|
|
d3031ecff2 | ||
|
|
9a0a2111d3 | ||
|
|
d0053dc2f4 | ||
|
|
60e9bf461c | ||
|
|
6add81add8 | ||
|
|
a16b5b5add | ||
|
|
8b0b5f3738 | ||
|
|
07816f7470 | ||
|
|
103a5026f9 | ||
|
|
c7ef42a65e | ||
|
|
ccaf73a477 | ||
|
|
dae57b5507 | ||
|
|
5dc1ffb70e | ||
|
|
1dc500d4f3 | ||
|
|
d9bb625534 | ||
|
|
47cb6b9d5b | ||
|
|
b1eed5122a | ||
|
|
97801c4fef | ||
|
|
77d2abe04e | ||
|
|
8b78e29827 | ||
|
|
e6bfc57fc4 | ||
|
|
7facf4f6a2 | ||
|
|
79c25ffb33 | ||
|
|
24dca39177 | ||
|
|
d209c02886 | ||
|
|
c3315ab0fc | ||
|
|
109803e802 | ||
|
|
43ddbc7fd9 | ||
|
|
c8af0d3c1d | ||
|
|
7db9e920de | ||
|
|
c8b726c0d1 | ||
|
|
180acc15cd | ||
|
|
c3d261c74a | ||
|
|
0db89109a5 | ||
|
|
6ee0476799 | ||
|
|
d86da0b0f7 | ||
|
|
eb483ccd48 | ||
|
|
e7d5af5944 | ||
|
|
0e3962e3f6 | ||
|
|
992de4d80e | ||
|
|
85586d2dc5 | ||
|
|
21ee5566a7 | ||
|
|
429a376ccf | ||
|
|
a48c09e688 | ||
|
|
03de09fcaf | ||
|
|
97cf38e501 | ||
|
|
c24c99de36 | ||
|
|
a1ab889fca | ||
|
|
170fd28c55 | ||
|
|
cdf5c5364a | ||
|
|
2e85b4041f | ||
|
|
a56e14f0a0 | ||
|
|
16c3476960 | ||
|
|
982ef3ae97 | ||
|
|
6057751e99 | ||
|
|
4c715111c5 | ||
|
|
0d8e9792ff | ||
|
|
ba5ccdb5cb | ||
|
|
579ca908c6 | ||
|
|
c7d7148132 | ||
|
|
18ad5b0a01 | ||
|
|
cde3298920 | ||
|
|
20e3fb6f5f | ||
|
|
f0b87db248 | ||
|
|
2d90b29612 | ||
|
|
81e6d7f3a7 | ||
|
|
a676a20054 | ||
|
|
aef5669b76 | ||
|
|
ffb809bbca | ||
|
|
fa7d917d0d | ||
|
|
ab20dc42f1 | ||
|
|
02fefd0a81 | ||
|
|
788e907502 | ||
|
|
bb4eb523fe | ||
|
|
11b42e58c1 | ||
|
|
8dba9b93b6 | ||
|
|
d4a564efb5 | ||
|
|
98ce4bb1fb | ||
|
|
0ec8809de1 | ||
|
|
981ac464b5 | ||
|
|
f04017a068 | ||
|
|
1a2b18fcbb | ||
|
|
12d05b96a0 | ||
|
|
4ed4d8287f | ||
|
|
1239f7b871 | ||
|
|
8c2e29eaa2 | ||
|
|
9d060a66ec | ||
|
|
06566d37c2 | ||
|
|
6a50fe7ced | ||
|
|
d40449f2ed | ||
|
|
23e1ab6f21 | ||
|
|
f23e52c699 | ||
|
|
10b4450214 | ||
|
|
4b639ae476 | ||
|
|
77c497e984 | ||
|
|
a4ee465439 | ||
|
|
b6890bc780 | ||
|
|
109accdb54 | ||
|
|
87491351cc | ||
|
|
c2a38bd4e0 | ||
|
|
25282fc44a | ||
|
|
cbbc61d1e1 | ||
|
|
6ed7ec35a4 | ||
|
|
a5d946fe83 | ||
|
|
cf8c1654e8 | ||
|
|
715a7846f7 | ||
|
|
82ea41c1a1 | ||
|
|
934f497118 | ||
|
|
e0de30baff | ||
|
|
a1cfe8730b | ||
|
|
91fc7cab7c | ||
|
|
5cdb3d059c | ||
|
|
b010dc28d2 | ||
|
|
9b51ba443a | ||
|
|
47c08cc8f8 | ||
|
|
c1329938f5 | ||
|
|
2c07b1146c | ||
|
|
fc51b230c4 | ||
|
|
4d5ee7887e | ||
|
|
ad63ec8a7b | ||
|
|
2b0e2ca0fb | ||
|
|
217ca9a053 | ||
|
|
6bf38611e8 | ||
|
|
7251967ef5 | ||
|
|
2c9b305721 | ||
|
|
6eac7405fd | ||
|
|
4b51fb9255 | ||
|
|
a2e5390d30 | ||
|
|
03a6ec3466 | ||
|
|
228d68b488 | ||
|
|
1af4ddb618 | ||
|
|
a30eedbec9 | ||
|
|
2bc3ab6971 | ||
|
|
b1f5e7d7ec | ||
|
|
84d4e0f013 | ||
|
|
61648db0fc | ||
|
|
7b72dfafc2 | ||
|
|
d3da2af8a1 | ||
|
|
55a60018b5 | ||
|
|
6df718aa4a | ||
|
|
a3d62de091 | ||
|
|
eb4007aa87 | ||
|
|
4438c9c85f | ||
|
|
c01380a897 | ||
|
|
ff7c053442 | ||
|
|
4f342bb530 | ||
|
|
99a7c663b3 | ||
|
|
49f2bb2bdf | ||
|
|
9cfce5f72b | ||
|
|
5c8bd3422d | ||
|
|
670d6a0489 | ||
|
|
45d0d94ab4 | ||
|
|
4d864465d7 | ||
|
|
56327ca25e | ||
|
|
0aef294df9 | ||
|
|
7fb471feb1 | ||
|
|
e2d2420aa2 | ||
|
|
bcb3551a0a | ||
|
|
20faf71338 | ||
|
|
63d866a1d2 | ||
|
|
6d9874601a | ||
|
|
3cbf33b482 | ||
|
|
4b2a778708 | ||
|
|
f5102c3558 | ||
|
|
0fdc6b2d8b | ||
|
|
e34c18b751 | ||
|
|
4f41f5968c | ||
|
|
b48d2b1610 | ||
|
|
ceb5c3f11c | ||
|
|
0585c515b3 | ||
|
|
e2074aeb31 | ||
|
|
bc3cec1172 | ||
|
|
83e8bb1bd7 | ||
|
|
88fb6d86bd | ||
|
|
e8c39f2074 | ||
|
|
386b80a66f | ||
|
|
fbb3fa6a27 | ||
|
|
e10debed2f | ||
|
|
437dd3adb1 | ||
|
|
e48191381e | ||
|
|
abb7c72b84 | ||
|
|
20d9ea5a00 | ||
|
|
0da29e4eb2 | ||
|
|
10f92c78eb | ||
|
|
924dbeeebb | ||
|
|
3c363595ce | ||
|
|
695154099d | ||
|
|
8516da8a44 | ||
|
|
21edc05f19 | ||
|
|
cf729b0d31 | ||
|
|
74b806737d | ||
|
|
88cbcc2959 | ||
|
|
eab15876cc | ||
|
|
c6a7b7c1d2 | ||
|
|
d76cd628cf | ||
|
|
7f2c91b230 | ||
|
|
dab0802a88 | ||
|
|
34f06267b8 | ||
|
|
99062175eb | ||
|
|
f47055c51b | ||
|
|
4752c2e689 | ||
|
|
57c1a97ebf | ||
|
|
793ccebf0b | ||
|
|
c688313239 | ||
|
|
afd7f6ef0a | ||
|
|
cc78251846 | ||
|
|
30693d7d16 | ||
|
|
bd2fbe60db | ||
|
|
a114a22770 | ||
|
|
8d657d93e0 | ||
|
|
5802cd3591 | ||
|
|
2d17cfa7e1 | ||
|
|
cf2a3b8948 | ||
|
|
b89a74e6e9 | ||
|
|
9b74602119 | ||
|
|
34463d9084 | ||
|
|
3dcab9e78b | ||
|
|
fce0003912 | ||
|
|
c720f65fee | ||
|
|
a725726e25 | ||
|
|
e5a64d9550 | ||
|
|
0572f8a932 | ||
|
|
23d37d7da1 | ||
|
|
474654fd16 | ||
|
|
c0d70e91b0 | ||
|
|
070d3ff242 | ||
|
|
d5a62cb1a7 | ||
|
|
ba1f82faa0 | ||
|
|
0cce110c52 | ||
|
|
bc91cde244 | ||
|
|
30c9934694 | ||
|
|
f56f3c62e9 | ||
|
|
ac180a7cc5 | ||
|
|
d1f3a16b7f | ||
|
|
e6c495417b | ||
|
|
c301608881 | ||
|
|
7d2290a773 | ||
|
|
ba0d0b755e | ||
|
|
b5e9923469 | ||
|
|
b5bbd9b6a5 | ||
|
|
5bf7618dde | ||
|
|
d02f6c7d18 | ||
|
|
e69a18e548 | ||
|
|
991e2d2024 | ||
|
|
9f31f30925 | ||
|
|
340658c747 | ||
|
|
5bbd9cfabb | ||
|
|
03a8b40ee9 | ||
|
|
386bc950f4 | ||
|
|
d0cc157003 | ||
|
|
c990738c57 | ||
|
|
01971e2a46 | ||
|
|
08cb82e255 | ||
|
|
962c563f2e | ||
|
|
bd6f2c2faf | ||
|
|
ee8fe1e7ff | ||
|
|
424d171656 | ||
|
|
7351133542 | ||
|
|
84deb403eb | ||
|
|
cae40b44e6 | ||
|
|
ab0bbcea94 | ||
|
|
06a7eff0ab | ||
|
|
341333c287 | ||
|
|
0f00985b75 | ||
|
|
c11a86d849 | ||
|
|
52e16c33b4 | ||
|
|
54e15644b6 | ||
|
|
6aa3a6c660 | ||
|
|
83350b3b57 | ||
|
|
8868128d6e | ||
|
|
f7ec537cd5 | ||
|
|
a81156b363 | ||
|
|
4b501ab90b | ||
|
|
90347c72e0 | ||
|
|
a51ab3573e | ||
|
|
96f5f855a3 | ||
|
|
a82498ba16 | ||
|
|
3b05b52e57 | ||
|
|
b02d6f3cdb | ||
|
|
6e67cae494 | ||
|
|
23eef1a058 | ||
|
|
2ad3f6d6e2 | ||
|
|
996a81ddaa | ||
|
|
4a8ab6e1f6 | ||
|
|
7f817241ed | ||
|
|
a6c53d06d2 | ||
|
|
a70c4d1bf2 | ||
|
|
cdfb907c75 | ||
|
|
c2abb50e9e | ||
|
|
99f4fc96bf | ||
|
|
043f81996e | ||
|
|
213f2f124d | ||
|
|
96e7e83e20 | ||
|
|
e5fccd93a8 | ||
|
|
78e46f5576 | ||
|
|
69ce07fbf7 | ||
|
|
827131e136 | ||
|
|
c764bf4884 | ||
|
|
972203d675 | ||
|
|
2c382e14b5 | ||
|
|
c298950d34 | ||
|
|
00f2e5ccd6 | ||
|
|
8d86cffabd | ||
|
|
95d6ec5fdd | ||
|
|
9fbee6cfeb | ||
|
|
9892f88530 | ||
|
|
10ad990f97 | ||
|
|
0ee7e80fd2 | ||
|
|
7d1520f858 | ||
|
|
77ba258fbf | ||
|
|
06366529cc | ||
|
|
d1c263f1b0 | ||
|
|
d7a031a457 | ||
|
|
b7201f424d | ||
|
|
89f2785244 | ||
|
|
cffe416f50 | ||
|
|
d0eea34e9b | ||
|
|
4e82718788 | ||
|
|
293ca60e72 | ||
|
|
af4a6a5960 | ||
|
|
e01c0b6f43 | ||
|
|
efc984f1dd | ||
|
|
a046db40d2 | ||
|
|
9068909b4d | ||
|
|
4bb2056bc9 | ||
|
|
ed0f05abc4 | ||
|
|
0c44e10ca7 | ||
|
|
d8c9014471 | ||
|
|
7cf1d96a80 | ||
|
|
ffd9fbda57 | ||
|
|
d8904aaf6e | ||
|
|
de2db211cc | ||
|
|
e8ab172d1b | ||
|
|
32f96c03dd | ||
|
|
5158606740 | ||
|
|
0de80165ed | ||
|
|
dcaa99d2bf | ||
|
|
1a1a4cd5d0 | ||
|
|
4cbad1fccd | ||
|
|
f818564dd1 | ||
|
|
8be690a26e | ||
|
|
8081d42477 | ||
|
|
b0b6069fe2 | ||
|
|
994aed9af7 | ||
|
|
852fc86cbd | ||
|
|
8801a87a58 | ||
|
|
1e10f0083c | ||
|
|
bb40f0f11a | ||
|
|
78d7243a08 | ||
|
|
09724dddd9 | ||
|
|
59286ff3cb | ||
|
|
3f79c90868 | ||
|
|
33c48d4dfb | ||
|
|
48fbcd7303 | ||
|
|
cdb7b6eb44 | ||
|
|
90b72523b7 | ||
|
|
a1fd30f4f5 | ||
|
|
fcae064dbb | ||
|
|
5815d8efe7 | ||
|
|
0be8a73621 | ||
|
|
76e1d7777c | ||
|
|
e10a84b294 | ||
|
|
5ff2d893b9 | ||
|
|
02f5dbb63f | ||
|
|
d4cfc32c8d | ||
|
|
d5820063a1 | ||
|
|
21de048738 | ||
|
|
06cdac8121 | ||
|
|
f1aa1bdd59 | ||
|
|
44f621b4de | ||
|
|
fd1a1f0f7e | ||
|
|
a38b5220ac | ||
|
|
428a07101a | ||
|
|
f1ef9daa8f | ||
|
|
46d5c9c9e9 | ||
|
|
ecd877551e | ||
|
|
76e071e965 | ||
|
|
5623c1024e | ||
|
|
1a20155f66 | ||
|
|
9a17313a0c | ||
|
|
b3ec61ddd8 | ||
|
|
7dcddf90e9 | ||
|
|
a94fdcae61 | ||
|
|
f9bb83815f | ||
|
|
dccaf16a02 | ||
|
|
f4ee831319 | ||
|
|
5bcf1c6379 | ||
|
|
d2eb87e821 | ||
|
|
be08925eb4 | ||
|
|
5698c22e75 | ||
|
|
2114532f62 | ||
|
|
815ba00a6b | ||
|
|
bcd9f046fb | ||
|
|
bf3b416662 | ||
|
|
736832c4e3 | ||
|
|
f08c0995a2 | ||
|
|
de8a4d1160 | ||
|
|
739c1f705e | ||
|
|
a32889291f | ||
|
|
1e7e4cafd4 | ||
|
|
d5bed6c50a | ||
|
|
8f827c9aae | ||
|
|
24fe033e2f | ||
|
|
85d04f931b | ||
|
|
41b111c8a8 | ||
|
|
72db60bb12 | ||
|
|
076eb009b9 | ||
|
|
789bd0ce82 | ||
|
|
5155fca0e4 | ||
|
|
3b6819f894 | ||
|
|
0dd1683298 | ||
|
|
2788d7433b | ||
|
|
504a2419f6 | ||
|
|
75e5316ba1 | ||
|
|
3be018521a | ||
|
|
6d375d5b5b | ||
|
|
5de39bd7e5 | ||
|
|
a08d576851 | ||
|
|
b8bdf65514 | ||
|
|
1c0c3d07ff | ||
|
|
832980eb9a | ||
|
|
7c158e9f2c | ||
|
|
a98d22ed72 | ||
|
|
63b7d34e29 | ||
|
|
67ae6061aa | ||
|
|
a8a861260e | ||
|
|
fc879d5801 | ||
|
|
2d2645e642 | ||
|
|
8e66f641ce | ||
|
|
82c49b5fc5 | ||
|
|
b57ee73035 | ||
|
|
ed8f493b8b | ||
|
|
9e853027da | ||
|
|
1e05487acd | ||
|
|
167da9dfd5 | ||
|
|
46e138a376 | ||
|
|
853db922f1 | ||
|
|
7bf51b11ee | ||
|
|
3a286ae978 | ||
|
|
82245f0b5c | ||
|
|
802a898394 | ||
|
|
2200fffa1e | ||
|
|
f4b2e78e72 | ||
|
|
ad1162c7de | ||
|
|
ed4daeb560 | ||
|
|
927ae16e4f | ||
|
|
917c89542b | ||
|
|
39570bd4d7 | ||
|
|
f368dfad81 | ||
|
|
205e17cc83 | ||
|
|
4e052c8059 | ||
|
|
0afecd6063 | ||
|
|
41511c5615 | ||
|
|
b9d39e3c64 | ||
|
|
c1f482a950 | ||
|
|
e424031ad9 | ||
|
|
f43dc2fc98 | ||
|
|
9cdc73edce | ||
|
|
3d254fa075 | ||
|
|
ed1e43015e | ||
|
|
e6cbbd73f0 | ||
|
|
67dff7b38c | ||
|
|
ced67176a3 | ||
|
|
00ac8afe64 | ||
|
|
a6964c4495 | ||
|
|
aedbc3c32f | ||
|
|
6541291e0d | ||
|
|
711d274398 | ||
|
|
6c5861b9fc | ||
|
|
f7ab27f9fd | ||
|
|
e4e789cc5b | ||
|
|
09b525fe58 | ||
|
|
9dabfc1367 | ||
|
|
bdb733352a | ||
|
|
75a4655a0f | ||
|
|
051c1516a0 | ||
|
|
62c5b78358 | ||
|
|
290913d07a | ||
|
|
77534d650a | ||
|
|
a4c715e3f6 | ||
|
|
7415d3cee5 | ||
|
|
e1ba9c89fe | ||
|
|
0fcbe7369a | ||
|
|
c8087b5b63 | ||
|
|
065faca8eb | ||
|
|
bcd6a38a05 | ||
|
|
fa67f9b82b | ||
|
|
39acd0bd47 | ||
|
|
c549fcfc27 | ||
|
|
45e838d4c3 | ||
|
|
64f49e4d4f | ||
|
|
61caa90901 | ||
|
|
3e85289318 | ||
|
|
a629f287f0 | ||
|
|
3a4906079b | ||
|
|
3edf21f457 | ||
|
|
785c44cd2a | ||
|
|
1ad8a5313b | ||
|
|
967e161288 | ||
|
|
fe8c3b190c | ||
|
|
993e7d77ad | ||
|
|
e0be052df4 | ||
|
|
d331bae548 | ||
|
|
d88229694a | ||
|
|
8da5b955d6 | ||
|
|
54882679c1 | ||
|
|
f2007be1b0 | ||
|
|
7a757f9e05 | ||
|
|
678f961110 | ||
|
|
f0464c44fd | ||
|
|
b323ee24c4 | ||
|
|
38aad2ee9c | ||
|
|
b69ca93d20 | ||
|
|
e21a039e70 | ||
|
|
11f8e428a0 | ||
|
|
704455c432 | ||
|
|
c25bee755c | ||
|
|
373d598c29 | ||
|
|
b8effffa96 | ||
|
|
ca6a7917ce | ||
|
|
033bdf7908 | ||
|
|
6fe03aa325 | ||
|
|
b93f41f564 | ||
|
|
3f6cc97a02 | ||
|
|
0bda5358bd | ||
|
|
e0af554caa | ||
|
|
b98f8476f5 | ||
|
|
30732305ff | ||
|
|
14102ab3e6 | ||
|
|
df16b14586 | ||
|
|
9dcbc1b1f6 | ||
|
|
d806fd502f | ||
|
|
db650caf18 | ||
|
|
2b6c9ffcdb | ||
|
|
1d4b8ab67d | ||
|
|
6404168bee | ||
|
|
6613366cff | ||
|
|
74349129f4 | ||
|
|
cdc2c1d718 | ||
|
|
f8cc155650 | ||
|
|
b6bdeab50b | ||
|
|
f528f6033f | ||
|
|
ef1bc13c38 | ||
|
|
2f54feac74 | ||
|
|
44a0f760de | ||
|
|
75d30baaa5 | ||
|
|
990fb57839 | ||
|
|
1883341ddb | ||
|
|
5f7cece6d1 | ||
|
|
1e5ac1df9c | ||
|
|
4cdc3085ff | ||
|
|
e483263d70 | ||
|
|
bd41bd4db8 | ||
|
|
89e77d34f4 | ||
|
|
5eeb8fd6fc | ||
|
|
808fca031a | ||
|
|
6b3c1e3802 | ||
|
|
f488ef7597 | ||
|
|
2c179c7465 | ||
|
|
f1cf37200e | ||
|
|
d2da6881d6 | ||
|
|
9c25de58de | ||
|
|
f9aeb676b4 | ||
|
|
396d769bc8 | ||
|
|
83901eecba | ||
|
|
019728cff5 | ||
|
|
a96241d151 | ||
|
|
ebc9771be5 | ||
|
|
0c75bac364 | ||
|
|
3f7e2c1e4a | ||
|
|
10bf38bdf0 | ||
|
|
02508d7d9e | ||
|
|
5cb295f722 | ||
|
|
b08d273996 | ||
|
|
1e27d1803a | ||
|
|
9bc018cc02 | ||
|
|
73cdfc6d45 | ||
|
|
1afd650997 | ||
|
|
9c8eabb46c | ||
|
|
b39884e68f | ||
|
|
451d457426 | ||
|
|
82853aa017 | ||
|
|
157226f75b | ||
|
|
509691a85a | ||
|
|
8b3aee7e2d | ||
|
|
4025e669eb | ||
|
|
1a01d7ed92 | ||
|
|
b4976d27f2 | ||
|
|
173d8444d7 | ||
|
|
aa150b76a5 | ||
|
|
e2b5e28e07 | ||
|
|
1ad07d9977 | ||
|
|
8ba4056894 | ||
|
|
9ad0316dff | ||
|
|
854aae7dc5 | ||
|
|
5b021cd42e | ||
|
|
33417d9b7e | ||
|
|
275184214a | ||
|
|
1f9adbd3cf | ||
|
|
092c207dce | ||
|
|
603c24faed |
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github: feross
|
||||||
|
custom: https://paypal.me/borewit
|
||||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- DO NOT POST LINKS OR REFERENCES TO COPYRIGHTED CONTENT IN YOUR ISSUE. -->
|
||||||
|
|
||||||
**What version of WebTorrent Desktop?** (See the 'About WebTorrent' menu)
|
**What version of WebTorrent Desktop?** (See the 'About WebTorrent' menu)
|
||||||
|
|
||||||
**What operating system and version?**
|
**What operating system and version?**
|
||||||
|
|||||||
15
.github/config.yml
vendored
Normal file
15
.github/config.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# ProBot Request Info (https://probot.github.io/apps/request-info/)
|
||||||
|
|
||||||
|
requestInfoReplyComment: >
|
||||||
|
👋 We would appreciate it if you could provide us with more information about this
|
||||||
|
issue. <br><br> 
|
||||||
|
requestInfoLabelToAdd: 'need more info'
|
||||||
|
|
||||||
|
# ProBot Welcome (https://probot.github.io/apps/welcome/)
|
||||||
|
|
||||||
|
newPRWelcomeComment: >
|
||||||
|
🙌 Thanks for opening this pull request! You're awesome.
|
||||||
|
<br><br> 
|
||||||
|
firstPRMergeComment: >
|
||||||
|
🎉 Congrats on getting your first pull request landed! <br><br>
|
||||||
|

|
||||||
4
.github/lock.yml
vendored
Normal file
4
.github/lock.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# ProBot Lock (https://probot.github.io/apps/lock/)
|
||||||
|
|
||||||
|
daysUntilLock: 365
|
||||||
|
lockComment: false
|
||||||
9
.github/no-response.yml
vendored
Normal file
9
.github/no-response.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
||||||
|
|
||||||
|
daysUntilClose: 14
|
||||||
|
responseRequiredLabel: 'need more info'
|
||||||
|
closeComment: >
|
||||||
|
This issue has been automatically closed because there was no response to a
|
||||||
|
request for more information from the issue opener. Please leave a comment or
|
||||||
|
open a new issue if you have additional information related to this issue.
|
||||||
|
<br><br> 
|
||||||
17
.github/stale.yml
vendored
Normal file
17
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||||
|
|
||||||
|
daysUntilStale: 90
|
||||||
|
daysUntilClose: 14
|
||||||
|
exemptLabels:
|
||||||
|
- accepted
|
||||||
|
- blocked
|
||||||
|
- bug
|
||||||
|
- enhancement
|
||||||
|
- greenkeeper
|
||||||
|
- 'help wanted'
|
||||||
|
- meta
|
||||||
|
staleLabel: stale
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs.
|
||||||
|
closeComment: false
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules/
|
||||||
build
|
build/
|
||||||
dist
|
dist/
|
||||||
|
npm-debug.log*
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- 'node'
|
- lts/*
|
||||||
install: npm install standard
|
|
||||||
|
|||||||
40
AUTHORS.md
40
AUTHORS.md
@@ -10,7 +10,6 @@
|
|||||||
- Romain Beaumont (romain.rom1@gmail.com)
|
- Romain Beaumont (romain.rom1@gmail.com)
|
||||||
- Dan Flettre (fletd01@yahoo.com)
|
- Dan Flettre (fletd01@yahoo.com)
|
||||||
- Liam Gray (liam.r.gray@gmail.com)
|
- Liam Gray (liam.r.gray@gmail.com)
|
||||||
- grunjol (grunjol@users.noreply.github.com)
|
|
||||||
- Rémi Jouannet (remijouannet@users.noreply.github.com)
|
- Rémi Jouannet (remijouannet@users.noreply.github.com)
|
||||||
- Evan Miller (miller.evan815@gmail.com)
|
- Evan Miller (miller.evan815@gmail.com)
|
||||||
- Alex (alxmorais8@msn.com)
|
- Alex (alxmorais8@msn.com)
|
||||||
@@ -28,5 +27,44 @@
|
|||||||
- Adam Gotlib (gotlib.adam+dev@gmail.com)
|
- Adam Gotlib (gotlib.adam+dev@gmail.com)
|
||||||
- Rémi Jouannet (remijouannet@gmail.com)
|
- Rémi Jouannet (remijouannet@gmail.com)
|
||||||
- Andrea Tupini (tupini07@gmail.com)
|
- Andrea Tupini (tupini07@gmail.com)
|
||||||
|
- grunjol (grunjol@gmail.com)
|
||||||
|
- Jason Kurian (jasonk92@gmail.com)
|
||||||
|
- Vamsi Krishna Avula (vamsi_ism@outlook.com)
|
||||||
|
- Noam Okman (noamokman@gmail.com)
|
||||||
|
- PurgingPanda (t3ch0wn3r@gmail.com)
|
||||||
|
- Kai Curtis (morecode@kcurtis.com)
|
||||||
|
- Omri Litov (omrilitov@gmail.com)
|
||||||
|
- Alexey Romanov (romanalexey@gmail.com)
|
||||||
|
- Karan Thakkar (karanjthakkar@gmail.com)
|
||||||
|
- Nuno Campos (nuno.campos@me.com)
|
||||||
|
- Ebrahim Byagowi (ebrahim@gnu.org)
|
||||||
|
- Josip Janzic (josip@jjanzic.com)
|
||||||
|
- Emil Bay (github@tixz.dk)
|
||||||
|
- Borewit (borewit@users.noreply.github.com)
|
||||||
|
- greenkeeper[bot] (greenkeeper[bot]@users.noreply.github.com)
|
||||||
|
- Auyer (rafa_auyer@icloud.com)
|
||||||
|
- Jon Koops (jonkoops@gmail.com)
|
||||||
|
- Michael George Attard (michaelgeorgeattard@gmail.com)
|
||||||
|
- SimplyAhmazing (ahmad19526@gmail.com)
|
||||||
|
- Cezar Carneiro (cezargcarneiro@gmail.com)
|
||||||
|
- Bilal Elmoussaoui (bil.elmoussaoui@gmail.com)
|
||||||
|
- Terry Hau (terryhau@gmail.com)
|
||||||
|
- Vítor Galvão (info@vitorgalvao.com)
|
||||||
|
- Borewit (Borewit@users.noreply.github.com)
|
||||||
|
- Diego Rodríguez (diegorbaquero@gmail.com)
|
||||||
|
- Dan Flettre (flettre@gmail.com)
|
||||||
|
- Sibiraj (dev.sibiraj@outlook.com)
|
||||||
|
- clujin (clujin@gmail.com)
|
||||||
|
- Linus Unnebäck (linus@folkdatorn.se)
|
||||||
|
- Adrian Tombu (adrian@otso.fr)
|
||||||
|
- Lucas (5874806+RecoX@users.noreply.github.com)
|
||||||
|
- David Ernst (dsernst@users.noreply.github.com)
|
||||||
|
- David Ernst (git@dsernst.com)
|
||||||
|
- Jimmy Wärting (jimmy@warting.se)
|
||||||
|
- Recox (5874806+RecoX@users.noreply.github.com)
|
||||||
|
- greenkeeper[bot] (23040076+greenkeeper[bot]@users.noreply.github.com)
|
||||||
|
- hicom150 (hicom150@gmail.com)
|
||||||
|
- Jimmy Wärting (jimmy@warting.se)
|
||||||
|
- Feross (feross@feross.org)
|
||||||
|
|
||||||
#### Generated by bin/update-authors.sh.
|
#### Generated by bin/update-authors.sh.
|
||||||
|
|||||||
248
CHANGELOG.md
248
CHANGELOG.md
@@ -1,21 +1,220 @@
|
|||||||
# WebTorrent Desktop Version History
|
# WebTorrent Desktop Version History
|
||||||
|
|
||||||
|
## v0.21.0 - 2019-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add YouTube style hotkeys [\#1579](https://github.com/webtorrent/webtorrent-desktop/pull/1579) ([dsernst](https://github.com/dsernst))
|
||||||
|
- Toggle sound notifications on/off [\#1536](https://github.com/webtorrent/webtorrent-desktop/pull/1536) ([adriantombu](https://github.com/adriantombu))
|
||||||
|
- Ability to play MPEG-4 Audio Book \(.m4b\) [\#1450](https://github.com/webtorrent/webtorrent-desktop/pull/1450) ([Borewit](https://github.com/Borewit))
|
||||||
|
- Add support for subtitles on Chromecast [\#1165](https://github.com/webtorrent/webtorrent-desktop/pull/1165) ([janza](https://github.com/janza))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Update to Electron 4 [\#1590](https://github.com/webtorrent/webtorrent-desktop/pull/1590) ([Borewit](https://github.com/Borewit))
|
||||||
|
- Remove '\(BETA\)' from app window title [\#1562](https://github.com/webtorrent/webtorrent-desktop/pull/1562) ([dsernst](https://github.com/dsernst))
|
||||||
|
- Update React (v16) and Material-UI (v0.20) [\#1483](https://github.com/webtorrent/webtorrent-desktop/pull/1483) ([mathiasvr](https://github.com/mathiasvr))
|
||||||
|
- Show audio track and disk number [\#1454](https://github.com/webtorrent/webtorrent-desktop/pull/1454) ([Borewit](https://github.com/Borewit))
|
||||||
|
- Asynchronous music metadata updates while streaming [\#1449](https://github.com/webtorrent/webtorrent-desktop/pull/1449) ([Borewit](https://github.com/Borewit))
|
||||||
|
- If torrent is not private, leave private flag unset [\#1411](https://github.com/webtorrent/webtorrent-desktop/pull/1411) ([feross](https://github.com/feross))
|
||||||
|
- Improve audio poster selection: [\#1368](https://github.com/webtorrent/webtorrent-desktop/pull/1368) ([Borewit](https://github.com/Borewit))
|
||||||
|
- Save preferences immediately when changed [\#1042](https://github.com/webtorrent/webtorrent-desktop/pull/1042) ([Flet](https://github.com/Flet))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Ensure that the minutes field in playback indicator is zero-padded. [\#1506](https://github.com/webtorrent/webtorrent-desktop/pull/1506) ([bnjmnt4n](https://github.com/bnjmnt4n))
|
||||||
|
- Bug Fix: Empty Array Reduce [\#1494](https://github.com/webtorrent/webtorrent-desktop/pull/1494) ([clujin](https://github.com/clujin))
|
||||||
|
- Fix startup problems [\#1419](https://github.com/webtorrent/webtorrent-desktop/pull/1419) ([Borewit](https://github.com/Borewit))
|
||||||
|
- Add back loading spinner for player page. [\#1311](https://github.com/webtorrent/webtorrent-desktop/pull/1311) ([bnjmnt4n](https://github.com/bnjmnt4n))
|
||||||
|
- Fix Linux desktop file [\#1309](https://github.com/webtorrent/webtorrent-desktop/pull/1309) ([bilelmoussaoui](https://github.com/bilelmoussaoui))
|
||||||
|
|
||||||
|
|
||||||
|
## v0.20.0 - 2018-04-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for additional audio extensions: 'aiff', 'ape', 'mp2', 'oga', 'opus', 'wma' (#1240)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Displaying filename while music metadata is being downloaded (#1361)
|
||||||
|
- Improved the poster selection for audio/music based torrents (#1334)
|
||||||
|
- Launch VLC player without the `--video-on-top` flag (#1286)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix silently failing to open magnets links on Linux (#1367)
|
||||||
|
|
||||||
|
## v0.19.0 - 2018-01-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added watch folder feature: Automatically add new torrent files added to a folder on disk (#1154)
|
||||||
|
- Added highest playback priority feature: pauses other active torrents when playback starts (#840)
|
||||||
|
- Add 'Start Speaking' and 'Stop Speaking' menu item (Mac) (#439)
|
||||||
|
- Add pinch-to-zoom gesture to enter/exit fullscreen (#1148)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [SECURITY] Mitigate Electron protocol handler issue (Windows)
|
||||||
|
- Moved project from Feross's GitHub account to the WebTorrent GitHub organization
|
||||||
|
- Updated to electron@1.6.16
|
||||||
|
- Updated to material-ui@0.17
|
||||||
|
- Treat .FLAC as playable audio (#1127)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix time and duration so it doesn't bounce in the UI (#1233)
|
||||||
|
- Fix 'About WebTorrent' menu location on Windows (#1120)
|
||||||
|
|
||||||
|
## v0.18.0 - 2017-02-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add a new "Transfers" menu for pausing or resuming all torrents (#1027)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Update Electron to 1.4.15
|
||||||
|
- Windows 32-bit: App can use 4GB of memory instead of just 2GB
|
||||||
|
- Fix "Portable App" writing crash reports to "%APPDATA%\Temp" (Windows)
|
||||||
|
- Updated WebTorrent engine to 0.98.5
|
||||||
|
- Fix issue where http web seeds would sometimes stall
|
||||||
|
- Don't send 'completed' event to tracker again if torrent is already complete
|
||||||
|
- Add more peer ID entropy
|
||||||
|
- Set user-agent header for tracker http requests
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix paste shortcut in tracker list on Create Torrent page (#1112)
|
||||||
|
- Auto-focus the 'OK' button in modal dialogs (#1058)
|
||||||
|
- Fix formatting issue in the speed stats on the Player page (#1039)
|
||||||
|
|
||||||
|
## v0.17.2 - 2016-10-10
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Windows: Fix impossible-to-delete "Wired CD" default torrent
|
||||||
|
- Throttle browser-window 'move' and 'resize' events
|
||||||
|
- Fix crash ("Cannot read property 'files' of null" error)
|
||||||
|
- Fix crash ("TypeError: Cannot read property 'startPiece' of undefined")
|
||||||
|
|
||||||
|
## v0.17.1 - 2016-10-03
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Faster startup (improved by ~25%)
|
||||||
|
- Update Electron to 1.4.2
|
||||||
|
- Remove support for pasting multiple newline-separated magnet links
|
||||||
|
- Reduce UX sound volume
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix external player (VLC, etc.) opening before HTTP server was ready
|
||||||
|
- Windows (Portable App): Fix "Portable App" mode
|
||||||
|
- Write application support files to the "Portable Settings" folder
|
||||||
|
- Stop writing Electron "single instance" lock file to "%APPDATA%\Roaming\WebTorrent"
|
||||||
|
- Some temp data is still written to "%APPDATA%\Temp" (will be fixed in future version)
|
||||||
|
- Don't show pointer cursor on torrent list checkbox
|
||||||
|
- Trim extra whitespace from magnet links pasted into "Open Torrent Address" dialog
|
||||||
|
- Fix weird outline on 'Create Torrent' button
|
||||||
|
|
||||||
|
## v0.17.0 - 2016-09-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Remember window size and position
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Torrent list redesign
|
||||||
|
- Quieter, more subtle sounds
|
||||||
|
- Got rid of the play button spinner, now goes to the player immediately
|
||||||
|
- Faster startup
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix bug where playback rate could go negative
|
||||||
|
- Don't hide header when moused over player controls
|
||||||
|
- Fix Delete Data File on Windows
|
||||||
|
- Fix a sad, sad bug that resulted in 100+ MB config files
|
||||||
|
- Fix app DMG background image
|
||||||
|
|
||||||
|
## v0.16.0 - 2016-09-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Windows 64-bit support!** (#931)
|
||||||
|
- Existing 32-bit users will update to 64-bit automatically in next release
|
||||||
|
- 64-bit reduces likelihood of out-of-memory errors by increasing the address space
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Mac: Fix background image on .DMG
|
||||||
|
|
||||||
|
## v0.15.0 - 2016-09-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Option to start automatically on login
|
||||||
|
- Add integration tests
|
||||||
|
- Add more detailed telemetry to diagnose "buffer allocation failed"
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Disable playback controls while in external player (#909)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix several uncaught errors (#889, #891, #892)
|
||||||
|
- Update to the latest webtorrent.js, fixing some more uncaught errors
|
||||||
|
- Clicking on the "torrent finished" notification works again (#912)
|
||||||
|
|
||||||
|
## v0.14.0 - 2016-09-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Autoplay through all files in a torrent (#871)
|
||||||
|
- Torrents now have a progress bar (#844)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Modals now use Material UI
|
||||||
|
- Torrent list style improvements
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix App.js crash in Linux (#882)
|
||||||
|
- Fix error on Windows caused by `setBadge` (#867)
|
||||||
|
- Don't crash when restarting after adding a magnet link (#869)
|
||||||
|
- Restore playback state when reopening player (#877)
|
||||||
|
|
||||||
|
## v0.13.1 - 2016-08-31
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed the Create Torrent page
|
||||||
|
|
||||||
|
## v0.13.0 - 2016-08-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Support .m4a audio
|
||||||
|
- Better telemetry: log error versions, report more types of errors
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- New look - Material UI. Rewrote Create Torrent and Preferences pages.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed telemetry [object Object] and [object HTMLMediaElement] bugs
|
||||||
|
- Don't render player controls when playing externally, eg in VLC
|
||||||
|
- Don't play notification sounds during media playback
|
||||||
|
|
||||||
|
## v0.12.0 - 2016-08-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Custom external media player
|
||||||
|
- Linux: add system-wide launcher and icons for Debian, including Ubuntu
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Telemetry improvements: redact stacktraces, log app version
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix playback and download of default torrents ("missing path" error) (#804)
|
||||||
|
- Fix Delete Torrent + Data for newly added magnet links
|
||||||
|
- Fix jumpToTime error (#804)
|
||||||
|
|
||||||
## v0.11.0 - 2016-08-19
|
## v0.11.0 - 2016-08-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- New Preference to "Set WebTorrent as default handler for torrents and magnet links" (#771)
|
- New Preference to "Set WebTorrent as default handler for torrents and magnet links" (#771)
|
||||||
- New Preference to "Always play in VLC" (#674)
|
- New Preference to "Always play in VLC" (#674)
|
||||||
- Check for missing default download path and torrent folders on start up (#776)
|
- Check for missing default download path and torrent folders on start up (#776)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Do not automatically set WebTorrent as the default handler for torrents (#771)
|
- Do not automatically set WebTorrent as the default handler for torrents (#771)
|
||||||
- Torrents can only be created from the home screen (#770)
|
- Torrents can only be created from the home screen (#770)
|
||||||
- Update Electron to 1.3.3 (#772)
|
- Update Electron to 1.3.3 (#772)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Allow modifying the default tracker list on the Create Torrent page (#775)
|
- Allow modifying the default tracker list on the Create Torrent page (#775)
|
||||||
- Prevent opening multiple stacked Preference windows or Create Torrent windows (#770)
|
- Prevent opening multiple stacked Preference windows or Create Torrent windows (#770)
|
||||||
- Windows: Player window auto-resize does not match video aspect ratio (#565)
|
- Windows: Player window auto-resize does not match video aspect ratio (#565)
|
||||||
@@ -24,13 +223,11 @@
|
|||||||
## v0.10.0 - 2016-08-05
|
## v0.10.0 - 2016-08-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Drag-and-drop magnet links (selected text) is now supported (#284)
|
- Drag-and-drop magnet links (selected text) is now supported (#284)
|
||||||
- Windows: Add "User Tasks" shortcuts to app icon in Start Menu (#114)
|
- Windows: Add "User Tasks" shortcuts to app icon in Start Menu (#114)
|
||||||
- Linux: Show badge count for completed torrent downloads
|
- Linux: Show badge count for completed torrent downloads
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Change WebTorrent Desktop peer ID prefix to 'WD' to distinguish from WebTorrent in the browser, 'WW' (#688)
|
- Change WebTorrent Desktop peer ID prefix to 'WD' to distinguish from WebTorrent in the browser, 'WW' (#688)
|
||||||
- Switch UI to React to improve UI rendering speed (#729)
|
- Switch UI to React to improve UI rendering speed (#729)
|
||||||
- The primary bottleneck was actually `hyperx`, not `virtual-dom`.
|
- The primary bottleneck was actually `hyperx`, not `virtual-dom`.
|
||||||
@@ -47,7 +244,6 @@
|
|||||||
- Location history abstraction released independently as [`location-history`](https://www.npmjs.com/package/location-history)
|
- Location history abstraction released independently as [`location-history`](https://www.npmjs.com/package/location-history)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- When streaming to VLC, set VLC window title to torrent file name (#746)
|
- When streaming to VLC, set VLC window title to torrent file name (#746)
|
||||||
- Fix "Cannot read property 'numPiecesPresent' of undefined" exception (#695)
|
- Fix "Cannot read property 'numPiecesPresent' of undefined" exception (#695)
|
||||||
- Fix rare case where config file could not be completely written (#733)
|
- Fix rare case where config file could not be completely written (#733)
|
||||||
@@ -55,35 +251,29 @@
|
|||||||
## v0.9.0 - 2016-07-20
|
## v0.9.0 - 2016-07-20
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Save selected subtitles
|
- Save selected subtitles
|
||||||
- Ask for confirmation before deleting torrents
|
- Ask for confirmation before deleting torrents
|
||||||
- Support Debian Jessie
|
- Support Debian Jessie
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Only send telemetry in production
|
- Only send telemetry in production
|
||||||
- Clean up the code. Split main.js, refactor lots of things
|
- Clean up the code. Split main.js, refactor lots of things
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix state.playing.jumpToTime behavior
|
- Fix state.playing.jumpToTime behavior
|
||||||
- Remove torrent file and poster image when deleting a torrent
|
- Remove torrent file and poster image when deleting a torrent
|
||||||
|
|
||||||
## v0.8.1 - 2016-06-24
|
## v0.8.1 - 2016-06-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- New URI handler: stream-magnet
|
- New URI handler: stream-magnet
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- DLNA crashing bug
|
- DLNA crashing bug
|
||||||
|
|
||||||
## v0.8.0 - 2016-06-23
|
## v0.8.0 - 2016-06-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Cast menu: choose which Chromecast, Airplay, or DLNA device you want to use
|
- Cast menu: choose which Chromecast, Airplay, or DLNA device you want to use
|
||||||
- Telemetry: send basic data, plus stats on how often the play button works
|
- Telemetry: send basic data, plus stats on how often the play button works
|
||||||
- Make posters from jpeg files, not just jpg
|
- Make posters from jpeg files, not just jpg
|
||||||
@@ -91,17 +281,14 @@
|
|||||||
- Windows thumbnail bar with a play/pause button
|
- Windows thumbnail bar with a play/pause button
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Nicer modal styles
|
- Nicer modal styles
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Windows tray icon now stays in the right state
|
- Windows tray icon now stays in the right state
|
||||||
|
|
||||||
## v0.7.2 - 2016-06-02
|
## v0.7.2 - 2016-06-02
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix exception that affects users upgrading from v0.5.1 or older
|
- Fix exception that affects users upgrading from v0.5.1 or older
|
||||||
- Ensure `state.saved.prefs` configuration exists
|
- Ensure `state.saved.prefs` configuration exists
|
||||||
- Fix window title on "About WebTorrent" window
|
- Fix window title on "About WebTorrent" window
|
||||||
@@ -109,23 +296,19 @@
|
|||||||
## v0.7.1 - 2016-06-02
|
## v0.7.1 - 2016-06-02
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Change "Step Forward" keyboard shortcut to `Alt+Left` (Windows)
|
- Change "Step Forward" keyboard shortcut to `Alt+Left` (Windows)
|
||||||
- Change "Step Backward" keyboard shortcut to to `Alt+Right` (Windows)
|
- Change "Step Backward" keyboard shortcut to to `Alt+Right` (Windows)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- First time startup bug -- invalid torrent/poster paths
|
- First time startup bug -- invalid torrent/poster paths
|
||||||
|
|
||||||
## v0.7.0 - 2016-06-02
|
## v0.7.0 - 2016-06-02
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Improved AirPlay support -- using the new [`airplayer`](https://www.npmjs.com/package/airplayer) package
|
- Improved AirPlay support -- using the new [`airplayer`](https://www.npmjs.com/package/airplayer) package
|
||||||
- Remember volume setting in player, for as long as the app is open
|
- Remember volume setting in player, for as long as the app is open
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Add (+) button now also accepts non .torrent files and creates a torrent from
|
- Add (+) button now also accepts non .torrent files and creates a torrent from
|
||||||
those files
|
those files
|
||||||
- Show prompt text in title bar for open dialogs (OS X)
|
- Show prompt text in title bar for open dialogs (OS X)
|
||||||
@@ -135,7 +318,6 @@
|
|||||||
- Fix crash reporter not working (Windows)
|
- Fix crash reporter not working (Windows)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Re-enable WebRTC (web peers)! (OS X, Windows)
|
- Re-enable WebRTC (web peers)! (OS X, Windows)
|
||||||
- Windows support was disabled in v0.6.1 to work around a bug in Electron
|
- Windows support was disabled in v0.6.1 to work around a bug in Electron
|
||||||
- OS X support was disabled in v0.4.0 to work around a 100% CPU bug
|
- OS X support was disabled in v0.4.0 to work around a 100% CPU bug
|
||||||
@@ -146,7 +328,6 @@
|
|||||||
- Fix torrent loading message UI misalignment
|
- Fix torrent loading message UI misalignment
|
||||||
|
|
||||||
### Known issues
|
### Known issues
|
||||||
|
|
||||||
- When upgrading to WebTorrent Desktop v0.7.0, some torrent metadata (file list,
|
- When upgrading to WebTorrent Desktop v0.7.0, some torrent metadata (file list,
|
||||||
selected files, whether torrent is streamable) will be cleared. Just start the
|
selected files, whether torrent is streamable) will be cleared. Just start the
|
||||||
torrent to re-populate the metadata.
|
torrent to re-populate the metadata.
|
||||||
@@ -154,7 +335,6 @@
|
|||||||
## v0.6.1 - 2016-05-26
|
## v0.6.1 - 2016-05-26
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Disable WebRTC to work around Electron crash (Windows)
|
- Disable WebRTC to work around Electron crash (Windows)
|
||||||
- Will be re-enabled in the next version of WebTorrent, which will be based on
|
- Will be re-enabled in the next version of WebTorrent, which will be based on
|
||||||
the next version of Electron, where the bug is fixed.
|
the next version of Electron, where the bug is fixed.
|
||||||
@@ -166,7 +346,6 @@
|
|||||||
## v0.6.0 - 2016-05-24
|
## v0.6.0 - 2016-05-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added Preferences page to set Download folder
|
- Added Preferences page to set Download folder
|
||||||
- Save video position, resume playback from saved position
|
- Save video position, resume playback from saved position
|
||||||
- Add additional video player keyboard shortcuts (#275)
|
- Add additional video player keyboard shortcuts (#275)
|
||||||
@@ -176,7 +355,6 @@
|
|||||||
- Add announcement feature
|
- Add announcement feature
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Nicer player UI
|
- Nicer player UI
|
||||||
- Reduce startup jank, improve startup time (#568)
|
- Reduce startup jank, improve startup time (#568)
|
||||||
- Cleanup unsupported codec detection (#569, #570)
|
- Cleanup unsupported codec detection (#569, #570)
|
||||||
@@ -184,7 +362,6 @@
|
|||||||
- Improve subtitle positioning (#551)
|
- Improve subtitle positioning (#551)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
|
- Fix Uncaught TypeError: Cannot read property 'update' of undefined (#567)
|
||||||
- Fix bugs in LocationHistory
|
- Fix bugs in LocationHistory
|
||||||
- When player is active, and magnet link is pasted, go back to list
|
- When player is active, and magnet link is pasted, go back to list
|
||||||
@@ -195,23 +372,19 @@
|
|||||||
## v0.5.1 - 2016-05-18
|
## v0.5.1 - 2016-05-18
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix auto-updater (OS X, Windows).
|
- Fix auto-updater (OS X, Windows).
|
||||||
|
|
||||||
## v0.5.0 - 2016-05-17
|
## v0.5.0 - 2016-05-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Select/deselect individual files to torrent.
|
- Select/deselect individual files to torrent.
|
||||||
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
|
- Automatically include subtitle files (.srt, .vtt) from torrent in the subtitles menu.
|
||||||
- "Add Subtitle File..." menu item.
|
- "Add Subtitle File..." menu item.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- When manually adding subtitle track(s), always switch to the new track.
|
- When manually adding subtitle track(s), always switch to the new track.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Magnet links throw exception on app launch. (OS X)
|
- Magnet links throw exception on app launch. (OS X)
|
||||||
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
|
- Multi-file torrents would not seed in-place, were copied to Downloads folder.
|
||||||
- Missing 'About WebTorrent' menu item. (Windows)
|
- Missing 'About WebTorrent' menu item. (Windows)
|
||||||
@@ -220,7 +393,6 @@
|
|||||||
## v0.4.0 - 2016-05-13
|
## v0.4.0 - 2016-05-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Better Windows support!
|
- Better Windows support!
|
||||||
- Windows 32-bit build.
|
- Windows 32-bit build.
|
||||||
- Windows Portable App build.
|
- Windows Portable App build.
|
||||||
@@ -243,7 +415,6 @@
|
|||||||
- New default torrent on first launch: The WIRED CD.
|
- New default torrent on first launch: The WIRED CD.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improve app startup time by 40%.
|
- Improve app startup time by 40%.
|
||||||
- UI tweaks: Reduce font size, reduce torrent list item height.
|
- UI tweaks: Reduce font size, reduce torrent list item height.
|
||||||
- Add Playback menu for playback-related functionality.
|
- Add Playback menu for playback-related functionality.
|
||||||
@@ -254,7 +425,6 @@
|
|||||||
- Remove "Add Fake Airplay/Chromecast" menu items.
|
- Remove "Add Fake Airplay/Chromecast" menu items.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is
|
- Disable WebRTC to fix 100% CPU usage/crashes caused by Chromium issue. This is
|
||||||
temporary. (OS X)
|
temporary. (OS X)
|
||||||
- When fullscreen, make controls use the full window. (OS X)
|
- When fullscreen, make controls use the full window. (OS X)
|
||||||
@@ -279,24 +449,20 @@ to this release!
|
|||||||
## v0.3.3 - 2016-04-07
|
## v0.3.3 - 2016-04-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- App icon was incorrect (OS X)
|
- App icon was incorrect (OS X)
|
||||||
|
|
||||||
## v0.3.2 - 2016-04-07
|
## v0.3.2 - 2016-04-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Register WebTorrent as default handler for magnet links (OS X)
|
- Register WebTorrent as default handler for magnet links (OS X)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Faster startup time (50ms)
|
- Faster startup time (50ms)
|
||||||
- Update Electron to 0.37.5
|
- Update Electron to 0.37.5
|
||||||
- Remove the white flash when loading pages and resizing the window
|
- Remove the white flash when loading pages and resizing the window
|
||||||
- Fix crash when sending IPC messages
|
- Fix crash when sending IPC messages
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix installation bugs with .deb file (Linux)
|
- Fix installation bugs with .deb file (Linux)
|
||||||
- Pause audio reliably when closing the window
|
- Pause audio reliably when closing the window
|
||||||
- Enforce minimimum window size when resizing player (for audio-only .mov files, which are 0x0)
|
- Enforce minimimum window size when resizing player (for audio-only .mov files, which are 0x0)
|
||||||
@@ -304,17 +470,14 @@ to this release!
|
|||||||
## v0.3.1 - 2016-04-06
|
## v0.3.1 - 2016-04-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Add crash reporter to torrent engine process
|
- Add crash reporter to torrent engine process
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fix cast screen background: cover, don't tile
|
- Fix cast screen background: cover, don't tile
|
||||||
|
|
||||||
## v0.3.0 - 2016-04-06
|
## v0.3.0 - 2016-04-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Ubuntu/Debian support!** (.deb installer)
|
- **Ubuntu/Debian support!** (.deb installer)
|
||||||
- **DLNA streaming support**
|
- **DLNA streaming support**
|
||||||
- Add "File > Quit" menu item (Linux)
|
- Add "File > Quit" menu item (Linux)
|
||||||
@@ -322,14 +485,12 @@ to this release!
|
|||||||
- Crash reporting
|
- Crash reporting
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- On startup, do not re-verify files when timestamps are unchanged
|
- On startup, do not re-verify files when timestamps are unchanged
|
||||||
- Moved torrent engine to an independent process, for better UI performance
|
- Moved torrent engine to an independent process, for better UI performance
|
||||||
- Removed media queries (UI resizing based on window width)
|
- Removed media queries (UI resizing based on window width)
|
||||||
- Improved Chromecast icon, when connected
|
- Improved Chromecast icon, when connected
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- "Download Complete" notification shows consistently
|
- "Download Complete" notification shows consistently
|
||||||
- Create new torrents and seed them without copying to temporary folder
|
- Create new torrents and seed them without copying to temporary folder
|
||||||
- Clicking the "Download Complete" notification will always activate app
|
- Clicking the "Download Complete" notification will always activate app
|
||||||
@@ -348,7 +509,6 @@ Thanks to @dcposch, @grunjol, and @feross for contributing to this release.
|
|||||||
## v0.2.0 - 2016-03-29
|
## v0.2.0 - 2016-03-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Minimise to tray (Windows, Linux)
|
- Minimise to tray (Windows, Linux)
|
||||||
- Show spinner and download speed when player is stalled waiting for data
|
- Show spinner and download speed when player is stalled waiting for data
|
||||||
- Highlight window on drag-and-drop
|
- Highlight window on drag-and-drop
|
||||||
@@ -357,12 +517,10 @@ Thanks to @dcposch, @grunjol, and @feross for contributing to this release.
|
|||||||
Linux users need to download new versions manually.
|
Linux users need to download new versions manually.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Renamed WebTorrent.app to WebTorrent Desktop
|
- Renamed WebTorrent.app to WebTorrent Desktop
|
||||||
- Add Cosmos Laundromat as a default torrent
|
- Add Cosmos Laundromat as a default torrent
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Only capture media keys when player is active
|
- Only capture media keys when player is active
|
||||||
- Update WebTorrent to 0.88.1 for performance improvements
|
- Update WebTorrent to 0.88.1 for performance improvements
|
||||||
- When seeding, do not proactively connect to new peers
|
- When seeding, do not proactively connect to new peers
|
||||||
@@ -422,7 +580,7 @@ Windows, and Linux. For now, we're only releasing binaries for OS X.
|
|||||||
|
|
||||||
WebTorrent Desktop is in ALPHA and under very active development – expect lots more polish in
|
WebTorrent Desktop is in ALPHA and under very active development – expect lots more polish in
|
||||||
the coming weeks! If you know JavaScript and want to help us out, there's
|
the coming weeks! If you know JavaScript and want to help us out, there's
|
||||||
[lots to do](https://github.com/feross/webtorrent-desktop/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+contribution%22)!
|
[lots to do](https://github.com/webtorrent/webtorrent-desktop/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+contribution%22)!
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
# Contributing Guidelines
|
|
||||||
|
|
||||||
Contributions welcome!
|
|
||||||
|
|
||||||
**Before spending lots of time on something, ask for feedback on your idea first!**
|
|
||||||
|
|
||||||
Please search issues and pull requests before adding something new to avoid duplicating
|
|
||||||
efforts and conversations.
|
|
||||||
|
|
||||||
This project welcomes non-code contributions, too! The following types of contributions
|
|
||||||
are welcome:
|
|
||||||
|
|
||||||
- **Ideas**: participate in an issue thread or start your own to have your voice heard.
|
|
||||||
- **Writing**: contribute your expertise in an area by helping expand the included docs.
|
|
||||||
- **Copy editing**: fix typos, clarify language, and improve the quality of the docs.
|
|
||||||
- **Formatting**: help keep docs easy to read with consistent formatting.
|
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
[![standard][standard-image]][standard-url]
|
|
||||||
|
|
||||||
This repository uses [`standard`][standard-url] to maintain code style and consistency,
|
|
||||||
and to avoid style arguments. `npm test` runs `standard` automatically, so you don't have
|
|
||||||
to!
|
|
||||||
|
|
||||||
[standard-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg
|
|
||||||
[standard-url]: https://github.com/feross/standard
|
|
||||||
|
|
||||||
## Project Governance
|
|
||||||
|
|
||||||
Individuals making significant and valuable contributions are given commit-access to the
|
|
||||||
project to contribute as they see fit. This project is more like an open wiki than a
|
|
||||||
standard guarded open source project.
|
|
||||||
|
|
||||||
### Rules
|
|
||||||
|
|
||||||
There are a few basic ground-rules for contributors:
|
|
||||||
|
|
||||||
1. **No `--force` pushes** or modifying the Git history in any way.
|
|
||||||
2. **Non-master branches** should be used for ongoing work.
|
|
||||||
3. **Significant modifications** like API changes should be subject to a **pull request**
|
|
||||||
to solicit feedback from other contributors.
|
|
||||||
4. **Pull requests** are *encouraged* for all contributions to solicit feedback, but left to
|
|
||||||
the discretion of the contributor.
|
|
||||||
|
|
||||||
### Releases
|
|
||||||
|
|
||||||
Declaring formal releases remains the prerogative of the project maintainer.
|
|
||||||
|
|
||||||
### Changes to this arrangement
|
|
||||||
|
|
||||||
This is an experiment and feedback is welcome! This document may also be subject to pull-
|
|
||||||
requests or changes by contributors where you believe you have something valuable to add
|
|
||||||
or change.
|
|
||||||
|
|
||||||
## Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
- (a) The contribution was created in whole or in part by me and I have the right to
|
|
||||||
submit it under the open source license indicated in the file; or
|
|
||||||
|
|
||||||
- (b) The contribution is based upon previous work that, to the best of my knowledge, is
|
|
||||||
covered under an appropriate open source license and I have the right under that license
|
|
||||||
to submit that work with modifications, whether created in whole or in part by me, under
|
|
||||||
the same open source license (unless I am permitted to submit under a different
|
|
||||||
license), as indicated in the file; or
|
|
||||||
|
|
||||||
- (c) The contribution was provided directly to me by some other person who certified
|
|
||||||
(a), (b) or (c) and I have not modified it.
|
|
||||||
|
|
||||||
- (d) I understand and agree that this project and the contribution are public and that a
|
|
||||||
record of the contribution (including all personal information I submit with it,
|
|
||||||
including my sign-off) is maintained indefinitely and may be redistributed consistent
|
|
||||||
with this project or the open source license(s) involved.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
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.
|
|
||||||
130
README.md
130
README.md
@@ -1,45 +1,110 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<br>
|
<br>
|
||||||
<a href="https://webtorrent.io"><img src="https://webtorrent.io/img/WebTorrent.png" alt="WebTorrent" width="200"></a>
|
<a href="https://webtorrent.io">
|
||||||
|
<img src="https://webtorrent.io/img/WebTorrent.png" alt="WebTorrent" width="200">
|
||||||
|
</a>
|
||||||
<br>
|
<br>
|
||||||
WebTorrent Desktop
|
WebTorrent Desktop
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<h4 align="center">The streaming torrent client. For Mac, Windows, and Linux.</h4>
|
<h4 align="center">The streaming torrent app. For Mac, Windows, and Linux.</h4>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://gitter.im/feross/webtorrent"><img src="https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg" alt="Gitter"></a>
|
<a href="https://discord.gg/cnXkm4Z"><img src="https://img.shields.io/discord/612575111718895616" alt="discord"></a>
|
||||||
<a href="https://travis-ci.org/feross/webtorrent-desktop"><img src="https://img.shields.io/travis/feross/webtorrent-desktop/master.svg" alt="Travis"></a>
|
<a href="https://travis-ci.org/webtorrent/webtorrent-desktop"><img src="https://img.shields.io/travis/webtorrent/webtorrent-desktop/master.svg" alt="travis"></a>
|
||||||
<a href="https://github.com/feross/webtorrent-desktop/releases"><img src="https://img.shields.io/github/release/feross/webtorrent-desktop.svg" alt="Release"></a>
|
<a href="https://github.com/webtorrent/webtorrent-desktop/releases"><img src="https://img.shields.io/github/release/webtorrent/webtorrent-desktop.svg" alt="github release version"></a>
|
||||||
|
<a href="https://github.com/webtorrent/webtorrent-desktop/releases"><img src="https://img.shields.io/github/downloads/webtorrent/webtorrent-desktop/total.svg" alt="github release downloads"></a>
|
||||||
|
<a href="https://standardjs.com"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Standard - JavaScript Style Guide"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
**WebTorrent Desktop** is still under very active development. You can download the latest version from the [releases](https://github.com/feross/webtorrent-desktop/releases) page.
|
### Recommended Install
|
||||||
|
|
||||||
## Screenshot
|
Download the latest version of WebTorrent Desktop from
|
||||||
|
[the official website](https://webtorrent.io/desktop/):
|
||||||
|
|
||||||
|
### [✨ Download WebTorrent Desktop ✨](https://webtorrent.io/desktop/)
|
||||||
|
|
||||||
|
### Advanced Install
|
||||||
|
|
||||||
|
- Download specific installer files from the [GitHub releases](https://github.com/webtorrent/webtorrent-desktop/releases) page.
|
||||||
|
|
||||||
|
- Use [Homebrew-Cask](https://github.com/caskroom/homebrew-cask) to install from the command line:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ brew cask install webtorrent
|
||||||
|
```
|
||||||
|
|
||||||
|
- Try the (unstable) development version by cloning the Git repository. See the
|
||||||
|
["How to Contribute"](#how-to-contribute) instructions.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
<img src="https://webtorrent.io/img/screenshot-player3.png" alt="screenshot" align="center">
|
||||||
<img src="https://webtorrent.io/img/screenshot-main.png" width="612" height="749" alt="screenshot" align="center">
|
<img src="https://webtorrent.io/img/screenshot-main.png" width="612" height="749" alt="screenshot" align="center">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## How to Contribute
|
## How to Contribute
|
||||||
|
|
||||||
### Install dependencies
|
### Get the code
|
||||||
|
|
||||||
```
|
```
|
||||||
|
$ git clone https://github.com/webtorrent/webtorrent-desktop.git
|
||||||
|
$ cd webtorrent-desktop
|
||||||
$ npm install
|
$ npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run app
|
### Run the app
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm start
|
$ npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
### Package app
|
### Watch the code
|
||||||
|
|
||||||
|
Restart the app automatically every time code changes. Useful during development.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run watch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run linters
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run integration tests
|
||||||
|
|
||||||
|
```
|
||||||
|
$ npm run test-integration
|
||||||
|
```
|
||||||
|
|
||||||
|
The integration tests use Spectron and Tape. They click through the app, taking screenshots and
|
||||||
|
comparing each one to a reference. Why screenshots?
|
||||||
|
|
||||||
|
* Ad-hoc checking makes the tests a lot more work to write
|
||||||
|
* Even diffing the whole HTML is not as thorough as screenshot diffing. For example, it wouldn't
|
||||||
|
catch an bug where hitting ESC from a video doesn't correctly restore window size.
|
||||||
|
* Chrome's own integration tests use screenshot diffing iirc
|
||||||
|
* Small UI changes will break a few tests, but the fix is as easy as deleting the offending
|
||||||
|
screenshots and running the tests, which will recreate them with the new look.
|
||||||
|
* The resulting Github PR will then show, pixel by pixel, the exact UI changes that were made! See
|
||||||
|
https://github.com/blog/817-behold-image-view-modes
|
||||||
|
|
||||||
|
For MacOS, you'll need a Retina screen for the integration tests to pass. Your screen should have
|
||||||
|
the same resolution as a 2016 12" Macbook.
|
||||||
|
|
||||||
|
For Windows, you'll need Windows 10 with a 1366x768 screen.
|
||||||
|
|
||||||
|
When running integration tests, keep the mouse on the edge of the screen and don't touch the mouse
|
||||||
|
or keyboard while the tests are running.
|
||||||
|
|
||||||
|
### Package the app
|
||||||
|
|
||||||
Builds app binaries for Mac, Linux, and Windows.
|
Builds app binaries for Mac, Linux, and Windows.
|
||||||
|
|
||||||
@@ -50,7 +115,7 @@ $ npm run package
|
|||||||
To build for one platform:
|
To build for one platform:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm run package -- [platform]
|
$ npm run package -- [platform] [options]
|
||||||
```
|
```
|
||||||
|
|
||||||
Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default).
|
Where `[platform]` is `darwin`, `linux`, `win32`, or `all` (default).
|
||||||
@@ -66,14 +131,18 @@ The following optional arguments are available:
|
|||||||
- `portable` - Windows portable app
|
- `portable` - Windows portable app
|
||||||
- `all` - All platforms (default)
|
- `all` - All platforms (default)
|
||||||
|
|
||||||
Note: Even with the `--package` option, the auto-update files (.nupkg for Windows, *-darwin.zip for Mac) will always be produced.
|
Note: Even with the `--package` option, the auto-update files (.nupkg for Windows,
|
||||||
|
-darwin.zip for Mac) will always be produced.
|
||||||
|
|
||||||
#### Windows build notes
|
#### Windows build notes
|
||||||
|
|
||||||
To package the Windows app from non-Windows platforms, [Wine](https://www.winehq.org/) needs
|
The Windows app can be packaged from **any** platform.
|
||||||
to be installed.
|
|
||||||
|
|
||||||
On Mac, first install [XQuartz](http://www.xquartz.org/), then run:
|
Note: Windows code signing only works from **Windows**, for now.
|
||||||
|
|
||||||
|
Note: To package the Windows app from non-Windows platforms,
|
||||||
|
[Wine](https://www.winehq.org/) needs to be installed. For example on Mac, first
|
||||||
|
install [XQuartz](http://www.xquartz.org/), then run:
|
||||||
|
|
||||||
```
|
```
|
||||||
brew install wine
|
brew install wine
|
||||||
@@ -81,15 +150,34 @@ brew install wine
|
|||||||
|
|
||||||
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
(Requires the [Homebrew](http://brew.sh/) package manager.)
|
||||||
|
|
||||||
|
#### Mac build notes
|
||||||
|
|
||||||
|
The Mac app can only be packaged from **macOS**.
|
||||||
|
|
||||||
|
#### Linux build notes
|
||||||
|
|
||||||
|
The Linux app can be packaged from **any** platform.
|
||||||
|
|
||||||
|
|
||||||
|
#### Recommended readings to start working in the app
|
||||||
|
|
||||||
|
Electron (Framework to make native apps for Windows, OSX and Linux in Javascript):
|
||||||
|
https://electronjs.org/docs/tutorial/quick-start
|
||||||
|
|
||||||
|
React.js (Framework to work with Frontend UI):
|
||||||
|
https://reactjs.org/docs/getting-started.html
|
||||||
|
|
||||||
|
Material UI (React components that implement Google's Material Design.):
|
||||||
|
https://material-ui.com/getting-started
|
||||||
|
|
||||||
### Privacy
|
### Privacy
|
||||||
|
|
||||||
WebTorrent Desktop collects some basic usage stats to help us make the app better. For example, we track how well the play button works. How often does it succeed? Time out? Show a missing codec error?
|
WebTorrent Desktop collects some basic usage stats to help us make the app better.
|
||||||
|
For example, we track how well the play button works. How often does it succeed?
|
||||||
|
Time out? Show a missing codec error?
|
||||||
|
|
||||||
The app never sends personally identifying information, nor does it track which swarms you join.
|
The app never sends any personally identifying information, nor does it track which
|
||||||
|
torrents you add.
|
||||||
### Code Style
|
|
||||||
|
|
||||||
[](https://github.com/feross/standard)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
112
RELEASE_PROCESS.md
Normal file
112
RELEASE_PROCESS.md
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
## Release Process
|
||||||
|
|
||||||
|
### 1. Create a new version
|
||||||
|
|
||||||
|
- Update `AUTHORS`
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run update-authors
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit if necessary. The commit message should be "authors".
|
||||||
|
|
||||||
|
- Write the changelog
|
||||||
|
|
||||||
|
You can use `git log --oneline <last version tag>..HEAD` to get a list of changes.
|
||||||
|
|
||||||
|
Summarize them concisely in `CHANGELOG.md`. The commit message should be "changelog".
|
||||||
|
|
||||||
|
- Update the version
|
||||||
|
|
||||||
|
```
|
||||||
|
npm version [major|minor|patch]
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates both a commit and a git tag.
|
||||||
|
|
||||||
|
- Make a PR
|
||||||
|
|
||||||
|
Once the PR is reviewed, merge it:
|
||||||
|
|
||||||
|
```
|
||||||
|
git push origin <branch-name>:master
|
||||||
|
```
|
||||||
|
|
||||||
|
This makes it so that the commit hash on master matches the commit hash of the version tag.
|
||||||
|
|
||||||
|
Finally, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create the release binaries
|
||||||
|
|
||||||
|
- On a Mac:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run package -- darwin --sign
|
||||||
|
npm run package -- linux --sign
|
||||||
|
```
|
||||||
|
|
||||||
|
- On Windows, or in a Windows VM:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run package -- win32 --sign
|
||||||
|
```
|
||||||
|
|
||||||
|
- Then, upload the release binaries to Github:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run gh-release
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow the URL to a newly created Github release page. Manually upload the binaries from
|
||||||
|
`webtorrent-desktop/dist/`. Open the previous release in another tab, and make sure that you
|
||||||
|
are uploading the same set of files, no more, no less.
|
||||||
|
|
||||||
|
### 3. Test it
|
||||||
|
|
||||||
|
**This is the most important part.**
|
||||||
|
|
||||||
|
- Manually download the binaries for each platform from Github.
|
||||||
|
|
||||||
|
**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
|
||||||
|
codesigning worked and is valid.
|
||||||
|
|
||||||
|
- Smoke test WebTorrent Desktop on each platform.
|
||||||
|
|
||||||
|
See Smoke Tests below for details. Open DevTools
|
||||||
|
on Windows and Mac, and ensure that the auto updater is running. If the auto updater does not
|
||||||
|
run, users will successfully auto update to this new version, and then be stuck there forever.
|
||||||
|
|
||||||
|
### 4. Ship it
|
||||||
|
|
||||||
|
- Update the website
|
||||||
|
|
||||||
|
Create a pull request in [webtorrent.io](https://github.com/webtorrent/webtorrent.io). Update
|
||||||
|
`config.js`, updating the desktop app version.
|
||||||
|
|
||||||
|
As soon as this PR is merged, Jenkins will automatically redeploy the WebTorrent website, and
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
var fs = require('fs')
|
|
||||||
var cp = require('child_process')
|
|
||||||
|
|
||||||
// We can't use `builtin-modules` here since our TravisCI
|
|
||||||
// setup expects this file to run with no dependencies
|
|
||||||
var BUILT_IN_NODE_MODULES = [
|
|
||||||
'assert',
|
|
||||||
'buffer',
|
|
||||||
'child_process',
|
|
||||||
'cluster',
|
|
||||||
'console',
|
|
||||||
'constants',
|
|
||||||
'crypto',
|
|
||||||
'dgram',
|
|
||||||
'dns',
|
|
||||||
'domain',
|
|
||||||
'events',
|
|
||||||
'fs',
|
|
||||||
'http',
|
|
||||||
'https',
|
|
||||||
'module',
|
|
||||||
'net',
|
|
||||||
'os',
|
|
||||||
'path',
|
|
||||||
'process',
|
|
||||||
'punycode',
|
|
||||||
'querystring',
|
|
||||||
'readline',
|
|
||||||
'repl',
|
|
||||||
'stream',
|
|
||||||
'string_decoder',
|
|
||||||
'timers',
|
|
||||||
'tls',
|
|
||||||
'tty',
|
|
||||||
'url',
|
|
||||||
'util',
|
|
||||||
'v8',
|
|
||||||
'vm',
|
|
||||||
'zlib'
|
|
||||||
]
|
|
||||||
|
|
||||||
var BUILT_IN_ELECTRON_MODULES = [ 'electron' ]
|
|
||||||
|
|
||||||
var BUILT_IN_DEPS = [].concat(BUILT_IN_NODE_MODULES, BUILT_IN_ELECTRON_MODULES)
|
|
||||||
|
|
||||||
var EXECUTABLE_DEPS = [
|
|
||||||
'gh-release',
|
|
||||||
'standard',
|
|
||||||
'babel-cli',
|
|
||||||
'babel-plugin-syntax-jsx',
|
|
||||||
'babel-plugin-transform-react-jsx'
|
|
||||||
]
|
|
||||||
|
|
||||||
main()
|
|
||||||
|
|
||||||
// Scans codebase for missing or unused dependencies. Exits with code 0 on success.
|
|
||||||
function main () {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
console.error('Sorry, check-deps only works on Mac and Linux')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var usedDeps = findUsedDeps()
|
|
||||||
var packageDeps = findPackageDeps()
|
|
||||||
|
|
||||||
var missingDeps = usedDeps.filter(
|
|
||||||
(dep) => !includes(packageDeps, dep) && !includes(BUILT_IN_DEPS, dep)
|
|
||||||
)
|
|
||||||
var unusedDeps = packageDeps.filter(
|
|
||||||
(dep) => !includes(usedDeps, dep) && !includes(EXECUTABLE_DEPS, dep)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (missingDeps.length > 0) {
|
|
||||||
console.error('Missing package dependencies: ' + missingDeps)
|
|
||||||
}
|
|
||||||
if (unusedDeps.length > 0) {
|
|
||||||
console.error('Unused package dependencies: ' + unusedDeps)
|
|
||||||
}
|
|
||||||
if (missingDeps.length + unusedDeps.length > 0) {
|
|
||||||
process.exitCode = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finds all dependencies specified in `package.json`
|
|
||||||
function findPackageDeps () {
|
|
||||||
var pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
||||||
|
|
||||||
var deps = Object.keys(pkg.dependencies)
|
|
||||||
var devDeps = Object.keys(pkg.devDependencies)
|
|
||||||
var optionalDeps = Object.keys(pkg.optionalDependencies)
|
|
||||||
|
|
||||||
return [].concat(deps, devDeps, optionalDeps)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finds all dependencies that used with `require()`
|
|
||||||
function findUsedDeps () {
|
|
||||||
var stdout = cp.execSync('./bin/list-deps.sh')
|
|
||||||
return stdout.toString().trim().split('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
function includes (arr, elem) {
|
|
||||||
return arr.indexOf(elem) >= 0
|
|
||||||
}
|
|
||||||
16
bin/clean.js
16
bin/clean.js
@@ -5,13 +5,13 @@
|
|||||||
* Useful for developers.
|
* Useful for developers.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var fs = require('fs')
|
const fs = require('fs')
|
||||||
var os = require('os')
|
const os = require('os')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var rimraf = require('rimraf')
|
const rimraf = require('rimraf')
|
||||||
|
|
||||||
var config = require('../src/config')
|
const config = require('../src/config')
|
||||||
var handlers = require('../src/main/handlers')
|
const handlers = require('../src/main/handlers')
|
||||||
|
|
||||||
// First, remove generated files
|
// First, remove generated files
|
||||||
rimraf.sync('build/')
|
rimraf.sync('build/')
|
||||||
@@ -21,11 +21,11 @@ rimraf.sync('dist/')
|
|||||||
rimraf.sync(config.CONFIG_PATH)
|
rimraf.sync(config.CONFIG_PATH)
|
||||||
|
|
||||||
// Remove any temporary files
|
// Remove any temporary files
|
||||||
var tmpPath
|
let tmpPath
|
||||||
try {
|
try {
|
||||||
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
|
tmpPath = path.join(fs.statSync('/tmp') && '/tmp', 'webtorrent')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
tmpPath = path.join(os.tmpDir(), 'webtorrent')
|
tmpPath = path.join(os.tmpdir(), 'webtorrent')
|
||||||
}
|
}
|
||||||
rimraf.sync(tmpPath)
|
rimraf.sync(tmpPath)
|
||||||
|
|
||||||
|
|||||||
10
bin/cmd.js
10
bin/cmd.js
@@ -1,10 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
var electron = require('electron-prebuilt')
|
|
||||||
var cp = require('child_process')
|
|
||||||
var path = require('path')
|
|
||||||
|
|
||||||
var child = cp.spawn(electron, [path.join(__dirname, '..')], {stdio: 'inherit'})
|
|
||||||
child.on('close', function (code) {
|
|
||||||
process.exitCode = code
|
|
||||||
})
|
|
||||||
39
bin/extra-lint.js
Executable file
39
bin/extra-lint.js
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/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!')
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# This is a truly heinous hack, but it works pretty nicely.
|
|
||||||
# Find all modules we're requiring---even conditional requires.
|
|
||||||
|
|
||||||
grep "require('" src/ bin/ -R |
|
|
||||||
grep '.js:' |
|
|
||||||
sed "s/.*require('\([^'\/]*\).*/\1/" |
|
|
||||||
grep -v '^\.' |
|
|
||||||
sort |
|
|
||||||
uniq
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
var config = require('../src/config')
|
const { CONFIG_PATH } = require('../src/config')
|
||||||
var open = require('open')
|
const opn = require('opn')
|
||||||
|
|
||||||
open(config.CONFIG_PATH)
|
opn(CONFIG_PATH)
|
||||||
|
|||||||
258
bin/package.js
258
bin/package.js
@@ -1,28 +1,29 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds app binaries for Mac, Linux, and Windows.
|
* Builds app binaries for Mac, Windows, and Linux.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var cp = require('child_process')
|
const cp = require('child_process')
|
||||||
var electronPackager = require('electron-packager')
|
const electronPackager = require('electron-packager')
|
||||||
var fs = require('fs')
|
const fs = require('fs')
|
||||||
var minimist = require('minimist')
|
const minimist = require('minimist')
|
||||||
var mkdirp = require('mkdirp')
|
const mkdirp = require('mkdirp')
|
||||||
var os = require('os')
|
const os = require('os')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var rimraf = require('rimraf')
|
const rimraf = require('rimraf')
|
||||||
var series = require('run-series')
|
const series = require('run-series')
|
||||||
var zip = require('cross-zip')
|
const zip = require('cross-zip')
|
||||||
|
|
||||||
var config = require('../src/config')
|
const config = require('../src/config')
|
||||||
var pkg = require('../package.json')
|
const pkg = require('../package.json')
|
||||||
|
|
||||||
var BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
const BUILD_NAME = config.APP_NAME + '-v' + config.APP_VERSION
|
||||||
var BUILD_PATH = path.join(config.ROOT_PATH, 'build')
|
const BUILD_PATH = path.join(config.ROOT_PATH, 'build')
|
||||||
var DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
const DIST_PATH = path.join(config.ROOT_PATH, 'dist')
|
||||||
|
const NODE_MODULES_PATH = path.join(config.ROOT_PATH, 'node_modules')
|
||||||
|
|
||||||
var argv = minimist(process.argv.slice(2), {
|
const argv = minimist(process.argv.slice(2), {
|
||||||
boolean: [
|
boolean: [
|
||||||
'sign'
|
'sign'
|
||||||
],
|
],
|
||||||
@@ -36,14 +37,19 @@ var argv = minimist(process.argv.slice(2), {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function build () {
|
function build () {
|
||||||
|
console.log('Reinstalling node_modules...')
|
||||||
|
rimraf.sync(NODE_MODULES_PATH)
|
||||||
|
cp.execSync('npm ci', { stdio: 'inherit' })
|
||||||
|
|
||||||
|
console.log('Nuking dist/ and build/...')
|
||||||
rimraf.sync(DIST_PATH)
|
rimraf.sync(DIST_PATH)
|
||||||
rimraf.sync(BUILD_PATH)
|
rimraf.sync(BUILD_PATH)
|
||||||
|
|
||||||
console.log('Babel: Building JSX...')
|
console.log('Build: Transpiling to ES5...')
|
||||||
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
|
cp.execSync('npm run build', { NODE_ENV: 'production', stdio: 'inherit' })
|
||||||
console.log('Babel: Built JSX.')
|
console.log('Build: Transpiled to ES5.')
|
||||||
|
|
||||||
var platform = argv._[0]
|
const platform = argv._[0]
|
||||||
if (platform === 'darwin') {
|
if (platform === 'darwin') {
|
||||||
buildDarwin(printDone)
|
buildDarwin(printDone)
|
||||||
} else if (platform === 'win32') {
|
} else if (platform === 'win32') {
|
||||||
@@ -61,35 +67,35 @@ function build () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var all = {
|
const all = {
|
||||||
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
|
// The human-readable copyright line for the app. Maps to the `LegalCopyright` metadata
|
||||||
// property on Windows, and `NSHumanReadableCopyright` on Mac.
|
// property on Windows, and `NSHumanReadableCopyright` on Mac.
|
||||||
'app-copyright': config.APP_COPYRIGHT,
|
appCopyright: config.APP_COPYRIGHT,
|
||||||
|
|
||||||
// The release version of the application. Maps to the `ProductVersion` metadata
|
// The release version of the application. Maps to the `ProductVersion` metadata
|
||||||
// property on Windows, and `CFBundleShortVersionString` on Mac.
|
// property on Windows, and `CFBundleShortVersionString` on Mac.
|
||||||
'app-version': pkg.version,
|
appVersion: pkg.version,
|
||||||
|
|
||||||
// Package the application's source code into an archive, using Electron's archive
|
// Package the application's source code into an archive, using Electron's archive
|
||||||
// format. Mitigates issues around long path names on Windows and slightly speeds up
|
// format. Mitigates issues around long path names on Windows and slightly speeds up
|
||||||
// require().
|
// require().
|
||||||
asar: true,
|
asar: {
|
||||||
|
// A glob expression, that unpacks the files with matching names to the
|
||||||
// A glob expression, that unpacks the files with matching names to the
|
// "app.asar.unpacked" directory.
|
||||||
// "app.asar.unpacked" directory.
|
unpack: 'WebTorrent*'
|
||||||
'asar-unpack': 'WebTorrent*',
|
},
|
||||||
|
|
||||||
// The build version of the application. Maps to the FileVersion metadata property on
|
// The build version of the application. Maps to the FileVersion metadata property on
|
||||||
// Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to
|
// Windows, and CFBundleVersion on Mac. Note: Windows requires the build version to
|
||||||
// start with a number. We're using the version of the underlying WebTorrent library.
|
// start with a number. We're using the version of the underlying WebTorrent library.
|
||||||
'build-version': require('webtorrent/package.json').version,
|
buildVersion: require('webtorrent/package.json').version,
|
||||||
|
|
||||||
// The application source directory.
|
// The application source directory.
|
||||||
dir: config.ROOT_PATH,
|
dir: config.ROOT_PATH,
|
||||||
|
|
||||||
// Pattern which specifies which files to ignore when copying files to create the
|
// Pattern which specifies which files to ignore when copying files to create the
|
||||||
// package(s).
|
// package(s).
|
||||||
ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^\/]*|.*\.md|.*\.markdown)$/,
|
ignore: /^\/src|^\/dist|\/(appveyor.yml|\.appveyor.yml|\.github|appdmg|AUTHORS|CONTRIBUTORS|bench|benchmark|benchmark\.js|bin|bower\.json|component\.json|coverage|doc|docs|docs\.mli|dragdrop\.min\.js|example|examples|example\.html|example\.js|externs|ipaddr\.min\.js|Makefile|min|minimist|perf|rusha|simplepeer\.min\.js|simplewebsocket\.min\.js|static\/screenshot\.png|test|tests|test\.js|tests\.js|webtorrent\.min\.js|\.[^/]*|.*\.md|.*\.markdown)$/,
|
||||||
|
|
||||||
// The application name.
|
// The application name.
|
||||||
name: config.APP_NAME,
|
name: config.APP_NAME,
|
||||||
@@ -104,40 +110,40 @@ var all = {
|
|||||||
// "devDependencies" before starting to package the app.
|
// "devDependencies" before starting to package the app.
|
||||||
prune: true,
|
prune: true,
|
||||||
|
|
||||||
// The Electron version with which the app is built (without the leading 'v')
|
// The Electron version that the app is built with (without the leading 'v')
|
||||||
version: require('electron-prebuilt/package.json').version
|
electronVersion: require('electron/package.json').version
|
||||||
}
|
}
|
||||||
|
|
||||||
var darwin = {
|
const darwin = {
|
||||||
// Build for Mac
|
// Build for Mac
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
|
|
||||||
// Build 64 bit binaries only.
|
// Build x64 binary only.
|
||||||
arch: 'x64',
|
arch: 'x64',
|
||||||
|
|
||||||
// The bundle identifier to use in the application's plist (Mac only).
|
// The bundle identifier to use in the application's plist (Mac only).
|
||||||
'app-bundle-id': 'io.webtorrent.webtorrent',
|
appBundleId: 'io.webtorrent.webtorrent',
|
||||||
|
|
||||||
// The application category type, as shown in the Finder via "View" -> "Arrange by
|
// The application category type, as shown in the Finder via "View" -> "Arrange by
|
||||||
// Application Category" when viewing the Applications directory (Mac only).
|
// Application Category" when viewing the Applications directory (Mac only).
|
||||||
'app-category-type': 'public.app-category.utilities',
|
appCategoryType: 'public.app-category.utilities',
|
||||||
|
|
||||||
// The bundle identifier to use in the application helper's plist (Mac only).
|
// The bundle identifier to use in the application helper's plist (Mac only).
|
||||||
'helper-bundle-id': 'io.webtorrent.webtorrent-helper',
|
helperBundleId: 'io.webtorrent.webtorrent-helper',
|
||||||
|
|
||||||
// Application icon.
|
// Application icon.
|
||||||
icon: config.APP_ICON + '.icns'
|
icon: config.APP_ICON + '.icns'
|
||||||
}
|
}
|
||||||
|
|
||||||
var win32 = {
|
const win32 = {
|
||||||
// Build for Windows.
|
// Build for Windows.
|
||||||
platform: 'win32',
|
platform: 'win32',
|
||||||
|
|
||||||
// Build 32 bit binaries only.
|
// Build x64 binary only.
|
||||||
arch: 'ia32',
|
arch: 'x64',
|
||||||
|
|
||||||
// Object hash of application metadata to embed into the executable (Windows only)
|
// Object hash of application metadata to embed into the executable (Windows only)
|
||||||
'version-string': {
|
win32metadata: {
|
||||||
|
|
||||||
// Company that produced the file.
|
// Company that produced the file.
|
||||||
CompanyName: config.APP_NAME,
|
CompanyName: config.APP_NAME,
|
||||||
@@ -163,12 +169,12 @@ var win32 = {
|
|||||||
icon: config.APP_ICON + '.ico'
|
icon: config.APP_ICON + '.ico'
|
||||||
}
|
}
|
||||||
|
|
||||||
var linux = {
|
const linux = {
|
||||||
// Build for Linux.
|
// Build for Linux.
|
||||||
platform: 'linux',
|
platform: 'linux',
|
||||||
|
|
||||||
// Build 32 and 64 bit binaries.
|
// Build x64 binary onle.
|
||||||
arch: 'all'
|
arch: 'x64'
|
||||||
|
|
||||||
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
// Note: Application icon for Linux is specified via the BrowserWindow `icon` option.
|
||||||
}
|
}
|
||||||
@@ -176,31 +182,30 @@ var linux = {
|
|||||||
build()
|
build()
|
||||||
|
|
||||||
function buildDarwin (cb) {
|
function buildDarwin (cb) {
|
||||||
var plist = require('plist')
|
const plist = require('plist')
|
||||||
|
|
||||||
console.log('Mac: Packaging electron...')
|
console.log('Mac: Packaging electron...')
|
||||||
electronPackager(Object.assign({}, all, darwin), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, darwin)).then(function (buildPath) {
|
||||||
if (err) return cb(err)
|
|
||||||
console.log('Mac: Packaged electron. ' + buildPath)
|
console.log('Mac: Packaged electron. ' + buildPath)
|
||||||
|
|
||||||
var appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
const appPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||||
var contentsPath = path.join(appPath, 'Contents')
|
const contentsPath = path.join(appPath, 'Contents')
|
||||||
var resourcesPath = path.join(contentsPath, 'Resources')
|
const resourcesPath = path.join(contentsPath, 'Resources')
|
||||||
var infoPlistPath = path.join(contentsPath, 'Info.plist')
|
const infoPlistPath = path.join(contentsPath, 'Info.plist')
|
||||||
var infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
|
const infoPlist = plist.parse(fs.readFileSync(infoPlistPath, 'utf8'))
|
||||||
|
|
||||||
infoPlist.CFBundleDocumentTypes = [
|
infoPlist.CFBundleDocumentTypes = [
|
||||||
{
|
{
|
||||||
CFBundleTypeExtensions: [ 'torrent' ],
|
CFBundleTypeExtensions: ['torrent'],
|
||||||
CFBundleTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
CFBundleTypeIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
||||||
CFBundleTypeName: 'BitTorrent Document',
|
CFBundleTypeName: 'BitTorrent Document',
|
||||||
CFBundleTypeRole: 'Editor',
|
CFBundleTypeRole: 'Editor',
|
||||||
LSHandlerRank: 'Owner',
|
LSHandlerRank: 'Owner',
|
||||||
LSItemContentTypes: [ 'org.bittorrent.torrent' ]
|
LSItemContentTypes: ['org.bittorrent.torrent']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
CFBundleTypeName: 'Any',
|
CFBundleTypeName: 'Any',
|
||||||
CFBundleTypeOSTypes: [ '****' ],
|
CFBundleTypeOSTypes: ['****'],
|
||||||
CFBundleTypeRole: 'Editor',
|
CFBundleTypeRole: 'Editor',
|
||||||
LSHandlerRank: 'Owner',
|
LSHandlerRank: 'Owner',
|
||||||
LSTypeIsPackage: false
|
LSTypeIsPackage: false
|
||||||
@@ -212,13 +217,13 @@ function buildDarwin (cb) {
|
|||||||
CFBundleTypeRole: 'Editor',
|
CFBundleTypeRole: 'Editor',
|
||||||
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
||||||
CFBundleURLName: 'BitTorrent Magnet URL',
|
CFBundleURLName: 'BitTorrent Magnet URL',
|
||||||
CFBundleURLSchemes: [ 'magnet' ]
|
CFBundleURLSchemes: ['magnet']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
CFBundleTypeRole: 'Editor',
|
CFBundleTypeRole: 'Editor',
|
||||||
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
CFBundleURLIconFile: path.basename(config.APP_FILE_ICON) + '.icns',
|
||||||
CFBundleURLName: 'BitTorrent Stream-Magnet URL',
|
CFBundleURLName: 'BitTorrent Stream-Magnet URL',
|
||||||
CFBundleURLSchemes: [ 'stream-magnet' ]
|
CFBundleURLSchemes: ['stream-magnet']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -235,7 +240,7 @@ function buildDarwin (cb) {
|
|||||||
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
|
UTTypeReferenceURL: 'http://www.bittorrent.org/beps/bep_0000.html',
|
||||||
UTTypeTagSpecification: {
|
UTTypeTagSpecification: {
|
||||||
'com.apple.ostype': 'TORR',
|
'com.apple.ostype': 'TORR',
|
||||||
'public.filename-extension': [ 'torrent' ],
|
'public.filename-extension': ['torrent'],
|
||||||
'public.mime-type': 'application/x-bittorrent'
|
'public.mime-type': 'application/x-bittorrent'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,7 +266,7 @@ function buildDarwin (cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function signApp (cb) {
|
function signApp (cb) {
|
||||||
var sign = require('electron-osx-sign')
|
const sign = require('electron-osx-sign')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
|
* Sign the app with Apple Developer ID certificates. We sign the app for 2 reasons:
|
||||||
@@ -276,7 +281,7 @@ function buildDarwin (cb) {
|
|||||||
* - Xcode Command Line Tools (xcode-select --install)
|
* - Xcode Command Line Tools (xcode-select --install)
|
||||||
* - Membership in the Apple Developer Program
|
* - Membership in the Apple Developer Program
|
||||||
*/
|
*/
|
||||||
var signOpts = {
|
const signOpts = {
|
||||||
app: appPath,
|
app: appPath,
|
||||||
platform: 'darwin',
|
platform: 'darwin',
|
||||||
verbose: true
|
verbose: true
|
||||||
@@ -302,8 +307,8 @@ function buildDarwin (cb) {
|
|||||||
// Create .zip file (used by the auto-updater)
|
// Create .zip file (used by the auto-updater)
|
||||||
console.log('Mac: Creating zip...')
|
console.log('Mac: Creating zip...')
|
||||||
|
|
||||||
var inPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
const inPath = path.join(buildPath[0], config.APP_NAME + '.app')
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
|
const outPath = path.join(DIST_PATH, BUILD_NAME + '-darwin.zip')
|
||||||
zip.zipSync(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log('Mac: Created zip.')
|
console.log('Mac: Created zip.')
|
||||||
@@ -312,13 +317,13 @@ function buildDarwin (cb) {
|
|||||||
function packageDmg (cb) {
|
function packageDmg (cb) {
|
||||||
console.log('Mac: Creating dmg...')
|
console.log('Mac: Creating dmg...')
|
||||||
|
|
||||||
var appDmg = require('appdmg')
|
const appDmg = require('appdmg')
|
||||||
|
|
||||||
var targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
|
const targetPath = path.join(DIST_PATH, BUILD_NAME + '.dmg')
|
||||||
rimraf.sync(targetPath)
|
rimraf.sync(targetPath)
|
||||||
|
|
||||||
// Create a .dmg (Mac disk image) file, for easy user installation.
|
// Create a .dmg (Mac disk image) file, for easy user installation.
|
||||||
var dmgOpts = {
|
const dmgOpts = {
|
||||||
basepath: config.ROOT_PATH,
|
basepath: config.ROOT_PATH,
|
||||||
target: targetPath,
|
target: targetPath,
|
||||||
specification: {
|
specification: {
|
||||||
@@ -339,7 +344,7 @@ function buildDarwin (cb) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dmg = appDmg(dmgOpts)
|
const dmg = appDmg(dmgOpts)
|
||||||
dmg.once('error', cb)
|
dmg.once('error', cb)
|
||||||
dmg.on('progress', function (info) {
|
dmg.on('progress', function (info) {
|
||||||
if (info.type === 'step-begin') console.log(info.title + '...')
|
if (info.type === 'step-begin') console.log(info.title + '...')
|
||||||
@@ -349,11 +354,13 @@ function buildDarwin (cb) {
|
|||||||
cb(null)
|
cb(null)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}).catch(function (err) {
|
||||||
|
cb(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWin32 (cb) {
|
function buildWin32 (cb) {
|
||||||
var installer = require('electron-winstaller')
|
const installer = require('electron-winstaller')
|
||||||
console.log('Windows: Packaging electron...')
|
console.log('Windows: Packaging electron...')
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -361,7 +368,7 @@ function buildWin32 (cb) {
|
|||||||
* - Windows Authenticode private key and cert (authenticode.p12)
|
* - Windows Authenticode private key and cert (authenticode.p12)
|
||||||
* - Windows Authenticode password file (authenticode.txt)
|
* - Windows Authenticode password file (authenticode.txt)
|
||||||
*/
|
*/
|
||||||
var CERT_PATH
|
let CERT_PATH
|
||||||
try {
|
try {
|
||||||
fs.accessSync('D:')
|
fs.accessSync('D:')
|
||||||
CERT_PATH = 'D:'
|
CERT_PATH = 'D:'
|
||||||
@@ -369,16 +376,15 @@ function buildWin32 (cb) {
|
|||||||
CERT_PATH = path.join(os.homedir(), 'Desktop')
|
CERT_PATH = path.join(os.homedir(), 'Desktop')
|
||||||
}
|
}
|
||||||
|
|
||||||
electronPackager(Object.assign({}, all, win32), function (err, buildPath) {
|
electronPackager(Object.assign({}, all, win32)).then(function (buildPath) {
|
||||||
if (err) return cb(err)
|
|
||||||
console.log('Windows: Packaged electron. ' + buildPath)
|
console.log('Windows: Packaged electron. ' + buildPath)
|
||||||
|
|
||||||
var signWithParams
|
let signWithParams
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
if (argv.sign) {
|
if (argv.sign) {
|
||||||
var certificateFile = path.join(CERT_PATH, 'authenticode.p12')
|
const certificateFile = path.join(CERT_PATH, 'authenticode.p12')
|
||||||
var certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
|
const certificatePassword = fs.readFileSync(path.join(CERT_PATH, 'authenticode.txt'), 'utf8')
|
||||||
var timestampServer = 'http://timestamp.comodoca.com'
|
const timestampServer = 'http://timestamp.comodoca.com'
|
||||||
signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256`
|
signWithParams = `/a /f "${certificateFile}" /p "${certificatePassword}" /tr "${timestampServer}" /td sha256`
|
||||||
} else {
|
} else {
|
||||||
printWarning()
|
printWarning()
|
||||||
@@ -387,20 +393,22 @@ function buildWin32 (cb) {
|
|||||||
printWarning()
|
printWarning()
|
||||||
}
|
}
|
||||||
|
|
||||||
var tasks = []
|
const tasks = []
|
||||||
if (argv.package === 'exe' || argv.package === 'all') {
|
buildPath.forEach(function (filesPath) {
|
||||||
tasks.push((cb) => packageInstaller(cb))
|
if (argv.package === 'exe' || argv.package === 'all') {
|
||||||
}
|
tasks.push((cb) => packageInstaller(filesPath, cb))
|
||||||
if (argv.package === 'portable' || argv.package === 'all') {
|
}
|
||||||
tasks.push((cb) => packagePortable(cb))
|
if (argv.package === 'portable' || argv.package === 'all') {
|
||||||
}
|
tasks.push((cb) => packagePortable(filesPath, cb))
|
||||||
|
}
|
||||||
|
})
|
||||||
series(tasks, cb)
|
series(tasks, cb)
|
||||||
|
|
||||||
function packageInstaller (cb) {
|
function packageInstaller (filesPath, cb) {
|
||||||
console.log('Windows: Creating installer...')
|
console.log('Windows: Creating installer...')
|
||||||
|
|
||||||
installer.createWindowsInstaller({
|
installer.createWindowsInstaller({
|
||||||
appDirectory: buildPath[0],
|
appDirectory: filesPath,
|
||||||
authors: config.APP_TEAM,
|
authors: config.APP_TEAM,
|
||||||
description: config.APP_NAME,
|
description: config.APP_NAME,
|
||||||
exe: config.APP_NAME + '.exe',
|
exe: config.APP_NAME + '.exe',
|
||||||
@@ -410,7 +418,16 @@ function buildWin32 (cb) {
|
|||||||
noMsi: true,
|
noMsi: true,
|
||||||
outputDirectory: DIST_PATH,
|
outputDirectory: DIST_PATH,
|
||||||
productName: config.APP_NAME,
|
productName: config.APP_NAME,
|
||||||
remoteReleases: config.GITHUB_URL,
|
// TODO: Re-enable Windows 64-bit delta updates when we confirm that they
|
||||||
|
// work correctly in the presence of the "ia32" .nupkg files. I
|
||||||
|
// (feross) noticed them listed in the 64-bit RELEASES file and
|
||||||
|
// manually edited them out for the v0.17 release. Shipping only
|
||||||
|
// full updates for now will work fine, with no ill-effects.
|
||||||
|
// remoteReleases: config.GITHUB_URL,
|
||||||
|
/**
|
||||||
|
* If you hit a "GitHub API rate limit exceeded" error, set this token!
|
||||||
|
*/
|
||||||
|
// remoteToken: process.env.WEBTORRENT_GITHUB_API_TOKEN,
|
||||||
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
setupExe: config.APP_NAME + 'Setup-v' + config.APP_VERSION + '.exe',
|
||||||
setupIcon: config.APP_ICON + '.ico',
|
setupIcon: config.APP_ICON + '.ico',
|
||||||
signWithParams: signWithParams,
|
signWithParams: signWithParams,
|
||||||
@@ -418,60 +435,79 @@ function buildWin32 (cb) {
|
|||||||
usePackageJson: false,
|
usePackageJson: false,
|
||||||
version: pkg.version
|
version: pkg.version
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
console.log('Windows: Created installer.')
|
console.log('Windows: Created installer.')
|
||||||
cb(null)
|
|
||||||
})
|
/**
|
||||||
.catch(cb)
|
* Delete extraneous Squirrel files (i.e. *.nupkg delta files for older
|
||||||
|
* versions of the app)
|
||||||
|
*/
|
||||||
|
fs.readdirSync(DIST_PATH)
|
||||||
|
.filter((name) => name.endsWith('.nupkg') && !name.includes(pkg.version))
|
||||||
|
.forEach((filename) => {
|
||||||
|
fs.unlinkSync(path.join(DIST_PATH, filename))
|
||||||
|
})
|
||||||
|
|
||||||
|
cb(null)
|
||||||
|
})
|
||||||
|
.catch(cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
function packagePortable (cb) {
|
function packagePortable (filesPath, cb) {
|
||||||
console.log('Windows: Creating portable app...')
|
console.log('Windows: Creating portable app...')
|
||||||
|
|
||||||
var portablePath = path.join(buildPath[0], 'Portable Settings')
|
const portablePath = path.join(filesPath, 'Portable Settings')
|
||||||
mkdirp.sync(portablePath)
|
mkdirp.sync(portablePath)
|
||||||
|
|
||||||
var inPath = path.join(DIST_PATH, path.basename(buildPath[0]))
|
const downloadsPath = path.join(portablePath, 'Downloads')
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
mkdirp.sync(downloadsPath)
|
||||||
|
|
||||||
|
const tempPath = path.join(portablePath, 'Temp')
|
||||||
|
mkdirp.sync(tempPath)
|
||||||
|
|
||||||
|
const inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||||
|
const outPath = path.join(DIST_PATH, BUILD_NAME + '-win.zip')
|
||||||
zip.zipSync(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log('Windows: Created portable app.')
|
console.log('Windows: Created portable app.')
|
||||||
cb(null)
|
cb(null)
|
||||||
}
|
}
|
||||||
|
}).catch(function (err) {
|
||||||
|
cb(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLinux (cb) {
|
function buildLinux (cb) {
|
||||||
console.log('Linux: Packaging electron...')
|
console.log('Linux: Packaging electron...')
|
||||||
electronPackager(Object.assign({}, all, linux), function (err, buildPath) {
|
|
||||||
if (err) return cb(err)
|
electronPackager(Object.assign({}, all, linux)).then(function (buildPath) {
|
||||||
console.log('Linux: Packaged electron. ' + buildPath)
|
console.log('Linux: Packaged electron. ' + buildPath)
|
||||||
|
|
||||||
var tasks = []
|
const tasks = []
|
||||||
buildPath.forEach(function (filesPath) {
|
buildPath.forEach(function (filesPath) {
|
||||||
var destArch = filesPath.split('-').pop()
|
|
||||||
|
|
||||||
if (argv.package === 'deb' || argv.package === 'all') {
|
if (argv.package === 'deb' || argv.package === 'all') {
|
||||||
tasks.push((cb) => packageDeb(filesPath, destArch, cb))
|
tasks.push((cb) => packageDeb(filesPath, cb))
|
||||||
}
|
}
|
||||||
if (argv.package === 'zip' || argv.package === 'all') {
|
if (argv.package === 'zip' || argv.package === 'all') {
|
||||||
tasks.push((cb) => packageZip(filesPath, destArch, cb))
|
tasks.push((cb) => packageZip(filesPath, cb))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
series(tasks, cb)
|
series(tasks, cb)
|
||||||
|
}).catch(function (err) {
|
||||||
|
cb(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
function packageDeb (filesPath, destArch, cb) {
|
function packageDeb (filesPath, cb) {
|
||||||
// Create .deb file for Debian-based platforms
|
// Create .deb file for Debian-based platforms
|
||||||
console.log(`Linux: Creating ${destArch} deb...`)
|
console.log('Linux: Creating deb...')
|
||||||
|
|
||||||
var deb = require('nobin-debian-installer')()
|
const deb = require('nobin-debian-installer')()
|
||||||
var destPath = path.join('/opt', pkg.name)
|
const destPath = path.join('/opt', pkg.name)
|
||||||
|
|
||||||
deb.pack({
|
deb.pack({
|
||||||
package: pkg,
|
package: pkg,
|
||||||
info: {
|
info: {
|
||||||
arch: destArch === 'x64' ? 'amd64' : 'i386',
|
arch: 'amd64',
|
||||||
targetDir: DIST_PATH,
|
targetDir: DIST_PATH,
|
||||||
depends: 'gconf2, libgtk2.0-0, libnss3, libxss1',
|
depends: 'gconf2, libgtk2.0-0, libnss3, libxss1',
|
||||||
scripts: {
|
scripts: {
|
||||||
@@ -491,20 +527,20 @@ function buildLinux (cb) {
|
|||||||
cwd: path.join(config.STATIC_PATH, 'linux', 'share')
|
cwd: path.join(config.STATIC_PATH, 'linux', 'share')
|
||||||
}], function (err) {
|
}], function (err) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
console.log(`Linux: Created ${destArch} deb.`)
|
console.log('Linux: Created deb.')
|
||||||
cb(null)
|
cb(null)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function packageZip (filesPath, destArch, cb) {
|
function packageZip (filesPath, cb) {
|
||||||
// Create .zip file for Linux
|
// Create .zip file for Linux
|
||||||
console.log(`Linux: Creating ${destArch} zip...`)
|
console.log('Linux: Creating zip...')
|
||||||
|
|
||||||
var inPath = path.join(DIST_PATH, path.basename(filesPath))
|
const inPath = path.join(DIST_PATH, path.basename(filesPath))
|
||||||
var outPath = path.join(DIST_PATH, BUILD_NAME + '-linux-' + destArch + '.zip')
|
const outPath = path.join(DIST_PATH, BUILD_NAME + '-linux.zip')
|
||||||
zip.zipSync(inPath, outPath)
|
zip.zipSync(inPath, outPath)
|
||||||
|
|
||||||
console.log(`Linux: Created ${destArch} zip.`)
|
console.log('Linux: Created zip.')
|
||||||
cb(null)
|
cb(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
npm run update-authors
|
|
||||||
git diff --exit-code
|
|
||||||
npm run package -- --sign
|
|
||||||
git push
|
|
||||||
git push --tags
|
|
||||||
npm publish
|
|
||||||
./node_modules/.bin/gh-release
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
git pull
|
|
||||||
rm -rf node_modules/
|
|
||||||
npm install
|
|
||||||
npm dedupe
|
|
||||||
npm test
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
BIN=`dirname $0`
|
|
||||||
|
|
||||||
$BIN/release-_pre.sh
|
|
||||||
npm version major
|
|
||||||
$BIN/release-_post.sh
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
BIN=`dirname $0`
|
|
||||||
|
|
||||||
$BIN/release-_pre.sh
|
|
||||||
npm version minor
|
|
||||||
$BIN/release-_post.sh
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
BIN=`dirname $0`
|
|
||||||
|
|
||||||
$BIN/release-_pre.sh
|
|
||||||
npm version patch
|
|
||||||
$BIN/release-_post.sh
|
|
||||||
@@ -12,6 +12,7 @@ while (<>) {
|
|||||||
next if /(ungoldman\@gmail.com)/;
|
next if /(ungoldman\@gmail.com)/;
|
||||||
next if /(dc\@DCs-MacBook.local)/;
|
next if /(dc\@DCs-MacBook.local)/;
|
||||||
next if /(rolandoguedes\@gmail.com)/;
|
next if /(rolandoguedes\@gmail.com)/;
|
||||||
|
next if /(grunjol\@users.noreply.github.com)/;
|
||||||
$seen{$_} = push @authors, "- ", $_;
|
$seen{$_} = push @authors, "- ", $_;
|
||||||
}
|
}
|
||||||
END {
|
END {
|
||||||
|
|||||||
10551
package-lock.json
generated
Normal file
10551
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
118
package.json
118
package.json
@@ -1,66 +1,75 @@
|
|||||||
{
|
{
|
||||||
"name": "webtorrent-desktop",
|
"name": "webtorrent-desktop",
|
||||||
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
"description": "WebTorrent, the streaming torrent client. For Mac, Windows, and Linux.",
|
||||||
"version": "0.12.0",
|
"version": "0.20.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "WebTorrent, LLC",
|
"name": "WebTorrent, LLC",
|
||||||
"email": "feross@webtorrent.io",
|
"email": "feross@webtorrent.io",
|
||||||
"url": "https://webtorrent.io"
|
"url": "https://webtorrent.io"
|
||||||
},
|
},
|
||||||
"bin": {
|
|
||||||
"webtorrent-desktop": "./bin/cmd.js"
|
|
||||||
},
|
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/feross/webtorrent-desktop/issues"
|
"url": "https://github.com/webtorrent/webtorrent-desktop/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"airplayer": "^2.0.0",
|
"airplayer": "^2.0.0",
|
||||||
"application-config": "^1.0.0",
|
"application-config": "^1.0.1",
|
||||||
"bitfield": "^1.0.2",
|
"arch": "^2.1.1",
|
||||||
"chromecasts": "^1.8.0",
|
"auto-launch": "^5.0.5",
|
||||||
"create-torrent": "^3.24.5",
|
"bitfield": "^3.0.0",
|
||||||
"deep-equal": "^1.0.1",
|
"capture-frame": "^3.0.0",
|
||||||
|
"chokidar": "^3.0.2",
|
||||||
|
"chromecasts": "^1.9.1",
|
||||||
|
"cp-file": "^7.0.0",
|
||||||
|
"create-torrent": "^4.3.1",
|
||||||
|
"debounce": "^1.2.0",
|
||||||
|
"deep-equal": "^1.1.0",
|
||||||
"dlnacasts": "^0.1.0",
|
"dlnacasts": "^0.1.0",
|
||||||
"drag-drop": "^2.12.1",
|
"drag-drop": "^5.0.1",
|
||||||
"electron-prebuilt": "1.3.3",
|
"es6-error": "^4.1.1",
|
||||||
"fs-extra": "^0.30.0",
|
"fn-getter": "^1.0.0",
|
||||||
"hat": "0.0.3",
|
"iso-639-1": "^2.1.0",
|
||||||
"iso-639-1": "^1.2.1",
|
"languagedetect": "^1.2.0",
|
||||||
"languagedetect": "^1.1.1",
|
"location-history": "^1.1.1",
|
||||||
"location-history": "^1.0.0",
|
"material-ui": "^0.20.2",
|
||||||
"musicmetadata": "^2.0.2",
|
"mkdirp": "^0.5.1",
|
||||||
"network-address": "^1.1.0",
|
"music-metadata": "^4.5.0",
|
||||||
"parse-torrent": "^5.7.3",
|
"network-address": "^1.1.2",
|
||||||
"prettier-bytes": "^1.0.1",
|
"nobin-debian-installer": "github:webtorrent/nobin-debian-installer",
|
||||||
"react": "^15.2.1",
|
"parse-torrent": "^7.0.1",
|
||||||
"react-dom": "^15.2.1",
|
"prettier-bytes": "^1.0.4",
|
||||||
"run-parallel": "^1.1.6",
|
"prop-types": "^15.7.2",
|
||||||
"semver": "^5.1.0",
|
"react": "^16.9.0",
|
||||||
|
"react-dom": "^16.9.0",
|
||||||
|
"rimraf": "^3.0.0",
|
||||||
|
"run-parallel": "^1.1.9",
|
||||||
|
"semver": "^6.3.0",
|
||||||
"simple-concat": "^1.0.0",
|
"simple-concat": "^1.0.0",
|
||||||
"simple-get": "^2.0.0",
|
"simple-get": "^3.0.3",
|
||||||
"srt-to-vtt": "^1.1.1",
|
"srt-to-vtt": "^1.1.3",
|
||||||
"vlc-command": "^1.0.1",
|
"vlc-command": "^1.2.0",
|
||||||
"webtorrent": "0.x",
|
"webtorrent": ">=0.107.7",
|
||||||
"winreg": "^1.2.0",
|
"winreg": "^1.2.4"
|
||||||
"zero-fill": "^2.2.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "^6.11.4",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-plugin-syntax-jsx": "^6.13.0",
|
"buble": "^0.19.8",
|
||||||
"babel-plugin-transform-react-jsx": "^6.8.0",
|
"cross-zip": "^2.1.6",
|
||||||
"cross-zip": "^2.0.1",
|
"depcheck": "^0.8.3",
|
||||||
"electron-osx-sign": "^0.3.0",
|
"electron": "~6.0.7",
|
||||||
"electron-packager": "^7.0.0",
|
"electron-osx-sign": "^0.4.12",
|
||||||
"electron-winstaller": "^2.3.0",
|
"electron-packager": "^14.0.5",
|
||||||
"gh-release": "^2.0.3",
|
"electron-winstaller": "^4.0.0",
|
||||||
|
"gh-release": "^3.5.0",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"mkdirp": "^0.5.1",
|
"nodemon": "^1.19.2",
|
||||||
"nobin-debian-installer": "^0.0.10",
|
"opn": "^6.0.0",
|
||||||
"open": "0.0.5",
|
"plist": "^3.0.1",
|
||||||
"plist": "^2.0.1",
|
"pngjs": "^3.4.0",
|
||||||
"rimraf": "^2.5.2",
|
"run-series": "^1.1.8",
|
||||||
"run-series": "^1.1.4",
|
"spectron": "^8.0.0",
|
||||||
"standard": "^8.0.0-beta.5"
|
"standard": "*",
|
||||||
|
"tape": "^4.11.0",
|
||||||
|
"walk-sync": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0.0"
|
"node": ">=4.0.0"
|
||||||
@@ -79,21 +88,28 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"appdmg": "^0.4.3"
|
"appdmg": "^0.6.0"
|
||||||
},
|
},
|
||||||
|
"private": true,
|
||||||
"productName": "WebTorrent",
|
"productName": "WebTorrent",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/feross/webtorrent-desktop.git"
|
"url": "git://github.com/webtorrent/webtorrent-desktop.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "babel --quiet src --out-dir build",
|
"build": "buble src --output build",
|
||||||
"clean": "node ./bin/clean.js",
|
"clean": "node ./bin/clean.js",
|
||||||
|
"gh-release": "gh-release",
|
||||||
"open-config": "node ./bin/open-config.js",
|
"open-config": "node ./bin/open-config.js",
|
||||||
"package": "node ./bin/package.js",
|
"package": "node ./bin/package.js",
|
||||||
"prepublish": "npm run build",
|
"prepublish": "npm run build",
|
||||||
"start": "npm run build && electron .",
|
"start": "npm run build && electron .",
|
||||||
"test": "standard && node ./bin/check-deps.js",
|
"test": "standard && depcheck --ignores=standard,babel-eslint --ignore-dirs=build,dist && node ./bin/extra-lint.js",
|
||||||
"update-authors": "./bin/update-authors.sh"
|
"test-integration": "npm run build && node ./test",
|
||||||
|
"update-authors": "./bin/update-authors.sh",
|
||||||
|
"watch": "nodemon --exec \"npm run start\" --ext js,css --ignore build/ --ignore dist/"
|
||||||
|
},
|
||||||
|
"standard": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": [
|
|
||||||
"syntax-jsx",
|
|
||||||
"transform-react-jsx"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
104
src/config.js
104
src/config.js
@@ -1,12 +1,21 @@
|
|||||||
var appConfig = require('application-config')('WebTorrent')
|
const appConfig = require('application-config')('WebTorrent')
|
||||||
var fs = require('fs')
|
const path = require('path')
|
||||||
var path = require('path')
|
const electron = require('electron')
|
||||||
|
const arch = require('arch')
|
||||||
|
|
||||||
var APP_NAME = 'WebTorrent'
|
const APP_NAME = 'WebTorrent'
|
||||||
var APP_TEAM = 'WebTorrent, LLC'
|
const APP_TEAM = 'WebTorrent, LLC'
|
||||||
var APP_VERSION = require('../package.json').version
|
const APP_VERSION = require('../package.json').version
|
||||||
|
|
||||||
var PORTABLE_PATH = path.join(path.dirname(process.execPath), 'Portable Settings')
|
const IS_TEST = isTest()
|
||||||
|
const PORTABLE_PATH = IS_TEST
|
||||||
|
? path.join(process.platform === 'win32' ? 'C:\\Windows\\Temp' : '/tmp', 'WebTorrentTest')
|
||||||
|
: path.join(path.dirname(process.execPath), 'Portable Settings')
|
||||||
|
const IS_PRODUCTION = isProduction()
|
||||||
|
const IS_PORTABLE = isPortable()
|
||||||
|
|
||||||
|
const UI_HEADER_HEIGHT = 38
|
||||||
|
const UI_TORRENT_HEIGHT = 100
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
ANNOUNCEMENT_URL: 'https://webtorrent.io/desktop/announcement',
|
||||||
@@ -14,39 +23,44 @@ module.exports = {
|
|||||||
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
CRASH_REPORT_URL: 'https://webtorrent.io/desktop/crash-report',
|
||||||
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
|
TELEMETRY_URL: 'https://webtorrent.io/desktop/telemetry',
|
||||||
|
|
||||||
APP_COPYRIGHT: 'Copyright © 2014-2016 ' + APP_TEAM,
|
APP_COPYRIGHT: 'Copyright © 2014-2019 ' + APP_TEAM,
|
||||||
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
|
APP_FILE_ICON: path.join(__dirname, '..', 'static', 'WebTorrentFile'),
|
||||||
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
|
APP_ICON: path.join(__dirname, '..', 'static', 'WebTorrent'),
|
||||||
APP_NAME: APP_NAME,
|
APP_NAME: APP_NAME,
|
||||||
APP_TEAM: APP_TEAM,
|
APP_TEAM: APP_TEAM,
|
||||||
APP_VERSION: APP_VERSION,
|
APP_VERSION: APP_VERSION,
|
||||||
APP_WINDOW_TITLE: APP_NAME + ' (BETA)',
|
APP_WINDOW_TITLE: APP_NAME,
|
||||||
|
|
||||||
CONFIG_PATH: getConfigPath(),
|
CONFIG_PATH: getConfigPath(),
|
||||||
|
|
||||||
DEFAULT_TORRENTS: [
|
DEFAULT_TORRENTS: [
|
||||||
{
|
{
|
||||||
|
testID: 'bbb',
|
||||||
name: 'Big Buck Bunny',
|
name: 'Big Buck Bunny',
|
||||||
posterFileName: 'bigBuckBunny.jpg',
|
posterFileName: 'bigBuckBunny.jpg',
|
||||||
torrentFileName: 'bigBuckBunny.torrent'
|
torrentFileName: 'bigBuckBunny.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'cosmos',
|
||||||
name: 'Cosmos Laundromat (Preview)',
|
name: 'Cosmos Laundromat (Preview)',
|
||||||
posterFileName: 'cosmosLaundromat.jpg',
|
posterFileName: 'cosmosLaundromat.jpg',
|
||||||
torrentFileName: 'cosmosLaundromat.torrent'
|
torrentFileName: 'cosmosLaundromat.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'sintel',
|
||||||
name: 'Sintel',
|
name: 'Sintel',
|
||||||
posterFileName: 'sintel.jpg',
|
posterFileName: 'sintel.jpg',
|
||||||
torrentFileName: 'sintel.torrent'
|
torrentFileName: 'sintel.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
testID: 'tears',
|
||||||
name: 'Tears of Steel',
|
name: 'Tears of Steel',
|
||||||
posterFileName: 'tearsOfSteel.jpg',
|
posterFileName: 'tearsOfSteel.jpg',
|
||||||
torrentFileName: 'tearsOfSteel.torrent'
|
torrentFileName: 'tearsOfSteel.torrent'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'The WIRED CD - Rip. Sample. Mash. Share.',
|
testID: 'wired',
|
||||||
|
name: 'The WIRED CD - Rip. Sample. Mash. Share',
|
||||||
posterFileName: 'wiredCd.jpg',
|
posterFileName: 'wiredCd.jpg',
|
||||||
torrentFileName: 'wiredCd.torrent'
|
torrentFileName: 'wiredCd.torrent'
|
||||||
}
|
}
|
||||||
@@ -56,14 +70,17 @@ module.exports = {
|
|||||||
|
|
||||||
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
|
DEFAULT_DOWNLOAD_PATH: getDefaultDownloadPath(),
|
||||||
|
|
||||||
GITHUB_URL: 'https://github.com/feross/webtorrent-desktop',
|
GITHUB_URL: 'https://github.com/webtorrent/webtorrent-desktop',
|
||||||
GITHUB_URL_ISSUES: 'https://github.com/feross/webtorrent-desktop/issues',
|
GITHUB_URL_ISSUES: 'https://github.com/webtorrent/webtorrent-desktop/issues',
|
||||||
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/feross/webtorrent-desktop/master',
|
GITHUB_URL_RAW: 'https://raw.githubusercontent.com/webtorrent/webtorrent-desktop/master',
|
||||||
|
|
||||||
HOME_PAGE_URL: 'https://webtorrent.io',
|
HOME_PAGE_URL: 'https://webtorrent.io',
|
||||||
|
|
||||||
IS_PORTABLE: isPortable(),
|
IS_PORTABLE: IS_PORTABLE,
|
||||||
IS_PRODUCTION: isProduction(),
|
IS_PRODUCTION: IS_PRODUCTION,
|
||||||
|
IS_TEST: IS_TEST,
|
||||||
|
|
||||||
|
OS_SYSARCH: arch() === 'x64' ? 'x64' : 'ia32',
|
||||||
|
|
||||||
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
POSTER_PATH: path.join(getConfigPath(), 'Posters'),
|
||||||
ROOT_PATH: path.join(__dirname, '..'),
|
ROOT_PATH: path.join(__dirname, '..'),
|
||||||
@@ -74,12 +91,19 @@ module.exports = {
|
|||||||
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
|
WINDOW_MAIN: 'file://' + path.join(__dirname, '..', 'static', 'main.html'),
|
||||||
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
|
WINDOW_WEBTORRENT: 'file://' + path.join(__dirname, '..', 'static', 'webtorrent.html'),
|
||||||
|
|
||||||
WINDOW_MIN_HEIGHT: 38 + (120 * 2), // header height + 2 torrents
|
WINDOW_INITIAL_BOUNDS: {
|
||||||
WINDOW_MIN_WIDTH: 425
|
width: 500,
|
||||||
|
height: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 6) // header + 6 torrents
|
||||||
|
},
|
||||||
|
WINDOW_MIN_HEIGHT: UI_HEADER_HEIGHT + (UI_TORRENT_HEIGHT * 2), // header + 2 torrents
|
||||||
|
WINDOW_MIN_WIDTH: 425,
|
||||||
|
|
||||||
|
UI_HEADER_HEIGHT: UI_HEADER_HEIGHT,
|
||||||
|
UI_TORRENT_HEIGHT: UI_TORRENT_HEIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfigPath () {
|
function getConfigPath () {
|
||||||
if (isPortable()) {
|
if (IS_PORTABLE) {
|
||||||
return PORTABLE_PATH
|
return PORTABLE_PATH
|
||||||
} else {
|
} else {
|
||||||
return path.dirname(appConfig.filePath)
|
return path.dirname(appConfig.filePath)
|
||||||
@@ -87,24 +111,47 @@ function getConfigPath () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultDownloadPath () {
|
function getDefaultDownloadPath () {
|
||||||
if (!process || !process.type) {
|
if (IS_PORTABLE) {
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPortable()) {
|
|
||||||
return path.join(getConfigPath(), 'Downloads')
|
return path.join(getConfigPath(), 'Downloads')
|
||||||
|
} else {
|
||||||
|
return getPath('downloads')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
function getPath (key) {
|
||||||
|
if (!process.versions.electron) {
|
||||||
|
// Node.js process
|
||||||
|
return ''
|
||||||
|
} else if (process.type === 'renderer') {
|
||||||
|
// Electron renderer process
|
||||||
|
return electron.remote.app.getPath(key)
|
||||||
|
} else {
|
||||||
|
// Electron main process
|
||||||
|
return electron.app.getPath(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return process.type === 'renderer'
|
function isTest () {
|
||||||
? electron.remote.app.getPath('downloads')
|
return process.env.NODE_ENV === 'test'
|
||||||
: electron.app.getPath('downloads')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPortable () {
|
function isPortable () {
|
||||||
|
if (IS_TEST) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform !== 'win32' || !IS_PRODUCTION) {
|
||||||
|
// Fast path: Non-Windows platforms should not check for path on disk
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return process.platform === 'win32' && isProduction() && !!fs.statSync(PORTABLE_PATH)
|
// This line throws if the "Portable Settings" folder does not exist, and does
|
||||||
|
// nothing otherwise.
|
||||||
|
fs.accessSync(PORTABLE_PATH, fs.constants.R_OK | fs.constants.W_OK)
|
||||||
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -112,6 +159,7 @@ function isPortable () {
|
|||||||
|
|
||||||
function isProduction () {
|
function isProduction () {
|
||||||
if (!process.versions.electron) {
|
if (!process.versions.electron) {
|
||||||
|
// Node.js process
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ module.exports = {
|
|||||||
init
|
init
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = require('./config')
|
|
||||||
var electron = require('electron')
|
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
|
const config = require('./config')
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
electron.crashReporter.start({
|
electron.crashReporter.start({
|
||||||
companyName: config.APP_NAME,
|
companyName: config.APP_NAME,
|
||||||
productName: config.APP_NAME,
|
productName: config.APP_NAME,
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ module.exports = {
|
|||||||
init
|
init
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
|
|
||||||
var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
const ANNOUNCEMENT_URL =
|
||||||
'?version=' + config.APP_VERSION +
|
`${config.ANNOUNCEMENT_URL}?version=${config.APP_VERSION}&platform=${process.platform}`
|
||||||
'&platform=' + process.platform
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In certain situations, the WebTorrent team may need to show an announcement to
|
* In certain situations, the WebTorrent team may need to show an announcement to
|
||||||
@@ -26,7 +25,7 @@ var ANNOUNCEMENT_URL = config.ANNOUNCEMENT_URL +
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
function init () {
|
function init () {
|
||||||
var get = require('simple-get')
|
const get = require('simple-get')
|
||||||
get.concat(ANNOUNCEMENT_URL, onResponse)
|
get.concat(ANNOUNCEMENT_URL, onResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ module.exports = {
|
|||||||
openFiles
|
openFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show open dialog to create a single-file torrent.
|
* Show open dialog to create a single-file torrent.
|
||||||
@@ -17,16 +17,11 @@ var windows = require('./windows')
|
|||||||
function openSeedFile () {
|
function openSeedFile () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openSeedFile')
|
log('openSeedFile')
|
||||||
var opts = {
|
const opts = {
|
||||||
title: 'Select a file for the torrent.',
|
title: 'Select a file for the torrent.',
|
||||||
properties: [ 'openFile' ]
|
properties: ['openFile']
|
||||||
}
|
}
|
||||||
setTitle(opts.title)
|
showOpenSeed(opts)
|
||||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
|
||||||
resetTitle()
|
|
||||||
if (!Array.isArray(selectedPaths)) return
|
|
||||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -37,21 +32,16 @@ function openSeedFile () {
|
|||||||
function openSeedDirectory () {
|
function openSeedDirectory () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openSeedDirectory')
|
log('openSeedDirectory')
|
||||||
var 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']
|
||||||
}
|
}
|
||||||
setTitle(opts.title)
|
showOpenSeed(opts)
|
||||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
|
||||||
resetTitle()
|
|
||||||
if (!Array.isArray(selectedPaths)) return
|
|
||||||
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -61,14 +51,14 @@ function openSeedDirectory () {
|
|||||||
function openFiles () {
|
function openFiles () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openFiles')
|
log('openFiles')
|
||||||
var 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)
|
||||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||||
@@ -84,10 +74,10 @@ function openFiles () {
|
|||||||
function openTorrentFile () {
|
function openTorrentFile () {
|
||||||
if (!windows.main.win) return
|
if (!windows.main.win) return
|
||||||
log('openTorrentFile')
|
log('openTorrentFile')
|
||||||
var opts = {
|
const opts = {
|
||||||
title: 'Select a .torrent file.',
|
title: 'Select a .torrent file.',
|
||||||
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
filters: [{ name: 'Torrent Files', extensions: ['torrent'] }],
|
||||||
properties: [ 'openFile', 'multiSelections' ]
|
properties: ['openFile', 'multiSelections']
|
||||||
}
|
}
|
||||||
setTitle(opts.title)
|
setTitle(opts.title)
|
||||||
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||||
@@ -119,3 +109,16 @@ function setTitle (title) {
|
|||||||
function resetTitle () {
|
function resetTitle () {
|
||||||
windows.main.dispatch('resetTitle')
|
windows.main.dispatch('resetTitle')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pops up an Open File dialog with the given options.
|
||||||
|
* After the user selects files / folders, shows the Create Torrent page.
|
||||||
|
*/
|
||||||
|
function showOpenSeed (opts) {
|
||||||
|
setTitle(opts.title)
|
||||||
|
electron.dialog.showOpenDialog(windows.main.win, opts, function (selectedPaths) {
|
||||||
|
resetTitle()
|
||||||
|
if (!Array.isArray(selectedPaths)) return
|
||||||
|
windows.main.dispatch('showCreateTorrent', selectedPaths)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ module.exports = {
|
|||||||
setBadge
|
setBadge
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const { app, Menu } = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const dialog = require('./dialog')
|
||||||
|
const log = require('./log')
|
||||||
var dialog = require('./dialog')
|
|
||||||
var log = require('./log')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a right-click menu to the dock icon. (Mac)
|
* Add a right-click menu to the dock icon. (Mac)
|
||||||
*/
|
*/
|
||||||
function init () {
|
function init () {
|
||||||
if (!app.dock) return
|
if (!app.dock) return
|
||||||
var menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
const menu = Menu.buildFromTemplate(getMenuTemplate())
|
||||||
app.dock.setMenu(menu)
|
app.dock.setMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,8 +31,11 @@ function downloadFinished (path) {
|
|||||||
* Display a counter badge for the app. (Mac, Linux)
|
* Display a counter badge for the app. (Mac, Linux)
|
||||||
*/
|
*/
|
||||||
function setBadge (count) {
|
function setBadge (count) {
|
||||||
log(`setBadge: ${count}`)
|
if (process.platform === 'darwin' ||
|
||||||
app.setBadgeCount(Number(count))
|
(process.platform === 'linux' && app.isUnityRunning())) {
|
||||||
|
log(`setBadge: ${count}`)
|
||||||
|
app.setBadgeCount(Number(count))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMenuTemplate () {
|
function getMenuTemplate () {
|
||||||
|
|||||||
@@ -4,50 +4,61 @@ module.exports = {
|
|||||||
checkInstall
|
checkInstall
|
||||||
}
|
}
|
||||||
|
|
||||||
var cp = require('child_process')
|
const cp = require('child_process')
|
||||||
var vlcCommand = require('vlc-command')
|
const path = require('path')
|
||||||
|
const vlcCommand = require('vlc-command')
|
||||||
|
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
var windows = require('./windows')
|
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
|
||||||
var proc
|
let proc = null
|
||||||
|
|
||||||
function checkInstall (path, cb) {
|
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 (path == null) return vlcCommand((err) => cb(!err))
|
if (playerPath == null) return vlcCommand(cb)
|
||||||
process.nextTick(() => cb(true))
|
process.nextTick(() => cb(null))
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawn (path, url, title) {
|
function spawn (playerPath, url, title) {
|
||||||
if (path != null) return spawnExternal(path, [url])
|
if (playerPath != null) 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
|
||||||
vlcCommand(function (err, vlcPath) {
|
vlcCommand((err, vlcPath) => {
|
||||||
if (err) return windows.main.dispatch('externalPlayerNotFound')
|
if (err) return windows.main.dispatch('externalPlayerNotFound')
|
||||||
var args = ['--play-and-exit', '--video-on-top', '--quiet', `--meta-title=${JSON.stringify(title)}`, url]
|
const args = [
|
||||||
|
'--play-and-exit',
|
||||||
|
'--quiet',
|
||||||
|
`--meta-title=${JSON.stringify(title)}`,
|
||||||
|
url
|
||||||
|
]
|
||||||
spawnExternal(vlcPath, args)
|
spawnExternal(vlcPath, args)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function kill () {
|
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
|
||||||
proc = null
|
proc = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnExternal (path, args) {
|
function spawnExternal (playerPath, args) {
|
||||||
log('Running external media player:', path + ' ' + args.join(' '))
|
log('Running external media player:', `${playerPath} ${args.join(' ')}`)
|
||||||
|
|
||||||
proc = cp.spawn(path, args, {stdio: 'ignore'})
|
if (process.platform === 'darwin' && path.extname(playerPath) === '.app') {
|
||||||
|
// Mac: Use executable in packaged .app bundle
|
||||||
|
playerPath += `/Contents/MacOS/${path.basename(playerPath, '.app')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
proc = cp.spawn(playerPath, args, { stdio: 'ignore' })
|
||||||
|
|
||||||
// If it works, close the modal after a second
|
// If it works, close the modal after a second
|
||||||
var closeModalTimeout = setTimeout(() =>
|
const closeModalTimeout = setTimeout(() =>
|
||||||
windows.main.dispatch('exitModal'), 1000)
|
windows.main.dispatch('exitModal'), 1000)
|
||||||
|
|
||||||
proc.on('close', function (code) {
|
proc.on('close', code => {
|
||||||
clearTimeout(closeModalTimeout)
|
clearTimeout(closeModalTimeout)
|
||||||
if (!proc) return // Killed
|
if (!proc) return // Killed
|
||||||
log('External player exited with code ', code)
|
log('External player exited with code ', code)
|
||||||
@@ -59,7 +70,7 @@ function spawnExternal (path, args) {
|
|||||||
proc = null
|
proc = null
|
||||||
})
|
})
|
||||||
|
|
||||||
proc.on('error', function (e) {
|
proc.on('error', err => {
|
||||||
log('External player error', e)
|
log('External player error', err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
50
src/main/folder-watcher.js
Normal file
50
src/main/folder-watcher.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const chokidar = require('chokidar')
|
||||||
|
const log = require('./log')
|
||||||
|
|
||||||
|
class FolderWatcher {
|
||||||
|
constructor ({ window, state }) {
|
||||||
|
this.window = window
|
||||||
|
this.state = state
|
||||||
|
this.torrentsFolderPath = null
|
||||||
|
this.watching = false
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled () {
|
||||||
|
return this.state.saved.prefs.autoAddTorrents
|
||||||
|
}
|
||||||
|
|
||||||
|
start () {
|
||||||
|
// Stop watching previous folder before
|
||||||
|
// start watching a new one.
|
||||||
|
if (this.watching) this.stop()
|
||||||
|
|
||||||
|
const torrentsFolderPath = this.state.saved.prefs.torrentsFolderPath
|
||||||
|
this.torrentsFolderPath = torrentsFolderPath
|
||||||
|
if (!torrentsFolderPath) return
|
||||||
|
|
||||||
|
const glob = `${torrentsFolderPath}/**/*.torrent`
|
||||||
|
log('Folder Watcher: watching: ', glob)
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
ignoreInitial: true,
|
||||||
|
awaitWriteFinish: true
|
||||||
|
}
|
||||||
|
this.watcher = chokidar.watch(glob, options)
|
||||||
|
this.watcher
|
||||||
|
.on('add', (path) => {
|
||||||
|
log('Folder Watcher: added torrent: ', path)
|
||||||
|
this.window.dispatch('addTorrent', path)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.watching = true
|
||||||
|
}
|
||||||
|
|
||||||
|
stop () {
|
||||||
|
log('Folder Watcher: stop.')
|
||||||
|
if (!this.watching) return
|
||||||
|
this.watcher.close()
|
||||||
|
this.watching = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FolderWatcher
|
||||||
@@ -3,36 +3,34 @@ module.exports = {
|
|||||||
uninstall
|
uninstall
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
function install () {
|
function install () {
|
||||||
if (process.platform === 'darwin') {
|
switch (process.platform) {
|
||||||
installDarwin()
|
case 'darwin': installDarwin()
|
||||||
}
|
break
|
||||||
if (process.platform === 'win32') {
|
case 'win32': installWin32()
|
||||||
installWin32()
|
break
|
||||||
}
|
case 'linux': installLinux()
|
||||||
if (process.platform === 'linux') {
|
break
|
||||||
installLinux()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstall () {
|
function uninstall () {
|
||||||
if (process.platform === 'darwin') {
|
switch (process.platform) {
|
||||||
uninstallDarwin()
|
case 'darwin': uninstallDarwin()
|
||||||
}
|
break
|
||||||
if (process.platform === 'win32') {
|
case 'win32': uninstallWin32()
|
||||||
uninstallWin32()
|
break
|
||||||
}
|
case 'linux': uninstallLinux()
|
||||||
if (process.platform === 'linux') {
|
break
|
||||||
uninstallLinux()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function installDarwin () {
|
function installDarwin () {
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var app = electron.app
|
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.
|
||||||
@@ -44,18 +42,18 @@ function installDarwin () {
|
|||||||
|
|
||||||
function uninstallDarwin () {}
|
function uninstallDarwin () {}
|
||||||
|
|
||||||
var EXEC_COMMAND = [ process.execPath ]
|
const EXEC_COMMAND = [process.execPath, '--']
|
||||||
|
|
||||||
if (!config.IS_PRODUCTION) {
|
if (!config.IS_PRODUCTION) {
|
||||||
EXEC_COMMAND.push(config.ROOT_PATH)
|
EXEC_COMMAND.push(config.ROOT_PATH)
|
||||||
}
|
}
|
||||||
|
|
||||||
function installWin32 () {
|
function installWin32 () {
|
||||||
var Registry = require('winreg')
|
const Registry = require('winreg')
|
||||||
|
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
|
|
||||||
var iconPath = path.join(
|
const iconPath = path.join(
|
||||||
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
|
process.resourcesPath, 'app.asar.unpacked', 'static', 'WebTorrentFile.ico'
|
||||||
)
|
)
|
||||||
registerProtocolHandlerWin32(
|
registerProtocolHandlerWin32(
|
||||||
@@ -100,7 +98,7 @@ function installWin32 () {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function registerProtocolHandlerWin32 (protocol, name, icon, command) {
|
function registerProtocolHandlerWin32 (protocol, name, icon, command) {
|
||||||
var protocolKey = new Registry({
|
const protocolKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + protocol
|
key: '\\Software\\Classes\\' + protocol
|
||||||
})
|
})
|
||||||
@@ -108,37 +106,37 @@ function installWin32 () {
|
|||||||
setProtocol()
|
setProtocol()
|
||||||
|
|
||||||
function setProtocol (err) {
|
function setProtocol (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
protocolKey.set('', Registry.REG_SZ, name, setURLProtocol)
|
protocolKey.set('', Registry.REG_SZ, name, setURLProtocol)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setURLProtocol (err) {
|
function setURLProtocol (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
protocolKey.set('URL Protocol', Registry.REG_SZ, '', setIcon)
|
protocolKey.set('URL Protocol', Registry.REG_SZ, '', setIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setIcon (err) {
|
function setIcon (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var iconKey = new Registry({
|
const iconKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + protocol + '\\DefaultIcon'
|
key: `\\Software\\Classes\\${protocol}\\DefaultIcon`
|
||||||
})
|
})
|
||||||
iconKey.set('', Registry.REG_SZ, icon, setCommand)
|
iconKey.set('', Registry.REG_SZ, icon, setCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCommand (err) {
|
function setCommand (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var commandKey = new Registry({
|
const commandKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
key: `\\Software\\Classes\\${protocol}\\shell\\open\\command`
|
||||||
})
|
})
|
||||||
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
|
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
|
||||||
}
|
}
|
||||||
|
|
||||||
function done (err) {
|
function done (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,51 +159,51 @@ function installWin32 () {
|
|||||||
setExt()
|
setExt()
|
||||||
|
|
||||||
function setExt () {
|
function setExt () {
|
||||||
var extKey = new Registry({
|
const extKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + ext
|
key: `\\Software\\Classes\\${ext}`
|
||||||
})
|
})
|
||||||
extKey.set('', Registry.REG_SZ, id, setId)
|
extKey.set('', Registry.REG_SZ, id, setId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setId (err) {
|
function setId (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var idKey = new Registry({
|
const idKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + id
|
key: `\\Software\\Classes\\${id}`
|
||||||
})
|
})
|
||||||
idKey.set('', Registry.REG_SZ, name, setIcon)
|
idKey.set('', Registry.REG_SZ, name, setIcon)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setIcon (err) {
|
function setIcon (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var iconKey = new Registry({
|
const iconKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + id + '\\DefaultIcon'
|
key: `\\Software\\Classes\\${id}\\DefaultIcon`
|
||||||
})
|
})
|
||||||
iconKey.set('', Registry.REG_SZ, icon, setCommand)
|
iconKey.set('', Registry.REG_SZ, icon, setCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCommand (err) {
|
function setCommand (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var commandKey = new Registry({
|
const commandKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + id + '\\shell\\open\\command'
|
key: `\\Software\\Classes\\${id}\\shell\\open\\command`
|
||||||
})
|
})
|
||||||
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
|
commandKey.set('', Registry.REG_SZ, `${commandToArgs(command)} "%1"`, done)
|
||||||
}
|
}
|
||||||
|
|
||||||
function done (err) {
|
function done (err) {
|
||||||
if (err) log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstallWin32 () {
|
function uninstallWin32 () {
|
||||||
var Registry = require('winreg')
|
const Registry = require('winreg')
|
||||||
|
|
||||||
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
unregisterProtocolHandlerWin32('magnet', EXEC_COMMAND)
|
||||||
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
|
unregisterProtocolHandlerWin32('stream-magnet', EXEC_COMMAND)
|
||||||
@@ -215,11 +213,11 @@ function uninstallWin32 () {
|
|||||||
getCommand()
|
getCommand()
|
||||||
|
|
||||||
function getCommand () {
|
function getCommand () {
|
||||||
var commandKey = new Registry({
|
const commandKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + protocol + '\\shell\\open\\command'
|
key: `\\Software\\Classes\\${protocol}\\shell\\open\\command`
|
||||||
})
|
})
|
||||||
commandKey.get('', function (err, item) {
|
commandKey.get('', (err, item) => {
|
||||||
if (!err && item.value.indexOf(commandToArgs(command)) >= 0) {
|
if (!err && item.value.indexOf(commandToArgs(command)) >= 0) {
|
||||||
destroyProtocol()
|
destroyProtocol()
|
||||||
}
|
}
|
||||||
@@ -227,11 +225,11 @@ function uninstallWin32 () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function destroyProtocol () {
|
function destroyProtocol () {
|
||||||
var protocolKey = new Registry({
|
const protocolKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + protocol
|
key: `\\Software\\Classes\\${protocol}`
|
||||||
})
|
})
|
||||||
protocolKey.destroy(function () {})
|
protocolKey.destroy(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,19 +237,19 @@ function uninstallWin32 () {
|
|||||||
eraseId()
|
eraseId()
|
||||||
|
|
||||||
function eraseId () {
|
function eraseId () {
|
||||||
var idKey = new Registry({
|
const idKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + id
|
key: `\\Software\\Classes\\${id}`
|
||||||
})
|
})
|
||||||
idKey.destroy(getExt)
|
idKey.destroy(getExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExt () {
|
function getExt () {
|
||||||
var extKey = new Registry({
|
const extKey = new Registry({
|
||||||
hive: Registry.HKCU,
|
hive: Registry.HKCU,
|
||||||
key: '\\Software\\Classes\\' + ext
|
key: `\\Software\\Classes\\${ext}`
|
||||||
})
|
})
|
||||||
extKey.get('', function (err, item) {
|
extKey.get('', (err, item) => {
|
||||||
if (!err && item.value === id) {
|
if (!err && item.value === id) {
|
||||||
destroyExt()
|
destroyExt()
|
||||||
}
|
}
|
||||||
@@ -259,11 +257,11 @@ function uninstallWin32 () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function destroyExt () {
|
function destroyExt () {
|
||||||
var extKey = new Registry({
|
const extKey = new Registry({
|
||||||
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
hive: Registry.HKCU, // HKEY_CURRENT_USER
|
||||||
key: '\\Software\\Classes\\' + ext
|
key: `\\Software\\Classes\\${ext}`
|
||||||
})
|
})
|
||||||
extKey.destroy(function () {})
|
extKey.destroy(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,12 +271,12 @@ function commandToArgs (command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function installLinux () {
|
function installLinux () {
|
||||||
var fs = require('fs-extra')
|
const fs = require('fs')
|
||||||
var os = require('os')
|
const os = require('os')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
|
|
||||||
// Do not install in user dir if running on system
|
// Do not install in user dir if running on system
|
||||||
if (/^\/opt/.test(process.execPath)) return
|
if (/^\/opt/.test(process.execPath)) return
|
||||||
@@ -287,7 +285,7 @@ function installLinux () {
|
|||||||
installIconFile()
|
installIconFile()
|
||||||
|
|
||||||
function installDesktopFile () {
|
function installDesktopFile () {
|
||||||
var templatePath = path.join(
|
const templatePath = path.join(
|
||||||
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
|
config.STATIC_PATH, 'linux', 'webtorrent-desktop.desktop'
|
||||||
)
|
)
|
||||||
fs.readFile(templatePath, 'utf8', writeDesktopFile)
|
fs.readFile(templatePath, 'utf8', writeDesktopFile)
|
||||||
@@ -296,16 +294,17 @@ function installLinux () {
|
|||||||
function writeDesktopFile (err, desktopFile) {
|
function writeDesktopFile (err, desktopFile) {
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var appPath = config.IS_PRODUCTION
|
const appPath = config.IS_PRODUCTION
|
||||||
? path.dirname(process.execPath)
|
? path.dirname(process.execPath)
|
||||||
: config.ROOT_PATH
|
: config.ROOT_PATH
|
||||||
|
|
||||||
desktopFile = desktopFile.replace(/\$APP_NAME/g, config.APP_NAME)
|
desktopFile = desktopFile
|
||||||
desktopFile = desktopFile.replace(/\$APP_PATH/g, appPath)
|
.replace(/\$APP_NAME/g, config.APP_NAME)
|
||||||
desktopFile = desktopFile.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
|
.replace(/\$APP_PATH/g, appPath)
|
||||||
desktopFile = desktopFile.replace(/\$TRY_EXEC_PATH/g, process.execPath)
|
.replace(/\$EXEC_PATH/g, EXEC_COMMAND.join(' '))
|
||||||
|
.replace(/\$TRY_EXEC_PATH/g, process.execPath)
|
||||||
|
|
||||||
var desktopFilePath = path.join(
|
const desktopFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
'.local',
|
'.local',
|
||||||
'share',
|
'share',
|
||||||
@@ -313,53 +312,57 @@ function installLinux () {
|
|||||||
'webtorrent-desktop.desktop'
|
'webtorrent-desktop.desktop'
|
||||||
)
|
)
|
||||||
fs.mkdirp(path.dirname(desktopFilePath))
|
fs.mkdirp(path.dirname(desktopFilePath))
|
||||||
fs.writeFile(desktopFilePath, desktopFile, function (err) {
|
fs.writeFile(desktopFilePath, desktopFile, err => {
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function installIconFile () {
|
function installIconFile () {
|
||||||
var iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
|
const iconStaticPath = path.join(config.STATIC_PATH, 'WebTorrent.png')
|
||||||
fs.readFile(iconStaticPath, writeIconFile)
|
fs.readFile(iconStaticPath, writeIconFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeIconFile (err, iconFile) {
|
function writeIconFile (err, iconFile) {
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
|
||||||
var iconFilePath = path.join(
|
const mkdirp = require('mkdirp')
|
||||||
|
|
||||||
|
const iconFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
'.local',
|
'.local',
|
||||||
'share',
|
'share',
|
||||||
'icons',
|
'icons',
|
||||||
'webtorrent-desktop.png'
|
'webtorrent-desktop.png'
|
||||||
)
|
)
|
||||||
fs.mkdirp(path.dirname(iconFilePath))
|
mkdirp(path.dirname(iconFilePath), err => {
|
||||||
fs.writeFile(iconFilePath, iconFile, function (err) {
|
|
||||||
if (err) return log.error(err.message)
|
if (err) return log.error(err.message)
|
||||||
|
fs.writeFile(iconFilePath, iconFile, err => {
|
||||||
|
if (err) log.error(err.message)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninstallLinux () {
|
function uninstallLinux () {
|
||||||
var os = require('os')
|
const os = require('os')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var fs = require('fs-extra')
|
const rimraf = require('rimraf')
|
||||||
|
|
||||||
var desktopFilePath = path.join(
|
const desktopFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
'.local',
|
'.local',
|
||||||
'share',
|
'share',
|
||||||
'applications',
|
'applications',
|
||||||
'webtorrent-desktop.desktop'
|
'webtorrent-desktop.desktop'
|
||||||
)
|
)
|
||||||
fs.removeSync(desktopFilePath)
|
rimraf.sync(desktopFilePath)
|
||||||
|
|
||||||
var iconFilePath = path.join(
|
const iconFilePath = path.join(
|
||||||
os.homedir(),
|
os.homedir(),
|
||||||
'.local',
|
'.local',
|
||||||
'share',
|
'share',
|
||||||
'icons',
|
'icons',
|
||||||
'webtorrent-desktop.png'
|
'webtorrent-desktop.png'
|
||||||
)
|
)
|
||||||
fs.removeSync(iconFilePath)
|
rimraf.sync(iconFilePath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,30 @@
|
|||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
const app = electron.app
|
||||||
|
|
||||||
var app = electron.app
|
const parallel = require('run-parallel')
|
||||||
var ipcMain = electron.ipcMain
|
|
||||||
|
|
||||||
var announcement = require('./announcement')
|
const config = require('../config')
|
||||||
var config = require('../config')
|
const crashReporter = require('../crash-reporter')
|
||||||
var crashReporter = require('../crash-reporter')
|
const ipc = require('./ipc')
|
||||||
var dialog = require('./dialog')
|
const log = require('./log')
|
||||||
var dock = require('./dock')
|
const menu = require('./menu')
|
||||||
var ipc = require('./ipc')
|
const State = require('../renderer/lib/state')
|
||||||
var log = require('./log')
|
const windows = require('./windows')
|
||||||
var menu = require('./menu')
|
|
||||||
var squirrelWin32 = require('./squirrel-win32')
|
|
||||||
var tray = require('./tray')
|
|
||||||
var updater = require('./updater')
|
|
||||||
var userTasks = require('./user-tasks')
|
|
||||||
var windows = require('./windows')
|
|
||||||
|
|
||||||
var shouldQuit = false
|
const WEBTORRENT_VERSION = require('webtorrent/package.json').version
|
||||||
var argv = sliceArgv(process.argv)
|
|
||||||
|
let shouldQuit = false
|
||||||
|
let argv = sliceArgv(process.argv)
|
||||||
|
|
||||||
|
// allow electron/chromium to play startup sounds (without user interaction)
|
||||||
|
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
|
||||||
|
|
||||||
|
// Start the app without showing the main window when auto launching on login
|
||||||
|
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
|
||||||
|
const hidden = argv.includes('--hidden') ||
|
||||||
|
(process.platform === 'darwin' && app.getLoginItemSettings().wasOpenedAsHidden)
|
||||||
|
|
||||||
if (config.IS_PRODUCTION) {
|
if (config.IS_PRODUCTION) {
|
||||||
// When Electron is running in production mode (packaged app), then run React
|
// When Electron is running in production mode (packaged app), then run React
|
||||||
@@ -29,33 +33,77 @@ if (config.IS_PRODUCTION) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
|
const squirrelWin32 = require('./squirrel-win32')
|
||||||
shouldQuit = squirrelWin32.handleEvent(argv[0])
|
shouldQuit = squirrelWin32.handleEvent(argv[0])
|
||||||
argv = argv.filter((arg) => !arg.includes('--squirrel'))
|
argv = argv.filter((arg) => !arg.includes('--squirrel'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldQuit) {
|
if (!shouldQuit && !config.IS_PORTABLE) {
|
||||||
// Prevent multiple instances of app from running at same time. New instances
|
// Prevent multiple instances of app from running at same time. New instances
|
||||||
// signal this instance and quit.
|
// signal this instance and quit. Note: This feature creates a lock file in
|
||||||
shouldQuit = app.makeSingleInstance(onAppOpen)
|
// %APPDATA%\Roaming\WebTorrent so we do not do it for the Portable App since
|
||||||
if (shouldQuit) {
|
// we want to be "silent" as well as "portable".
|
||||||
app.quit()
|
if (!app.requestSingleInstanceLock()) {
|
||||||
|
shouldQuit = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldQuit) {
|
if (shouldQuit) {
|
||||||
|
app.quit()
|
||||||
|
} else {
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
|
app.on('second-instance', (event, commandLine, workingDirectory) => onAppOpen(commandLine))
|
||||||
if (config.IS_PORTABLE) {
|
if (config.IS_PORTABLE) {
|
||||||
|
const path = require('path')
|
||||||
|
// 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
|
||||||
|
app.setPath('temp', path.join(config.CONFIG_PATH, 'Temp'))
|
||||||
}
|
}
|
||||||
|
|
||||||
var isReady = false // app ready, windows can be created
|
const ipcMain = electron.ipcMain
|
||||||
|
|
||||||
|
let isReady = false // app ready, windows can be created
|
||||||
app.ipcReady = false // main window has finished loading and IPC is ready
|
app.ipcReady = false // main window has finished loading and IPC is ready
|
||||||
app.isQuitting = false
|
app.isQuitting = false
|
||||||
|
|
||||||
// Open handlers must be added as early as possible
|
parallel({
|
||||||
|
appReady: (cb) => app.on('ready', () => cb(null)),
|
||||||
|
state: (cb) => State.load(cb)
|
||||||
|
}, onReady)
|
||||||
|
|
||||||
|
function onReady (err, results) {
|
||||||
|
if (err) throw err
|
||||||
|
|
||||||
|
isReady = true
|
||||||
|
const state = results.state
|
||||||
|
|
||||||
|
windows.main.init(state, { hidden: hidden })
|
||||||
|
windows.webtorrent.init()
|
||||||
|
menu.init()
|
||||||
|
|
||||||
|
// To keep app startup fast, some code is delayed.
|
||||||
|
setTimeout(() => {
|
||||||
|
delayedInit(state)
|
||||||
|
}, config.DELAYED_INIT)
|
||||||
|
|
||||||
|
// Report uncaught exceptions
|
||||||
|
process.on('uncaughtException', (err) => {
|
||||||
|
console.error(err)
|
||||||
|
const error = { message: err.message, stack: err.stack }
|
||||||
|
windows.main.dispatch('uncaughtError', 'main', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable app logging into default directory, i.e. /Library/Logs/WebTorrent
|
||||||
|
// on Mac, %APPDATA% on Windows, $XDG_CONFIG_HOME or ~/.config on Linux.
|
||||||
|
app.setAppLogsPath()
|
||||||
|
|
||||||
|
app.userAgentFallback = `WebTorrent/${WEBTORRENT_VERSION} (https://webtorrent.io)`
|
||||||
|
|
||||||
app.on('open-file', onOpen)
|
app.on('open-file', onOpen)
|
||||||
app.on('open-url', onOpen)
|
app.on('open-url', onOpen)
|
||||||
|
|
||||||
@@ -65,24 +113,6 @@ function init () {
|
|||||||
crashReporter.init()
|
crashReporter.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('ready', function () {
|
|
||||||
isReady = true
|
|
||||||
|
|
||||||
windows.main.init()
|
|
||||||
windows.webtorrent.init()
|
|
||||||
menu.init()
|
|
||||||
|
|
||||||
// To keep app startup fast, some code is delayed.
|
|
||||||
setTimeout(delayedInit, config.DELAYED_INIT)
|
|
||||||
|
|
||||||
// Report uncaught exceptions
|
|
||||||
process.on('uncaughtException', (err) => {
|
|
||||||
console.error(err)
|
|
||||||
var errJSON = {message: err.message, stack: err.stack}
|
|
||||||
windows.main.dispatch('uncaughtError', 'main', errJSON)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
app.once('ipcReady', function () {
|
app.once('ipcReady', function () {
|
||||||
log('Command line args:', argv)
|
log('Command line args:', argv)
|
||||||
processArgv(argv)
|
processArgv(argv)
|
||||||
@@ -94,12 +124,12 @@ function init () {
|
|||||||
|
|
||||||
app.isQuitting = true
|
app.isQuitting = true
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
windows.main.dispatch('saveState') // try to save state on exit
|
windows.main.dispatch('stateSaveImmediate') // try to save state on exit
|
||||||
ipcMain.once('savedState', () => app.quit())
|
ipcMain.once('stateSaved', () => app.quit())
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.error('Saving state took too long. Quitting.')
|
console.error('Saving state took too long. Quitting.')
|
||||||
app.quit()
|
app.quit()
|
||||||
}, 2000) // quit after 2 secs, at most
|
}, 4000) // quit after 4 secs, at most
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
@@ -107,12 +137,33 @@ function init () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function delayedInit () {
|
function delayedInit (state) {
|
||||||
|
if (app.isQuitting) return
|
||||||
|
|
||||||
|
const announcement = require('./announcement')
|
||||||
|
const dock = require('./dock')
|
||||||
|
const updater = require('./updater')
|
||||||
|
const FolderWatcher = require('./folder-watcher')
|
||||||
|
const folderWatcher = new FolderWatcher({ window: windows.main, state })
|
||||||
|
|
||||||
announcement.init()
|
announcement.init()
|
||||||
dock.init()
|
dock.init()
|
||||||
tray.init()
|
|
||||||
updater.init()
|
updater.init()
|
||||||
userTasks.init()
|
|
||||||
|
ipc.setModule('folderWatcher', folderWatcher)
|
||||||
|
if (folderWatcher.isEnabled()) {
|
||||||
|
folderWatcher.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const userTasks = require('./user-tasks')
|
||||||
|
userTasks.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
const tray = require('./tray')
|
||||||
|
tray.init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpen (e, torrentId) {
|
function onOpen (e, torrentId) {
|
||||||
@@ -124,7 +175,7 @@ function onOpen (e, torrentId) {
|
|||||||
// Electron issue: https://github.com/atom/electron/issues/4338
|
// Electron issue: https://github.com/atom/electron/issues/4338
|
||||||
setTimeout(() => windows.main.show(), 100)
|
setTimeout(() => windows.main.show(), 100)
|
||||||
|
|
||||||
processArgv([ torrentId ])
|
processArgv([torrentId])
|
||||||
} else {
|
} else {
|
||||||
argv.push(torrentId)
|
argv.push(torrentId)
|
||||||
}
|
}
|
||||||
@@ -143,22 +194,38 @@ function onAppOpen (newArgv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove leading args.
|
||||||
|
// Production: 1 arg, eg: /Applications/WebTorrent.app/Contents/MacOS/WebTorrent
|
||||||
|
// Development: 2 args, eg: electron .
|
||||||
|
// Test: 4 args, eg: electron -r .../mocks.js .
|
||||||
function sliceArgv (argv) {
|
function sliceArgv (argv) {
|
||||||
return argv.slice(config.IS_PRODUCTION ? 1 : 2)
|
return argv.slice(config.IS_PRODUCTION ? 1
|
||||||
|
: config.IS_TEST ? 4
|
||||||
|
: 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
function processArgv (argv) {
|
function processArgv (argv) {
|
||||||
var torrentIds = []
|
const torrentIds = []
|
||||||
argv.forEach(function (arg) {
|
argv.forEach(function (arg) {
|
||||||
if (arg === '-n') {
|
if (arg === '-n' || arg === '-o' || arg === '-u') {
|
||||||
dialog.openSeedDirectory()
|
// Critical path: Only load the 'dialog' package if it is needed
|
||||||
} else if (arg === '-o') {
|
const dialog = require('./dialog')
|
||||||
dialog.openTorrentFile()
|
if (arg === '-n') {
|
||||||
} else if (arg === '-u') {
|
dialog.openSeedDirectory()
|
||||||
dialog.openTorrentAddress()
|
} else if (arg === '-o') {
|
||||||
|
dialog.openTorrentFile()
|
||||||
|
} else if (arg === '-u') {
|
||||||
|
dialog.openTorrentAddress()
|
||||||
|
}
|
||||||
|
} else if (arg === '--hidden') {
|
||||||
|
// Ignore hidden argument, already being handled
|
||||||
} else if (arg.startsWith('-psn')) {
|
} else if (arg.startsWith('-psn')) {
|
||||||
// Ignore Mac launchd "process serial number" argument
|
// Ignore Mac launchd "process serial number" argument
|
||||||
// Issue: https://github.com/feross/webtorrent-desktop/issues/214
|
// Issue: https://github.com/webtorrent/webtorrent-desktop/issues/214
|
||||||
|
} else if (arg.startsWith('--')) {
|
||||||
|
// Ignore Spectron flags
|
||||||
|
} else if (arg === 'data:,') {
|
||||||
|
// Ignore weird Spectron argument
|
||||||
} else if (arg !== '.') {
|
} else if (arg !== '.') {
|
||||||
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
|
// Ignore '.' argument, which gets misinterpreted as a torrent id, when a
|
||||||
// development copy of WebTorrent is started while a production version is
|
// development copy of WebTorrent is started while a production version is
|
||||||
|
|||||||
161
src/main/ipc.js
161
src/main/ipc.js
@@ -1,28 +1,29 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init
|
init,
|
||||||
|
setModule
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
var dialog = require('./dialog')
|
const log = require('./log')
|
||||||
var dock = require('./dock')
|
const menu = require('./menu')
|
||||||
var handlers = require('./handlers')
|
const windows = require('./windows')
|
||||||
var log = require('./log')
|
|
||||||
var menu = require('./menu')
|
|
||||||
var powerSaveBlocker = require('./power-save-blocker')
|
|
||||||
var shell = require('./shell')
|
|
||||||
var shortcuts = require('./shortcuts')
|
|
||||||
var externalPlayer = require('./external-player')
|
|
||||||
var windows = require('./windows')
|
|
||||||
var thumbar = require('./thumbar')
|
|
||||||
|
|
||||||
// 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
|
||||||
var messageQueueMainToWebTorrent = []
|
const messageQueueMainToWebTorrent = []
|
||||||
|
|
||||||
|
// Will hold modules injected from the app that will be used on fired
|
||||||
|
// IPC events.
|
||||||
|
const modules = {}
|
||||||
|
|
||||||
|
function setModule (name, module) {
|
||||||
|
modules[name] = module
|
||||||
|
}
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
var ipc = electron.ipcMain
|
const ipc = electron.ipcMain
|
||||||
|
|
||||||
ipc.once('ipcReady', function (e) {
|
ipc.once('ipcReady', function (e) {
|
||||||
app.ipcReady = true
|
app.ipcReady = true
|
||||||
@@ -43,65 +44,143 @@ function init () {
|
|||||||
* Dialog
|
* Dialog
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('openTorrentFile', () => dialog.openTorrentFile())
|
ipc.on('openTorrentFile', () => {
|
||||||
ipc.on('openFiles', () => dialog.openFiles())
|
const dialog = require('./dialog')
|
||||||
|
dialog.openTorrentFile()
|
||||||
|
})
|
||||||
|
ipc.on('openFiles', () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openFiles()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dock
|
* Dock
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('setBadge', (e, ...args) => dock.setBadge(...args))
|
ipc.on('setBadge', (e, ...args) => {
|
||||||
ipc.on('downloadFinished', (e, ...args) => dock.downloadFinished(...args))
|
const dock = require('./dock')
|
||||||
|
dock.setBadge(...args)
|
||||||
|
})
|
||||||
|
ipc.on('downloadFinished', (e, ...args) => {
|
||||||
|
const dock = require('./dock')
|
||||||
|
dock.downloadFinished(...args)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Events
|
* Player Events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('onPlayerOpen', function () {
|
ipc.on('onPlayerOpen', function () {
|
||||||
menu.setPlayerOpen(true)
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
|
const shortcuts = require('./shortcuts')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
|
menu.togglePlaybackControls(true)
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
shortcuts.enable()
|
shortcuts.enable()
|
||||||
thumbar.enable()
|
thumbar.enable()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipc.on('onPlayerUpdate', function (e, ...args) {
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
|
menu.onPlayerUpdate(...args)
|
||||||
|
thumbar.onPlayerUpdate(...args)
|
||||||
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerClose', function () {
|
ipc.on('onPlayerClose', function () {
|
||||||
menu.setPlayerOpen(false)
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
|
const shortcuts = require('./shortcuts')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
|
menu.togglePlaybackControls(false)
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
shortcuts.disable()
|
shortcuts.disable()
|
||||||
thumbar.disable()
|
thumbar.disable()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerPlay', function () {
|
ipc.on('onPlayerPlay', function () {
|
||||||
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
powerSaveBlocker.enable()
|
powerSaveBlocker.enable()
|
||||||
thumbar.onPlayerPlay()
|
thumbar.onPlayerPlay()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerPause', function () {
|
ipc.on('onPlayerPause', function () {
|
||||||
|
const powerSaveBlocker = require('./power-save-blocker')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
thumbar.onPlayerPause()
|
thumbar.onPlayerPause()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Folder Watcher Events
|
||||||
|
*/
|
||||||
|
|
||||||
|
ipc.on('startFolderWatcher', function () {
|
||||||
|
if (!modules.folderWatcher) {
|
||||||
|
log('IPC ERR: folderWatcher module is not defined.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.folderWatcher.start()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipc.on('stopFolderWatcher', function () {
|
||||||
|
if (!modules.folderWatcher) {
|
||||||
|
log('IPC ERR: folderWatcher module is not defined.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modules.folderWatcher.stop()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shell
|
* Shell
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('openItem', (e, ...args) => shell.openItem(...args))
|
ipc.on('openItem', (e, ...args) => {
|
||||||
ipc.on('showItemInFolder', (e, ...args) => shell.showItemInFolder(...args))
|
const shell = require('./shell')
|
||||||
ipc.on('moveItemToTrash', (e, ...args) => shell.moveItemToTrash(...args))
|
shell.openItem(...args)
|
||||||
|
})
|
||||||
|
ipc.on('showItemInFolder', (e, ...args) => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.showItemInFolder(...args)
|
||||||
|
})
|
||||||
|
ipc.on('moveItemToTrash', (e, ...args) => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.moveItemToTrash(...args)
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File handlers
|
* File handlers
|
||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('setDefaultFileHandler', (e, flag) => {
|
ipc.on('setDefaultFileHandler', (e, flag) => {
|
||||||
|
const handlers = require('./handlers')
|
||||||
|
|
||||||
if (flag) handlers.install()
|
if (flag) handlers.install()
|
||||||
else handlers.uninstall()
|
else handlers.uninstall()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto start on login
|
||||||
|
*/
|
||||||
|
|
||||||
|
ipc.on('setStartup', (e, flag) => {
|
||||||
|
const startup = require('./startup')
|
||||||
|
|
||||||
|
if (flag) startup.install()
|
||||||
|
else startup.uninstall()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Windows: Main
|
* Windows: Main
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var main = windows.main
|
const main = windows.main
|
||||||
|
|
||||||
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
|
ipc.on('setAspectRatio', (e, ...args) => main.setAspectRatio(...args))
|
||||||
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
|
ipc.on('setBounds', (e, ...args) => main.setBounds(...args))
|
||||||
@@ -116,16 +195,32 @@ function init () {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
ipc.on('checkForExternalPlayer', function (e, path) {
|
ipc.on('checkForExternalPlayer', function (e, path) {
|
||||||
externalPlayer.checkInstall(path, function (isInstalled) {
|
const externalPlayer = require('./external-player')
|
||||||
windows.main.send('checkForExternalPlayer', isInstalled)
|
|
||||||
|
externalPlayer.checkInstall(path, function (err) {
|
||||||
|
windows.main.send('checkForExternalPlayer', !err)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ipc.on('openExternalPlayer', (e, ...args) => externalPlayer.spawn(...args))
|
ipc.on('openExternalPlayer', (e, ...args) => {
|
||||||
ipc.on('quitExternalPlayer', () => externalPlayer.kill())
|
const externalPlayer = require('./external-player')
|
||||||
|
const thumbar = require('./thumbar')
|
||||||
|
|
||||||
// Capture all events
|
menu.togglePlaybackControls(false)
|
||||||
var oldEmit = ipc.emit
|
thumbar.disable()
|
||||||
|
externalPlayer.spawn(...args)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipc.on('quitExternalPlayer', () => {
|
||||||
|
const externalPlayer = require('./external-player')
|
||||||
|
externalPlayer.kill()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message passing
|
||||||
|
*/
|
||||||
|
|
||||||
|
const oldEmit = ipc.emit
|
||||||
ipc.emit = function (name, e, ...args) {
|
ipc.emit = function (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) {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ module.exports.error = error
|
|||||||
* where they can be viewed in Developer Tools.
|
* where they can be viewed in Developer Tools.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
function log (...args) {
|
function log (...args) {
|
||||||
if (app.ipcReady) {
|
if (app.ipcReady) {
|
||||||
|
|||||||
133
src/main/menu.js
133
src/main/menu.js
@@ -1,30 +1,31 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
setPlayerOpen,
|
togglePlaybackControls,
|
||||||
setWindowFocus,
|
setWindowFocus,
|
||||||
setAllowNav,
|
setAllowNav,
|
||||||
|
onPlayerUpdate,
|
||||||
onToggleAlwaysOnTop,
|
onToggleAlwaysOnTop,
|
||||||
onToggleFullScreen
|
onToggleFullScreen
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var dialog = require('./dialog')
|
const windows = require('./windows')
|
||||||
var shell = require('./shell')
|
|
||||||
var windows = require('./windows')
|
|
||||||
|
|
||||||
var menu
|
let menu = null
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
menu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||||
electron.Menu.setApplicationMenu(menu)
|
electron.Menu.setApplicationMenu(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPlayerOpen (flag) {
|
function togglePlaybackControls (flag) {
|
||||||
getMenuItem('Play/Pause').enabled = flag
|
getMenuItem('Play/Pause').enabled = flag
|
||||||
|
getMenuItem('Skip Next').enabled = flag
|
||||||
|
getMenuItem('Skip Previous').enabled = flag
|
||||||
getMenuItem('Increase Volume').enabled = flag
|
getMenuItem('Increase Volume').enabled = flag
|
||||||
getMenuItem('Decrease Volume').enabled = flag
|
getMenuItem('Decrease Volume').enabled = flag
|
||||||
getMenuItem('Step Forward').enabled = flag
|
getMenuItem('Step Forward').enabled = flag
|
||||||
@@ -32,6 +33,16 @@ function setPlayerOpen (flag) {
|
|||||||
getMenuItem('Increase Speed').enabled = flag
|
getMenuItem('Increase Speed').enabled = flag
|
||||||
getMenuItem('Decrease Speed').enabled = flag
|
getMenuItem('Decrease Speed').enabled = flag
|
||||||
getMenuItem('Add Subtitles File...').enabled = flag
|
getMenuItem('Add Subtitles File...').enabled = flag
|
||||||
|
|
||||||
|
if (flag === false) {
|
||||||
|
getMenuItem('Skip Next').enabled = false
|
||||||
|
getMenuItem('Skip Previous').enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlayerUpdate (hasNext, hasPrevious) {
|
||||||
|
getMenuItem('Skip Next').enabled = hasNext
|
||||||
|
getMenuItem('Skip Previous').enabled = hasPrevious
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWindowFocus (flag) {
|
function setWindowFocus (flag) {
|
||||||
@@ -42,9 +53,12 @@ function setWindowFocus (flag) {
|
|||||||
// Disallow opening more screens on top of the current one.
|
// Disallow opening more screens on top of the current one.
|
||||||
function setAllowNav (flag) {
|
function setAllowNav (flag) {
|
||||||
getMenuItem('Preferences').enabled = flag
|
getMenuItem('Preferences').enabled = flag
|
||||||
getMenuItem('Create New Torrent...').enabled = flag
|
if (process.platform === 'darwin') {
|
||||||
var item = getMenuItem('Create New Torrent from File...')
|
getMenuItem('Create New Torrent...').enabled = flag
|
||||||
if (item) item.enabled = flag
|
} else {
|
||||||
|
getMenuItem('Create New Torrent from Folder...').enabled = flag
|
||||||
|
getMenuItem('Create New Torrent from File...').enabled = flag
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleAlwaysOnTop (flag) {
|
function onToggleAlwaysOnTop (flag) {
|
||||||
@@ -56,8 +70,8 @@ function onToggleFullScreen (flag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMenuItem (label) {
|
function getMenuItem (label) {
|
||||||
for (var i = 0; i < menu.items.length; i++) {
|
for (let i = 0; i < menu.items.length; i++) {
|
||||||
var menuItem = menu.items[i].submenu.items.find(function (item) {
|
const menuItem = menu.items[i].submenu.items.find(function (item) {
|
||||||
return item.label === label
|
return item.label === label
|
||||||
})
|
})
|
||||||
if (menuItem) return menuItem
|
if (menuItem) return menuItem
|
||||||
@@ -65,7 +79,7 @@ function getMenuItem (label) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMenuTemplate () {
|
function getMenuTemplate () {
|
||||||
var template = [
|
const template = [
|
||||||
{
|
{
|
||||||
label: 'File',
|
label: 'File',
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -74,17 +88,26 @@ 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: () => dialog.openSeedDirectory()
|
click: () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openSeedDirectory()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent File...',
|
label: 'Open Torrent File...',
|
||||||
accelerator: 'CmdOrCtrl+O',
|
accelerator: 'CmdOrCtrl+O',
|
||||||
click: () => dialog.openTorrentFile()
|
click: () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openTorrentFile()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Open Torrent Address...',
|
label: 'Open Torrent Address...',
|
||||||
accelerator: 'CmdOrCtrl+U',
|
accelerator: 'CmdOrCtrl+U',
|
||||||
click: () => dialog.openTorrentAddress()
|
click: () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openTorrentAddress()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
@@ -184,6 +207,21 @@ function getMenuTemplate () {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Skip Next',
|
||||||
|
accelerator: 'N',
|
||||||
|
click: () => windows.main.dispatch('nextTrack'),
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Skip Previous',
|
||||||
|
accelerator: 'P',
|
||||||
|
click: () => windows.main.dispatch('previousTrack'),
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Increase Volume',
|
label: 'Increase Volume',
|
||||||
accelerator: 'CmdOrCtrl+Up',
|
accelerator: 'CmdOrCtrl+Up',
|
||||||
@@ -240,31 +278,53 @@ function getMenuTemplate () {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Transfers',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Pause All',
|
||||||
|
click: () => windows.main.dispatch('pauseAllTorrents')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Resume All',
|
||||||
|
click: () => windows.main.dispatch('resumeAllTorrents')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Help',
|
label: 'Help',
|
||||||
role: 'help',
|
role: 'help',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Learn more about ' + config.APP_NAME,
|
label: 'Learn more about ' + config.APP_NAME,
|
||||||
click: () => shell.openExternal(config.HOME_PAGE_URL)
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.openExternal(config.HOME_PAGE_URL)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Contribute on GitHub',
|
label: 'Contribute on GitHub',
|
||||||
click: () => shell.openExternal(config.GITHUB_URL)
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.openExternal(config.GITHUB_URL)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Report an Issue...',
|
label: 'Report an Issue...',
|
||||||
click: () => shell.openExternal(config.GITHUB_URL_ISSUES)
|
click: () => {
|
||||||
|
const shell = require('./shell')
|
||||||
|
shell.openExternal(config.GITHUB_URL_ISSUES)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
// Add WebTorrent app menu (Mac)
|
// WebTorrent menu (Mac)
|
||||||
template.unshift({
|
template.unshift({
|
||||||
label: config.APP_NAME,
|
label: config.APP_NAME,
|
||||||
submenu: [
|
submenu: [
|
||||||
@@ -307,8 +367,26 @@ function getMenuTemplate () {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add Window menu (Mac)
|
// Edit menu (Mac)
|
||||||
template.splice(5, 0, {
|
template[2].submenu.push(
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Speech',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
role: 'startspeaking'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'stopspeaking'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Window menu (Mac)
|
||||||
|
template.splice(6, 0, {
|
||||||
role: 'window',
|
role: 'window',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
@@ -330,7 +408,10 @@ 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: () => dialog.openSeedFile()
|
click: () => {
|
||||||
|
const dialog = require('./dialog')
|
||||||
|
dialog.openSeedFile()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Edit menu (Windows, Linux)
|
// Edit menu (Windows, Linux)
|
||||||
@@ -345,7 +426,7 @@ function getMenuTemplate () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Help menu (Windows, Linux)
|
// Help menu (Windows, Linux)
|
||||||
template[4].submenu.push(
|
template[5].submenu.push(
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ module.exports = {
|
|||||||
disable
|
disable
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
|
|
||||||
var blockId = 0
|
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
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ module.exports = {
|
|||||||
moveItemToTrash
|
moveItemToTrash
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the given external protocol URL in the desktop’s default manner.
|
* Open the given external protocol URL in the desktop’s default manner.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ module.exports = {
|
|||||||
enable
|
enable
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
function enable () {
|
function enable () {
|
||||||
// Register play/pause media key, available on some keyboards.
|
// Register play/pause media key, available on some keyboards.
|
||||||
@@ -12,9 +12,19 @@ function enable () {
|
|||||||
'MediaPlayPause',
|
'MediaPlayPause',
|
||||||
() => windows.main.dispatch('playPause')
|
() => windows.main.dispatch('playPause')
|
||||||
)
|
)
|
||||||
|
electron.globalShortcut.register(
|
||||||
|
'MediaNextTrack',
|
||||||
|
() => windows.main.dispatch('nextTrack')
|
||||||
|
)
|
||||||
|
electron.globalShortcut.register(
|
||||||
|
'MediaPreviousTrack',
|
||||||
|
() => windows.main.dispatch('previousTrack')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function disable () {
|
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')
|
electron.globalShortcut.unregister('MediaPlayPause')
|
||||||
|
electron.globalShortcut.unregister('MediaNextTrack')
|
||||||
|
electron.globalShortcut.unregister('MediaPreviousTrack')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,18 @@ module.exports = {
|
|||||||
handleEvent
|
handleEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
var cp = require('child_process')
|
const cp = require('child_process')
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var fs = require('fs')
|
const fs = require('fs')
|
||||||
var os = require('os')
|
const os = require('os')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
var handlers = require('./handlers')
|
const handlers = require('./handlers')
|
||||||
|
|
||||||
var EXE_NAME = path.basename(process.execPath)
|
const EXE_NAME = path.basename(process.execPath)
|
||||||
var UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
const UPDATE_EXE = path.join(process.execPath, '..', '..', 'Update.exe')
|
||||||
|
|
||||||
function handleEvent (cmd) {
|
function handleEvent (cmd) {
|
||||||
if (cmd === '--squirrel-install') {
|
if (cmd === '--squirrel-install') {
|
||||||
@@ -73,9 +73,9 @@ function handleEvent (cmd) {
|
|||||||
* the output from standard out.
|
* the output from standard out.
|
||||||
*/
|
*/
|
||||||
function spawn (command, args, cb) {
|
function spawn (command, args, cb) {
|
||||||
var stdout = ''
|
let stdout = ''
|
||||||
|
let error = null
|
||||||
var child
|
let child = null
|
||||||
try {
|
try {
|
||||||
child = cp.spawn(command, args)
|
child = cp.spawn(command, args)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -90,10 +90,10 @@ function spawn (command, args, cb) {
|
|||||||
stdout += data
|
stdout += data
|
||||||
})
|
})
|
||||||
|
|
||||||
var error = null
|
|
||||||
child.on('error', function (processError) {
|
child.on('error', function (processError) {
|
||||||
error = processError
|
error = processError
|
||||||
})
|
})
|
||||||
|
|
||||||
child.on('close', function (code, signal) {
|
child.on('close', function (code, signal) {
|
||||||
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
|
if (code !== 0 && !error) error = new Error('Command failed: #{signal || code}')
|
||||||
if (error) error.stdout = stdout
|
if (error) error.stdout = stdout
|
||||||
@@ -122,12 +122,12 @@ function createShortcuts (cb) {
|
|||||||
* command.
|
* command.
|
||||||
*/
|
*/
|
||||||
function updateShortcuts (cb) {
|
function updateShortcuts (cb) {
|
||||||
var homeDir = os.homedir()
|
const homeDir = os.homedir()
|
||||||
if (homeDir) {
|
if (homeDir) {
|
||||||
var desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
const desktopShortcutPath = path.join(homeDir, 'Desktop', 'WebTorrent.lnk')
|
||||||
// If the desktop shortcut was deleted by the user, then keep it deleted.
|
// If the desktop shortcut was deleted by the user, then keep it deleted.
|
||||||
fs.access(desktopShortcutPath, function (err) {
|
fs.access(desktopShortcutPath, function (err) {
|
||||||
var desktopShortcutExists = !err
|
const desktopShortcutExists = !err
|
||||||
createShortcuts(function () {
|
createShortcuts(function () {
|
||||||
if (desktopShortcutExists) {
|
if (desktopShortcutExists) {
|
||||||
cb()
|
cb()
|
||||||
|
|||||||
28
src/main/startup.js
Normal file
28
src/main/startup.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
module.exports = {
|
||||||
|
install,
|
||||||
|
uninstall
|
||||||
|
}
|
||||||
|
|
||||||
|
const { APP_NAME } = require('../config')
|
||||||
|
const AutoLaunch = require('auto-launch')
|
||||||
|
|
||||||
|
const appLauncher = new AutoLaunch({
|
||||||
|
name: APP_NAME,
|
||||||
|
isHidden: true
|
||||||
|
})
|
||||||
|
|
||||||
|
function install () {
|
||||||
|
return appLauncher
|
||||||
|
.isEnabled()
|
||||||
|
.then(enabled => {
|
||||||
|
if (!enabled) return appLauncher.enable()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninstall () {
|
||||||
|
return appLauncher
|
||||||
|
.isEnabled()
|
||||||
|
.then(enabled => {
|
||||||
|
if (enabled) return appLauncher.disable()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@ module.exports = {
|
|||||||
disable,
|
disable,
|
||||||
enable,
|
enable,
|
||||||
onPlayerPause,
|
onPlayerPause,
|
||||||
onPlayerPlay
|
onPlayerPlay,
|
||||||
|
onPlayerUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,44 +12,80 @@ module.exports = {
|
|||||||
* or activating the window.
|
* or activating the window.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
|
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
|
const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png')
|
||||||
|
const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
|
||||||
|
const PAUSE_ICON = path.join(config.STATIC_PATH, 'PauseThumbnailBarButton.png')
|
||||||
|
const NEXT_ICON = path.join(config.STATIC_PATH, 'NextTrackThumbnailBarButton.png')
|
||||||
|
|
||||||
|
// Array indices for each button
|
||||||
|
const PREV = 0
|
||||||
|
const PLAY_PAUSE = 1
|
||||||
|
const NEXT = 2
|
||||||
|
|
||||||
|
let buttons = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the Windows thumbnail toolbar buttons.
|
* Show the Windows thumbnail toolbar buttons.
|
||||||
*/
|
*/
|
||||||
function enable () {
|
function enable () {
|
||||||
update(false)
|
buttons = [
|
||||||
|
{
|
||||||
|
tooltip: 'Previous Track',
|
||||||
|
icon: PREV_ICON,
|
||||||
|
click: () => windows.main.dispatch('previousTrack')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'Pause',
|
||||||
|
icon: PAUSE_ICON,
|
||||||
|
click: () => windows.main.dispatch('playPause')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'Next Track',
|
||||||
|
icon: NEXT_ICON,
|
||||||
|
click: () => windows.main.dispatch('nextTrack')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the Windows thumbnail toolbar buttons.
|
* Hide the Windows thumbnail toolbar buttons.
|
||||||
*/
|
*/
|
||||||
function disable () {
|
function disable () {
|
||||||
windows.main.win.setThumbarButtons([])
|
buttons = []
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerPause () {
|
function onPlayerPause () {
|
||||||
update(true)
|
if (!isEnabled()) return
|
||||||
|
buttons[PLAY_PAUSE].tooltip = 'Play'
|
||||||
|
buttons[PLAY_PAUSE].icon = PLAY_ICON
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerPlay () {
|
function onPlayerPlay () {
|
||||||
update(false)
|
if (!isEnabled()) return
|
||||||
|
buttons[PLAY_PAUSE].tooltip = 'Pause'
|
||||||
|
buttons[PLAY_PAUSE].icon = PAUSE_ICON
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function update (isPaused) {
|
function onPlayerUpdate (state) {
|
||||||
var icon = isPaused
|
if (!isEnabled()) return
|
||||||
? 'PlayThumbnailBarButton.png'
|
buttons[PREV].flags = [state.hasPrevious ? 'enabled' : 'disabled']
|
||||||
: 'PauseThumbnailBarButton.png'
|
buttons[NEXT].flags = [state.hasNext ? 'enabled' : 'disabled']
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
var buttons = [
|
function isEnabled () {
|
||||||
{
|
return buttons.length > 0
|
||||||
tooltip: isPaused ? 'Play' : 'Pause',
|
}
|
||||||
icon: path.join(config.STATIC_PATH, icon),
|
|
||||||
click: () => windows.main.dispatch('playPause')
|
function update () {
|
||||||
}
|
|
||||||
]
|
|
||||||
windows.main.win.setThumbarButtons(buttons)
|
windows.main.win.setThumbarButtons(buttons)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ module.exports = {
|
|||||||
setWindowFocus
|
setWindowFocus
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
var tray
|
let tray
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
@@ -36,8 +36,8 @@ function setWindowFocus (flag) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initLinux () {
|
function initLinux () {
|
||||||
checkLinuxTraySupport(function (supportsTray) {
|
checkLinuxTraySupport(function (err) {
|
||||||
if (supportsTray) createTray()
|
if (!err) createTray()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,16 +49,20 @@ function initWin32 () {
|
|||||||
* Check for libappindicator1 support before creating tray icon
|
* Check for libappindicator1 support before creating tray icon
|
||||||
*/
|
*/
|
||||||
function checkLinuxTraySupport (cb) {
|
function checkLinuxTraySupport (cb) {
|
||||||
var cp = require('child_process')
|
const cp = require('child_process')
|
||||||
|
|
||||||
// Check that we're on Ubuntu (or another debian system) and that we have
|
// Check that we're on Ubuntu (or another debian system) and that we have
|
||||||
// libappindicator1. If WebTorrent was installed from the deb file, we should
|
// libappindicator1. If WebTorrent was installed from the deb file, we should
|
||||||
// always have it. If it was installed from the zip file, we might not.
|
// always have it. If it was installed from the zip file, we might not.
|
||||||
cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) {
|
cp.exec('dpkg --get-selections libappindicator1', function (err, stdout) {
|
||||||
if (err) return cb(false)
|
if (err) return cb(err)
|
||||||
// Unfortunately there's no cleaner way, as far as I can tell, to check
|
// Unfortunately there's no cleaner way, as far as I can tell, to check
|
||||||
// whether a debian package is installed:
|
// whether a debian package is installed:
|
||||||
cb(stdout.endsWith('\tinstall\n'))
|
if (stdout.endsWith('\tinstall\n')) {
|
||||||
|
cb(null)
|
||||||
|
} else {
|
||||||
|
cb(new Error('debian package not installed'))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +78,7 @@ function createTray () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateTrayMenu () {
|
function updateTrayMenu () {
|
||||||
var contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
const contextMenu = electron.Menu.buildFromTemplate(getMenuTemplate())
|
||||||
tray.setContextMenu(contextMenu)
|
tray.setContextMenu(contextMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,17 @@ module.exports = {
|
|||||||
init
|
init
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
var get = require('simple-get')
|
const get = require('simple-get')
|
||||||
|
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var log = require('./log')
|
const log = require('./log')
|
||||||
var windows = require('./windows')
|
const windows = require('./windows')
|
||||||
|
|
||||||
var AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
const AUTO_UPDATE_URL = config.AUTO_UPDATE_URL +
|
||||||
'?version=' + config.APP_VERSION +
|
'?version=' + config.APP_VERSION +
|
||||||
'&platform=' + process.platform
|
'&platform=' + process.platform +
|
||||||
|
'&sysarch=' + config.OS_SYSARCH
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ module.exports = {
|
|||||||
init
|
init
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var app = electron.app
|
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)
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
var about = module.exports = {
|
const about = module.exports = {
|
||||||
init,
|
init,
|
||||||
win: null
|
win: null
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
if (about.win) {
|
if (about.win) {
|
||||||
return about.win.show()
|
return about.win.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
var win = about.win = new electron.BrowserWindow({
|
const win = about.win = new electron.BrowserWindow({
|
||||||
backgroundColor: '#ECECEC',
|
backgroundColor: '#ECECEC',
|
||||||
center: true,
|
center: true,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
@@ -24,6 +24,9 @@ function init () {
|
|||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
title: 'About ' + config.APP_WINDOW_TITLE,
|
title: 'About ' + config.APP_WINDOW_TITLE,
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true
|
||||||
|
},
|
||||||
width: 300
|
width: 300
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ function init () {
|
|||||||
// No menu on the About window
|
// No menu on the About window
|
||||||
win.setMenu(null)
|
win.setMenu(null)
|
||||||
|
|
||||||
win.webContents.once('did-finish-load', function () {
|
win.once('ready-to-show', function () {
|
||||||
win.show()
|
win.show()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
var main = module.exports = {
|
const main = module.exports = {
|
||||||
dispatch,
|
dispatch,
|
||||||
hide,
|
hide,
|
||||||
init,
|
init,
|
||||||
@@ -14,65 +14,97 @@ var main = module.exports = {
|
|||||||
win: null
|
win: null
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
const debounce = require('debounce')
|
||||||
|
|
||||||
var app = electron.app
|
const app = electron.app
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var log = require('../log')
|
const log = require('../log')
|
||||||
var menu = require('../menu')
|
const menu = require('../menu')
|
||||||
var tray = require('../tray')
|
|
||||||
|
|
||||||
var HEADER_HEIGHT = 37
|
function init (state, options) {
|
||||||
var TORRENT_HEIGHT = 100
|
|
||||||
|
|
||||||
function init () {
|
|
||||||
if (main.win) {
|
if (main.win) {
|
||||||
return main.win.show()
|
return main.win.show()
|
||||||
}
|
}
|
||||||
var win = main.win = new electron.BrowserWindow({
|
|
||||||
backgroundColor: '#1E1E1E',
|
const initialBounds = Object.assign(config.WINDOW_INITIAL_BOUNDS, state.saved.bounds)
|
||||||
|
|
||||||
|
const win = main.win = new electron.BrowserWindow({
|
||||||
|
backgroundColor: '#282828',
|
||||||
|
backgroundThrottling: false, // do not throttle animations/timers when page is background
|
||||||
darkTheme: true, // Forces dark theme (GTK+3)
|
darkTheme: true, // Forces dark theme (GTK+3)
|
||||||
|
height: initialBounds.height,
|
||||||
icon: getIconPath(), // Window icon (Windows, Linux)
|
icon: getIconPath(), // Window icon (Windows, Linux)
|
||||||
minWidth: config.WINDOW_MIN_WIDTH,
|
|
||||||
minHeight: config.WINDOW_MIN_HEIGHT,
|
minHeight: config.WINDOW_MIN_HEIGHT,
|
||||||
|
minWidth: config.WINDOW_MIN_WIDTH,
|
||||||
|
show: false,
|
||||||
title: config.APP_WINDOW_TITLE,
|
title: config.APP_WINDOW_TITLE,
|
||||||
titleBarStyle: 'hidden-inset', // Hide title bar (Mac)
|
titleBarStyle: 'hiddenInset', // Hide title bar (Mac)
|
||||||
useContentSize: true, // Specify web page size without OS chrome
|
useContentSize: true, // Specify web page size without OS chrome
|
||||||
width: 500,
|
width: initialBounds.width,
|
||||||
height: HEADER_HEIGHT + (TORRENT_HEIGHT * 6) // header height + 5 torrents
|
webPreferences: {
|
||||||
|
nodeIntegration: true
|
||||||
|
},
|
||||||
|
x: initialBounds.x,
|
||||||
|
y: initialBounds.y
|
||||||
})
|
})
|
||||||
|
|
||||||
win.loadURL(config.WINDOW_MAIN)
|
win.loadURL(config.WINDOW_MAIN)
|
||||||
|
|
||||||
if (win.setSheetOffset) win.setSheetOffset(HEADER_HEIGHT)
|
win.once('ready-to-show', () => {
|
||||||
|
if (!options.hidden) win.show()
|
||||||
|
})
|
||||||
|
|
||||||
win.webContents.on('dom-ready', function () {
|
if (win.setSheetOffset) {
|
||||||
|
win.setSheetOffset(config.UI_HEADER_HEIGHT)
|
||||||
|
}
|
||||||
|
|
||||||
|
win.webContents.on('dom-ready', () => {
|
||||||
menu.onToggleFullScreen(main.win.isFullScreen())
|
menu.onToggleFullScreen(main.win.isFullScreen())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
win.webContents.on('will-navigate', (e, url) => {
|
||||||
|
// Prevent drag-and-drop from navigating the Electron window, which can happen
|
||||||
|
// before our drag-and-drop handlers have been initialized.
|
||||||
|
e.preventDefault()
|
||||||
|
})
|
||||||
|
|
||||||
win.on('blur', onWindowBlur)
|
win.on('blur', onWindowBlur)
|
||||||
win.on('focus', onWindowFocus)
|
win.on('focus', onWindowFocus)
|
||||||
|
|
||||||
win.on('hide', onWindowBlur)
|
win.on('hide', onWindowBlur)
|
||||||
win.on('show', onWindowFocus)
|
win.on('show', onWindowFocus)
|
||||||
|
|
||||||
win.on('enter-full-screen', function () {
|
win.on('enter-full-screen', () => {
|
||||||
menu.onToggleFullScreen(true)
|
menu.onToggleFullScreen(true)
|
||||||
send('fullscreenChanged', true)
|
send('fullscreenChanged', true)
|
||||||
win.setMenuBarVisibility(false)
|
win.setMenuBarVisibility(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
win.on('leave-full-screen', function () {
|
win.on('leave-full-screen', () => {
|
||||||
menu.onToggleFullScreen(false)
|
menu.onToggleFullScreen(false)
|
||||||
send('fullscreenChanged', false)
|
send('fullscreenChanged', false)
|
||||||
win.setMenuBarVisibility(true)
|
win.setMenuBarVisibility(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
win.on('close', function (e) {
|
win.on('move', debounce(e => {
|
||||||
if (process.platform !== 'darwin' && !tray.hasTray()) {
|
send('windowBoundsChanged', e.sender.getBounds())
|
||||||
app.quit()
|
}, 1000))
|
||||||
} else if (!app.isQuitting) {
|
|
||||||
|
win.on('resize', debounce(e => {
|
||||||
|
send('windowBoundsChanged', e.sender.getBounds())
|
||||||
|
}, 1000))
|
||||||
|
|
||||||
|
win.on('close', e => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
const tray = require('../tray')
|
||||||
|
if (!tray.hasTray()) {
|
||||||
|
app.quit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!app.isQuitting) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
@@ -85,7 +117,7 @@ function dispatch (...args) {
|
|||||||
|
|
||||||
function hide () {
|
function hide () {
|
||||||
if (!main.win) return
|
if (!main.win) return
|
||||||
main.win.send('dispatch', 'backToList')
|
dispatch('backToList')
|
||||||
main.win.hide()
|
main.win.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,37 +141,29 @@ function setAspectRatio (aspectRatio) {
|
|||||||
function setBounds (bounds, maximize) {
|
function setBounds (bounds, maximize) {
|
||||||
// Do nothing in fullscreen
|
// Do nothing in fullscreen
|
||||||
if (!main.win || main.win.isFullScreen()) {
|
if (!main.win || main.win.isFullScreen()) {
|
||||||
log('setBounds: not setting bounds because we\'re in full screen')
|
log('setBounds: not setting bounds because already in full screen mode')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximize or minimize, if the second argument is present
|
// Maximize or minimize, if the second argument is present
|
||||||
var willBeMaximized
|
if (maximize === true && !main.win.isMaximized()) {
|
||||||
if (maximize === true) {
|
log('setBounds: maximizing')
|
||||||
if (!main.win.isMaximized()) {
|
main.win.maximize()
|
||||||
log('setBounds: maximizing')
|
} else if (maximize === false && main.win.isMaximized()) {
|
||||||
main.win.maximize()
|
log('setBounds: unmaximizing')
|
||||||
}
|
main.win.unmaximize()
|
||||||
willBeMaximized = true
|
|
||||||
} else if (maximize === false) {
|
|
||||||
if (main.win.isMaximized()) {
|
|
||||||
log('setBounds: unmaximizing')
|
|
||||||
main.win.unmaximize()
|
|
||||||
}
|
|
||||||
willBeMaximized = false
|
|
||||||
} else {
|
|
||||||
willBeMaximized = main.win.isMaximized()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const willBeMaximized = typeof maximize === 'boolean' ? maximize : main.win.isMaximized()
|
||||||
// Assuming we're not maximized or maximizing, set the window size
|
// Assuming we're not maximized or maximizing, set the window size
|
||||||
if (!willBeMaximized) {
|
if (!willBeMaximized) {
|
||||||
log('setBounds: setting bounds to ' + JSON.stringify(bounds))
|
log(`setBounds: setting bounds to ${JSON.stringify(bounds)}`)
|
||||||
if (bounds.x === null && bounds.y === null) {
|
if (bounds.x === null && bounds.y === null) {
|
||||||
// X and Y not specified? By default, center on current screen
|
// X and Y not specified? By default, center on current screen
|
||||||
var scr = electron.screen.getDisplayMatching(main.win.getBounds())
|
const scr = electron.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)}`)
|
||||||
}
|
}
|
||||||
// Resize the window's content area (so window border doesn't need to be taken
|
// Resize the window's content area (so window border doesn't need to be taken
|
||||||
// into account)
|
// into account)
|
||||||
@@ -188,7 +212,7 @@ function toggleDevTools () {
|
|||||||
if (main.win.webContents.isDevToolsOpened()) {
|
if (main.win.webContents.isDevToolsOpened()) {
|
||||||
main.win.webContents.closeDevTools()
|
main.win.webContents.closeDevTools()
|
||||||
} else {
|
} else {
|
||||||
main.win.webContents.openDevTools({ detach: true })
|
main.win.webContents.openDevTools({ mode: 'detach' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,12 +235,20 @@ function toggleFullScreen (flag) {
|
|||||||
|
|
||||||
function onWindowBlur () {
|
function onWindowBlur () {
|
||||||
menu.setWindowFocus(false)
|
menu.setWindowFocus(false)
|
||||||
tray.setWindowFocus(false)
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
const tray = require('../tray')
|
||||||
|
tray.setWindowFocus(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWindowFocus () {
|
function onWindowFocus () {
|
||||||
menu.setWindowFocus(true)
|
menu.setWindowFocus(true)
|
||||||
tray.setWindowFocus(true)
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
const tray = require('../tray')
|
||||||
|
tray.setWindowFocus(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconPath () {
|
function getIconPath () {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
var webtorrent = module.exports = {
|
const webtorrent = module.exports = {
|
||||||
init,
|
init,
|
||||||
send,
|
send,
|
||||||
show,
|
show,
|
||||||
@@ -6,14 +6,14 @@ var webtorrent = module.exports = {
|
|||||||
win: null
|
win: null
|
||||||
}
|
}
|
||||||
|
|
||||||
var electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var log = require('../log')
|
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
var win = webtorrent.win = new electron.BrowserWindow({
|
const win = webtorrent.win = new electron.BrowserWindow({
|
||||||
backgroundColor: '#1E1E1E',
|
backgroundColor: '#1E1E1E',
|
||||||
|
backgroundThrottling: false, // do not throttle animations/timers when page is background
|
||||||
center: true,
|
center: true,
|
||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
fullscreenable: false,
|
fullscreenable: false,
|
||||||
@@ -25,6 +25,9 @@ function init () {
|
|||||||
skipTaskbar: true,
|
skipTaskbar: true,
|
||||||
title: 'webtorrent-hidden-window',
|
title: 'webtorrent-hidden-window',
|
||||||
useContentSize: true,
|
useContentSize: true,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true
|
||||||
|
},
|
||||||
width: 150
|
width: 150
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -52,11 +55,10 @@ function send (...args) {
|
|||||||
|
|
||||||
function toggleDevTools () {
|
function toggleDevTools () {
|
||||||
if (!webtorrent.win) return
|
if (!webtorrent.win) return
|
||||||
log('toggleDevTools')
|
|
||||||
if (webtorrent.win.webContents.isDevToolsOpened()) {
|
if (webtorrent.win.webContents.isDevToolsOpened()) {
|
||||||
webtorrent.win.webContents.closeDevTools()
|
webtorrent.win.webContents.closeDevTools()
|
||||||
webtorrent.win.hide()
|
webtorrent.win.hide()
|
||||||
} else {
|
} else {
|
||||||
webtorrent.win.webContents.openDevTools({ detach: true })
|
webtorrent.win.webContents.openDevTools({ mode: 'detach' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const React = require('react')
|
const React = require('react')
|
||||||
|
|
||||||
const {dispatcher} = require('../lib/dispatcher')
|
const { dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class CreateTorrentErrorPage extends React.Component {
|
module.exports = class CreateTorrentErrorPage extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
@@ -1,24 +1,31 @@
|
|||||||
const React = require('react')
|
const React = require('react')
|
||||||
|
|
||||||
const {dispatcher} = require('../lib/dispatcher')
|
const { dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class Header extends React.Component {
|
class Header extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
var loc = this.props.state.location
|
const loc = this.props.state.location
|
||||||
return (
|
return (
|
||||||
<div key='header' className='header'>
|
<div
|
||||||
|
className='header'
|
||||||
|
onMouseMove={dispatcher('mediaMouseMoved')}
|
||||||
|
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
|
||||||
|
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
|
||||||
|
>
|
||||||
{this.getTitle()}
|
{this.getTitle()}
|
||||||
<div className='nav left float-left'>
|
<div className='nav left float-left'>
|
||||||
<i
|
<i
|
||||||
className={'icon back ' + (loc.hasBack() ? '' : 'disabled')}
|
className={'icon back ' + (loc.hasBack() ? '' : 'disabled')}
|
||||||
title='Back'
|
title='Back'
|
||||||
onClick={dispatcher('back')}>
|
onClick={dispatcher('back')}
|
||||||
|
>
|
||||||
chevron_left
|
chevron_left
|
||||||
</i>
|
</i>
|
||||||
<i
|
<i
|
||||||
className={'icon forward ' + (loc.hasForward() ? '' : 'disabled')}
|
className={'icon forward ' + (loc.hasForward() ? '' : 'disabled')}
|
||||||
title='Forward'
|
title='Forward'
|
||||||
onClick={dispatcher('forward')}>
|
onClick={dispatcher('forward')}
|
||||||
|
>
|
||||||
chevron_right
|
chevron_right
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,20 +38,23 @@ module.exports = class Header extends React.Component {
|
|||||||
|
|
||||||
getTitle () {
|
getTitle () {
|
||||||
if (process.platform !== 'darwin') return null
|
if (process.platform !== 'darwin') return null
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
return (<div className='title ellipsis'>{state.window.title}</div>)
|
return (<div className='title ellipsis'>{state.window.title}</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddButton () {
|
getAddButton () {
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
if (state.location.url() !== 'home') return null
|
if (state.location.url() !== 'home') return null
|
||||||
return (
|
return (
|
||||||
<i
|
<i
|
||||||
className='icon add'
|
className='icon add'
|
||||||
title='Add torrent'
|
title='Add torrent'
|
||||||
onClick={dispatcher('openFiles')}>
|
onClick={dispatcher('openFiles')}
|
||||||
|
>
|
||||||
add
|
add
|
||||||
</i>
|
</i>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = Header
|
||||||
35
src/renderer/components/heading.js
Normal file
35
src/renderer/components/heading.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const React = require('react')
|
||||||
|
const PropTypes = require('prop-types')
|
||||||
|
|
||||||
|
const colors = require('material-ui/styles/colors')
|
||||||
|
|
||||||
|
class Heading extends React.Component {
|
||||||
|
static get propTypes () {
|
||||||
|
return {
|
||||||
|
level: PropTypes.number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultProps () {
|
||||||
|
return {
|
||||||
|
level: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const HeadingTag = 'h' + this.props.level
|
||||||
|
const style = {
|
||||||
|
color: colors.grey100,
|
||||||
|
fontSize: 20,
|
||||||
|
marginBottom: 15,
|
||||||
|
marginTop: 30
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<HeadingTag style={style}>
|
||||||
|
{this.props.children}
|
||||||
|
</HeadingTag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Heading
|
||||||
27
src/renderer/components/modal-ok-cancel.js
Normal file
27
src/renderer/components/modal-ok-cancel.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const React = require('react')
|
||||||
|
const FlatButton = require('material-ui/FlatButton').default
|
||||||
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
|
||||||
|
module.exports = class ModalOKCancel extends React.Component {
|
||||||
|
render () {
|
||||||
|
const cancelStyle = { marginRight: 10, color: 'black' }
|
||||||
|
const { cancelText, onCancel, okText, onOK } = this.props
|
||||||
|
return (
|
||||||
|
<div className='float-right'>
|
||||||
|
<FlatButton
|
||||||
|
className='control cancel'
|
||||||
|
style={cancelStyle}
|
||||||
|
label={cancelText}
|
||||||
|
onClick={onCancel}
|
||||||
|
/>
|
||||||
|
<RaisedButton
|
||||||
|
className='control ok'
|
||||||
|
primary
|
||||||
|
label={okText}
|
||||||
|
onClick={onOK}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/renderer/components/open-torrent-address-modal.js
Normal file
43
src/renderer/components/open-torrent-address-modal.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
const React = require('react')
|
||||||
|
const TextField = require('material-ui/TextField').default
|
||||||
|
|
||||||
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
|
const { dispatch, dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class OpenTorrentAddressModal extends React.Component {
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className='open-torrent-address-modal'>
|
||||||
|
<p><label>Enter torrent address or magnet link</label></p>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
id='torrent-address-field'
|
||||||
|
className='control'
|
||||||
|
ref={(c) => { this.torrentURL = c }}
|
||||||
|
fullWidth
|
||||||
|
onKeyDown={handleKeyDown.bind(this)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ModalOKCancel
|
||||||
|
cancelText='CANCEL'
|
||||||
|
onCancel={dispatcher('exitModal')}
|
||||||
|
okText='OK'
|
||||||
|
onOK={handleOK.bind(this)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.torrentURL.input.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyDown (e) {
|
||||||
|
if (e.which === 13) handleOK.call(this) /* hit Enter to submit */
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOK () {
|
||||||
|
dispatch('exitModal')
|
||||||
|
dispatch('addTorrent', this.torrentURL.input.value)
|
||||||
|
}
|
||||||
92
src/renderer/components/path-selector.js
Normal file
92
src/renderer/components/path-selector.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const colors = require('material-ui/styles/colors')
|
||||||
|
const electron = require('electron')
|
||||||
|
const React = require('react')
|
||||||
|
const PropTypes = require('prop-types')
|
||||||
|
|
||||||
|
const remote = electron.remote
|
||||||
|
|
||||||
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
const TextField = require('material-ui/TextField').default
|
||||||
|
|
||||||
|
// Lets you pick a file or directory.
|
||||||
|
// Uses the system Open File dialog.
|
||||||
|
// You can't edit the text field directly.
|
||||||
|
class PathSelector extends React.Component {
|
||||||
|
static propTypes () {
|
||||||
|
return {
|
||||||
|
className: PropTypes.string,
|
||||||
|
dialog: PropTypes.object,
|
||||||
|
id: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.handleClick = this.handleClick.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick () {
|
||||||
|
const opts = Object.assign({
|
||||||
|
defaultPath: this.props.value && path.dirname(this.props.value),
|
||||||
|
properties: ['openFile', 'openDirectory']
|
||||||
|
}, this.props.dialog)
|
||||||
|
|
||||||
|
remote.dialog.showOpenDialog(
|
||||||
|
remote.getCurrentWindow(),
|
||||||
|
opts,
|
||||||
|
(filenames) => {
|
||||||
|
if (!Array.isArray(filenames)) return
|
||||||
|
this.props.onChange && this.props.onChange(filenames[0])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const id = this.props.title.replace(' ', '-').toLowerCase()
|
||||||
|
const wrapperStyle = {
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%'
|
||||||
|
}
|
||||||
|
const labelStyle = {
|
||||||
|
flex: '0 auto',
|
||||||
|
marginRight: 10,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}
|
||||||
|
const textareaStyle = {
|
||||||
|
color: colors.grey50
|
||||||
|
}
|
||||||
|
const textFieldStyle = {
|
||||||
|
flex: '1'
|
||||||
|
}
|
||||||
|
const text = this.props.value || ''
|
||||||
|
const buttonStyle = {
|
||||||
|
marginLeft: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={this.props.className} style={wrapperStyle}>
|
||||||
|
<div className='label' style={labelStyle}>
|
||||||
|
{this.props.title}:
|
||||||
|
</div>
|
||||||
|
<TextField
|
||||||
|
className='control' disabled id={id} value={text}
|
||||||
|
inputStyle={textareaStyle} style={textFieldStyle}
|
||||||
|
/>
|
||||||
|
<RaisedButton
|
||||||
|
className='control' label='Change' onClick={this.handleClick}
|
||||||
|
style={buttonStyle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PathSelector
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
const React = require('react')
|
const React = require('react')
|
||||||
|
|
||||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
|
const { dispatch, dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class RemoveTorrentModal extends React.Component {
|
module.exports = class RemoveTorrentModal extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
var message = state.modal.deleteData
|
const message = state.modal.deleteData
|
||||||
? 'Are you sure you want to remove this torrent from the list and delete the data file?'
|
? 'Are you sure you want to remove this torrent from the list and delete the data file?'
|
||||||
: 'Are you sure you want to remove this torrent from the list?'
|
: 'Are you sure you want to remove this torrent from the list?'
|
||||||
var buttonText = state.modal.deleteData ? 'Remove Data' : 'Remove'
|
const buttonText = state.modal.deleteData ? 'REMOVE DATA' : 'REMOVE'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p><strong>{message}</strong></p>
|
<p><strong>{message}</strong></p>
|
||||||
<p className='float-right'>
|
<ModalOKCancel
|
||||||
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
|
cancelText='CANCEL'
|
||||||
<button className='button button-raised' onClick={handleRemove}>{buttonText}</button>
|
onCancel={dispatcher('exitModal')}
|
||||||
</p>
|
okText={buttonText}
|
||||||
|
onOK={handleRemove}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
55
src/renderer/components/show-more.js
Normal file
55
src/renderer/components/show-more.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
const React = require('react')
|
||||||
|
const PropTypes = require('prop-types')
|
||||||
|
|
||||||
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
|
||||||
|
class ShowMore extends React.Component {
|
||||||
|
static get propTypes () {
|
||||||
|
return {
|
||||||
|
defaultExpanded: PropTypes.bool,
|
||||||
|
hideLabel: PropTypes.string,
|
||||||
|
showLabel: PropTypes.string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultProps () {
|
||||||
|
return {
|
||||||
|
hideLabel: 'Hide more...',
|
||||||
|
showLabel: 'Show more...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
expanded: !!this.props.defaultExpanded
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handleClick = this.handleClick.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick () {
|
||||||
|
this.setState({
|
||||||
|
expanded: !this.state.expanded
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const label = this.state.expanded
|
||||||
|
? this.props.hideLabel
|
||||||
|
: this.props.showLabel
|
||||||
|
return (
|
||||||
|
<div className='show-more' style={this.props.style}>
|
||||||
|
{this.state.expanded ? this.props.children : null}
|
||||||
|
<RaisedButton
|
||||||
|
className='control'
|
||||||
|
onClick={this.handleClick}
|
||||||
|
label={label}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ShowMore
|
||||||
45
src/renderer/components/unsupported-media-modal.js
Normal file
45
src/renderer/components/unsupported-media-modal.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
const React = require('react')
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
|
const { dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class UnsupportedMediaModal extends React.Component {
|
||||||
|
render () {
|
||||||
|
const state = this.props.state
|
||||||
|
const err = state.modal.error
|
||||||
|
const message = (err && err.getMessage)
|
||||||
|
? err.getMessage()
|
||||||
|
: err
|
||||||
|
const onAction = state.modal.externalPlayerInstalled
|
||||||
|
? dispatcher('openExternalPlayer')
|
||||||
|
: () => this.onInstall()
|
||||||
|
const actionText = state.modal.externalPlayerInstalled
|
||||||
|
? 'PLAY IN ' + state.getExternalPlayerName().toUpperCase()
|
||||||
|
: 'INSTALL VLC'
|
||||||
|
const errorMessage = state.modal.externalPlayerNotFound
|
||||||
|
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
||||||
|
: ''
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p><strong>Sorry, we can't play that file.</strong></p>
|
||||||
|
<p>{message}</p>
|
||||||
|
<ModalOKCancel
|
||||||
|
cancelText='CANCEL'
|
||||||
|
onCancel={dispatcher('backToList')}
|
||||||
|
okText={actionText}
|
||||||
|
onOK={onAction}
|
||||||
|
/>
|
||||||
|
<p className='error-text'>{errorMessage}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onInstall () {
|
||||||
|
electron.shell.openExternal('http://www.videolan.org/vlc/')
|
||||||
|
|
||||||
|
// TODO: dcposch send a dispatch rather than modifying state directly
|
||||||
|
const state = this.props.state
|
||||||
|
state.modal.externalPlayerInstalled = true // Assume they'll install it successfully
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/renderer/components/update-available-modal.js
Normal file
37
src/renderer/components/update-available-modal.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const React = require('react')
|
||||||
|
const electron = require('electron')
|
||||||
|
|
||||||
|
const ModalOKCancel = require('./modal-ok-cancel')
|
||||||
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
module.exports = class UpdateAvailableModal extends React.Component {
|
||||||
|
render () {
|
||||||
|
const state = this.props.state
|
||||||
|
return (
|
||||||
|
<div className='update-available-modal'>
|
||||||
|
<p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p>
|
||||||
|
<p>
|
||||||
|
We have an auto-updater for Windows and Mac.
|
||||||
|
We don't have one for Linux yet, so you'll have to download the new version manually.
|
||||||
|
</p>
|
||||||
|
<ModalOKCancel
|
||||||
|
cancelText='SKIP THIS RELEASE'
|
||||||
|
onCancel={handleSkip}
|
||||||
|
okText='SHOW DOWNLOAD PAGE'
|
||||||
|
onOK={handleShow}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleShow () {
|
||||||
|
// TODO: use the GitHub urls from config.js
|
||||||
|
electron.shell.openExternal('https://github.com/webtorrent/webtorrent-desktop/releases')
|
||||||
|
dispatch('exitModal')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSkip () {
|
||||||
|
dispatch('skipVersion', state.modal.version)
|
||||||
|
dispatch('exitModal')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/renderer/controllers/folder-watcher-controller.js
Normal file
13
src/renderer/controllers/folder-watcher-controller.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const { ipcRenderer } = require('electron')
|
||||||
|
|
||||||
|
module.exports = class FolderWatcherController {
|
||||||
|
start () {
|
||||||
|
console.log('-- IPC: start folder watcher')
|
||||||
|
ipcRenderer.send('startFolderWatcher')
|
||||||
|
}
|
||||||
|
|
||||||
|
stop () {
|
||||||
|
console.log('-- IPC: stop folder watcher')
|
||||||
|
ipcRenderer.send('stopFolderWatcher')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const electron = require('electron')
|
const { ipcRenderer } = require('electron')
|
||||||
|
const telemetry = require('../lib/telemetry')
|
||||||
const ipcRenderer = electron.ipcRenderer
|
const Playlist = require('../lib/playlist')
|
||||||
|
|
||||||
// Controls local play back: the <video>/<audio> tag and VLC
|
// Controls local play back: the <video>/<audio> tag and VLC
|
||||||
// Does not control remote casting (Chromecast etc)
|
// Does not control remote casting (Chromecast etc)
|
||||||
@@ -10,7 +10,7 @@ module.exports = class MediaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mediaSuccess () {
|
mediaSuccess () {
|
||||||
this.state.playing.result = 'success'
|
telemetry.logPlayAttempt('success')
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaStalled () {
|
mediaStalled () {
|
||||||
@@ -18,9 +18,9 @@ module.exports = class MediaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mediaError (error) {
|
mediaError (error) {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (state.location.url() === 'player') {
|
if (state.location.url() === 'player') {
|
||||||
state.playing.result = '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', function (e, isInstalled) {
|
||||||
@@ -42,14 +42,37 @@ module.exports = class MediaController {
|
|||||||
this.state.playing.mouseStationarySince = new Date().getTime()
|
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
controlsMouseEnter () {
|
||||||
|
this.state.playing.mouseInControls = true
|
||||||
|
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
controlsMouseLeave () {
|
||||||
|
this.state.playing.mouseInControls = false
|
||||||
|
this.state.playing.mouseStationarySince = new Date().getTime()
|
||||||
|
}
|
||||||
|
|
||||||
openExternalPlayer () {
|
openExternalPlayer () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title)
|
|
||||||
state.playing.location = 'external'
|
state.playing.location = 'external'
|
||||||
|
|
||||||
|
const onServerRunning = function () {
|
||||||
|
state.playing.isReady = true
|
||||||
|
telemetry.logPlayAttempt('external')
|
||||||
|
|
||||||
|
const mediaURL = Playlist.getCurrentLocalURL(state)
|
||||||
|
ipcRenderer.send('openExternalPlayer',
|
||||||
|
state.saved.prefs.externalPlayerPath,
|
||||||
|
mediaURL,
|
||||||
|
state.window.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.server != null) onServerRunning()
|
||||||
|
else ipcRenderer.once('wt-server-running', onServerRunning)
|
||||||
}
|
}
|
||||||
|
|
||||||
externalPlayerNotFound () {
|
externalPlayerNotFound () {
|
||||||
var modal = this.state.modal
|
const modal = this.state.modal
|
||||||
if (modal && modal.id === 'unsupported-media-modal') {
|
if (modal && modal.id === 'unsupported-media-modal') {
|
||||||
modal.externalPlayerNotFound = true
|
modal.externalPlayerNotFound = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ const electron = require('electron')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const Cast = require('../lib/cast')
|
const Cast = require('../lib/cast')
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
const telemetry = require('../lib/telemetry')
|
const telemetry = require('../lib/telemetry')
|
||||||
const errors = require('../lib/errors')
|
const { UnplayableFileError, UnplayableTorrentError } = require('../lib/errors')
|
||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const Playlist = require('../lib/playlist')
|
||||||
const State = require('../lib/state')
|
const State = require('../lib/state')
|
||||||
|
|
||||||
const ipcRenderer = electron.ipcRenderer
|
const ipcRenderer = electron.ipcRenderer
|
||||||
@@ -24,24 +25,45 @@ module.exports = class PlaybackController {
|
|||||||
// Play a file in a torrent.
|
// Play a file in a torrent.
|
||||||
// * Start torrenting, if necessary
|
// * Start torrenting, if necessary
|
||||||
// * Stream, if not already fully downloaded
|
// * Stream, if not already fully downloaded
|
||||||
// * If no file index is provided, pick the default file to play
|
// * 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.state.location.go({
|
this.pauseActiveTorrents(infoHash)
|
||||||
url: 'player',
|
|
||||||
setup: (cb) => {
|
const state = this.state
|
||||||
this.play()
|
if (state.location.url() === 'player') {
|
||||||
this.openPlayer(infoHash, index, cb)
|
this.updatePlayer(infoHash, index, false, (err) => {
|
||||||
},
|
if (err) dispatch('error', err)
|
||||||
destroy: () => this.closePlayer()
|
else this.play()
|
||||||
}, (err) => {
|
})
|
||||||
if (err) dispatch('error', err)
|
} else {
|
||||||
})
|
let initialized = false
|
||||||
|
state.location.go({
|
||||||
|
url: 'player',
|
||||||
|
setup: (cb) => {
|
||||||
|
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||||
|
|
||||||
|
if (index === undefined || initialized) index = torrentSummary.mostRecentFileIndex
|
||||||
|
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
|
||||||
|
if (index === undefined) return cb(new UnplayableTorrentError())
|
||||||
|
|
||||||
|
initialized = true
|
||||||
|
|
||||||
|
this.openPlayer(infoHash, index, (err) => {
|
||||||
|
if (!err) this.play()
|
||||||
|
cb(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
destroy: () => this.closePlayer()
|
||||||
|
}, (err) => {
|
||||||
|
if (err) dispatch('error', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show a file in the OS, eg in Finder on a Mac
|
// Open a file in OS default app.
|
||||||
openItem (infoHash, index) {
|
openItem (infoHash, index) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
var filePath = path.join(
|
const filePath = path.join(
|
||||||
torrentSummary.path,
|
torrentSummary.path,
|
||||||
torrentSummary.files[index].path)
|
torrentSummary.files[index].path)
|
||||||
ipcRenderer.send('openItem', filePath)
|
ipcRenderer.send('openItem', filePath)
|
||||||
@@ -49,12 +71,12 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Toggle (play or pause) the currently playing media
|
// Toggle (play or pause) the currently playing media
|
||||||
playPause () {
|
playPause () {
|
||||||
var 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
|
||||||
var 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()
|
||||||
@@ -64,9 +86,44 @@ module.exports = class PlaybackController {
|
|||||||
else this.pause()
|
else this.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pauseActiveTorrents (infoHash) {
|
||||||
|
// Playback Priority: pause all active torrents if needed.
|
||||||
|
if (!this.state.saved.prefs.highestPlaybackPriority) return
|
||||||
|
|
||||||
|
// Do not pause active torrents if playing a fully downloaded torrent.
|
||||||
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
|
if (torrentSummary.status === 'seeding') return
|
||||||
|
|
||||||
|
dispatch('prioritizeTorrent', infoHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play next file in list (if any)
|
||||||
|
nextTrack () {
|
||||||
|
const state = this.state
|
||||||
|
if (Playlist.hasNext(state) && state.playing.location !== 'external') {
|
||||||
|
this.updatePlayer(
|
||||||
|
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
||||||
|
if (err) dispatch('error', err)
|
||||||
|
else this.play()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play previous track in list (if any)
|
||||||
|
previousTrack () {
|
||||||
|
const state = this.state
|
||||||
|
if (Playlist.hasPrevious(state) && state.playing.location !== 'external') {
|
||||||
|
this.updatePlayer(
|
||||||
|
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
||||||
|
if (err) dispatch('error', err)
|
||||||
|
else this.play()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Play (unpause) the current media
|
// Play (unpause) the current media
|
||||||
play () {
|
play () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (!state.playing.isPaused) return
|
if (!state.playing.isPaused) return
|
||||||
state.playing.isPaused = false
|
state.playing.isPaused = false
|
||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
@@ -77,7 +134,7 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Pause the currently playing media
|
// Pause the currently playing media
|
||||||
pause () {
|
pause () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (state.playing.isPaused) return
|
if (state.playing.isPaused) return
|
||||||
state.playing.isPaused = true
|
state.playing.isPaused = true
|
||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
@@ -106,25 +163,23 @@ module.exports = class PlaybackController {
|
|||||||
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
|
// to 0.25 (quarter-speed playback), then goes to -0.25, -0.5, -1, -2, etc
|
||||||
// until -16 (fast rewind)
|
// until -16 (fast rewind)
|
||||||
changePlaybackRate (direction) {
|
changePlaybackRate (direction) {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
var rate = state.playing.playbackRate
|
let rate = state.playing.playbackRate
|
||||||
if (direction > 0 && rate >= 0.25 && rate < 2) {
|
if (direction > 0 && rate < 2) {
|
||||||
rate += 0.25
|
rate += 0.25
|
||||||
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
} else if (direction < 0 && rate > 0.25 && rate <= 2) {
|
||||||
rate -= 0.25
|
rate -= 0.25
|
||||||
} else if (direction < 0 && rate === 0.25) { /* when we set playback rate at 0 in html 5, playback hangs ;( */
|
} else if (direction > 0 && rate >= 1 && rate < 16) {
|
||||||
rate = -1
|
|
||||||
} else if (direction > 0 && rate === -1) {
|
|
||||||
rate = 0.25
|
|
||||||
} else if ((direction > 0 && rate >= 1 && rate < 16) || (direction < 0 && rate > -16 && rate <= -1)) {
|
|
||||||
rate *= 2
|
rate *= 2
|
||||||
} else if ((direction < 0 && rate > 1 && rate <= 16) || (direction > 0 && rate >= -16 && rate < -1)) {
|
} else if (direction < 0 && rate > 1 && rate <= 16) {
|
||||||
rate /= 2
|
rate /= 2
|
||||||
}
|
}
|
||||||
state.playing.playbackRate = rate
|
state.playing.playbackRate = rate
|
||||||
if (isCasting(state) && !Cast.setRate(rate)) {
|
if (isCasting(state) && !Cast.setRate(rate)) {
|
||||||
state.playing.playbackRate = 1
|
state.playing.playbackRate = 1
|
||||||
}
|
}
|
||||||
|
// Wait a bit before we hide the controls and header again
|
||||||
|
state.playing.mouseStationarySince = new Date().getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the volume, in range [0, 1], by some amount
|
// Change the volume, in range [0, 1], by some amount
|
||||||
@@ -139,7 +194,7 @@ module.exports = class PlaybackController {
|
|||||||
// 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))
|
||||||
|
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
Cast.setVolume(volume)
|
Cast.setVolume(volume)
|
||||||
} else {
|
} else {
|
||||||
@@ -153,12 +208,8 @@ module.exports = class PlaybackController {
|
|||||||
// * The video is paused
|
// * The video is paused
|
||||||
// * The video is playing remotely on Chromecast or Airplay
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
showOrHidePlayerControls () {
|
showOrHidePlayerControls () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
var hideControls = state.location.url() === 'player' &&
|
const hideControls = state.shouldHidePlayerControls()
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
|
||||||
!state.playing.isPaused &&
|
|
||||||
state.playing.location === 'local'
|
|
||||||
|
|
||||||
if (hideControls !== state.playing.hideControls) {
|
if (hideControls !== state.playing.hideControls) {
|
||||||
state.playing.hideControls = hideControls
|
state.playing.hideControls = hideControls
|
||||||
@@ -169,60 +220,82 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Opens the video player to a specific torrent
|
// Opens the video player to a specific torrent
|
||||||
openPlayer (infoHash, index, cb) {
|
openPlayer (infoHash, index, cb) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const state = this.state
|
||||||
|
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||||
|
|
||||||
// automatically choose which file in the torrent to play, if necessary
|
state.playing.infoHash = torrentSummary.infoHash
|
||||||
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
|
state.playing.isReady = false
|
||||||
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
|
|
||||||
if (index === undefined) return cb(new errors.UnplayableError())
|
|
||||||
|
|
||||||
// update UI to show pending playback
|
// update UI to show pending playback
|
||||||
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
sound.play('PLAY')
|
||||||
// TODO: remove torrentSummary.playStatus
|
|
||||||
torrentSummary.playStatus = 'requested'
|
|
||||||
this.update()
|
|
||||||
|
|
||||||
var timeout = setTimeout(() => {
|
this.startServer(torrentSummary)
|
||||||
telemetry.logPlayAttempt('timeout')
|
ipcRenderer.send('onPlayerOpen')
|
||||||
// TODO: remove torrentSummary.playStatus
|
this.updatePlayer(infoHash, index, true, cb)
|
||||||
torrentSummary.playStatus = 'timeout' /* no seeders available? */
|
}
|
||||||
sound.play('ERROR')
|
|
||||||
cb(new Error('Playback timed out. Try again.'))
|
// Starts WebTorrent server for media streaming
|
||||||
this.update()
|
startServer (torrentSummary) {
|
||||||
}, 10000) /* give it a few seconds */
|
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,
|
||||||
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
() => onTorrentReady())
|
||||||
} else {
|
} else {
|
||||||
this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
|
onTorrentReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTorrentReady () {
|
||||||
|
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
|
||||||
|
ipcRenderer.once('wt-server-running', () => { state.playing.isReady = true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
// Called each time the current file changes
|
||||||
var fileSummary = torrentSummary.files[index]
|
updatePlayer (infoHash, index, resume, cb) {
|
||||||
|
const state = this.state
|
||||||
|
|
||||||
// update state
|
const torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||||
var state = this.state
|
const fileSummary = torrentSummary.files[index]
|
||||||
state.playing.infoHash = torrentSummary.infoHash
|
|
||||||
state.playing.fileIndex = index
|
|
||||||
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
|
||||||
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
|
||||||
: 'other'
|
|
||||||
|
|
||||||
// pick up where we left off
|
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
||||||
if (fileSummary.currentTime) {
|
torrentSummary.mostRecentFileIndex = undefined
|
||||||
var fraction = fileSummary.currentTime / fileSummary.duration
|
return cb(new UnplayableFileError())
|
||||||
var secondsLeft = fileSummary.duration - fileSummary.currentTime
|
|
||||||
if (fraction < 0.9 && secondsLeft > 10) {
|
|
||||||
state.playing.jumpToTime = fileSummary.currentTime
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
torrentSummary.mostRecentFileIndex = index
|
||||||
|
|
||||||
|
// update state
|
||||||
|
state.playing.infoHash = infoHash
|
||||||
|
state.playing.fileIndex = index
|
||||||
|
state.playing.fileName = fileSummary.name
|
||||||
|
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
||||||
|
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||||
|
: 'other'
|
||||||
|
|
||||||
|
// pick up where we left off
|
||||||
|
let jumpToTime = 0
|
||||||
|
if (resume && fileSummary.currentTime) {
|
||||||
|
const fraction = fileSummary.currentTime / fileSummary.duration
|
||||||
|
const secondsLeft = fileSummary.duration - fileSummary.currentTime
|
||||||
|
if (fraction < 0.9 && secondsLeft > 10) {
|
||||||
|
jumpToTime = fileSummary.currentTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
if (torrentSummary.status === 'paused') {
|
||||||
ipcRenderer.send('wt-get-audio-metadata', torrentSummary.infoHash, index)
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash, getAudioMetadata)
|
||||||
|
} else {
|
||||||
|
getAudioMetadata()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAudioMetadata () {
|
||||||
|
if (state.playing.type === 'audio') {
|
||||||
|
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
|
||||||
@@ -233,41 +306,28 @@ module.exports = class PlaybackController {
|
|||||||
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
state.window.title = fileSummary.name
|
||||||
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
|
|
||||||
// if we timed out (user clicked play a long time ago), don't autoplay
|
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
||||||
var timedOut = torrentSummary.playStatus === 'timeout'
|
if (this.state.saved.prefs.openExternalPlayer) {
|
||||||
delete torrentSummary.playStatus
|
dispatch('openExternalPlayer')
|
||||||
if (timedOut) {
|
|
||||||
ipcRenderer.send('wt-stop-server')
|
|
||||||
return this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
state.window.title = torrentSummary.files[state.playing.fileIndex].name
|
|
||||||
|
|
||||||
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
|
||||||
if (this.state.saved.prefs.openExternalPlayer) {
|
|
||||||
dispatch('openExternalPlayer')
|
|
||||||
this.update()
|
|
||||||
cb()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, play the video
|
|
||||||
this.update()
|
this.update()
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerOpen')
|
|
||||||
cb()
|
cb()
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, play the video
|
||||||
|
this.update()
|
||||||
|
|
||||||
|
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
|
||||||
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
closePlayer () {
|
closePlayer () {
|
||||||
console.log('closePlayer')
|
console.log('closePlayer')
|
||||||
|
|
||||||
// Quit any external players, like Chromecast/Airplay/etc or VLC
|
// Quit any external players, like Chromecast/Airplay/etc or VLC
|
||||||
var state = this.state
|
const state = this.state
|
||||||
if (isCasting(state)) {
|
if (isCasting(state)) {
|
||||||
Cast.stop()
|
Cast.stop()
|
||||||
}
|
}
|
||||||
@@ -278,12 +338,7 @@ module.exports = class PlaybackController {
|
|||||||
// Save volume (this session only, not in state.saved)
|
// Save volume (this session only, not in state.saved)
|
||||||
state.previousVolume = state.playing.volume
|
state.previousVolume = state.playing.volume
|
||||||
|
|
||||||
// Telemetry: track what happens after the user clicks play
|
if (!state.playing.isReady) telemetry.logPlayAttempt('abandoned') // user gave up waiting
|
||||||
var result = state.playing.result // 'success' or 'error'
|
|
||||||
if (result === 'success') telemetry.logPlayAttempt('success') // first frame displayed
|
|
||||||
else if (result === 'error') telemetry.logPlayAttempt('error') // codec missing, etc
|
|
||||||
else if (result === undefined) telemetry.logPlayAttempt('abandoned') // user exited before first frame
|
|
||||||
else console.error('Unknown state.playing.result', state.playing.result)
|
|
||||||
|
|
||||||
// Reset the window contents back to the home screen
|
// Reset the window contents back to the home screen
|
||||||
state.playing = State.getDefaultPlayState()
|
state.playing = State.getDefaultPlayState()
|
||||||
@@ -300,6 +355,11 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
ipcRenderer.send('onPlayerClose')
|
ipcRenderer.send('onPlayerClose')
|
||||||
|
|
||||||
|
// Playback Priority: resume previously paused downloads.
|
||||||
|
if (this.state.saved.prefs.highestPlaybackPriority) {
|
||||||
|
dispatch('resumePausedTorrents')
|
||||||
|
}
|
||||||
|
|
||||||
this.update()
|
this.update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const State = require('../lib/state')
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
|
||||||
const ipcRenderer = require('electron').ipcRenderer
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
// Controls the Preferences screen
|
// Controls the Preferences screen
|
||||||
@@ -11,46 +10,29 @@ module.exports = class PrefsController {
|
|||||||
|
|
||||||
// Goes to the Preferences screen
|
// Goes to the Preferences screen
|
||||||
show () {
|
show () {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
state.location.go({
|
state.location.go({
|
||||||
url: 'preferences',
|
url: 'preferences',
|
||||||
setup: function (cb) {
|
setup: function (cb) {
|
||||||
// initialize preferences
|
// initialize preferences
|
||||||
state.window.title = 'Preferences'
|
state.window.title = 'Preferences'
|
||||||
state.unsaved = Object.assign(state.unsaved || {}, {prefs: state.saved.prefs || {}})
|
|
||||||
ipcRenderer.send('setAllowNav', false)
|
ipcRenderer.send('setAllowNav', false)
|
||||||
cb()
|
cb()
|
||||||
},
|
},
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
ipcRenderer.send('setAllowNav', true)
|
ipcRenderer.send('setAllowNav', true)
|
||||||
this.save()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates a single property in the UNSAVED prefs
|
// Updates a single property in the saved prefs
|
||||||
// For example: updatePreferences('foo.bar', 'baz')
|
// For example: updatePreferences('isFileHandler', true)
|
||||||
// Call save() to save to config.json
|
|
||||||
update (property, value) {
|
update (property, value) {
|
||||||
var path = property.split('.')
|
if (property === 'isFileHandler') ipcRenderer.send('setDefaultFileHandler', value)
|
||||||
var key = this.state.unsaved.prefs
|
else if (property === 'startup') ipcRenderer.send('setStartup', value)
|
||||||
for (var i = 0; i < path.length - 1; i++) {
|
|
||||||
if (typeof key[path[i]] === 'undefined') {
|
|
||||||
key[path[i]] = {}
|
|
||||||
}
|
|
||||||
key = key[path[i]]
|
|
||||||
}
|
|
||||||
key[path[i]] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// All unsaved prefs take effect atomically, and are saved to config.json
|
this.state.saved.prefs[property] = value
|
||||||
save () {
|
dispatch('stateSaveImmediate')
|
||||||
var state = this.state
|
|
||||||
if (state.unsaved.prefs.isFileHandler !== state.saved.prefs.isFileHandler) {
|
|
||||||
ipcRenderer.send('setDefaultFileHandler', state.unsaved.prefs.isFileHandler)
|
|
||||||
}
|
|
||||||
state.saved.prefs = Object.assign(state.saved.prefs || {}, state.unsaved.prefs)
|
|
||||||
State.save(state)
|
|
||||||
dispatch('checkDownloadPath')
|
dispatch('checkDownloadPath')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const parallel = require('run-parallel')
|
const parallel = require('run-parallel')
|
||||||
|
|
||||||
const remote = electron.remote
|
const remote = electron.remote
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class SubtitlesController {
|
module.exports = class SubtitlesController {
|
||||||
constructor (state) {
|
constructor (state) {
|
||||||
@@ -15,8 +15,8 @@ module.exports = class SubtitlesController {
|
|||||||
openSubtitles () {
|
openSubtitles () {
|
||||||
remote.dialog.showOpenDialog({
|
remote.dialog.showOpenDialog({
|
||||||
title: 'Select a subtitles file.',
|
title: 'Select a subtitles file.',
|
||||||
filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ],
|
filters: [{ name: 'Subtitles', extensions: ['vtt', 'srt'] }],
|
||||||
properties: [ 'openFile' ]
|
properties: ['openFile']
|
||||||
}, (filenames) => {
|
}, (filenames) => {
|
||||||
if (!Array.isArray(filenames)) return
|
if (!Array.isArray(filenames)) return
|
||||||
this.addSubtitles(filenames, true)
|
this.addSubtitles(filenames, true)
|
||||||
@@ -28,36 +28,35 @@ module.exports = class SubtitlesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleSubtitlesMenu () {
|
toggleSubtitlesMenu () {
|
||||||
var subtitles = this.state.playing.subtitles
|
const subtitles = this.state.playing.subtitles
|
||||||
subtitles.showMenu = !subtitles.showMenu
|
subtitles.showMenu = !subtitles.showMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
addSubtitles (files, autoSelect) {
|
addSubtitles (files, autoSelect) {
|
||||||
var state = this.state
|
|
||||||
// Subtitles are only supported when playing video files
|
// Subtitles are only supported when playing video files
|
||||||
if (state.playing.type !== 'video') return
|
if (this.state.playing.type !== 'video') return
|
||||||
if (files.length === 0) return
|
if (files.length === 0) return
|
||||||
var subtitles = state.playing.subtitles
|
const subtitles = this.state.playing.subtitles
|
||||||
|
|
||||||
// Read the files concurrently, then add all resulting subtitle tracks
|
// Read the files concurrently, then add all resulting subtitle tracks
|
||||||
var tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
const tasks = files.map((file) => (cb) => loadSubtitle(file, cb))
|
||||||
parallel(tasks, function (err, tracks) {
|
parallel(tasks, function (err, tracks) {
|
||||||
if (err) return dispatch('error', err)
|
if (err) return dispatch('error', err)
|
||||||
|
|
||||||
for (var i = 0; i < tracks.length; i++) {
|
for (let i = 0; i < tracks.length; i++) {
|
||||||
// No dupes allowed
|
// No dupes allowed
|
||||||
var track = tracks[i]
|
const track = tracks[i]
|
||||||
var trackIndex = state.playing.subtitles.tracks
|
let trackIndex = subtitles.tracks.findIndex((t) =>
|
||||||
.findIndex((t) => track.filePath === t.filePath)
|
track.filePath === t.filePath)
|
||||||
|
|
||||||
// Add the track
|
// Add the track
|
||||||
if (trackIndex === -1) {
|
if (trackIndex === -1) {
|
||||||
trackIndex = state.playing.subtitles.tracks.push(track) - 1
|
trackIndex = subtitles.tracks.push(track) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're auto-selecting a track, try to find one in the user's language
|
// If we're auto-selecting a track, try to find one in the user's language
|
||||||
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
if (autoSelect && (i === 0 || isSystemLanguage(track.language))) {
|
||||||
state.playing.subtitles.selectedIndex = trackIndex
|
subtitles.selectedIndex = trackIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,46 +67,46 @@ module.exports = class SubtitlesController {
|
|||||||
|
|
||||||
checkForSubtitles () {
|
checkForSubtitles () {
|
||||||
if (this.state.playing.type !== 'video') return
|
if (this.state.playing.type !== 'video') return
|
||||||
var torrentSummary = this.state.getPlayingTorrentSummary()
|
const torrentSummary = this.state.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary || !torrentSummary.progress) return
|
if (!torrentSummary || !torrentSummary.progress) return
|
||||||
|
|
||||||
torrentSummary.progress.files.forEach((fp, ix) => {
|
torrentSummary.progress.files.forEach((fp, ix) => {
|
||||||
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
|
if (fp.numPieces !== fp.numPiecesPresent) return // ignore incomplete files
|
||||||
var file = torrentSummary.files[ix]
|
const file = torrentSummary.files[ix]
|
||||||
if (!this.isSubtitle(file.name)) return
|
if (!this.isSubtitle(file.name)) return
|
||||||
var filePath = path.join(torrentSummary.path, file.path)
|
const filePath = path.join(torrentSummary.path, file.path)
|
||||||
this.addSubtitles([filePath], false)
|
this.addSubtitles([filePath], false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isSubtitle (file) {
|
isSubtitle (file) {
|
||||||
var name = typeof file === 'string' ? file : file.name
|
const name = typeof file === 'string' ? file : file.name
|
||||||
var ext = path.extname(name).toLowerCase()
|
const ext = path.extname(name).toLowerCase()
|
||||||
return ext === '.srt' || ext === '.vtt'
|
return ext === '.srt' || ext === '.vtt'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSubtitle (file, cb) {
|
function loadSubtitle (file, cb) {
|
||||||
// Lazy load to keep startup fast
|
// Lazy load to keep startup fast
|
||||||
var concat = require('simple-concat')
|
const concat = require('simple-concat')
|
||||||
var LanguageDetect = require('languagedetect')
|
const LanguageDetect = require('languagedetect')
|
||||||
var srtToVtt = require('srt-to-vtt')
|
const srtToVtt = require('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
|
||||||
var filePath = file.path || file
|
const filePath = file.path || file
|
||||||
|
|
||||||
var vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
const vttStream = fs.createReadStream(filePath).pipe(srtToVtt())
|
||||||
|
|
||||||
concat(vttStream, function (err, buf) {
|
concat(vttStream, function (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
|
||||||
var vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
const vttContents = buf.toString().replace(/(.*-->.*)/g, '')
|
||||||
var langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
let langDetected = (new LanguageDetect()).detect(vttContents, 2)
|
||||||
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
langDetected = langDetected.length ? langDetected[0][0] : 'subtitle'
|
||||||
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
langDetected = langDetected.slice(0, 1).toUpperCase() + langDetected.slice(1)
|
||||||
|
|
||||||
var track = {
|
const track = {
|
||||||
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
buffer: 'data:text/vtt;base64,' + buf.toString('base64'),
|
||||||
language: langDetected,
|
language: langDetected,
|
||||||
label: langDetected,
|
label: langDetected,
|
||||||
@@ -121,18 +120,18 @@ 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) {
|
function isSystemLanguage (language) {
|
||||||
var iso639 = require('iso-639-1')
|
const iso639 = require('iso-639-1')
|
||||||
var osLangISO = window.navigator.language.split('-')[0] // eg 'en'
|
const osLangISO = window.navigator.language.split('-')[0] // eg 'en'
|
||||||
var 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we don't have two subtitle tracks with the same label
|
// Make sure we don't have two subtitle tracks with the same label
|
||||||
// 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) {
|
||||||
var counts = {}
|
const counts = {}
|
||||||
subtitles.tracks.forEach(function (track) {
|
subtitles.tracks.forEach(function (track) {
|
||||||
var 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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ const path = require('path')
|
|||||||
const ipcRenderer = require('electron').ipcRenderer
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
|
||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class TorrentController {
|
module.exports = class TorrentController {
|
||||||
constructor (state) {
|
constructor (state) {
|
||||||
@@ -12,12 +11,12 @@ module.exports = class TorrentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
torrentInfoHash (torrentKey, infoHash) {
|
torrentInfoHash (torrentKey, infoHash) {
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
let torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
console.log('got infohash for %s torrent %s',
|
console.log('got infohash for %s torrent %s',
|
||||||
torrentSummary ? 'existing' : 'new', torrentKey)
|
torrentSummary ? 'existing' : 'new', torrentKey)
|
||||||
|
|
||||||
if (!torrentSummary) {
|
if (!torrentSummary) {
|
||||||
var torrents = this.state.saved.torrents
|
const torrents = this.state.saved.torrents
|
||||||
|
|
||||||
// Check if an existing (non-active) torrent has the same info hash
|
// Check if an existing (non-active) torrent has the same info hash
|
||||||
if (torrents.find((t) => t.infoHash === infoHash)) {
|
if (torrents.find((t) => t.infoHash === infoHash)) {
|
||||||
@@ -49,7 +48,7 @@ module.exports = class TorrentController {
|
|||||||
}
|
}
|
||||||
dispatch('error', message)
|
dispatch('error', message)
|
||||||
|
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
if (torrentSummary) {
|
if (torrentSummary) {
|
||||||
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
|
console.log('Pausing torrent %s due to error: %s', torrentSummary.infoHash, message)
|
||||||
torrentSummary.status = 'paused'
|
torrentSummary.status = 'paused'
|
||||||
@@ -59,21 +58,20 @@ module.exports = class TorrentController {
|
|||||||
|
|
||||||
torrentMetadata (torrentKey, torrentInfo) {
|
torrentMetadata (torrentKey, torrentInfo) {
|
||||||
// Summarize torrent
|
// Summarize torrent
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.status = 'downloading'
|
torrentSummary.status = 'downloading'
|
||||||
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
torrentSummary.name = torrentSummary.displayName || torrentInfo.name
|
||||||
torrentSummary.path = torrentInfo.path
|
torrentSummary.path = torrentInfo.path
|
||||||
torrentSummary.magnetURI = torrentInfo.magnetURI
|
torrentSummary.magnetURI = torrentInfo.magnetURI
|
||||||
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
// TODO: make torrentInfo immutable, save separately as torrentSummary.info
|
||||||
// For now, check whether torrentSummary.files has already been set:
|
// For now, check whether torrentSummary.files has already been set:
|
||||||
var hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
const hasDetailedFileInfo = torrentSummary.files && torrentSummary.files[0].path
|
||||||
if (!hasDetailedFileInfo) {
|
if (!hasDetailedFileInfo) {
|
||||||
torrentSummary.files = torrentInfo.files
|
torrentSummary.files = torrentInfo.files
|
||||||
}
|
}
|
||||||
if (!torrentSummary.selections) {
|
if (!torrentSummary.selections) {
|
||||||
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||||
}
|
}
|
||||||
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
|
|
||||||
dispatch('update')
|
dispatch('update')
|
||||||
|
|
||||||
// Save the .torrent file, if it hasn't been saved already
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
@@ -85,7 +83,7 @@ module.exports = class TorrentController {
|
|||||||
|
|
||||||
torrentDone (torrentKey, torrentInfo) {
|
torrentDone (torrentKey, torrentInfo) {
|
||||||
// Update the torrent summary
|
// Update the torrent summary
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.status = 'seeding'
|
torrentSummary.status = 'seeding'
|
||||||
|
|
||||||
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
|
// Notify the user that a torrent finished, but only if we actually DL'd at least part of it.
|
||||||
@@ -102,22 +100,18 @@ module.exports = class TorrentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
torrentProgress (progressInfo) {
|
torrentProgress (progressInfo) {
|
||||||
// Overall progress across all active torrents, 0 to 1
|
// Overall progress across all active torrents, 0 to 1, or -1 to hide the progress bar
|
||||||
var progress = progressInfo.progress
|
|
||||||
var hasActiveTorrents = progressInfo.hasActiveTorrents
|
|
||||||
|
|
||||||
// Hide progress bar when client has no torrents, or progress is 100%
|
// Hide progress bar when client has no torrents, or progress is 100%
|
||||||
// TODO: isn't this equivalent to: if (progress === 1) ?
|
const progress = (!progressInfo.hasActiveTorrents || progressInfo.progress === 1)
|
||||||
if (!hasActiveTorrents || progress === 1) {
|
? -1
|
||||||
progress = -1
|
: progressInfo.progress
|
||||||
}
|
|
||||||
|
|
||||||
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
// Show progress bar under the WebTorrent taskbar icon, on OSX
|
||||||
this.state.dock.progress = progress
|
this.state.dock.progress = progress
|
||||||
|
|
||||||
// Update progress for each individual torrent
|
// Update progress for each individual torrent
|
||||||
progressInfo.torrents.forEach((p) => {
|
progressInfo.torrents.forEach((p) => {
|
||||||
var torrentSummary = this.getTorrentSummary(p.torrentKey)
|
const torrentSummary = this.getTorrentSummary(p.torrentKey)
|
||||||
if (!torrentSummary) {
|
if (!torrentSummary) {
|
||||||
console.log('warning: got progress for missing torrent %s', p.torrentKey)
|
console.log('warning: got progress for missing torrent %s', p.torrentKey)
|
||||||
return
|
return
|
||||||
@@ -132,27 +126,28 @@ module.exports = class TorrentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
torrentFileModtimes (torrentKey, fileModtimes) {
|
torrentFileModtimes (torrentKey, fileModtimes) {
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
|
if (!torrentSummary) throw new Error('Not saving modtimes for deleted torrent ' + torrentKey)
|
||||||
torrentSummary.fileModtimes = fileModtimes
|
torrentSummary.fileModtimes = fileModtimes
|
||||||
dispatch('saveStateThrottled')
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentFileSaved (torrentKey, torrentFileName) {
|
torrentFileSaved (torrentKey, torrentFileName) {
|
||||||
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
console.log('torrent file saved %s: %s', torrentKey, torrentFileName)
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.torrentFileName = torrentFileName
|
torrentSummary.torrentFileName = torrentFileName
|
||||||
dispatch('saveStateThrottled')
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentPosterSaved (torrentKey, posterFileName) {
|
torrentPosterSaved (torrentKey, posterFileName) {
|
||||||
var torrentSummary = this.getTorrentSummary(torrentKey)
|
const torrentSummary = this.getTorrentSummary(torrentKey)
|
||||||
torrentSummary.posterFileName = posterFileName
|
torrentSummary.posterFileName = posterFileName
|
||||||
dispatch('saveStateThrottled')
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentAudioMetadata (infoHash, index, info) {
|
torrentAudioMetadata (infoHash, index, info) {
|
||||||
var torrentSummary = this.getTorrentSummary(infoHash)
|
const torrentSummary = this.getTorrentSummary(infoHash)
|
||||||
var fileSummary = torrentSummary.files[index]
|
const fileSummary = torrentSummary.files[index]
|
||||||
fileSummary.audioInfo = info
|
fileSummary.audioInfo = info
|
||||||
dispatch('update')
|
dispatch('update')
|
||||||
}
|
}
|
||||||
@@ -169,7 +164,7 @@ module.exports = class TorrentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTorrentPath (torrentSummary) {
|
function getTorrentPath (torrentSummary) {
|
||||||
var itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
|
let itemPath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
if (torrentSummary.files.length > 1) {
|
if (torrentSummary.files.length > 1) {
|
||||||
itemPath = path.dirname(itemPath)
|
itemPath = path.dirname(itemPath)
|
||||||
}
|
}
|
||||||
@@ -177,14 +172,15 @@ function getTorrentPath (torrentSummary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showDoneNotification (torrent) {
|
function showDoneNotification (torrent) {
|
||||||
var notif = new window.Notification('Download Complete', {
|
const notif = new window.Notification('Download Complete', {
|
||||||
body: torrent.name,
|
body: torrent.name,
|
||||||
silent: true
|
silent: true
|
||||||
})
|
})
|
||||||
|
|
||||||
notif.onClick = function () {
|
notif.onclick = function () {
|
||||||
ipcRenderer.send('show')
|
ipcRenderer.send('show')
|
||||||
}
|
}
|
||||||
|
|
||||||
sound.play('DONE')
|
// Only play notification sound if player is inactive
|
||||||
|
if (this.state.playing.isPaused) sound.play('DONE')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ const fs = require('fs')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
const State = require('../lib/state')
|
const { TorrentKeyNotFoundError } = require('../lib/errors')
|
||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
|
||||||
@@ -17,21 +17,26 @@ module.exports = class TorrentListController {
|
|||||||
this.state = state
|
this.state = state
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds a torrent to the list, starts downloading/seeding. TorrentID can be a
|
// Adds a torrent to the list, starts downloading/seeding.
|
||||||
// magnet URI, infohash, or torrent file: https://github.com/feross/webtorrent#clientaddtorrentid-opts-function-ontorrent-torrent-
|
// TorrentID can be a magnet URI, infohash, or torrent file: https://git.io/vik9M
|
||||||
addTorrent (torrentId) {
|
addTorrent (torrentId) {
|
||||||
if (torrentId.path) {
|
if (torrentId.path) {
|
||||||
// Use path string instead of W3C File object
|
// Use path string instead of W3C File object
|
||||||
torrentId = torrentId.path
|
torrentId = torrentId.path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trim extra spaces off pasted magnet links
|
||||||
|
if (typeof torrentId === 'string') {
|
||||||
|
torrentId = torrentId.trim()
|
||||||
|
}
|
||||||
|
|
||||||
// Allow a instant.io link to be pasted
|
// Allow a instant.io link to be pasted
|
||||||
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
if (typeof torrentId === 'string' && instantIoRegex.test(torrentId)) {
|
||||||
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
torrentId = torrentId.slice(torrentId.indexOf('#') + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var torrentKey = this.state.nextTorrentKey++
|
const torrentKey = this.state.nextTorrentKey++
|
||||||
var path = this.state.saved.prefs.downloadPath
|
const path = this.state.saved.prefs.downloadPath
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path)
|
||||||
|
|
||||||
@@ -64,25 +69,18 @@ module.exports = class TorrentListController {
|
|||||||
findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles))
|
findFilesRecursive(files, (allFiles) => this.showCreateTorrent(allFiles))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switches between the advanced and simple Create Torrent UI
|
|
||||||
toggleCreateTorrentAdvanced () {
|
|
||||||
var info = this.state.location.current()
|
|
||||||
if (info.url !== 'create-torrent') return
|
|
||||||
info.showAdvanced = !info.showAdvanced
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new torrent and start seeeding
|
// Creates a new torrent and start seeeding
|
||||||
createTorrent (options) {
|
createTorrent (options) {
|
||||||
var state = this.state
|
const state = this.state
|
||||||
var torrentKey = state.nextTorrentKey++
|
const torrentKey = state.nextTorrentKey++
|
||||||
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
ipcRenderer.send('wt-create-torrent', torrentKey, options)
|
||||||
state.location.cancel()
|
state.location.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts downloading and/or seeding a given torrentSummary.
|
// Starts downloading and/or seeding a given torrentSummary.
|
||||||
startTorrentingSummary (torrentKey) {
|
startTorrentingSummary (torrentKey) {
|
||||||
var s = TorrentSummary.getByKey(this.state, torrentKey)
|
const s = TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
if (!s) throw new Error('Missing key: ' + torrentKey)
|
if (!s) throw new TorrentKeyNotFoundError(torrentKey)
|
||||||
|
|
||||||
// New torrent: give it a path
|
// New torrent: give it a path
|
||||||
if (!s.path) {
|
if (!s.path) {
|
||||||
@@ -91,10 +89,16 @@ module.exports = class TorrentListController {
|
|||||||
return start()
|
return start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileOrFolder = TorrentSummary.getFileOrFolder(s)
|
||||||
|
|
||||||
|
// New torrent: metadata not yet received
|
||||||
|
if (!fileOrFolder) return start()
|
||||||
|
|
||||||
// Existing torrent: check that the path is still there
|
// Existing torrent: check that the path is still there
|
||||||
fs.stat(TorrentSummary.getFileOrFolder(s), function (err) {
|
fs.stat(fileOrFolder, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
s.error = 'path-missing'
|
s.error = 'path-missing'
|
||||||
|
dispatch('backToList')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
start()
|
start()
|
||||||
@@ -103,7 +107,7 @@ module.exports = class TorrentListController {
|
|||||||
function start () {
|
function start () {
|
||||||
ipcRenderer.send('wt-start-torrenting',
|
ipcRenderer.send('wt-start-torrenting',
|
||||||
s.torrentKey,
|
s.torrentKey,
|
||||||
TorrentSummary.getTorrentID(s),
|
TorrentSummary.getTorrentId(s),
|
||||||
s.path,
|
s.path,
|
||||||
s.fileModtimes,
|
s.fileModtimes,
|
||||||
s.selections)
|
s.selections)
|
||||||
@@ -112,24 +116,81 @@ module.exports = class TorrentListController {
|
|||||||
|
|
||||||
// TODO: use torrentKey, not infoHash
|
// TODO: use torrentKey, not infoHash
|
||||||
toggleTorrent (infoHash) {
|
toggleTorrent (infoHash) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
torrentSummary.status = 'new'
|
torrentSummary.status = 'new'
|
||||||
this.startTorrentingSummary(torrentSummary.torrentKey)
|
this.startTorrentingSummary(torrentSummary.torrentKey)
|
||||||
sound.play('ENABLE')
|
sound.play('ENABLE')
|
||||||
} else {
|
return
|
||||||
torrentSummary.status = 'paused'
|
|
||||||
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
|
||||||
sound.play('DISABLE')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pauseTorrent(torrentSummary, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseAllTorrents () {
|
||||||
|
this.state.saved.torrents.forEach((torrentSummary) => {
|
||||||
|
if (torrentSummary.status === 'downloading' ||
|
||||||
|
torrentSummary.status === 'seeding') {
|
||||||
|
torrentSummary.status = 'paused'
|
||||||
|
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
sound.play('DISABLE')
|
||||||
|
}
|
||||||
|
|
||||||
|
resumeAllTorrents () {
|
||||||
|
this.state.saved.torrents.forEach((torrentSummary) => {
|
||||||
|
if (torrentSummary.status === 'paused') {
|
||||||
|
torrentSummary.status = 'downloading'
|
||||||
|
this.startTorrentingSummary(torrentSummary.torrentKey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
sound.play('ENABLE')
|
||||||
|
}
|
||||||
|
|
||||||
|
pauseTorrent (torrentSummary, playSound) {
|
||||||
|
torrentSummary.status = 'paused'
|
||||||
|
ipcRenderer.send('wt-stop-torrenting', torrentSummary.infoHash)
|
||||||
|
|
||||||
|
if (playSound) sound.play('DISABLE')
|
||||||
|
}
|
||||||
|
|
||||||
|
prioritizeTorrent (infoHash) {
|
||||||
|
this.state.saved.torrents
|
||||||
|
.filter((torrent) => { // We're interested in active torrents only.
|
||||||
|
return (['downloading', 'seeding'].indexOf(torrent.status) !== -1)
|
||||||
|
})
|
||||||
|
.map((torrent) => { // Pause all active torrents except the one that started playing.
|
||||||
|
if (infoHash === torrent.infoHash) return
|
||||||
|
|
||||||
|
// Pause torrent without playing sounds.
|
||||||
|
this.pauseTorrent(torrent, false)
|
||||||
|
|
||||||
|
this.state.saved.torrentsToResume.push(torrent.infoHash)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Playback Priority: paused torrents: ', this.state.saved.torrentsToResume)
|
||||||
|
}
|
||||||
|
|
||||||
|
resumePausedTorrents () {
|
||||||
|
console.log('Playback Priority: resuming paused torrents')
|
||||||
|
if (!this.state.saved.torrentsToResume || !this.state.saved.torrentsToResume.length) return
|
||||||
|
this.state.saved.torrentsToResume.map((infoHash) => {
|
||||||
|
this.toggleTorrent(infoHash)
|
||||||
|
})
|
||||||
|
|
||||||
|
// reset paused torrents
|
||||||
|
this.state.saved.torrentsToResume = []
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleTorrentFile (infoHash, index) {
|
toggleTorrentFile (infoHash, index) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
torrentSummary.selections[index] = !torrentSummary.selections[index]
|
||||||
|
|
||||||
// Let the WebTorrent process know to start or stop fetching that file
|
// Let the WebTorrent process know to start or stop fetching that file
|
||||||
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
if (torrentSummary.status !== 'paused') {
|
||||||
|
ipcRenderer.send('wt-select-files', infoHash, torrentSummary.selections)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmDeleteTorrent (infoHash, deleteData) {
|
confirmDeleteTorrent (infoHash, deleteData) {
|
||||||
@@ -144,24 +205,25 @@ module.exports = class TorrentListController {
|
|||||||
deleteTorrent (infoHash, deleteData) {
|
deleteTorrent (infoHash, deleteData) {
|
||||||
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
ipcRenderer.send('wt-stop-torrenting', infoHash)
|
||||||
|
|
||||||
var index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
const index = this.state.saved.torrents.findIndex((x) => x.infoHash === infoHash)
|
||||||
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
var summary = this.state.saved.torrents[index]
|
const summary = this.state.saved.torrents[index]
|
||||||
|
|
||||||
// remove torrent and poster file
|
// remove torrent and poster file
|
||||||
deleteFile(TorrentSummary.getTorrentPath(summary))
|
deleteFile(TorrentSummary.getTorrentPath(summary))
|
||||||
deleteFile(TorrentSummary.getPosterPath(summary)) // TODO: will the css path hack affect windows?
|
deleteFile(TorrentSummary.getPosterPath(summary))
|
||||||
|
|
||||||
// optionally delete the torrent data
|
// optionally delete the torrent data
|
||||||
if (deleteData) moveItemToTrash(summary)
|
if (deleteData) moveItemToTrash(summary)
|
||||||
|
|
||||||
// remove torrent from saved list
|
// remove torrent from saved list
|
||||||
this.state.saved.torrents.splice(index, 1)
|
this.state.saved.torrents.splice(index, 1)
|
||||||
State.saveThrottled(this.state)
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.location.clearForward('player') // prevent user from going forward to a deleted torrent
|
// prevent user from going forward to a deleted torrent
|
||||||
|
this.state.location.clearForward('player')
|
||||||
sound.play('DELETE')
|
sound.play('DELETE')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,8 +236,8 @@ module.exports = class TorrentListController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openTorrentContextMenu (infoHash) {
|
openTorrentContextMenu (infoHash) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
const torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
||||||
var menu = new electron.remote.Menu()
|
const menu = new electron.remote.Menu()
|
||||||
|
|
||||||
menu.append(new electron.remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Remove From List',
|
label: 'Remove From List',
|
||||||
@@ -213,39 +275,70 @@ module.exports = class TorrentListController {
|
|||||||
|
|
||||||
menu.append(new electron.remote.MenuItem({
|
menu.append(new electron.remote.MenuItem({
|
||||||
label: 'Save Torrent File As...',
|
label: 'Save Torrent File As...',
|
||||||
click: () => saveTorrentFileAs(torrentSummary)
|
click: () => dispatch('saveTorrentFileAs', torrentSummary.torrentKey),
|
||||||
|
enabled: torrentSummary.torrentFileName != null
|
||||||
}))
|
}))
|
||||||
|
|
||||||
menu.popup(electron.remote.getCurrentWindow())
|
menu.popup(electron.remote.getCurrentWindow())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Takes a torrentSummary or torrentKey
|
||||||
|
// Shows a Save File dialog, then saves the .torrent file wherever the user requests
|
||||||
|
saveTorrentFileAs (torrentKey) {
|
||||||
|
const torrentSummary = TorrentSummary.getByKey(this.state, torrentKey)
|
||||||
|
if (!torrentSummary) throw new Error('Missing torrentKey: ' + torrentKey)
|
||||||
|
const downloadPath = this.state.saved.prefs.downloadPath
|
||||||
|
const newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
||||||
|
const win = electron.remote.getCurrentWindow()
|
||||||
|
const opts = {
|
||||||
|
title: 'Save Torrent File',
|
||||||
|
defaultPath: path.join(downloadPath, newFileName),
|
||||||
|
filters: [
|
||||||
|
{ name: 'Torrent Files', extensions: ['torrent'] },
|
||||||
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.remote.dialog.showSaveDialog(win, opts, function (savePath) {
|
||||||
|
console.log('Saving torrent ' + torrentKey + ' to ' + savePath)
|
||||||
|
if (!savePath) return // They clicked Cancel
|
||||||
|
const torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
||||||
|
fs.readFile(torrentPath, function (err, torrentFile) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
fs.writeFile(savePath, torrentFile, function (err) {
|
||||||
|
if (err) return dispatch('error', err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively finds {name, path, size} for all files in a folder
|
// Recursively finds {name, path, size} for all files in a folder
|
||||||
// Calls `cb` on success, calls `onError` on failure
|
// Calls `cb` on success, calls `onError` on failure
|
||||||
function findFilesRecursive (paths, cb) {
|
function findFilesRecursive (paths, cb_) {
|
||||||
if (paths.length > 1) {
|
if (paths.length > 1) {
|
||||||
var numComplete = 0
|
let numComplete = 0
|
||||||
var ret = []
|
const ret = []
|
||||||
paths.forEach(function (path) {
|
paths.forEach(function (path) {
|
||||||
findFilesRecursive([path], function (fileObjs) {
|
findFilesRecursive([path], function (fileObjs) {
|
||||||
ret = ret.concat(fileObjs)
|
ret.push(...fileObjs)
|
||||||
if (++numComplete === paths.length) {
|
if (++numComplete === paths.length) {
|
||||||
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
|
ret.sort((a, b) => a.path < b.path ? -1 : a.path > b.path)
|
||||||
cb(ret)
|
cb_(ret)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileOrFolder = paths[0]
|
const fileOrFolder = paths[0]
|
||||||
fs.stat(fileOrFolder, function (err, stat) {
|
fs.stat(fileOrFolder, function (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
|
||||||
if (!stat.isDirectory()) {
|
if (!stat.isDirectory()) {
|
||||||
var filePath = fileOrFolder
|
const filePath = fileOrFolder
|
||||||
return cb([{
|
return cb_([{
|
||||||
name: path.basename(filePath),
|
name: path.basename(filePath),
|
||||||
path: filePath,
|
path: filePath,
|
||||||
size: stat.size
|
size: stat.size
|
||||||
@@ -253,11 +346,11 @@ function findFilesRecursive (paths, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Folders: recurse, make a list of all the files
|
// Folders: recurse, make a list of all the files
|
||||||
var folderPath = fileOrFolder
|
const folderPath = fileOrFolder
|
||||||
fs.readdir(folderPath, function (err, fileNames) {
|
fs.readdir(folderPath, function (err, fileNames) {
|
||||||
if (err) return dispatch('error', err)
|
if (err) return dispatch('error', err)
|
||||||
var paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
const paths = fileNames.map((fileName) => path.join(folderPath, fileName))
|
||||||
findFilesRecursive(paths, cb)
|
findFilesRecursive(paths, cb_)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -271,33 +364,10 @@ function deleteFile (path) {
|
|||||||
|
|
||||||
// Delete all files in a torrent
|
// Delete all files in a torrent
|
||||||
function moveItemToTrash (torrentSummary) {
|
function moveItemToTrash (torrentSummary) {
|
||||||
var filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
const filePath = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
|
if (filePath) ipcRenderer.send('moveItemToTrash', filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function showItemInFolder (torrentSummary) {
|
function showItemInFolder (torrentSummary) {
|
||||||
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
ipcRenderer.send('showItemInFolder', TorrentSummary.getFileOrFolder(torrentSummary))
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTorrentFileAs (torrentSummary) {
|
|
||||||
var downloadPath = this.state.saved.prefs.downloadPath
|
|
||||||
var newFileName = path.parse(torrentSummary.name).name + '.torrent'
|
|
||||||
var opts = {
|
|
||||||
title: 'Save Torrent File',
|
|
||||||
defaultPath: path.join(downloadPath, newFileName),
|
|
||||||
filters: [
|
|
||||||
{ name: 'Torrent Files', extensions: ['torrent'] },
|
|
||||||
{ name: 'All Files', extensions: ['*'] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
electron.remote.dialog.showSaveDialog(electron.remote.getCurrentWindow(), opts, function (savePath) {
|
|
||||||
if (!savePath) return // They clicked Cancel
|
|
||||||
var torrentPath = TorrentSummary.getTorrentPath(torrentSummary)
|
|
||||||
fs.readFile(torrentPath, function (err, torrentFile) {
|
|
||||||
if (err) return dispatch('error', err)
|
|
||||||
fs.writeFile(savePath, torrentFile, function (err) {
|
|
||||||
if (err) return dispatch('error', err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const State = require('../lib/state')
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
|
|
||||||
// Controls the UI checking for new versions of the app, prompting install
|
// Controls the UI checking for new versions of the app, prompting install
|
||||||
module.exports = class UpdateController {
|
module.exports = class UpdateController {
|
||||||
@@ -8,7 +8,7 @@ module.exports = class UpdateController {
|
|||||||
|
|
||||||
// Shows a modal saying that we have an update
|
// Shows a modal saying that we have an update
|
||||||
updateAvailable (version) {
|
updateAvailable (version) {
|
||||||
var skipped = this.state.saved.skippedVersions
|
const skipped = this.state.saved.skippedVersions
|
||||||
if (skipped && skipped.includes(version)) {
|
if (skipped && skipped.includes(version)) {
|
||||||
console.log('new version skipped by user: v' + version)
|
console.log('new version skipped by user: v' + version)
|
||||||
return
|
return
|
||||||
@@ -18,9 +18,9 @@ module.exports = class UpdateController {
|
|||||||
|
|
||||||
// Don't show the modal again until the next version
|
// Don't show the modal again until the next version
|
||||||
skipVersion (version) {
|
skipVersion (version) {
|
||||||
var skipped = this.state.saved.skippedVersions
|
let skipped = this.state.saved.skippedVersions
|
||||||
if (!skipped) skipped = this.state.saved.skippedVersions = []
|
if (!skipped) skipped = this.state.saved.skippedVersions = []
|
||||||
skipped.push(version)
|
skipped.push(version)
|
||||||
State.saveThrottled(this.state)
|
dispatch('stateSave')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
module.exports = captureVideoFrame
|
|
||||||
|
|
||||||
function captureVideoFrame (video, format) {
|
|
||||||
if (typeof video === 'string') {
|
|
||||||
video = document.querySelector(video)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (video == null || video.nodeName !== 'VIDEO') {
|
|
||||||
throw new Error('First argument must be a <video> element or selector')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format == null) {
|
|
||||||
format = 'png'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format !== 'png' && format !== 'jpg' && format !== 'webp') {
|
|
||||||
throw new Error('Second argument must be one of "png", "jpg", or "webp"')
|
|
||||||
}
|
|
||||||
|
|
||||||
var canvas = document.createElement('canvas')
|
|
||||||
canvas.width = video.videoWidth
|
|
||||||
canvas.height = video.videoHeight
|
|
||||||
|
|
||||||
canvas.getContext('2d').drawImage(video, 0, 0)
|
|
||||||
|
|
||||||
var dataUri = canvas.toDataURL('image/' + format)
|
|
||||||
var data = dataUri.split(',')[1]
|
|
||||||
|
|
||||||
return new Buffer(data, 'base64')
|
|
||||||
}
|
|
||||||
@@ -13,25 +13,37 @@ module.exports = {
|
|||||||
setRate
|
setRate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy load these for a ~300ms improvement in startup time
|
const http = require('http')
|
||||||
var airplayer, chromecasts, dlnacasts
|
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
|
const { CastingError } = require('./errors')
|
||||||
|
|
||||||
|
// Lazy load these for a ~300ms improvement in startup time
|
||||||
|
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
|
||||||
var state
|
let state
|
||||||
|
|
||||||
// Callback to notify module users when state has changed
|
// Callback to notify module users when state has changed
|
||||||
var update
|
let update
|
||||||
|
|
||||||
// setInterval() for updating cast status
|
// setInterval() for updating cast status
|
||||||
var 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
|
||||||
|
// (Otherwise you'd need a physical Chromecast + AppleTV + DLNA TV to run them.)
|
||||||
|
if (config.IS_TEST) {
|
||||||
|
state.devices.chromecast = testPlayer('chromecast')
|
||||||
|
state.devices.airplay = testPlayer('airplay')
|
||||||
|
state.devices.dlna = testPlayer('dlna')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Load modules, scan the network for devices
|
// Load modules, scan the network for devices
|
||||||
airplayer = require('airplayer')()
|
airplayer = require('airplayer')()
|
||||||
chromecasts = require('chromecasts')()
|
chromecasts = require('chromecasts')()
|
||||||
@@ -57,9 +69,35 @@ function init (appState, callback) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// integration test player implementation
|
||||||
|
function testPlayer (type) {
|
||||||
|
return {
|
||||||
|
getDevices,
|
||||||
|
open,
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
stop,
|
||||||
|
status,
|
||||||
|
seek,
|
||||||
|
volume
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDevices () {
|
||||||
|
return [{ name: type + '-1' }, { name: type + '-2' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function open () {}
|
||||||
|
function play () {}
|
||||||
|
function pause () {}
|
||||||
|
function stop () {}
|
||||||
|
function status () {}
|
||||||
|
function seek () {}
|
||||||
|
function volume () {}
|
||||||
|
}
|
||||||
|
|
||||||
// chromecast player implementation
|
// chromecast player implementation
|
||||||
function chromecastPlayer () {
|
function chromecastPlayer () {
|
||||||
var ret = {
|
const ret = {
|
||||||
device: null,
|
device: null,
|
||||||
addDevice,
|
addDevice,
|
||||||
getDevices,
|
getDevices,
|
||||||
@@ -94,22 +132,47 @@ function chromecastPlayer () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
function serveSubtitles (callback) {
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
const subtitles = state.playing.subtitles
|
||||||
ret.device.play(state.server.networkURL, {
|
const selectedSubtitle = subtitles.tracks[subtitles.selectedIndex]
|
||||||
type: 'video/mp4',
|
if (!selectedSubtitle) {
|
||||||
title: config.APP_NAME + ' - ' + torrentSummary.name
|
callback()
|
||||||
}, function (err) {
|
} else {
|
||||||
if (err) {
|
ret.subServer = http.createServer((req, res) => {
|
||||||
state.playing.location = 'local'
|
res.writeHead(200, {
|
||||||
state.errors.push({
|
'Content-Type': 'text/vtt;charset=utf-8',
|
||||||
time: new Date().getTime(),
|
'Access-Control-Allow-Origin': '*',
|
||||||
message: 'Could not connect to Chromecast. ' + err.message
|
'Transfer-Encoding': 'chunked'
|
||||||
})
|
})
|
||||||
} else {
|
res.end(Buffer.from(selectedSubtitle.buffer.substr(21), 'base64'))
|
||||||
state.playing.location = 'chromecast'
|
}).listen(0, function () {
|
||||||
}
|
const port = ret.subServer.address().port
|
||||||
update()
|
const subtitlesUrl = 'http://' + state.server.networkAddress + ':' + port + '/'
|
||||||
|
callback(subtitlesUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function open () {
|
||||||
|
const torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
|
serveSubtitles(function (subtitlesUrl) {
|
||||||
|
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
|
||||||
|
type: 'video/mp4',
|
||||||
|
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
||||||
|
subtitles: subtitlesUrl ? [subtitlesUrl] : [],
|
||||||
|
autoSubtitles: !!subtitlesUrl
|
||||||
|
}, function (err) {
|
||||||
|
if (err) {
|
||||||
|
state.playing.location = 'local'
|
||||||
|
state.errors.push({
|
||||||
|
time: new Date().getTime(),
|
||||||
|
message: 'Could not connect to Chromecast. ' + err.message
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
state.playing.location = 'chromecast'
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,16 +186,13 @@ function chromecastPlayer () {
|
|||||||
|
|
||||||
function stop (callback) {
|
function stop (callback) {
|
||||||
ret.device.stop(callback)
|
ret.device.stop(callback)
|
||||||
|
if (ret.subServer) {
|
||||||
|
ret.subServer.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
ret.device.status(function (err, status) {
|
ret.device.status(handleStatus)
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
|
||||||
state.playing.currentTime = status.currentTime
|
|
||||||
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
@@ -146,7 +206,7 @@ function chromecastPlayer () {
|
|||||||
|
|
||||||
// airplay player implementation
|
// airplay player implementation
|
||||||
function airplayPlayer () {
|
function airplayPlayer () {
|
||||||
var ret = {
|
const ret = {
|
||||||
device: null,
|
device: null,
|
||||||
addDevice,
|
addDevice,
|
||||||
getDevices,
|
getDevices,
|
||||||
@@ -183,7 +243,7 @@ function airplayPlayer () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
ret.device.play(state.server.networkURL, function (err, res) {
|
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -238,7 +298,7 @@ function airplayPlayer () {
|
|||||||
|
|
||||||
// DLNA player implementation
|
// DLNA player implementation
|
||||||
function dlnaPlayer (player) {
|
function dlnaPlayer (player) {
|
||||||
var ret = {
|
const ret = {
|
||||||
device: null,
|
device: null,
|
||||||
addDevice,
|
addDevice,
|
||||||
getDevices,
|
getDevices,
|
||||||
@@ -274,8 +334,8 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
var 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, {
|
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
|
||||||
@@ -306,13 +366,7 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function status () {
|
function status () {
|
||||||
ret.device.status(function (err, status) {
|
ret.device.status(handleStatus)
|
||||||
if (err) return console.log('error getting %s status: %o', state.playing.location, err)
|
|
||||||
state.playing.isPaused = status.playerState === 'PAUSED'
|
|
||||||
state.playing.currentTime = status.currentTime
|
|
||||||
state.playing.volume = status.volume.level
|
|
||||||
update()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek (time, callback) {
|
function seek (time, callback) {
|
||||||
@@ -328,10 +382,22 @@ function dlnaPlayer (player) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleStatus (err, status) {
|
||||||
|
if (err || !status) {
|
||||||
|
return console.log('error getting %s status: %o',
|
||||||
|
state.playing.location,
|
||||||
|
err || 'missing response')
|
||||||
|
}
|
||||||
|
state.playing.isPaused = status.playerState === 'PAUSED'
|
||||||
|
state.playing.currentTime = status.currentTime
|
||||||
|
state.playing.volume = status.volume.muted ? 0 : status.volume.level
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
// Start polling cast device state, whenever we're connected
|
// Start polling cast device state, whenever we're connected
|
||||||
function startStatusInterval () {
|
function startStatusInterval () {
|
||||||
statusInterval = setInterval(function () {
|
statusInterval = setInterval(function () {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.status()
|
if (player) player.status()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
@@ -350,23 +416,27 @@ function toggleMenu (location) {
|
|||||||
|
|
||||||
// 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 Error('You can\'t connect to ' + location + ' when already connected to another device')
|
throw new CastingError(
|
||||||
|
`You can't connect to ${location} when already connected to another device`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all cast devices of the given type
|
// Find all cast devices of the given type
|
||||||
var player = getPlayer(location)
|
const player = getPlayer(location)
|
||||||
var devices = player ? player.getDevices() : []
|
const devices = player ? player.getDevices() : []
|
||||||
if (devices.length === 0) throw new Error('No ' + location + ' devices available')
|
if (devices.length === 0) {
|
||||||
|
throw new CastingError(`No ${location} devices available`)
|
||||||
|
}
|
||||||
|
|
||||||
// Show a menu
|
// Show a menu
|
||||||
state.devices.castMenu = {location, devices}
|
state.devices.castMenu = { location, devices }
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectDevice (index) {
|
function selectDevice (index) {
|
||||||
var {location, devices} = state.devices.castMenu
|
const { location, devices } = state.devices.castMenu
|
||||||
|
|
||||||
// Start casting
|
// Start casting
|
||||||
var player = getPlayer(location)
|
const player = getPlayer(location)
|
||||||
player.device = devices[index]
|
player.device = devices[index]
|
||||||
player.open()
|
player.open()
|
||||||
|
|
||||||
@@ -382,7 +452,7 @@ function selectDevice (index) {
|
|||||||
|
|
||||||
// Stops casting, move video back to local screen
|
// Stops casting, move video back to local screen
|
||||||
function stop () {
|
function stop () {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) {
|
if (player) {
|
||||||
player.stop(function () {
|
player.stop(function () {
|
||||||
player.device = null
|
player.device = null
|
||||||
@@ -417,18 +487,18 @@ function getPlayer (location) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function play () {
|
function play () {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.play(castCallback)
|
if (player) player.play(castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause () {
|
function pause () {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.pause(castCallback)
|
if (player) player.pause(castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRate (rate) {
|
function setRate (rate) {
|
||||||
var player
|
let player
|
||||||
var result = true
|
let result = true
|
||||||
if (state.playing.location === 'chromecast') {
|
if (state.playing.location === 'chromecast') {
|
||||||
// TODO find how to control playback rate on chromecast
|
// TODO find how to control playback rate on chromecast
|
||||||
castCallback()
|
castCallback()
|
||||||
@@ -443,12 +513,12 @@ function setRate (rate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function seek (time) {
|
function seek (time) {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.seek(time, castCallback)
|
if (player) player.seek(time, castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVolume (volume) {
|
function setVolume (volume) {
|
||||||
var player = getPlayer()
|
const player = getPlayer()
|
||||||
if (player) player.volume(volume, castCallback)
|
if (player) player.volume(volume, castCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ module.exports = {
|
|||||||
setDispatch
|
setDispatch
|
||||||
}
|
}
|
||||||
|
|
||||||
var dispatchers = {}
|
const dispatchers = {}
|
||||||
var _dispatch = function () {}
|
let _dispatch = function () {}
|
||||||
|
|
||||||
function setDispatch (dispatch) {
|
function setDispatch (dispatch) {
|
||||||
_dispatch = dispatch
|
_dispatch = dispatch
|
||||||
@@ -20,8 +20,8 @@ function dispatch (...args) {
|
|||||||
// function. This prevents React from updating the listener functions on
|
// function. This prevents React from updating the listener functions on
|
||||||
// each update().
|
// each update().
|
||||||
function dispatcher (...args) {
|
function dispatcher (...args) {
|
||||||
var str = JSON.stringify(args)
|
const str = JSON.stringify(args)
|
||||||
var handler = dispatchers[str]
|
let handler = dispatchers[str]
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
handler = dispatchers[str] = function (e) {
|
handler = dispatchers[str] = function (e) {
|
||||||
// Do not propagate click to elements below the button
|
// Do not propagate click to elements below the button
|
||||||
|
|||||||
@@ -1,8 +1,41 @@
|
|||||||
module.exports = {
|
const ExtendableError = require('es6-error')
|
||||||
UnplayableError
|
|
||||||
|
/* Generic errors */
|
||||||
|
|
||||||
|
class CastingError extends ExtendableError {}
|
||||||
|
class PlaybackError extends ExtendableError {}
|
||||||
|
class SoundError extends ExtendableError {}
|
||||||
|
class TorrentError extends ExtendableError {}
|
||||||
|
|
||||||
|
/* Playback */
|
||||||
|
|
||||||
|
class UnplayableTorrentError extends PlaybackError {
|
||||||
|
constructor () { super('Can\'t play any files in torrent') }
|
||||||
}
|
}
|
||||||
|
|
||||||
function UnplayableError () {
|
class UnplayableFileError extends PlaybackError {
|
||||||
this.message = 'Can\'t play any files in torrent'
|
constructor () { super('Can\'t play that file') }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sound */
|
||||||
|
|
||||||
|
class InvalidSoundNameError extends SoundError {
|
||||||
|
constructor (name) { super(`Invalid sound name: ${name}`) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Torrent */
|
||||||
|
|
||||||
|
class TorrentKeyNotFoundError extends TorrentError {
|
||||||
|
constructor (torrentKey) { super(`Can't resolve torrent key ${torrentKey}`) }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CastingError,
|
||||||
|
PlaybackError,
|
||||||
|
SoundError,
|
||||||
|
TorrentError,
|
||||||
|
UnplayableTorrentError,
|
||||||
|
UnplayableFileError,
|
||||||
|
InvalidSoundNameError,
|
||||||
|
TorrentKeyNotFoundError
|
||||||
}
|
}
|
||||||
UnplayableError.prototype = Error
|
|
||||||
|
|||||||
12
src/renderer/lib/media-extensions.js
Normal file
12
src/renderer/lib/media-extensions.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const mediaExtensions = {
|
||||||
|
audio: [
|
||||||
|
'.aac', '.aif', '.aiff', '.asf', '.dff', '.dsf', '.flac', '.m2a',
|
||||||
|
'.m4a', '.m4b', '.mp2', '.mp3', '.mpc', '.oga', '.ogg', '.opus',
|
||||||
|
'.spx', '.wma', '.wav', '.wv', '.wvp'],
|
||||||
|
video: [
|
||||||
|
'.avi', '.mp4', '.m4v', '.webm', '.mov', '.mkv', '.mpg', '.mpeg',
|
||||||
|
'.ogv', '.webm', '.wmv'],
|
||||||
|
image: ['.gif', '.jpg', '.jpeg', '.png']
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = mediaExtensions
|
||||||
@@ -4,10 +4,11 @@ module.exports = {
|
|||||||
run
|
run
|
||||||
}
|
}
|
||||||
|
|
||||||
const semver = require('semver')
|
|
||||||
const config = require('../../config')
|
|
||||||
const TorrentSummary = require('./torrent-summary')
|
|
||||||
const fs = require('fs')
|
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
|
||||||
@@ -17,34 +18,28 @@ function run (state) {
|
|||||||
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
|
state.saved.version = '0.0.0' // Pre-0.7.0 version, so run all migrations
|
||||||
}
|
}
|
||||||
|
|
||||||
var version = state.saved.version
|
const version = state.saved.version
|
||||||
|
const saved = state.saved
|
||||||
|
|
||||||
if (semver.lt(version, '0.7.0')) {
|
if (semver.lt(version, '0.7.0')) migrate_0_7_0(saved)
|
||||||
migrate_0_7_0(state.saved)
|
if (semver.lt(version, '0.7.2')) migrate_0_7_2(saved)
|
||||||
}
|
if (semver.lt(version, '0.11.0')) migrate_0_11_0(saved)
|
||||||
|
if (semver.lt(version, '0.12.0')) migrate_0_12_0(saved)
|
||||||
if (semver.lt(version, '0.7.2')) {
|
if (semver.lt(version, '0.14.0')) migrate_0_14_0(saved)
|
||||||
migrate_0_7_2(state.saved)
|
if (semver.lt(version, '0.17.0')) migrate_0_17_0(saved)
|
||||||
}
|
if (semver.lt(version, '0.17.2')) migrate_0_17_2(saved)
|
||||||
|
if (semver.lt(version, '0.21.0')) migrate_0_21_0(saved)
|
||||||
if (semver.lt(version, '0.11.0')) {
|
|
||||||
migrate_0_11_0(state.saved)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (semver.lt(version, '0.12.0')) {
|
|
||||||
migrate_0_12_0(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) {
|
function migrate_0_7_0 (saved) {
|
||||||
var fs = require('fs-extra')
|
const cpFile = require('cp-file')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
saved.torrents.forEach(function (ts) {
|
saved.torrents.forEach(function (ts) {
|
||||||
var infoHash = ts.infoHash
|
const infoHash = ts.infoHash
|
||||||
|
|
||||||
// Replace torrentPath with torrentFileName
|
// Replace torrentPath with torrentFileName
|
||||||
// There are a number of cases to handle here:
|
// There are a number of cases to handle here:
|
||||||
@@ -52,7 +47,7 @@ function migrate_0_7_0 (saved) {
|
|||||||
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
// * Then, relative paths for the default torrents, eg '../static/sintel.torrent'
|
||||||
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
// * Then, paths computed at runtime for default torrents, eg 'sintel.torrent'
|
||||||
// * Finally, now we're getting rid of torrentPath altogether
|
// * Finally, now we're getting rid of torrentPath altogether
|
||||||
var src, dst
|
let src, dst
|
||||||
if (ts.torrentPath) {
|
if (ts.torrentPath) {
|
||||||
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
if (path.isAbsolute(ts.torrentPath) || ts.torrentPath.startsWith('..')) {
|
||||||
src = ts.torrentPath
|
src = ts.torrentPath
|
||||||
@@ -62,7 +57,7 @@ function migrate_0_7_0 (saved) {
|
|||||||
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
dst = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
// that only runs once
|
// that only runs once
|
||||||
if (src !== dst) fs.copySync(src, dst)
|
if (src !== dst) cpFile.sync(src, dst)
|
||||||
|
|
||||||
delete ts.torrentPath
|
delete ts.torrentPath
|
||||||
ts.torrentFileName = infoHash + '.torrent'
|
ts.torrentFileName = infoHash + '.torrent'
|
||||||
@@ -70,21 +65,21 @@ function migrate_0_7_0 (saved) {
|
|||||||
|
|
||||||
// Replace posterURL with posterFileName
|
// Replace posterURL with posterFileName
|
||||||
if (ts.posterURL) {
|
if (ts.posterURL) {
|
||||||
var extension = path.extname(ts.posterURL)
|
const extension = path.extname(ts.posterURL)
|
||||||
src = path.isAbsolute(ts.posterURL)
|
src = path.isAbsolute(ts.posterURL)
|
||||||
? ts.posterURL
|
? ts.posterURL
|
||||||
: path.join(config.STATIC_PATH, ts.posterURL)
|
: path.join(config.STATIC_PATH, ts.posterURL)
|
||||||
dst = path.join(config.POSTER_PATH, infoHash + extension)
|
dst = path.join(config.POSTER_PATH, infoHash + extension)
|
||||||
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
// Synchronous FS calls aren't ideal, but probably OK in a migration
|
||||||
// that only runs once
|
// that only runs once
|
||||||
if (src !== dst) fs.copySync(src, dst)
|
if (src !== dst) cpFile.sync(src, dst)
|
||||||
|
|
||||||
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/feross/webtorrent-desktop/pull/604#issuecomment-222805214
|
// https://github.com/webtorrent/webtorrent-desktop/pull/604#issuecomment-222805214
|
||||||
delete ts.defaultPlayFileIndex
|
delete ts.defaultPlayFileIndex
|
||||||
delete ts.files
|
delete ts.files
|
||||||
delete ts.selections
|
delete ts.selections
|
||||||
@@ -108,6 +103,8 @@ function migrate_0_11_0 (saved) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function migrate_0_12_0 (saved) {
|
function migrate_0_12_0 (saved) {
|
||||||
|
const TorrentSummary = require('./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
|
||||||
}
|
}
|
||||||
@@ -115,8 +112,8 @@ function migrate_0_12_0 (saved) {
|
|||||||
|
|
||||||
// Undo a terrible bug where clicking Play on a default torrent on a fresh
|
// Undo a terrible bug where clicking Play on a default torrent on a fresh
|
||||||
// install results in a "path missing" error
|
// install results in a "path missing" error
|
||||||
// See https://github.com/feross/webtorrent-desktop/pull/806
|
// See https://github.com/webtorrent/webtorrent-desktop/pull/806
|
||||||
var defaultTorrentFiles = [
|
const defaultTorrentFiles = [
|
||||||
'6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent',
|
'6a9759bffd5c0af65319979fb7832189f4f3c35d.torrent',
|
||||||
'88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent',
|
'88594aaacbde40ef3e2510c47374ec0aa396c08e.torrent',
|
||||||
'6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent',
|
'6a02592d2bbc069628cd5ed8a54f88ee06ac0ba5.torrent',
|
||||||
@@ -125,13 +122,95 @@ function migrate_0_12_0 (saved) {
|
|||||||
]
|
]
|
||||||
saved.torrents.forEach(function (torrentSummary) {
|
saved.torrents.forEach(function (torrentSummary) {
|
||||||
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
|
if (!defaultTorrentFiles.includes(torrentSummary.torrentFileName)) return
|
||||||
var fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
|
const fileOrFolder = TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
if (!fileOrFolder) return
|
if (!fileOrFolder) return
|
||||||
try {
|
try {
|
||||||
fs.statSync(fileOrFolder)
|
fs.statSync(fileOrFolder)
|
||||||
} catch (e) {
|
} catch (err) {
|
||||||
// Default torrent with "missing path" error. Clear path.
|
// Default torrent with "missing path" error. Clear path.
|
||||||
delete torrentSummary.path
|
delete torrentSummary.path
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrate_0_14_0 (saved) {
|
||||||
|
saved.torrents.forEach(function (ts) {
|
||||||
|
delete ts.defaultPlayFileIndex
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrate_0_17_0 (saved) {
|
||||||
|
// Fix a sad, sad bug that resulted in 100MB+ config.json files
|
||||||
|
saved.torrents.forEach(function (ts) {
|
||||||
|
if (!ts.files) return
|
||||||
|
ts.files.forEach(function (file) {
|
||||||
|
if (!file.audioInfo || !file.audioInfo.picture) return
|
||||||
|
// This contained a Buffer, which 30x'd in size when serialized to JSON
|
||||||
|
delete file.audioInfo.picture
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrate_0_17_2 (saved) {
|
||||||
|
// 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
|
||||||
|
// Windows Explorer. See: https://github.com/webtorrent/webtorrent-desktop/issues/905
|
||||||
|
|
||||||
|
const cpFile = require('cp-file')
|
||||||
|
const rimraf = require('rimraf')
|
||||||
|
|
||||||
|
const OLD_NAME = 'The WIRED CD - Rip. Sample. Mash. Share.'
|
||||||
|
const NEW_NAME = 'The WIRED CD - Rip. Sample. Mash. Share'
|
||||||
|
|
||||||
|
const OLD_HASH = '3ba219a8634bf7bae3d848192b2da75ae995589d'
|
||||||
|
const NEW_HASH = 'a88fda5954e89178c372716a6a78b8180ed4dad3'
|
||||||
|
|
||||||
|
const ts = saved.torrents.find((ts) => {
|
||||||
|
return ts.infoHash === OLD_HASH
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!ts) return // Wired CD torrent does not exist
|
||||||
|
|
||||||
|
// New versions of WebTorrent ship with a fixed torrent file. Let's fix up the
|
||||||
|
// name in existing versions of WebTorrent.
|
||||||
|
ts.name = ts.displayName = NEW_NAME
|
||||||
|
ts.files.forEach((file) => {
|
||||||
|
file.path = file.path.replace(OLD_NAME, NEW_NAME)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Changing the torrent name causes the info hash to change
|
||||||
|
ts.infoHash = NEW_HASH
|
||||||
|
ts.magnetURI = ts.magnetURI.replace(OLD_HASH, NEW_HASH)
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.renameSync(
|
||||||
|
path.join(config.POSTER_PATH, ts.posterFileName),
|
||||||
|
path.join(config.POSTER_PATH, NEW_HASH + '.jpg')
|
||||||
|
)
|
||||||
|
} catch (err) {}
|
||||||
|
ts.posterFileName = NEW_HASH + '.jpg'
|
||||||
|
|
||||||
|
rimraf.sync(path.join(config.TORRENT_PATH, ts.torrentFileName))
|
||||||
|
cpFile.sync(
|
||||||
|
path.join(config.STATIC_PATH, 'wiredCd.torrent'),
|
||||||
|
path.join(config.TORRENT_PATH, NEW_HASH + '.torrent')
|
||||||
|
)
|
||||||
|
ts.torrentFileName = NEW_HASH + '.torrent'
|
||||||
|
|
||||||
|
if (ts.path) {
|
||||||
|
// If torrent folder already exists on disk, try to rename it
|
||||||
|
try {
|
||||||
|
fs.renameSync(
|
||||||
|
path.join(ts.path, OLD_NAME),
|
||||||
|
path.join(ts.path, NEW_NAME)
|
||||||
|
)
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrate_0_21_0 (saved) {
|
||||||
|
if (saved.prefs.soundNotifications == null) {
|
||||||
|
// The app used to always have sound notifications enabled
|
||||||
|
saved.prefs.soundNotifications = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
88
src/renderer/lib/playlist.js
Normal file
88
src/renderer/lib/playlist.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
module.exports = {
|
||||||
|
hasNext,
|
||||||
|
getNextIndex,
|
||||||
|
hasPrevious,
|
||||||
|
getPreviousIndex,
|
||||||
|
getCurrentLocalURL
|
||||||
|
}
|
||||||
|
|
||||||
|
const TorrentSummary = require('./torrent-summary')
|
||||||
|
const TorrentPlayer = require('./torrent-player')
|
||||||
|
|
||||||
|
const cache = {
|
||||||
|
infoHash: null,
|
||||||
|
previousIndex: null,
|
||||||
|
currentIndex: null,
|
||||||
|
nextIndex: null
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNext (state) {
|
||||||
|
updateCache(state)
|
||||||
|
return cache.nextIndex !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextIndex (state) {
|
||||||
|
updateCache(state)
|
||||||
|
return cache.nextIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPrevious (state) {
|
||||||
|
updateCache(state)
|
||||||
|
return cache.previousIndex !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviousIndex (state) {
|
||||||
|
updateCache(state)
|
||||||
|
return cache.previousIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentLocalURL (state) {
|
||||||
|
return state.server
|
||||||
|
? state.server.localURL + '/' + state.playing.fileIndex + '/' +
|
||||||
|
encodeURIComponent(state.playing.fileName)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCache (state) {
|
||||||
|
const infoHash = state.playing.infoHash
|
||||||
|
const fileIndex = state.playing.fileIndex
|
||||||
|
|
||||||
|
if (infoHash === cache.infoHash) {
|
||||||
|
switch (fileIndex) {
|
||||||
|
case cache.currentIndex:
|
||||||
|
return
|
||||||
|
case cache.nextIndex:
|
||||||
|
cache.previousIndex = cache.currentIndex
|
||||||
|
cache.currentIndex = fileIndex
|
||||||
|
cache.nextIndex = findNextIndex(state)
|
||||||
|
return
|
||||||
|
case cache.previousIndex:
|
||||||
|
cache.previousIndex = findPreviousIndex(state)
|
||||||
|
cache.nextIndex = cache.currentIndex
|
||||||
|
cache.currentIndex = fileIndex
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache.infoHash = infoHash
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.previousIndex = findPreviousIndex(state)
|
||||||
|
cache.currentIndex = fileIndex
|
||||||
|
cache.nextIndex = findNextIndex(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPreviousIndex (state) {
|
||||||
|
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
||||||
|
for (let i = state.playing.fileIndex - 1; i >= 0; i--) {
|
||||||
|
if (TorrentPlayer.isPlayable(files[i])) return i
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNextIndex (state) {
|
||||||
|
const files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
||||||
|
for (let i = state.playing.fileIndex + 1; i < files.length; i++) {
|
||||||
|
if (TorrentPlayer.isPlayable(files[i])) return i
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,28 +1,32 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
preload,
|
init,
|
||||||
play
|
play
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var path = require('path')
|
const { InvalidSoundNameError } = require('./errors')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
var VOLUME = 0.15
|
const VOLUME = 0.25
|
||||||
|
|
||||||
|
// App state to access the soundNotifications preference
|
||||||
|
let state
|
||||||
|
|
||||||
/* Cache of Audio elements, for instant playback */
|
/* Cache of Audio elements, for instant playback */
|
||||||
var cache = {}
|
const cache = {}
|
||||||
|
|
||||||
var 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'),
|
||||||
volume: VOLUME
|
volume: VOLUME
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'delete.wav'),
|
||||||
volume: VOLUME
|
volume: VOLUME * 0.5
|
||||||
},
|
},
|
||||||
DISABLE: {
|
DISABLE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'disable.wav'),
|
||||||
volume: VOLUME
|
volume: VOLUME * 0.5
|
||||||
},
|
},
|
||||||
DONE: {
|
DONE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'done.wav'),
|
||||||
@@ -30,7 +34,7 @@ var sounds = {
|
|||||||
},
|
},
|
||||||
ENABLE: {
|
ENABLE: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'enable.wav'),
|
||||||
volume: VOLUME
|
volume: VOLUME * 0.5
|
||||||
},
|
},
|
||||||
ERROR: {
|
ERROR: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'error.wav'),
|
||||||
@@ -42,27 +46,28 @@ var sounds = {
|
|||||||
},
|
},
|
||||||
STARTUP: {
|
STARTUP: {
|
||||||
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
url: 'file://' + path.join(config.STATIC_PATH, 'sound', 'startup.wav'),
|
||||||
volume: VOLUME * 2
|
volume: VOLUME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function preload () {
|
function init (appState) {
|
||||||
for (var name in sounds) {
|
state = appState
|
||||||
if (!cache[name]) {
|
|
||||||
var sound = sounds[name]
|
|
||||||
var audio = cache[name] = new window.Audio()
|
|
||||||
audio.volume = sound.volume
|
|
||||||
audio.src = sound.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function play (name) {
|
function play (name) {
|
||||||
var audio = cache[name]
|
if (state == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.saved.prefs.soundNotifications) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let audio = cache[name]
|
||||||
if (!audio) {
|
if (!audio) {
|
||||||
var sound = sounds[name]
|
const sound = sounds[name]
|
||||||
if (!sound) {
|
if (!sound) {
|
||||||
throw new Error('Invalid sound name')
|
throw new InvalidSoundNameError(name)
|
||||||
}
|
}
|
||||||
audio = cache[name] = new window.Audio()
|
audio = cache[name] = new window.Audio()
|
||||||
audio.volume = sound.volume
|
audio.volume = sound.volume
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
var appConfig = require('application-config')('WebTorrent')
|
const appConfig = require('application-config')('WebTorrent')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var {EventEmitter} = require('events')
|
const { EventEmitter } = require('events')
|
||||||
|
|
||||||
var config = require('../../config')
|
const config = require('../../config')
|
||||||
var migrations = require('./migrations')
|
|
||||||
|
|
||||||
var State = module.exports = Object.assign(new EventEmitter(), {
|
const SAVE_DEBOUNCE_INTERVAL = 1000
|
||||||
getDefaultPlayState,
|
|
||||||
load,
|
|
||||||
save,
|
|
||||||
saveThrottled
|
|
||||||
})
|
|
||||||
|
|
||||||
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(), {
|
||||||
|
getDefaultPlayState,
|
||||||
|
load,
|
||||||
|
// state.save() calls are rate-limited. Use state.saveImmediate() to skip limit.
|
||||||
|
save: function () {
|
||||||
|
// Perf optimization: Lazy-require debounce (and it's dependencies)
|
||||||
|
const debounce = require('debounce')
|
||||||
|
// After first State.save() invokation, future calls go straight to the
|
||||||
|
// debounced function
|
||||||
|
State.save = debounce(saveImmediate, SAVE_DEBOUNCE_INTERVAL)
|
||||||
|
State.save(...arguments)
|
||||||
|
},
|
||||||
|
saveImmediate
|
||||||
|
})
|
||||||
|
|
||||||
function getDefaultState () {
|
function getDefaultState () {
|
||||||
var LocationHistory = require('location-history')
|
const LocationHistory = require('location-history')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/*
|
/*
|
||||||
@@ -24,7 +33,11 @@ function getDefaultState () {
|
|||||||
*/
|
*/
|
||||||
client: null, /* the WebTorrent client */
|
client: null, /* the WebTorrent client */
|
||||||
server: null, /* local WebTorrent-to-HTTP server */
|
server: null, /* local WebTorrent-to-HTTP server */
|
||||||
prev: {}, /* used for state diffing in updateElectron() */
|
prev: { /* used for state diffing in updateElectron() */
|
||||||
|
title: null,
|
||||||
|
progress: -1,
|
||||||
|
badge: null
|
||||||
|
},
|
||||||
location: new LocationHistory(),
|
location: new LocationHistory(),
|
||||||
window: {
|
window: {
|
||||||
bounds: null, /* {x, y, width, height } */
|
bounds: null, /* {x, y, width, height } */
|
||||||
@@ -64,7 +77,9 @@ function getDefaultState () {
|
|||||||
* Getters, for convenience
|
* Getters, for convenience
|
||||||
*/
|
*/
|
||||||
getPlayingTorrentSummary,
|
getPlayingTorrentSummary,
|
||||||
getPlayingFileSummary
|
getPlayingFileSummary,
|
||||||
|
getExternalPlayerName,
|
||||||
|
shouldHidePlayerControls
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,10 +88,12 @@ function getDefaultPlayState () {
|
|||||||
return {
|
return {
|
||||||
infoHash: null, /* the info hash of the torrent we're playing */
|
infoHash: null, /* the info hash of the torrent we're playing */
|
||||||
fileIndex: null, /* the zero-based index within the torrent */
|
fileIndex: null, /* the zero-based index within the torrent */
|
||||||
|
fileName: null, /* name of the file that is playing */
|
||||||
location: 'local', /* 'local', 'chromecast', 'airplay' */
|
location: 'local', /* 'local', 'chromecast', 'airplay' */
|
||||||
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
|
type: null, /* 'audio' or 'video', could be 'other' if ever support eg streaming to VLC */
|
||||||
currentTime: 0, /* seconds */
|
currentTime: 0, /* seconds */
|
||||||
duration: 1, /* seconds */
|
duration: 1, /* seconds */
|
||||||
|
isReady: false,
|
||||||
isPaused: true,
|
isPaused: true,
|
||||||
isStalled: false,
|
isStalled: false,
|
||||||
lastTimeUpdate: 0, /* Unix time in ms */
|
lastTimeUpdate: 0, /* Unix time in ms */
|
||||||
@@ -93,50 +110,54 @@ 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 setupSavedState (cb) {
|
function setupStateSaved (cb) {
|
||||||
var fs = require('fs-extra')
|
const cpFile = require('cp-file')
|
||||||
var parseTorrent = require('parse-torrent')
|
const fs = require('fs')
|
||||||
var parallel = require('run-parallel')
|
const parseTorrent = require('parse-torrent')
|
||||||
|
|
||||||
var saved = {
|
const saved = {
|
||||||
prefs: {
|
prefs: {
|
||||||
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
downloadPath: config.DEFAULT_DOWNLOAD_PATH,
|
||||||
isFileHandler: false,
|
isFileHandler: false,
|
||||||
openExternalPlayer: false,
|
openExternalPlayer: false,
|
||||||
externalPlayerPath: null
|
externalPlayerPath: null,
|
||||||
|
startup: false,
|
||||||
|
soundNotifications: true,
|
||||||
|
autoAddTorrents: false,
|
||||||
|
torrentsFolderPath: '',
|
||||||
|
highestPlaybackPriority: true
|
||||||
},
|
},
|
||||||
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
torrents: config.DEFAULT_TORRENTS.map(createTorrentObject),
|
||||||
|
torrentsToResume: [],
|
||||||
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
version: config.APP_VERSION /* make sure we can upgrade gracefully later */
|
||||||
}
|
}
|
||||||
|
|
||||||
var tasks = []
|
const tasks = []
|
||||||
|
|
||||||
config.DEFAULT_TORRENTS.map(function (t, i) {
|
config.DEFAULT_TORRENTS.forEach((t, i) => {
|
||||||
var infoHash = saved.torrents[i].infoHash
|
const infoHash = saved.torrents[i].infoHash
|
||||||
tasks.push(function (cb) {
|
tasks.push(
|
||||||
fs.copy(
|
cpFile(
|
||||||
path.join(config.STATIC_PATH, t.posterFileName),
|
path.join(config.STATIC_PATH, t.posterFileName),
|
||||||
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName)),
|
path.join(config.POSTER_PATH, infoHash + path.extname(t.posterFileName))
|
||||||
cb
|
|
||||||
)
|
)
|
||||||
})
|
)
|
||||||
tasks.push(function (cb) {
|
tasks.push(
|
||||||
fs.copy(
|
cpFile(
|
||||||
path.join(config.STATIC_PATH, t.torrentFileName),
|
path.join(config.STATIC_PATH, t.torrentFileName),
|
||||||
path.join(config.TORRENT_PATH, infoHash + '.torrent'),
|
path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
||||||
cb
|
|
||||||
)
|
)
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
parallel(tasks, function (err) {
|
Promise.all(tasks)
|
||||||
if (err) return cb(err)
|
.then(() => cb(null, saved))
|
||||||
cb(null, saved)
|
.catch(err => cb(err))
|
||||||
})
|
|
||||||
|
|
||||||
function createTorrentObject (t) {
|
function createTorrentObject (t) {
|
||||||
var torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
// TODO: Doing several fs.readFileSync calls during first startup is not ideal
|
||||||
var parsedTorrent = parseTorrent(torrent)
|
const torrent = fs.readFileSync(path.join(config.STATIC_PATH, t.torrentFileName))
|
||||||
|
const parsedTorrent = parseTorrent(torrent)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 'paused',
|
status: 'paused',
|
||||||
@@ -147,62 +168,80 @@ function setupSavedState (cb) {
|
|||||||
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
torrentFileName: parsedTorrent.infoHash + '.torrent',
|
||||||
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
magnetURI: parseTorrent.toMagnetURI(parsedTorrent),
|
||||||
files: parsedTorrent.files,
|
files: parsedTorrent.files,
|
||||||
selections: parsedTorrent.files.map((x) => true)
|
selections: parsedTorrent.files.map((x) => true),
|
||||||
|
testID: t.testID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayingTorrentSummary () {
|
function getPlayingTorrentSummary () {
|
||||||
var infoHash = this.playing.infoHash
|
const infoHash = this.playing.infoHash
|
||||||
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
return this.saved.torrents.find((x) => x.infoHash === infoHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlayingFileSummary () {
|
function getPlayingFileSummary () {
|
||||||
var torrentSummary = this.getPlayingTorrentSummary()
|
const torrentSummary = this.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary) return null
|
if (!torrentSummary) return null
|
||||||
return torrentSummary.files[this.playing.fileIndex]
|
return torrentSummary.files[this.playing.fileIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
function load (cb) {
|
function getExternalPlayerName () {
|
||||||
var state = getDefaultState()
|
const playerPath = this.saved.prefs.externalPlayerPath
|
||||||
|
if (!playerPath) return 'VLC'
|
||||||
|
return path.basename(playerPath).split('.')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldHidePlayerControls () {
|
||||||
|
return this.location.url() === 'player' &&
|
||||||
|
this.playing.mouseStationarySince !== 0 &&
|
||||||
|
new Date().getTime() - this.playing.mouseStationarySince > 2000 &&
|
||||||
|
!this.playing.mouseInControls &&
|
||||||
|
!this.playing.isPaused &&
|
||||||
|
this.playing.location === 'local' &&
|
||||||
|
this.playing.playbackRate === 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function load (cb) {
|
||||||
appConfig.read(function (err, saved) {
|
appConfig.read(function (err, saved) {
|
||||||
if (err || !saved.version) {
|
if (err || !saved.version) {
|
||||||
console.log('Missing config file: Creating new one')
|
console.log('Missing config file: Creating new one')
|
||||||
setupSavedState(onSaved)
|
setupStateSaved(onSavedState)
|
||||||
} else {
|
} else {
|
||||||
onSaved(null, saved)
|
onSavedState(null, saved)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function onSaved (err, saved) {
|
function onSavedState (err, saved) {
|
||||||
if (err) return cb(err)
|
if (err) return cb(err)
|
||||||
|
const state = getDefaultState()
|
||||||
state.saved = saved
|
state.saved = saved
|
||||||
migrations.run(state)
|
|
||||||
|
if (process.type === 'renderer') {
|
||||||
|
// Perf optimization: Save require() calls in the main process
|
||||||
|
const migrations = require('./migrations')
|
||||||
|
migrations.run(state)
|
||||||
|
}
|
||||||
|
|
||||||
cb(null, state)
|
cb(null, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write state.saved to the JSON state file
|
// Write state.saved to the JSON state file
|
||||||
function save (state, cb) {
|
function saveImmediate (state, cb) {
|
||||||
console.log('Saving state to ' + appConfig.filePath)
|
console.log('Saving state to ' + appConfig.filePath)
|
||||||
delete state.saveStateTimeout
|
|
||||||
|
|
||||||
// Clean up, so that we're not saving any pending state
|
// Clean up, so that we're not saving any pending state
|
||||||
var copy = Object.assign({}, state.saved)
|
const copy = Object.assign({}, state.saved)
|
||||||
// Remove torrents pending addition to the list, where we haven't finished
|
// Remove torrents pending addition to the list, where we haven't finished
|
||||||
// reading the torrent file or file(s) to seed & don't have an infohash
|
// 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(function (x) {
|
||||||
var torrent = {}
|
const torrent = {}
|
||||||
for (var key in x) {
|
for (const key in x) {
|
||||||
if (key === 'progress' || key === 'torrentKey') {
|
if (key === 'progress' || key === 'torrentKey') {
|
||||||
continue // Don't save progress info or key for the webtorrent process
|
continue // Don't save progress info or key for the webtorrent process
|
||||||
}
|
}
|
||||||
if (key === 'playStatus') {
|
|
||||||
continue // Don't save whether a torrent is playing / pending
|
|
||||||
}
|
|
||||||
if (key === 'error') {
|
if (key === 'error') {
|
||||||
continue // Don't save error states
|
continue // Don't save error states
|
||||||
}
|
}
|
||||||
@@ -213,15 +252,6 @@ function save (state, cb) {
|
|||||||
|
|
||||||
appConfig.write(copy, (err) => {
|
appConfig.write(copy, (err) => {
|
||||||
if (err) console.error(err)
|
if (err) console.error(err)
|
||||||
else State.emit('savedState')
|
else State.emit('stateSaved')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write, but no more than once a second
|
|
||||||
function saveThrottled (state) {
|
|
||||||
if (state.saveStateTimeout) return
|
|
||||||
state.saveStateTimeout = setTimeout(function () {
|
|
||||||
if (!state.saveStateTimeout) return
|
|
||||||
save(state)
|
|
||||||
}, 1000)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,95 +2,79 @@
|
|||||||
// Reports back so that we can improve WebTorrent Desktop
|
// Reports back so that we can improve WebTorrent Desktop
|
||||||
module.exports = {
|
module.exports = {
|
||||||
init,
|
init,
|
||||||
|
send,
|
||||||
logUncaughtError,
|
logUncaughtError,
|
||||||
logPlayAttempt
|
logPlayAttempt
|
||||||
}
|
}
|
||||||
|
|
||||||
const crypto = require('crypto')
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const https = require('https')
|
|
||||||
const os = require('os')
|
|
||||||
const url = require('url')
|
|
||||||
|
|
||||||
const config = require('../../config')
|
const config = require('../../config')
|
||||||
|
|
||||||
var telemetry
|
let telemetry
|
||||||
|
|
||||||
function init (state) {
|
function init (state) {
|
||||||
telemetry = state.saved.telemetry
|
telemetry = state.saved.telemetry
|
||||||
|
|
||||||
|
// First app run
|
||||||
if (!telemetry) {
|
if (!telemetry) {
|
||||||
telemetry = state.saved.telemetry = createSummary()
|
const crypto = require('crypto')
|
||||||
|
telemetry = state.saved.telemetry = {
|
||||||
|
userID: crypto.randomBytes(32).toString('hex') // 256-bit random ID
|
||||||
|
}
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var now = new Date()
|
function send (state) {
|
||||||
|
const now = new Date()
|
||||||
telemetry.version = config.APP_VERSION
|
telemetry.version = config.APP_VERSION
|
||||||
telemetry.timestamp = now.toISOString()
|
telemetry.timestamp = now.toISOString()
|
||||||
telemetry.localTime = now.toTimeString()
|
telemetry.localTime = now.toTimeString()
|
||||||
telemetry.screens = getScreenInfo()
|
telemetry.screens = getScreenInfo()
|
||||||
telemetry.system = getSystemInfo()
|
telemetry.system = getSystemInfo()
|
||||||
telemetry.approxNumTorrents = getApproxNumTorrents(state)
|
telemetry.torrentStats = getTorrentStats(state)
|
||||||
|
telemetry.approxNumTorrents = telemetry.torrentStats.approxCount
|
||||||
|
|
||||||
if (config.IS_PRODUCTION) {
|
if (!config.IS_PRODUCTION) {
|
||||||
postToServer()
|
|
||||||
// If the user keeps WebTorrent running for a long time, post every 24h
|
|
||||||
setInterval(postToServer, 12 * 3600 * 1000)
|
|
||||||
} else {
|
|
||||||
// Development: telemetry used only for local debugging
|
// Development: telemetry used only for local debugging
|
||||||
// Empty uncaught errors, etc at the start of every run
|
// Empty uncaught errors, etc at the start of every run
|
||||||
reset()
|
return reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const get = require('simple-get')
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
url: config.TELEMETRY_URL,
|
||||||
|
body: telemetry,
|
||||||
|
json: true
|
||||||
|
}
|
||||||
|
|
||||||
|
get.post(opts, function (err, res) {
|
||||||
|
if (err) return console.error('Error sending telemetry', err)
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
return console.error(`Error sending telemetry, status code: ${res.statusCode}`)
|
||||||
|
}
|
||||||
|
console.log('Sent telemetry')
|
||||||
|
reset()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset () {
|
function reset () {
|
||||||
telemetry.uncaughtErrors = []
|
telemetry.uncaughtErrors = []
|
||||||
telemetry.playAttempts = {
|
telemetry.playAttempts = {
|
||||||
|
minVersion: config.APP_VERSION,
|
||||||
total: 0,
|
total: 0,
|
||||||
success: 0,
|
success: 0,
|
||||||
timeout: 0,
|
|
||||||
error: 0,
|
error: 0,
|
||||||
|
external: 0,
|
||||||
abandoned: 0
|
abandoned: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function postToServer () {
|
|
||||||
// Serialize the telemetry summary
|
|
||||||
var payload = new Buffer(JSON.stringify(telemetry), 'utf8')
|
|
||||||
|
|
||||||
// POST to our server
|
|
||||||
var options = url.parse(config.TELEMETRY_URL)
|
|
||||||
options.method = 'POST'
|
|
||||||
options.headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Length': payload.length
|
|
||||||
}
|
|
||||||
|
|
||||||
var req = https.request(options, function (res) {
|
|
||||||
if (res.statusCode === 200) {
|
|
||||||
console.log('Successfully posted telemetry summary')
|
|
||||||
reset()
|
|
||||||
} else {
|
|
||||||
console.error('Couldn\'t post telemetry summary, got HTTP ' + res.statusCode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
req.on('error', function (e) {
|
|
||||||
console.error('Couldn\'t post telemetry summary', e)
|
|
||||||
})
|
|
||||||
req.write(payload)
|
|
||||||
req.end()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a new telemetry summary. Gives the user a unique ID,
|
|
||||||
// collects screen resolution, etc
|
|
||||||
function createSummary () {
|
|
||||||
// Make a 256-bit random unique ID
|
|
||||||
var userID = crypto.randomBytes(32).toString('hex')
|
|
||||||
return { userID }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track screen resolution
|
// Track screen resolution
|
||||||
function getScreenInfo () {
|
function getScreenInfo () {
|
||||||
return electron.screen.getAllDisplays().map((screen) => ({
|
return electron.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
|
||||||
@@ -99,58 +83,141 @@ 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 () {
|
function getSystemInfo () {
|
||||||
|
const os = require('os')
|
||||||
return {
|
return {
|
||||||
osPlatform: process.platform,
|
osPlatform: process.platform,
|
||||||
osRelease: os.type() + ' ' + os.release(),
|
osRelease: os.type() + ' ' + os.release(),
|
||||||
architecture: os.arch(),
|
architecture: os.arch(),
|
||||||
totalMemoryMB: os.totalmem() / (1 << 20),
|
systemArchitecture: config.OS_SYSARCH,
|
||||||
|
totalMemoryMB: roundPow2(os.totalmem() / (1 << 20)),
|
||||||
numCores: os.cpus().length
|
numCores: os.cpus().length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the number of torrents, rounded to the nearest power of two
|
// Get stats like the # of torrents currently active, # in list, total size
|
||||||
function getApproxNumTorrents (state) {
|
function getTorrentStats (state) {
|
||||||
var exactNum = state.saved.torrents.length
|
const count = state.saved.torrents.length
|
||||||
if (exactNum === 0) return 0
|
let sizeMB = 0
|
||||||
|
const byStatus = {
|
||||||
|
new: { count: 0, sizeMB: 0 },
|
||||||
|
downloading: { count: 0, sizeMB: 0 },
|
||||||
|
seeding: { count: 0, sizeMB: 0 },
|
||||||
|
paused: { count: 0, sizeMB: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, count torrents & total file size
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const t = state.saved.torrents[i]
|
||||||
|
const stat = byStatus[t.status]
|
||||||
|
if (!t || !t.files || !stat) continue
|
||||||
|
stat.count++
|
||||||
|
for (let j = 0; j < t.files.length; j++) {
|
||||||
|
const f = t.files[j]
|
||||||
|
if (!f || !f.length) continue
|
||||||
|
const fileSizeMB = f.length / (1 << 20)
|
||||||
|
sizeMB += fileSizeMB
|
||||||
|
stat.sizeMB += fileSizeMB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, round all the counts and sums to the nearest power of 2
|
||||||
|
const ret = roundTorrentStats({ count, sizeMB })
|
||||||
|
ret.byStatus = {
|
||||||
|
new: roundTorrentStats(byStatus.new),
|
||||||
|
downloading: roundTorrentStats(byStatus.downloading),
|
||||||
|
seeding: roundTorrentStats(byStatus.seeding),
|
||||||
|
paused: roundTorrentStats(byStatus.paused)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundTorrentStats (stats) {
|
||||||
|
return {
|
||||||
|
approxCount: roundPow2(stats.count),
|
||||||
|
approxSizeMB: roundPow2(stats.sizeMB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rounds to the nearest power of 2, for privacy and easy bucketing.
|
||||||
|
// Rounds 35 to 32, 70 to 64, 5 to 4, 1 to 1, 0 to 0.
|
||||||
|
// Supports nonnegative numbers only.
|
||||||
|
function roundPow2 (n) {
|
||||||
|
if (n <= 0) return 0
|
||||||
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
// Otherwise, return 1, 2, 4, 8, etc by rounding in log space
|
||||||
var log2 = Math.log(exactNum) / Math.log(2)
|
const log2 = Math.log(n) / Math.log(2)
|
||||||
return 1 << Math.round(log2)
|
return Math.pow(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
|
||||||
function logUncaughtError (procName, err) {
|
function logUncaughtError (procName, e) {
|
||||||
// Not initialized yet? Ignore.
|
// Not initialized yet? Ignore.
|
||||||
// Hopefully uncaught errors immediately on startup are fixed in dev
|
// Hopefully uncaught errors immediately on startup are fixed in dev
|
||||||
if (!telemetry) return
|
if (!telemetry) return
|
||||||
|
|
||||||
var message, stack
|
let message
|
||||||
if (err instanceof Error) {
|
let stack = ''
|
||||||
message = err.message
|
if (e == null) {
|
||||||
// Remove the first part of each file path in the stack trace.
|
message = 'Unexpected undefined error'
|
||||||
// - Privacy: remove personal info like C:\Users\<full name>
|
} else if (e.error) {
|
||||||
// - Aggregation: this lets us find which stacktraces occur often
|
// Uncaught Javascript errors (window.onerror), err is an ErrorEvent
|
||||||
stack = err.stack.replace(/\(.*app.asar/g, '(...')
|
if (!e.error.message) {
|
||||||
|
message = 'Unexpected ErrorEvent.error: ' + Object.keys(e.error).join(' ')
|
||||||
|
} else {
|
||||||
|
message = e.error.message
|
||||||
|
stack = e.error.stack
|
||||||
|
}
|
||||||
|
} else if (e.message) {
|
||||||
|
// err is either an Error or a plain object {message, stack}
|
||||||
|
message = e.message
|
||||||
|
stack = e.stack
|
||||||
} else {
|
} else {
|
||||||
message = String(err)
|
// Resource errors (captured element.onerror), err is an Event
|
||||||
stack = ''
|
if (!e.target) {
|
||||||
|
message = 'Unexpected unknown error'
|
||||||
|
} else if (!e.target.error) {
|
||||||
|
message = 'Unexpected resource loading error: ' + getElemString(e.target)
|
||||||
|
} else {
|
||||||
|
message = 'Resource error ' + getElemString(e.target) + ': ' + e.target.error.code
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof stack !== 'string') stack = 'Unexpected stack: ' + stack
|
||||||
|
if (typeof message !== 'string') message = 'Unexpected message: ' + message
|
||||||
|
|
||||||
|
// Remove the first part of each file path in the stack trace.
|
||||||
|
// - Privacy: remove personal info like C:\Users\<full name>
|
||||||
|
// - Aggregation: this lets us find which stacktraces occur often
|
||||||
|
stack = stack.replace(/\(.*app.asar/g, '(...')
|
||||||
|
stack = stack.replace(/at .*app.asar/g, 'at ...')
|
||||||
|
|
||||||
// We need to POST the telemetry object, make sure it stays < 100kb
|
// We need to POST the telemetry object, make sure it stays < 100kb
|
||||||
if (telemetry.uncaughtErrors.length > 20) return
|
if (telemetry.uncaughtErrors.length > 20) return
|
||||||
if (message.length > 1000) message = message.substring(0, 1000)
|
if (message.length > 1000) message = message.substring(0, 1000)
|
||||||
if (stack.length > 1000) stack = stack.substring(0, 1000)
|
if (stack.length > 1000) stack = stack.substring(0, 1000)
|
||||||
|
|
||||||
telemetry.uncaughtErrors.push({process: procName, message, stack})
|
// Log the app version *at the time of the error*
|
||||||
|
const version = config.APP_VERSION
|
||||||
|
|
||||||
|
telemetry.uncaughtErrors.push({ process: procName, message, stack, version })
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user pressed play. It either worked, timed out, or showed the
|
// Turns a DOM element into a string, eg "DIV.my-class.visible"
|
||||||
// 'Play in VLC' codec error
|
function getElemString (elem) {
|
||||||
|
let ret = elem.tagName
|
||||||
|
try {
|
||||||
|
ret += '.' + Array.from(elem.classList).join('.')
|
||||||
|
} catch (err) {}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user pressed play. Did it work, display an error,
|
||||||
|
// open an external player or did user abandon the attempt?
|
||||||
function logPlayAttempt (result) {
|
function logPlayAttempt (result) {
|
||||||
if (!['success', 'timeout', 'error', 'abandoned'].includes(result)) {
|
if (!['success', 'error', 'external', 'abandoned'].includes(result)) {
|
||||||
return console.error('Unknown play attempt result', result)
|
return console.error('Unknown play attempt result', result)
|
||||||
}
|
}
|
||||||
|
|
||||||
var attempts = telemetry.playAttempts
|
const attempts = telemetry.playAttempts
|
||||||
attempts.total = (attempts.total || 0) + 1
|
attempts.total = (attempts.total || 0) + 1
|
||||||
attempts[result] = (attempts[result] || 0) + 1
|
attempts[result] = (attempts[result] || 0) + 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ module.exports = {
|
|||||||
isVideo,
|
isVideo,
|
||||||
isAudio,
|
isAudio,
|
||||||
isTorrent,
|
isTorrent,
|
||||||
isPlayableTorrentSummary,
|
isPlayableTorrentSummary
|
||||||
pickFileToPlay
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
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
|
||||||
@@ -17,28 +18,12 @@ function isPlayable (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 [
|
return mediaExtensions.video.includes(getFileExtension(file))
|
||||||
'.avi',
|
|
||||||
'.m4v',
|
|
||||||
'.mkv',
|
|
||||||
'.mov',
|
|
||||||
'.mp4',
|
|
||||||
'.mpg',
|
|
||||||
'.ogv',
|
|
||||||
'.webm',
|
|
||||||
'.wmv'
|
|
||||||
].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 [
|
return mediaExtensions.audio.includes(getFileExtension(file))
|
||||||
'.aac',
|
|
||||||
'.ac3',
|
|
||||||
'.mp3',
|
|
||||||
'.ogg',
|
|
||||||
'.wav'
|
|
||||||
].includes(getFileExtension(file))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the argument is either:
|
// Checks if the argument is either:
|
||||||
@@ -46,38 +31,16 @@ function isAudio (file) {
|
|||||||
// - a file object where obj.name is ends in .torrent
|
// - a file object where obj.name is ends in .torrent
|
||||||
// - a string that's a magnet link (magnet://...)
|
// - a string that's a magnet link (magnet://...)
|
||||||
function isTorrent (file) {
|
function isTorrent (file) {
|
||||||
var isTorrentFile = getFileExtension(file) === '.torrent'
|
const isTorrentFile = getFileExtension(file) === '.torrent'
|
||||||
var isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
const isMagnet = typeof file === 'string' && /^(stream-)?magnet:/.test(file)
|
||||||
return isTorrentFile || isMagnet
|
return isTorrentFile || isMagnet
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileExtension (file) {
|
function getFileExtension (file) {
|
||||||
var 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Picks the default file to play from a list of torrent or torrentSummary files
|
|
||||||
// Returns an index or undefined, if no files are playable
|
|
||||||
function pickFileToPlay (files) {
|
|
||||||
// first, try to find the biggest video file
|
|
||||||
var videoFiles = files.filter(isVideo)
|
|
||||||
if (videoFiles.length > 0) {
|
|
||||||
var largestVideoFile = videoFiles.reduce(function (a, b) {
|
|
||||||
return a.length > b.length ? a : b
|
|
||||||
})
|
|
||||||
return files.indexOf(largestVideoFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there are no videos, play the first audio file
|
|
||||||
var audioFiles = files.filter(isAudio)
|
|
||||||
if (audioFiles.length > 0) {
|
|
||||||
return files.indexOf(audioFiles[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// no video or audio means nothing is playable
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,50 +1,155 @@
|
|||||||
module.exports = torrentPoster
|
module.exports = torrentPoster
|
||||||
|
|
||||||
var captureVideoFrame = require('./capture-video-frame')
|
const captureFrame = require('capture-frame')
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
|
const mediaExtensions = require('./media-extensions')
|
||||||
|
|
||||||
|
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
|
||||||
var posterFile = torrent.files.filter(function (file) {
|
const posterFile = torrent.files.filter(function (file) {
|
||||||
return /^poster\.(jpg|png|gif)$/.test(file.name)
|
return /^poster\.(jpg|png|gif)$/.test(file.name)
|
||||||
})[0]
|
})[0]
|
||||||
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
|
if (posterFile) return extractPoster(posterFile, cb)
|
||||||
|
|
||||||
// Second, try to use the largest video file
|
// 'score' each media type based on total size present in torrent
|
||||||
// Filter out file formats that the <video> tag definitely can't play
|
const bestScore = ['audio', 'video', 'image'].map(mediaType => {
|
||||||
var videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
|
return {
|
||||||
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
|
type: mediaType,
|
||||||
|
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])
|
||||||
|
}
|
||||||
|
}).sort((a, b) => { // sort descending on size
|
||||||
|
return b.size - a.size
|
||||||
|
})[0]
|
||||||
|
|
||||||
// Third, try to use the largest image file
|
if (bestScore.size === 0) {
|
||||||
var imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
|
// Admit defeat, no video, audio or image had a significant presence
|
||||||
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
|
return cb(new Error(msgNoSuitablePoster))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: generate a waveform from the largest sound file
|
// Based on which media type is dominant we select the corresponding poster function
|
||||||
// Finally, admit defeat
|
switch (bestScore.type) {
|
||||||
return cb(new Error('Cannot generate a poster from any files in the torrent'))
|
case 'audio':
|
||||||
|
return torrentPosterFromAudio(torrent, cb)
|
||||||
|
case 'image':
|
||||||
|
return torrentPosterFromImage(torrent, cb)
|
||||||
|
case 'video':
|
||||||
|
return torrentPosterFromVideo(torrent, cb)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total data size of file matching one of the provided extensions
|
||||||
|
* @param torrent
|
||||||
|
* @param extensions List of extension to match
|
||||||
|
* @returns {number} total size, of matches found (>= 0)
|
||||||
|
*/
|
||||||
|
function calculateDataLengthByExtension (torrent, extensions) {
|
||||||
|
const files = filterOnExtension(torrent, extensions)
|
||||||
|
if (files.length === 0) return 0
|
||||||
|
return files
|
||||||
|
.map(file => file.length)
|
||||||
|
.reduce((a, b) => {
|
||||||
|
return a + b
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the largest file of a given torrent, filtered by provided extension
|
||||||
|
* @param torrent Torrent to search in
|
||||||
|
* @param extensions Extension whitelist filter
|
||||||
|
* @returns Torrent file object
|
||||||
|
*/
|
||||||
function getLargestFileByExtension (torrent, extensions) {
|
function getLargestFileByExtension (torrent, extensions) {
|
||||||
var files = torrent.files.filter(function (file) {
|
const files = filterOnExtension(torrent, extensions)
|
||||||
var extname = path.extname(file.name).toLowerCase()
|
|
||||||
return extensions.indexOf(extname) !== -1
|
|
||||||
})
|
|
||||||
if (files.length === 0) return undefined
|
if (files.length === 0) return undefined
|
||||||
return files.reduce(function (a, b) {
|
return files.reduce((a, b) => {
|
||||||
return a.length > b.length ? a : b
|
return a.length > b.length ? a : b
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentPosterFromVideo (file, torrent, cb) {
|
/**
|
||||||
var index = torrent.files.indexOf(file)
|
* Filter file on a list extension, can be used to find al image files
|
||||||
|
* @param torrent Torrent to filter files from
|
||||||
|
* @param extensions File extensions to filter on
|
||||||
|
* @returns {number} Array of torrent file objects matching one of the given extensions
|
||||||
|
*/
|
||||||
|
function filterOnExtension (torrent, extensions) {
|
||||||
|
return torrent.files.filter(file => {
|
||||||
|
const extname = path.extname(file.name).toLowerCase()
|
||||||
|
return extensions.indexOf(extname) !== -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var server = torrent.createServer(0)
|
/**
|
||||||
|
* Returns a score how likely the file is suitable as a poster
|
||||||
|
* @param imgFile File object of an image
|
||||||
|
* @returns {number} Score, higher score is a better match
|
||||||
|
*/
|
||||||
|
function scoreAudioCoverFile (imgFile) {
|
||||||
|
const fileName = path.basename(imgFile.name, path.extname(imgFile.name)).toLowerCase()
|
||||||
|
const relevanceScore = {
|
||||||
|
cover: 80,
|
||||||
|
folder: 80,
|
||||||
|
album: 80,
|
||||||
|
front: 80,
|
||||||
|
back: 20,
|
||||||
|
spectrogram: -80
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const keyword in relevanceScore) {
|
||||||
|
if (fileName === keyword) {
|
||||||
|
return relevanceScore[keyword]
|
||||||
|
}
|
||||||
|
if (fileName.indexOf(keyword) !== -1) {
|
||||||
|
return relevanceScore[keyword]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function torrentPosterFromAudio (torrent, cb) {
|
||||||
|
const imageFiles = filterOnExtension(torrent, mediaExtensions.image)
|
||||||
|
|
||||||
|
if (imageFiles.length === 0) return cb(new Error(msgNoSuitablePoster))
|
||||||
|
|
||||||
|
const bestCover = imageFiles.map(file => {
|
||||||
|
return {
|
||||||
|
file: file,
|
||||||
|
score: scoreAudioCoverFile(file)
|
||||||
|
}
|
||||||
|
}).reduce((a, b) => {
|
||||||
|
if (a.score > b.score) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
if (b.score > a.score) {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
// If score is equal, pick the largest file, aiming for highest resolution
|
||||||
|
if (a.file.length > b.file.length) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
})
|
||||||
|
|
||||||
|
const extname = path.extname(bestCover.file.name)
|
||||||
|
bestCover.file.getBuffer((err, buf) => cb(err, buf, extname))
|
||||||
|
}
|
||||||
|
|
||||||
|
function torrentPosterFromVideo (torrent, cb) {
|
||||||
|
const file = getLargestFileByExtension(torrent, mediaExtensions.video)
|
||||||
|
|
||||||
|
const index = torrent.files.indexOf(file)
|
||||||
|
|
||||||
|
const server = torrent.createServer(0)
|
||||||
server.listen(0, onListening)
|
server.listen(0, onListening)
|
||||||
|
|
||||||
function onListening () {
|
function onListening () {
|
||||||
var port = server.address().port
|
const port = server.address().port
|
||||||
var url = 'http://localhost:' + port + '/' + index
|
const url = 'http://localhost:' + port + '/' + index
|
||||||
var video = document.createElement('video')
|
const video = document.createElement('video')
|
||||||
video.addEventListener('canplay', onCanPlay)
|
video.addEventListener('canplay', onCanPlay)
|
||||||
|
|
||||||
video.volume = 0
|
video.volume = 0
|
||||||
@@ -61,7 +166,7 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
|||||||
function onSeeked () {
|
function onSeeked () {
|
||||||
video.removeEventListener('seeked', onSeeked)
|
video.removeEventListener('seeked', onSeeked)
|
||||||
|
|
||||||
var buf = captureVideoFrame(video)
|
const buf = captureFrame(video)
|
||||||
|
|
||||||
// unload video element
|
// unload video element
|
||||||
video.pause()
|
video.pause()
|
||||||
@@ -70,14 +175,19 @@ function torrentPosterFromVideo (file, torrent, cb) {
|
|||||||
|
|
||||||
server.destroy()
|
server.destroy()
|
||||||
|
|
||||||
if (buf.length === 0) return cb(new Error('Generated poster contains no data'))
|
if (buf.length === 0) return cb(new Error(msgNoSuitablePoster))
|
||||||
|
|
||||||
cb(null, buf, '.jpg')
|
cb(null, buf, '.jpg')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentPosterFromImage (file, torrent, cb) {
|
function torrentPosterFromImage (torrent, cb) {
|
||||||
var extname = path.extname(file.name)
|
const file = getLargestFileByExtension(torrent, mediaExtensions.image)
|
||||||
file.getBuffer((err, buf) => cb(err, buf, extname))
|
extractPoster(file, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPoster (file, cb) {
|
||||||
|
const extname = path.extname(file.name)
|
||||||
|
file.getBuffer((err, buf) => { return cb(err, buf, extname) })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ module.exports = {
|
|||||||
getPosterPath,
|
getPosterPath,
|
||||||
getTorrentPath,
|
getTorrentPath,
|
||||||
getByKey,
|
getByKey,
|
||||||
getTorrentID,
|
getTorrentId,
|
||||||
getFileOrFolder
|
getFileOrFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
const path = require('path')
|
||||||
var config = require('../../config')
|
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
|
||||||
@@ -20,7 +20,7 @@ function getTorrentPath (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) {
|
||||||
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
if (!torrentSummary || !torrentSummary.posterFileName) return null
|
||||||
var posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
|
const posterPath = path.join(config.POSTER_PATH, torrentSummary.posterFileName)
|
||||||
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
// Work around a Chrome bug (reproduced in vanilla Chrome, not just Electron):
|
||||||
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
// Backslashes in URLS in CSS cause bizarre string encoding issues
|
||||||
return posterPath.replace(/\\/g, '/')
|
return posterPath.replace(/\\/g, '/')
|
||||||
@@ -28,8 +28,8 @@ function getPosterPath (torrentSummary) {
|
|||||||
|
|
||||||
// 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) {
|
||||||
var s = torrentSummary
|
const s = torrentSummary
|
||||||
if (s.torrentFileName) { // Load torrent file from disk
|
if (s.torrentFileName) { // Load torrent file from disk
|
||||||
return getTorrentPath(s)
|
return getTorrentPath(s)
|
||||||
} else { // Load torrent from DHT
|
} else { // Load torrent from DHT
|
||||||
@@ -51,7 +51,8 @@ function getByKey (state, torrentKey) {
|
|||||||
// TODO: make this assumption explicit, enforce it in the `create-torrent`
|
// TODO: make this assumption explicit, enforce it in the `create-torrent`
|
||||||
// module. Store root folder explicitly to avoid hacky path processing below.
|
// module. Store root folder explicitly to avoid hacky path processing below.
|
||||||
function getFileOrFolder (torrentSummary) {
|
function getFileOrFolder (torrentSummary) {
|
||||||
var ts = torrentSummary
|
const ts = torrentSummary
|
||||||
if (!ts.path || !ts.files || ts.files.length === 0) return null
|
if (!ts.path || !ts.files || ts.files.length === 0) return null
|
||||||
return path.join(ts.path, ts.files[0].path.split('/')[0])
|
const dirname = ts.files[0].path.split(path.sep)[0]
|
||||||
|
return path.join(ts.path, dirname)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,119 @@
|
|||||||
|
/**
|
||||||
|
* 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')
|
||||||
|
|
||||||
const crashReporter = require('../crash-reporter')
|
const crashReporter = require('../crash-reporter')
|
||||||
crashReporter.init()
|
crashReporter.init()
|
||||||
|
|
||||||
|
// Perf optimization: Start asynchronously read on config file before all the
|
||||||
|
// blocking require() calls below.
|
||||||
|
|
||||||
|
const State = require('./lib/state')
|
||||||
|
State.load(onState)
|
||||||
|
|
||||||
|
const createGetter = require('fn-getter')
|
||||||
|
const debounce = require('debounce')
|
||||||
const dragDrop = require('drag-drop')
|
const dragDrop = require('drag-drop')
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
|
const fs = require('fs')
|
||||||
const React = require('react')
|
const React = require('react')
|
||||||
const ReactDOM = require('react-dom')
|
const ReactDOM = require('react-dom')
|
||||||
const fs = require('fs')
|
|
||||||
|
|
||||||
const config = require('../config')
|
const config = require('../config')
|
||||||
const App = require('./views/app')
|
|
||||||
const telemetry = require('./lib/telemetry')
|
const telemetry = require('./lib/telemetry')
|
||||||
const sound = require('./lib/sound')
|
const sound = require('./lib/sound')
|
||||||
const State = require('./lib/state')
|
|
||||||
const TorrentPlayer = require('./lib/torrent-player')
|
const TorrentPlayer = require('./lib/torrent-player')
|
||||||
|
|
||||||
const MediaController = require('./controllers/media-controller')
|
// Perf optimization: Needed immediately, so do not lazy load it below
|
||||||
const UpdateController = require('./controllers/update-controller')
|
|
||||||
const PrefsController = require('./controllers/prefs-controller')
|
|
||||||
const PlaybackController = require('./controllers/playback-controller')
|
|
||||||
const SubtitlesController = require('./controllers/subtitles-controller')
|
|
||||||
const TorrentListController = require('./controllers/torrent-list-controller')
|
const TorrentListController = require('./controllers/torrent-list-controller')
|
||||||
const TorrentController = require('./controllers/torrent-controller')
|
|
||||||
|
const App = require('./pages/app')
|
||||||
|
|
||||||
|
// 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)
|
require('./lib/dispatcher').setDispatch(dispatch)
|
||||||
|
|
||||||
// From dispatch(...), events are sent to one of the controllers
|
// From dispatch(...), events are sent to one of the controllers
|
||||||
var controllers = null
|
let controllers = null
|
||||||
|
|
||||||
// This dependency is the slowest-loading, so we lazy load it
|
// This dependency is the slowest-loading, so we lazy load it
|
||||||
var Cast = null
|
let Cast = null
|
||||||
|
|
||||||
// Electron apps have two processes: a main process (node) runs first and starts
|
|
||||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
|
||||||
// and this IPC channel receives from and sends messages to the main process
|
|
||||||
var ipcRenderer = electron.ipcRenderer
|
|
||||||
|
|
||||||
// All state lives in state.js. `state.saved` is read from and written to a file.
|
// All state lives in state.js. `state.saved` is read from and written to a file.
|
||||||
// All other state is ephemeral. First we load state.saved then initialize the app.
|
// All other state is ephemeral. First we load state.saved then initialize the app.
|
||||||
var state
|
let state
|
||||||
|
|
||||||
// Root React component
|
// Root React component
|
||||||
var app
|
let app
|
||||||
|
|
||||||
State.load(onState)
|
|
||||||
|
|
||||||
// Called once when the application loads. (Not once per window.)
|
// Called once when the application loads. (Not once per window.)
|
||||||
// Connects to the torrent networks, sets up the UI and OS integrations like
|
// Connects to the torrent networks, sets up the UI and OS integrations like
|
||||||
// the dock icon and drag+drop.
|
// the dock icon and drag+drop.
|
||||||
function onState (err, _state) {
|
function onState (err, _state) {
|
||||||
if (err) return onError(err)
|
if (err) return onError(err)
|
||||||
state = window.state = _state // Make available for easier debugging
|
|
||||||
|
// Make available for easier debugging
|
||||||
|
state = window.state = _state
|
||||||
|
window.dispatch = dispatch
|
||||||
|
|
||||||
|
telemetry.init(state)
|
||||||
|
sound.init(state)
|
||||||
|
|
||||||
|
// Log uncaught JS errors
|
||||||
|
window.addEventListener(
|
||||||
|
'error', (e) => telemetry.logUncaughtError('window', e), true /* capture */
|
||||||
|
)
|
||||||
|
|
||||||
// Create controllers
|
// Create controllers
|
||||||
controllers = {
|
controllers = {
|
||||||
media: new MediaController(state),
|
media: createGetter(() => {
|
||||||
update: new UpdateController(state),
|
const MediaController = require('./controllers/media-controller')
|
||||||
prefs: new PrefsController(state, config),
|
return new MediaController(state)
|
||||||
playback: new PlaybackController(state, config, update),
|
}),
|
||||||
subtitles: new SubtitlesController(state),
|
playback: createGetter(() => {
|
||||||
torrentList: new TorrentListController(state),
|
const PlaybackController = require('./controllers/playback-controller')
|
||||||
torrent: new TorrentController(state)
|
return new PlaybackController(state, config, update)
|
||||||
|
}),
|
||||||
|
prefs: createGetter(() => {
|
||||||
|
const PrefsController = require('./controllers/prefs-controller')
|
||||||
|
return new PrefsController(state, config)
|
||||||
|
}),
|
||||||
|
subtitles: createGetter(() => {
|
||||||
|
const SubtitlesController = require('./controllers/subtitles-controller')
|
||||||
|
return new SubtitlesController(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
|
||||||
@@ -78,23 +128,18 @@ function onState (err, _state) {
|
|||||||
// Restart everything we were torrenting last time the app ran
|
// Restart everything we were torrenting last time the app ran
|
||||||
resumeTorrents()
|
resumeTorrents()
|
||||||
|
|
||||||
|
// Initialize ReactDOM
|
||||||
|
app = ReactDOM.render(<App state={state} />, 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
|
||||||
// a progress bar and to keep the cursor in sync when playing a video
|
// a progress bar and to keep the cursor in sync when playing a video
|
||||||
setInterval(update, 1000)
|
setInterval(update, 1000)
|
||||||
app = ReactDOM.render(<App state={state} />, document.querySelector('#body'))
|
|
||||||
|
|
||||||
// Lazy-load other stuff, like the AppleTV module, later to keep startup fast
|
|
||||||
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
|
||||||
|
|
||||||
// Listen for messages from the main process
|
// Listen for messages from the main process
|
||||||
setupIpc()
|
setupIpc()
|
||||||
|
|
||||||
// Warn if the download dir is gone, eg b/c an external drive is unplugged
|
// Drag and drop files/text to start torrenting or seeding
|
||||||
checkDownloadPath()
|
|
||||||
|
|
||||||
// OS integrations:
|
|
||||||
// ...drag and drop files/text to start torrenting or seeding
|
|
||||||
dragDrop('body', {
|
dragDrop('body', {
|
||||||
onDrop: onOpen,
|
onDrop: onOpen,
|
||||||
onDropText: onOpen
|
onDropText: onOpen
|
||||||
@@ -103,27 +148,52 @@ function onState (err, _state) {
|
|||||||
// ...same thing if you paste a torrent
|
// ...same thing if you paste a torrent
|
||||||
document.addEventListener('paste', onPaste)
|
document.addEventListener('paste', onPaste)
|
||||||
|
|
||||||
|
// Add YouTube style hotkey shortcuts
|
||||||
|
window.addEventListener('keydown', onKeydown)
|
||||||
|
|
||||||
|
const debouncedFullscreenToggle = debounce(function () {
|
||||||
|
dispatch('toggleFullScreen')
|
||||||
|
}, 1000, true)
|
||||||
|
|
||||||
|
document.addEventListener('wheel', function (event) {
|
||||||
|
// ctrlKey detects pinch to zoom, http://crbug.com/289887
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
debouncedFullscreenToggle()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// ...focus and blur. Needed to show correct dock icon text ('badge') in OSX
|
// ...focus and blur. Needed to show correct dock icon text ('badge') in OSX
|
||||||
window.addEventListener('focus', onFocus)
|
window.addEventListener('focus', onFocus)
|
||||||
window.addEventListener('blur', onBlur)
|
window.addEventListener('blur', onBlur)
|
||||||
|
|
||||||
// ...window visibility state.
|
if (electron.remote.getCurrentWindow().isVisible()) {
|
||||||
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
sound.play('STARTUP')
|
||||||
|
}
|
||||||
|
|
||||||
// Log uncaught JS errors
|
// To keep app startup fast, some code is delayed.
|
||||||
window.addEventListener('error',
|
window.setTimeout(delayedInit, config.DELAYED_INIT)
|
||||||
(e) => telemetry.logUncaughtError('window', e.error || e.target), true)
|
|
||||||
|
|
||||||
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
// Done! Ideally we want to get here < 500ms after the user clicks the app
|
||||||
sound.play('STARTUP')
|
|
||||||
console.timeEnd('init')
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runs a few seconds after the app loads, to avoid slowing down startup time
|
// Runs a few seconds after the app loads, to avoid slowing down startup time
|
||||||
function delayedInit () {
|
function delayedInit () {
|
||||||
|
telemetry.send(state)
|
||||||
|
|
||||||
|
// Send telemetry data every 12 hours, for users who keep the app running
|
||||||
|
// for extended periods of time
|
||||||
|
setInterval(() => telemetry.send(state), 12 * 3600 * 1000)
|
||||||
|
|
||||||
|
// Warn if the download dir is gone, eg b/c an external drive is unplugged
|
||||||
|
checkDownloadPath()
|
||||||
|
|
||||||
|
// ...window visibility state.
|
||||||
|
document.addEventListener('webkitvisibilitychange', onVisibilityChange)
|
||||||
|
onVisibilityChange()
|
||||||
|
|
||||||
lazyLoadCast()
|
lazyLoadCast()
|
||||||
sound.preload()
|
|
||||||
telemetry.init(state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazily loads Chromecast and Airplay support
|
// Lazily loads Chromecast and Airplay support
|
||||||
@@ -141,7 +211,7 @@ function lazyLoadCast () {
|
|||||||
// 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller
|
// 3. dispatch - the event handler calls dispatch(), main.js sends it to a controller
|
||||||
// 4. controller - the controller handles the event, changing the state object
|
// 4. controller - the controller handles the event, changing the state object
|
||||||
function update () {
|
function update () {
|
||||||
controllers.playback.showOrHidePlayerControls()
|
controllers.playback().showOrHidePlayerControls()
|
||||||
app.setState(state)
|
app.setState(state)
|
||||||
updateElectron()
|
updateElectron()
|
||||||
}
|
}
|
||||||
@@ -153,7 +223,7 @@ function updateElectron () {
|
|||||||
state.prev.title = state.window.title
|
state.prev.title = state.window.title
|
||||||
ipcRenderer.send('setTitle', state.window.title)
|
ipcRenderer.send('setTitle', state.window.title)
|
||||||
}
|
}
|
||||||
if (state.dock.progress !== state.prev.progress) {
|
if (state.dock.progress.toFixed(2) !== state.prev.progress.toFixed(2)) {
|
||||||
state.prev.progress = state.dock.progress
|
state.prev.progress = state.dock.progress
|
||||||
ipcRenderer.send('setProgress', state.dock.progress)
|
ipcRenderer.send('setProgress', state.dock.progress)
|
||||||
}
|
}
|
||||||
@@ -165,83 +235,100 @@ function updateElectron () {
|
|||||||
|
|
||||||
const dispatchHandlers = {
|
const dispatchHandlers = {
|
||||||
// Torrent list: creating, deleting, selecting torrents
|
// Torrent list: creating, deleting, selecting torrents
|
||||||
'openTorrentFile': () => ipcRenderer.send('openTorrentFile'),
|
openTorrentFile: () => ipcRenderer.send('openTorrentFile'),
|
||||||
'openFiles': () => ipcRenderer.send('openFiles'), /* shows the open file dialog */
|
openFiles: () => ipcRenderer.send('openFiles'), /* shows the open file dialog */
|
||||||
'openTorrentAddress': () => { state.modal = { id: 'open-torrent-address-modal' } },
|
openTorrentAddress: () => { state.modal = { id: 'open-torrent-address-modal' } },
|
||||||
|
|
||||||
'addTorrent': (torrentId) => controllers.torrentList.addTorrent(torrentId),
|
addTorrent: (torrentId) => controllers.torrentList().addTorrent(torrentId),
|
||||||
'showCreateTorrent': (paths) => controllers.torrentList.showCreateTorrent(paths),
|
showCreateTorrent: (paths) => controllers.torrentList().showCreateTorrent(paths),
|
||||||
'toggleCreateTorrentAdvanced': () => controllers.torrentList.toggleCreateTorrentAdvanced(),
|
createTorrent: (options) => controllers.torrentList().createTorrent(options),
|
||||||
'createTorrent': (options) => controllers.torrentList.createTorrent(options),
|
toggleTorrent: (infoHash) => controllers.torrentList().toggleTorrent(infoHash),
|
||||||
'toggleTorrent': (infoHash) => controllers.torrentList.toggleTorrent(infoHash),
|
pauseAllTorrents: () => controllers.torrentList().pauseAllTorrents(),
|
||||||
'toggleTorrentFile': (infoHash, index) => controllers.torrentList.toggleTorrentFile(infoHash, index),
|
resumeAllTorrents: () => controllers.torrentList().resumeAllTorrents(),
|
||||||
'confirmDeleteTorrent': (infoHash, deleteData) => controllers.torrentList.confirmDeleteTorrent(infoHash, deleteData),
|
toggleTorrentFile: (infoHash, index) =>
|
||||||
'deleteTorrent': (infoHash, deleteData) => controllers.torrentList.deleteTorrent(infoHash, deleteData),
|
controllers.torrentList().toggleTorrentFile(infoHash, index),
|
||||||
'toggleSelectTorrent': (infoHash) => controllers.torrentList.toggleSelectTorrent(infoHash),
|
confirmDeleteTorrent: (infoHash, deleteData) =>
|
||||||
'openTorrentContextMenu': (infoHash) => controllers.torrentList.openTorrentContextMenu(infoHash),
|
controllers.torrentList().confirmDeleteTorrent(infoHash, deleteData),
|
||||||
'startTorrentingSummary': (torrentKey) => controllers.torrentList.startTorrentingSummary(torrentKey),
|
deleteTorrent: (infoHash, deleteData) =>
|
||||||
|
controllers.torrentList().deleteTorrent(infoHash, deleteData),
|
||||||
|
toggleSelectTorrent: (infoHash) =>
|
||||||
|
controllers.torrentList().toggleSelectTorrent(infoHash),
|
||||||
|
openTorrentContextMenu: (infoHash) =>
|
||||||
|
controllers.torrentList().openTorrentContextMenu(infoHash),
|
||||||
|
startTorrentingSummary: (torrentKey) =>
|
||||||
|
controllers.torrentList().startTorrentingSummary(torrentKey),
|
||||||
|
saveTorrentFileAs: (torrentKey) =>
|
||||||
|
controllers.torrentList().saveTorrentFileAs(torrentKey),
|
||||||
|
prioritizeTorrent: (infoHash) => controllers.torrentList().prioritizeTorrent(infoHash),
|
||||||
|
resumePausedTorrents: () => controllers.torrentList().resumePausedTorrents(),
|
||||||
|
|
||||||
// Playback
|
// Playback
|
||||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
playFile: (infoHash, index) => controllers.playback().playFile(infoHash, index),
|
||||||
'playPause': () => controllers.playback.playPause(),
|
playPause: () => controllers.playback().playPause(),
|
||||||
'skip': (time) => controllers.playback.skip(time),
|
nextTrack: () => controllers.playback().nextTrack(),
|
||||||
'skipTo': (time) => controllers.playback.skipTo(time),
|
previousTrack: () => controllers.playback().previousTrack(),
|
||||||
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
|
skip: (time) => controllers.playback().skip(time),
|
||||||
'changeVolume': (delta) => controllers.playback.changeVolume(delta),
|
skipTo: (time) => controllers.playback().skipTo(time),
|
||||||
'setVolume': (vol) => controllers.playback.setVolume(vol),
|
changePlaybackRate: (dir) => controllers.playback().changePlaybackRate(dir),
|
||||||
'openItem': (infoHash, index) => controllers.playback.openItem(infoHash, index),
|
changeVolume: (delta) => controllers.playback().changeVolume(delta),
|
||||||
|
setVolume: (vol) => controllers.playback().setVolume(vol),
|
||||||
|
openItem: (infoHash, index) => controllers.playback().openItem(infoHash, index),
|
||||||
|
|
||||||
// Subtitles
|
// Subtitles
|
||||||
'openSubtitles': () => controllers.subtitles.openSubtitles(),
|
openSubtitles: () => controllers.subtitles().openSubtitles(),
|
||||||
'selectSubtitle': (index) => controllers.subtitles.selectSubtitle(index),
|
selectSubtitle: (index) => controllers.subtitles().selectSubtitle(index),
|
||||||
'toggleSubtitlesMenu': () => controllers.subtitles.toggleSubtitlesMenu(),
|
toggleSubtitlesMenu: () => controllers.subtitles().toggleSubtitlesMenu(),
|
||||||
'checkForSubtitles': () => controllers.subtitles.checkForSubtitles(),
|
checkForSubtitles: () => controllers.subtitles().checkForSubtitles(),
|
||||||
'addSubtitles': (files, autoSelect) => controllers.subtitles.addSubtitles(files, autoSelect),
|
addSubtitles: (files, autoSelect) => controllers.subtitles().addSubtitles(files, autoSelect),
|
||||||
|
|
||||||
// Local media: <video>, <audio>, external players
|
// Local media: <video>, <audio>, external players
|
||||||
'mediaStalled': () => controllers.media.mediaStalled(),
|
mediaStalled: () => controllers.media().mediaStalled(),
|
||||||
'mediaError': (err) => controllers.media.mediaError(err),
|
mediaError: (err) => controllers.media().mediaError(err),
|
||||||
'mediaSuccess': () => controllers.media.mediaSuccess(),
|
mediaSuccess: () => controllers.media().mediaSuccess(),
|
||||||
'mediaTimeUpdate': () => controllers.media.mediaTimeUpdate(),
|
mediaTimeUpdate: () => controllers.media().mediaTimeUpdate(),
|
||||||
'mediaMouseMoved': () => controllers.media.mediaMouseMoved(),
|
mediaMouseMoved: () => controllers.media().mediaMouseMoved(),
|
||||||
'openExternalPlayer': () => controllers.media.openExternalPlayer(),
|
mediaControlsMouseEnter: () => controllers.media().controlsMouseEnter(),
|
||||||
'externalPlayerNotFound': () => controllers.media.externalPlayerNotFound(),
|
mediaControlsMouseLeave: () => controllers.media().controlsMouseLeave(),
|
||||||
|
openExternalPlayer: () => controllers.media().openExternalPlayer(),
|
||||||
|
externalPlayerNotFound: () => controllers.media().externalPlayerNotFound(),
|
||||||
|
|
||||||
// Remote casting: Chromecast, Airplay, etc
|
// Remote casting: Chromecast, Airplay, etc
|
||||||
'toggleCastMenu': (deviceType) => lazyLoadCast().toggleMenu(deviceType),
|
toggleCastMenu: (deviceType) => lazyLoadCast().toggleMenu(deviceType),
|
||||||
'selectCastDevice': (index) => lazyLoadCast().selectDevice(index),
|
selectCastDevice: (index) => lazyLoadCast().selectDevice(index),
|
||||||
'stopCasting': () => lazyLoadCast().stop(),
|
stopCasting: () => lazyLoadCast().stop(),
|
||||||
|
|
||||||
// Preferences screen
|
// Preferences screen
|
||||||
'preferences': () => controllers.prefs.show(),
|
preferences: () => controllers.prefs().show(),
|
||||||
'updatePreferences': (key, value) => controllers.prefs.update(key, value),
|
updatePreferences: (key, value) => controllers.prefs().update(key, value),
|
||||||
'checkDownloadPath': checkDownloadPath,
|
checkDownloadPath: checkDownloadPath,
|
||||||
|
startFolderWatcher: () => controllers.folderWatcher().start(),
|
||||||
|
stopFolderWatcher: () => controllers.folderWatcher().stop(),
|
||||||
|
|
||||||
// Update (check for new versions on Linux, where there's no auto updater)
|
// Update (check for new versions on Linux, where there's no auto updater)
|
||||||
'updateAvailable': (version) => controllers.update.updateAvailable(version),
|
updateAvailable: (version) => controllers.update().updateAvailable(version),
|
||||||
'skipVersion': (version) => controllers.update.skipVersion(version),
|
skipVersion: (version) => controllers.update().skipVersion(version),
|
||||||
|
|
||||||
// Navigation between screens (back, forward, ESC, etc)
|
// Navigation between screens (back, forward, ESC, etc)
|
||||||
'exitModal': () => { state.modal = null },
|
exitModal: () => { state.modal = null },
|
||||||
'backToList': backToList,
|
backToList: backToList,
|
||||||
'escapeBack': escapeBack,
|
escapeBack: escapeBack,
|
||||||
'back': () => state.location.back(),
|
back: () => state.location.back(),
|
||||||
'forward': () => state.location.forward(),
|
forward: () => state.location.forward(),
|
||||||
'cancel': () => state.location.cancel(),
|
cancel: () => state.location.cancel(),
|
||||||
|
|
||||||
// Controlling the window
|
// Controlling the window
|
||||||
'setDimensions': setDimensions,
|
setDimensions: setDimensions,
|
||||||
'toggleFullScreen': (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
|
toggleFullScreen: (setTo) => ipcRenderer.send('toggleFullScreen', setTo),
|
||||||
'setTitle': (title) => { state.window.title = title },
|
setTitle: (title) => { state.window.title = title },
|
||||||
'resetTitle': () => { state.window.title = config.APP_WINDOW_TITLE },
|
resetTitle: () => { state.window.title = config.APP_WINDOW_TITLE },
|
||||||
|
|
||||||
// Everything else
|
// Everything else
|
||||||
'onOpen': onOpen,
|
onOpen: onOpen,
|
||||||
'error': onError,
|
error: onError,
|
||||||
'uncaughtError': (proc, err) => telemetry.logUncaughtError(proc, err),
|
uncaughtError: (proc, err) => telemetry.logUncaughtError(proc, err),
|
||||||
'saveState': () => State.save(state),
|
stateSave: () => State.save(state),
|
||||||
'saveStateThrottled': () => State.saveThrottled(state),
|
stateSaveImmediate: () => State.saveImmediate(state),
|
||||||
'update': () => {} // No-op, just trigger an update
|
update: () => {} // No-op, just trigger an update
|
||||||
}
|
}
|
||||||
|
|
||||||
// Events from the UI never modify state directly. Instead they call dispatch()
|
// Events from the UI never modify state directly. Instead they call dispatch()
|
||||||
@@ -251,13 +338,13 @@ function dispatch (action, ...args) {
|
|||||||
console.log('dispatch: %s %o', action, args)
|
console.log('dispatch: %s %o', action, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
var handler = dispatchHandlers[action]
|
const handler = dispatchHandlers[action]
|
||||||
if (handler) handler(...args)
|
if (handler) handler(...args)
|
||||||
else console.error('Missing dispatch handler: ' + action)
|
else console.error('Missing dispatch handler: ' + action)
|
||||||
|
|
||||||
// Update the virtual DOM, unless it's just a mouse move event
|
// Update the virtual DOM, unless it's just a mouse move event
|
||||||
if (action !== 'mediaMouseMoved' ||
|
if (action !== 'mediaMouseMoved' ||
|
||||||
controllers.playback.showOrHidePlayerControls()) {
|
controllers.playback().showOrHidePlayerControls()) {
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,11 +357,13 @@ function setupIpc () {
|
|||||||
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
|
ipcRenderer.on('dispatch', (e, ...args) => dispatch(...args))
|
||||||
|
|
||||||
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
|
ipcRenderer.on('fullscreenChanged', onFullscreenChanged)
|
||||||
|
ipcRenderer.on('windowBoundsChanged', onWindowBoundsChanged)
|
||||||
|
|
||||||
var tc = controllers.torrent
|
const tc = controllers.torrent()
|
||||||
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
|
ipcRenderer.on('wt-infohash', (e, ...args) => tc.torrentInfoHash(...args))
|
||||||
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
|
ipcRenderer.on('wt-metadata', (e, ...args) => tc.torrentMetadata(...args))
|
||||||
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
|
ipcRenderer.on('wt-done', (e, ...args) => tc.torrentDone(...args))
|
||||||
|
ipcRenderer.on('wt-done', () => controllers.torrentList().resumePausedTorrents())
|
||||||
ipcRenderer.on('wt-warning', (e, ...args) => tc.torrentWarning(...args))
|
ipcRenderer.on('wt-warning', (e, ...args) => tc.torrentWarning(...args))
|
||||||
ipcRenderer.on('wt-error', (e, ...args) => tc.torrentError(...args))
|
ipcRenderer.on('wt-error', (e, ...args) => tc.torrentError(...args))
|
||||||
|
|
||||||
@@ -289,7 +378,7 @@ function setupIpc () {
|
|||||||
|
|
||||||
ipcRenderer.send('ipcReady')
|
ipcRenderer.send('ipcReady')
|
||||||
|
|
||||||
State.on('savedState', () => ipcRenderer.send('savedState'))
|
State.on('stateSaved', () => ipcRenderer.send('stateSaved'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quits any modal popovers and returns to the torrent list screen
|
// Quits any modal popovers and returns to the torrent list screen
|
||||||
@@ -298,7 +387,7 @@ function backToList () {
|
|||||||
state.modal = null
|
state.modal = null
|
||||||
state.location.backToFirst(function () {
|
state.location.backToFirst(function () {
|
||||||
// If we were already on the torrent list, scroll to the top
|
// If we were already on the torrent list, scroll to the top
|
||||||
var contentTag = document.querySelector('.content')
|
const contentTag = document.querySelector('.content')
|
||||||
if (contentTag) contentTag.scrollTop = 0
|
if (contentTag) contentTag.scrollTop = 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -324,7 +413,7 @@ function resumeTorrents () {
|
|||||||
return torrentSummary
|
return torrentSummary
|
||||||
})
|
})
|
||||||
.filter((s) => s.status !== 'paused')
|
.filter((s) => s.status !== 'paused')
|
||||||
.forEach((s) => controllers.torrentList.startTorrentingSummary(s.torrentKey))
|
.forEach((s) => controllers.torrentList().startTorrentingSummary(s.torrentKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set window dimensions to match video dimensions or fill the screen
|
// Set window dimensions to match video dimensions or fill the screen
|
||||||
@@ -345,48 +434,55 @@ function setDimensions (dimensions) {
|
|||||||
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
|
state.window.wasMaximized = electron.remote.getCurrentWindow().isMaximized
|
||||||
|
|
||||||
// Limit window size to screen size
|
// Limit window size to screen size
|
||||||
var screenWidth = window.screen.width
|
const screenWidth = window.screen.width
|
||||||
var screenHeight = window.screen.height
|
const screenHeight = window.screen.height
|
||||||
var aspectRatio = dimensions.width / dimensions.height
|
const aspectRatio = dimensions.width / dimensions.height
|
||||||
var scaleFactor = Math.min(
|
const scaleFactor = Math.min(
|
||||||
Math.min(screenWidth / dimensions.width, 1),
|
Math.min(screenWidth / dimensions.width, 1),
|
||||||
Math.min(screenHeight / dimensions.height, 1)
|
Math.min(screenHeight / dimensions.height, 1)
|
||||||
)
|
)
|
||||||
var width = Math.max(
|
const width = Math.max(
|
||||||
Math.floor(dimensions.width * scaleFactor),
|
Math.floor(dimensions.width * scaleFactor),
|
||||||
config.WINDOW_MIN_WIDTH
|
config.WINDOW_MIN_WIDTH
|
||||||
)
|
)
|
||||||
var height = Math.max(
|
const height = Math.max(
|
||||||
Math.floor(dimensions.height * scaleFactor),
|
Math.floor(dimensions.height * scaleFactor),
|
||||||
config.WINDOW_MIN_HEIGHT
|
config.WINDOW_MIN_HEIGHT
|
||||||
)
|
)
|
||||||
|
|
||||||
ipcRenderer.send('setAspectRatio', aspectRatio)
|
ipcRenderer.send('setAspectRatio', aspectRatio)
|
||||||
ipcRenderer.send('setBounds', {contentBounds: true, x: null, y: null, width, height})
|
ipcRenderer.send('setBounds', { contentBounds: true, x: null, y: null, width, height })
|
||||||
state.playing.aspectRatio = aspectRatio
|
state.playing.aspectRatio = aspectRatio
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
|
// Called when the user adds files (.torrent, files to seed, subtitles) to the app
|
||||||
// via any method (drag-drop, drag to app icon, command line)
|
// via any method (drag-drop, drag to app icon, command line)
|
||||||
function onOpen (files) {
|
function onOpen (files) {
|
||||||
if (!Array.isArray(files)) files = [ files ]
|
if (!Array.isArray(files)) files = [files]
|
||||||
|
|
||||||
var url = state.location.url()
|
// File API seems to transform "magnet:?foo" in "magnet:///?foo"
|
||||||
var allTorrents = files.every(TorrentPlayer.isTorrent)
|
// this is a sanitization
|
||||||
var allSubtitles = files.every(controllers.subtitles.isSubtitle)
|
files = files.map(file => {
|
||||||
|
if (typeof file !== 'string') return file
|
||||||
|
return file.replace(/^magnet:\/+\?/i, 'magnet:?')
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = state.location.url()
|
||||||
|
const allTorrents = files.every(TorrentPlayer.isTorrent)
|
||||||
|
const allSubtitles = files.every(controllers.subtitles().isSubtitle)
|
||||||
|
|
||||||
if (allTorrents) {
|
if (allTorrents) {
|
||||||
// Drop torrents onto the app: go to home screen, add torrents, no matter what
|
// Drop torrents onto the app: go to home screen, add torrents, no matter what
|
||||||
dispatch('backToList')
|
dispatch('backToList')
|
||||||
// All .torrent files? Add them.
|
// All .torrent files? Add them.
|
||||||
files.forEach((file) => controllers.torrentList.addTorrent(file))
|
files.forEach((file) => controllers.torrentList().addTorrent(file))
|
||||||
} else if (url === 'player' && allSubtitles) {
|
} else if (url === 'player' && allSubtitles) {
|
||||||
// Drop subtitles onto a playing video: add subtitles
|
// Drop subtitles onto a playing video: add subtitles
|
||||||
controllers.subtitles.addSubtitles(files, true)
|
controllers.subtitles().addSubtitles(files, true)
|
||||||
} else if (url === 'home') {
|
} else if (url === 'home') {
|
||||||
// Drop files onto home screen: show Create Torrent
|
// Drop files onto home screen: show Create Torrent
|
||||||
state.modal = null
|
state.modal = null
|
||||||
controllers.torrentList.showCreateTorrent(files)
|
controllers.torrentList().showCreateTorrent(files)
|
||||||
} else {
|
} else {
|
||||||
// Drop files onto any other screen: show error
|
// Drop files onto any other screen: show error
|
||||||
return onError('Please go back to the torrent list before creating a new torrent.')
|
return onError('Please go back to the torrent list before creating a new torrent.')
|
||||||
@@ -406,15 +502,39 @@ function onError (err) {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPaste (e) {
|
const editableHtmlTags = new Set(['input', 'textarea'])
|
||||||
if (e.target.tagName.toLowerCase() === 'input') return
|
|
||||||
|
|
||||||
var torrentIds = electron.clipboard.readText().split('\n')
|
function onPaste (e) {
|
||||||
torrentIds.forEach(function (torrentId) {
|
if (editableHtmlTags.has(e.target.tagName.toLowerCase())) return
|
||||||
torrentId = torrentId.trim()
|
controllers.torrentList().addTorrent(electron.clipboard.readText())
|
||||||
if (torrentId.length === 0) return
|
|
||||||
controllers.torrentList.addTorrent(torrentId)
|
update()
|
||||||
})
|
}
|
||||||
|
|
||||||
|
function onKeydown (e) {
|
||||||
|
const key = e.key
|
||||||
|
|
||||||
|
if (key === 'ArrowLeft') {
|
||||||
|
dispatch('skip', -5)
|
||||||
|
} else if (key === 'ArrowRight') {
|
||||||
|
dispatch('skip', 5)
|
||||||
|
} else if (key === 'ArrowUp') {
|
||||||
|
dispatch('changeVolume', 0.1)
|
||||||
|
} else if (key === 'ArrowDown') {
|
||||||
|
dispatch('changeVolume', -0.1)
|
||||||
|
} else if (key === 'j') {
|
||||||
|
dispatch('skip', -10)
|
||||||
|
} else if (key === 'l') {
|
||||||
|
dispatch('skip', 10)
|
||||||
|
} else if (key === 'k') {
|
||||||
|
dispatch('playPause')
|
||||||
|
} else if (key === '>') {
|
||||||
|
dispatch('changePlaybackRate', 1)
|
||||||
|
} else if (key === '<') {
|
||||||
|
dispatch('changePlaybackRate', -1)
|
||||||
|
} else if (key === 'f') {
|
||||||
|
dispatch('toggleFullScreen')
|
||||||
|
}
|
||||||
|
|
||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
@@ -444,6 +564,13 @@ function onFullscreenChanged (e, isFullScreen) {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onWindowBoundsChanged (e, newBounds) {
|
||||||
|
if (state.location.url() !== 'player') {
|
||||||
|
state.saved.bounds = newBounds
|
||||||
|
dispatch('stateSave')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function checkDownloadPath () {
|
function checkDownloadPath () {
|
||||||
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
|
fs.stat(state.saved.prefs.downloadPath, function (err, stat) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
132
src/renderer/pages/app.js
Normal file
132
src/renderer/pages/app.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
const colors = require('material-ui/styles/colors')
|
||||||
|
const createGetter = require('fn-getter')
|
||||||
|
const React = require('react')
|
||||||
|
|
||||||
|
const darkBaseTheme = require('material-ui/styles/baseThemes/darkBaseTheme').default
|
||||||
|
const getMuiTheme = require('material-ui/styles/getMuiTheme').default
|
||||||
|
const MuiThemeProvider = require('material-ui/styles/MuiThemeProvider').default
|
||||||
|
|
||||||
|
const Header = require('../components/header')
|
||||||
|
|
||||||
|
// Perf optimization: Needed immediately, so do not lazy load it below
|
||||||
|
const TorrentListPage = require('./torrent-list-page')
|
||||||
|
|
||||||
|
const Views = {
|
||||||
|
home: createGetter(() => TorrentListPage),
|
||||||
|
player: createGetter(() => require('./player-page')),
|
||||||
|
'create-torrent': createGetter(() => require('./create-torrent-page')),
|
||||||
|
preferences: createGetter(() => require('./preferences-page'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modals = {
|
||||||
|
'open-torrent-address-modal': createGetter(
|
||||||
|
() => require('../components/open-torrent-address-modal')
|
||||||
|
),
|
||||||
|
'remove-torrent-modal': createGetter(() => require('../components/remove-torrent-modal')),
|
||||||
|
'update-available-modal': createGetter(() => require('../components/update-available-modal')),
|
||||||
|
'unsupported-media-modal': createGetter(() => require('../components/unsupported-media-modal'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontFamily = process.platform === 'win32'
|
||||||
|
? '"Segoe UI", sans-serif'
|
||||||
|
: 'BlinkMacSystemFont, "Helvetica Neue", Helvetica, sans-serif'
|
||||||
|
|
||||||
|
darkBaseTheme.fontFamily = fontFamily
|
||||||
|
darkBaseTheme.userAgent = false
|
||||||
|
darkBaseTheme.palette.primary1Color = colors.grey50
|
||||||
|
darkBaseTheme.palette.primary2Color = colors.grey50
|
||||||
|
darkBaseTheme.palette.primary3Color = colors.grey600
|
||||||
|
darkBaseTheme.palette.accent1Color = colors.redA200
|
||||||
|
darkBaseTheme.palette.accent2Color = colors.redA400
|
||||||
|
darkBaseTheme.palette.accent3Color = colors.redA100
|
||||||
|
|
||||||
|
let darkMuiTheme
|
||||||
|
let lightMuiTheme
|
||||||
|
|
||||||
|
class App extends React.Component {
|
||||||
|
render () {
|
||||||
|
const state = this.props.state
|
||||||
|
|
||||||
|
// Hide player controls while playing video, if the mouse stays still for a while
|
||||||
|
// Never hide the controls when:
|
||||||
|
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||||
|
// * The video is paused
|
||||||
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
|
const hideControls = state.shouldHidePlayerControls()
|
||||||
|
|
||||||
|
const cls = [
|
||||||
|
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
||||||
|
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
||||||
|
]
|
||||||
|
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
||||||
|
if (state.window.isFocused) cls.push('is-focused')
|
||||||
|
if (hideControls) cls.push('hide-video-controls')
|
||||||
|
|
||||||
|
if (!darkMuiTheme) {
|
||||||
|
darkMuiTheme = getMuiTheme(darkBaseTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiThemeProvider muiTheme={darkMuiTheme}>
|
||||||
|
<div className={'app ' + cls.join(' ')}>
|
||||||
|
<Header state={state} />
|
||||||
|
{this.getErrorPopover()}
|
||||||
|
<div key='content' className='content'>{this.getView()}</div>
|
||||||
|
{this.getModal()}
|
||||||
|
</div>
|
||||||
|
</MuiThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrorPopover () {
|
||||||
|
const state = this.props.state
|
||||||
|
const now = new Date().getTime()
|
||||||
|
const recentErrors = state.errors.filter((x) => now - x.time < 5000)
|
||||||
|
const hasErrors = recentErrors.length > 0
|
||||||
|
|
||||||
|
const errorElems = recentErrors.map(function (error, i) {
|
||||||
|
return (<div key={i} className='error'>{error.message}</div>)
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key='errors'
|
||||||
|
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}
|
||||||
|
>
|
||||||
|
<div key='title' className='title'>Error</div>
|
||||||
|
{errorElems}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getModal () {
|
||||||
|
const state = this.props.state
|
||||||
|
if (!state.modal) return
|
||||||
|
|
||||||
|
if (!lightMuiTheme) {
|
||||||
|
const lightBaseTheme = require('material-ui/styles/baseThemes/lightBaseTheme').default
|
||||||
|
lightBaseTheme.fontFamily = fontFamily
|
||||||
|
lightBaseTheme.userAgent = false
|
||||||
|
lightMuiTheme = getMuiTheme(lightBaseTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalContents = Modals[state.modal.id]()
|
||||||
|
return (
|
||||||
|
<MuiThemeProvider muiTheme={lightMuiTheme}>
|
||||||
|
<div key='modal' className='modal'>
|
||||||
|
<div key='modal-background' className='modal-background' />
|
||||||
|
<div key='modal-content' className='modal-content'>
|
||||||
|
<ModalContents state={state} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MuiThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getView () {
|
||||||
|
const state = this.props.state
|
||||||
|
const View = Views[state.location.url()]()
|
||||||
|
return (<View state={state} />)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = App
|
||||||
227
src/renderer/pages/create-torrent-page.js
Normal file
227
src/renderer/pages/create-torrent-page.js
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
const createTorrent = require('create-torrent')
|
||||||
|
const path = require('path')
|
||||||
|
const prettyBytes = require('prettier-bytes')
|
||||||
|
const React = require('react')
|
||||||
|
|
||||||
|
const { dispatch, dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
|
const FlatButton = require('material-ui/FlatButton').default
|
||||||
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
const TextField = require('material-ui/TextField').default
|
||||||
|
const Checkbox = require('material-ui/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.
|
||||||
|
// Includes a "Show Advanced..." button and more advanced UI.
|
||||||
|
class CreateTorrentPage extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
const state = this.props.state
|
||||||
|
const info = state.location.current()
|
||||||
|
|
||||||
|
// First, extract the base folder that the files are all in
|
||||||
|
let pathPrefix = info.folderPath
|
||||||
|
if (!pathPrefix) {
|
||||||
|
pathPrefix = info.files.map((x) => x.path).reduce(findCommonPrefix)
|
||||||
|
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
||||||
|
pathPrefix = path.dirname(pathPrefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, exclude .DS_Store and other dotfiles
|
||||||
|
const files = info.files
|
||||||
|
.filter((f) => !containsDots(f.path, pathPrefix))
|
||||||
|
.map((f) => ({ name: f.name, path: f.path, size: f.size }))
|
||||||
|
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
|
||||||
|
|
||||||
|
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
||||||
|
// as the default name. Show all files relative to the base folder.
|
||||||
|
let defaultName, basePath
|
||||||
|
if (files.length === 1) {
|
||||||
|
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
|
||||||
|
defaultName = files[0].name
|
||||||
|
basePath = pathPrefix
|
||||||
|
} else {
|
||||||
|
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a'
|
||||||
|
defaultName = path.basename(pathPrefix)
|
||||||
|
basePath = path.dirname(pathPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default trackers
|
||||||
|
const trackers = createTorrent.announceList.join('\n')
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
comment: '',
|
||||||
|
isPrivate: false,
|
||||||
|
pathPrefix,
|
||||||
|
basePath,
|
||||||
|
defaultName,
|
||||||
|
files,
|
||||||
|
trackers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create React event handlers only once
|
||||||
|
this.handleSetIsPrivate = (_, isPrivate) => this.setState({ isPrivate })
|
||||||
|
this.handleSetComment = (_, comment) => this.setState({ comment })
|
||||||
|
this.handleSetTrackers = (_, trackers) => this.setState({ trackers })
|
||||||
|
this.handleSubmit = handleSubmit.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const files = this.state.files
|
||||||
|
|
||||||
|
// Sanity check: show the number of files and total size
|
||||||
|
const numFiles = files.length
|
||||||
|
const totalBytes = files
|
||||||
|
.map((f) => f.size)
|
||||||
|
.reduce((a, b) => a + b, 0)
|
||||||
|
const torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='create-torrent'>
|
||||||
|
<Heading level={1}>Create torrent {this.state.defaultName}</Heading>
|
||||||
|
<div className='torrent-info'>{torrentInfo}</div>
|
||||||
|
<div className='torrent-attribute'>
|
||||||
|
<label>Path:</label>
|
||||||
|
<div>{this.state.pathPrefix}</div>
|
||||||
|
</div>
|
||||||
|
<ShowMore
|
||||||
|
style={{
|
||||||
|
marginBottom: 10
|
||||||
|
}}
|
||||||
|
hideLabel='Hide advanced settings...'
|
||||||
|
showLabel='Show advanced settings...'
|
||||||
|
>
|
||||||
|
{this.renderAdvanced()}
|
||||||
|
</ShowMore>
|
||||||
|
<div className='float-right'>
|
||||||
|
<FlatButton
|
||||||
|
className='control cancel'
|
||||||
|
label='Cancel'
|
||||||
|
style={{
|
||||||
|
marginRight: 10
|
||||||
|
}}
|
||||||
|
onClick={dispatcher('cancel')}
|
||||||
|
/>
|
||||||
|
<RaisedButton
|
||||||
|
className='control create-torrent-button'
|
||||||
|
label='Create Torrent'
|
||||||
|
primary
|
||||||
|
onClick={this.handleSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders everything after clicking Show Advanced...:
|
||||||
|
// * Is Private? (private torrents, not announced to DHT)
|
||||||
|
// * Announce list (trackers)
|
||||||
|
// * Comment
|
||||||
|
renderAdvanced () {
|
||||||
|
// Create file list
|
||||||
|
const maxFileElems = 100
|
||||||
|
const files = this.state.files
|
||||||
|
const fileElems = files.slice(0, maxFileElems).map((file, i) => {
|
||||||
|
const relativePath = path.relative(this.state.pathPrefix, file.path)
|
||||||
|
return (<div key={i}>{relativePath}</div>)
|
||||||
|
})
|
||||||
|
if (files.length > maxFileElems) {
|
||||||
|
fileElems.push(<div key='more'>+ {files.length - maxFileElems} more</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align the text fields
|
||||||
|
const textFieldStyle = { width: '' }
|
||||||
|
const textareaStyle = { margin: 0 }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key='advanced' className='create-torrent-advanced'>
|
||||||
|
<div key='private' className='torrent-attribute'>
|
||||||
|
<label>Private:</label>
|
||||||
|
<Checkbox
|
||||||
|
className='torrent-is-private control'
|
||||||
|
style={{ display: '' }}
|
||||||
|
checked={this.state.isPrivate}
|
||||||
|
onCheck={this.handleSetIsPrivate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div key='trackers' className='torrent-attribute'>
|
||||||
|
<label>Trackers:</label>
|
||||||
|
<TextField
|
||||||
|
className='torrent-trackers control'
|
||||||
|
style={textFieldStyle}
|
||||||
|
textareaStyle={textareaStyle}
|
||||||
|
multiLine
|
||||||
|
rows={2}
|
||||||
|
rowsMax={10}
|
||||||
|
value={this.state.trackers}
|
||||||
|
onChange={this.handleSetTrackers}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div key='comment' className='torrent-attribute'>
|
||||||
|
<label>Comment:</label>
|
||||||
|
<TextField
|
||||||
|
className='torrent-comment control'
|
||||||
|
style={textFieldStyle}
|
||||||
|
textareaStyle={textareaStyle}
|
||||||
|
hintText='Optionally describe your torrent...'
|
||||||
|
multiLine
|
||||||
|
rows={2}
|
||||||
|
rowsMax={10}
|
||||||
|
value={this.state.comment}
|
||||||
|
onChange={this.handleSetComment}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div key='files' className='torrent-attribute'>
|
||||||
|
<label>Files:</label>
|
||||||
|
<div>{fileElems}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit () {
|
||||||
|
const announceList = this.state.trackers
|
||||||
|
.split('\n')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s !== '')
|
||||||
|
const options = {
|
||||||
|
// We can't let the user choose their own name if we want WebTorrent
|
||||||
|
// to use the files in place rather than creating a new folder.
|
||||||
|
name: this.state.defaultName,
|
||||||
|
path: this.state.basePath,
|
||||||
|
files: this.state.files,
|
||||||
|
announce: announceList,
|
||||||
|
comment: this.state.comment.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If torrent is not private, leave private flag unset. This ensures that
|
||||||
|
// the torrent info hash will match the result generated by other tools,
|
||||||
|
// including webtorrent-cli.
|
||||||
|
if (this.state.isPrivate) options.private = true
|
||||||
|
|
||||||
|
dispatch('createTorrent', options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds the longest common prefix
|
||||||
|
function findCommonPrefix (a, b) {
|
||||||
|
let i
|
||||||
|
for (i = 0; i < a.length && i < b.length; i++) {
|
||||||
|
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
||||||
|
}
|
||||||
|
if (i === a.length) return a
|
||||||
|
if (i === b.length) return b
|
||||||
|
return a.substring(0, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function containsDots (path, pathPrefix) {
|
||||||
|
const suffix = path.substring(pathPrefix.length).replace(/\\/g, '/')
|
||||||
|
return ('/' + suffix).includes('/.')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CreateTorrentPage
|
||||||
@@ -1,29 +1,42 @@
|
|||||||
|
/* global HTMLMediaElement */
|
||||||
|
|
||||||
const React = require('react')
|
const React = require('react')
|
||||||
const Bitfield = require('bitfield')
|
const Bitfield = require('bitfield')
|
||||||
const prettyBytes = require('prettier-bytes')
|
const prettyBytes = require('prettier-bytes')
|
||||||
const zeroFill = require('zero-fill')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
const Playlist = require('../lib/playlist')
|
||||||
|
const { dispatch, dispatcher } = require('../lib/dispatcher')
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||||
module.exports = class Player extends React.Component {
|
module.exports = 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
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
var showVideo = state.playing.location === 'local'
|
const showVideo = state.playing.location === 'local'
|
||||||
|
const showControls = state.playing.location !== 'external'
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='player'
|
className='player'
|
||||||
onWheel={handleVolumeWheel}
|
onWheel={handleVolumeWheel}
|
||||||
onMouseMove={dispatcher('mediaMouseMoved')}>
|
onMouseMove={dispatcher('mediaMouseMoved')}
|
||||||
|
>
|
||||||
{showVideo ? renderMedia(state) : renderCastScreen(state)}
|
{showVideo ? renderMedia(state) : renderCastScreen(state)}
|
||||||
{renderPlayerControls(state)}
|
{showControls ? renderPlayerControls(state) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onComponentWillUnmount () {
|
||||||
|
// Unload the media element so that Chromium stops trying to fetch data
|
||||||
|
const tag = document.querySelector('audio,video')
|
||||||
|
if (!tag) return
|
||||||
|
tag.pause()
|
||||||
|
tag.src = ''
|
||||||
|
tag.load()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles volume change by wheel
|
// Handles volume change by wheel
|
||||||
@@ -37,7 +50,7 @@ function renderMedia (state) {
|
|||||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||||
// Instead, grab the DOM node and play/pause it if necessary
|
// Instead, grab the DOM node and play/pause it if necessary
|
||||||
// Get the <video> or <audio> tag
|
// Get the <video> or <audio> tag
|
||||||
var mediaElement = document.querySelector(state.playing.type)
|
const mediaElement = document.querySelector(state.playing.type)
|
||||||
if (mediaElement !== null) {
|
if (mediaElement !== null) {
|
||||||
if (state.playing.isPaused && !mediaElement.paused) {
|
if (state.playing.isPaused && !mediaElement.paused) {
|
||||||
mediaElement.pause()
|
mediaElement.pause()
|
||||||
@@ -65,20 +78,20 @@ function renderMedia (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Switch to the newly added subtitle track, if available
|
// Switch to the newly added subtitle track, if available
|
||||||
var tracks = mediaElement.textTracks || []
|
const tracks = mediaElement.textTracks || []
|
||||||
for (var j = 0; j < tracks.length; j++) {
|
for (let j = 0; j < tracks.length; j++) {
|
||||||
var isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
const isSelectedTrack = j === state.playing.subtitles.selectedIndex
|
||||||
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
|
tracks[j].mode = isSelectedTrack ? 'showing' : 'hidden'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save video position
|
// Save video position
|
||||||
var file = state.getPlayingFileSummary()
|
const file = state.getPlayingFileSummary()
|
||||||
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
file.currentTime = state.playing.currentTime = mediaElement.currentTime
|
||||||
file.duration = state.playing.duration = mediaElement.duration
|
file.duration = state.playing.duration = mediaElement.duration
|
||||||
|
|
||||||
// Save selected subtitle
|
// Save selected subtitle
|
||||||
if (state.playing.subtitles.selectedIndex !== -1) {
|
if (state.playing.subtitles.selectedIndex !== -1) {
|
||||||
var index = state.playing.subtitles.selectedIndex
|
const index = state.playing.subtitles.selectedIndex
|
||||||
file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath
|
file.selectedSubtitle = state.playing.subtitles.tracks[index].filePath
|
||||||
} else if (file.selectedSubtitle != null) {
|
} else if (file.selectedSubtitle != null) {
|
||||||
delete file.selectedSubtitle
|
delete file.selectedSubtitle
|
||||||
@@ -88,27 +101,28 @@ function renderMedia (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add subtitles to the <video> tag
|
// Add subtitles to the <video> tag
|
||||||
var trackTags = []
|
const trackTags = []
|
||||||
if (state.playing.subtitles.selectedIndex >= 0) {
|
if (state.playing.subtitles.selectedIndex >= 0) {
|
||||||
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
for (let i = 0; i < state.playing.subtitles.tracks.length; i++) {
|
||||||
var track = state.playing.subtitles.tracks[i]
|
const track = state.playing.subtitles.tracks[i]
|
||||||
var isSelected = state.playing.subtitles.selectedIndex === i
|
const isSelected = state.playing.subtitles.selectedIndex === i
|
||||||
trackTags.push(
|
trackTags.push(
|
||||||
<track
|
<track
|
||||||
key={i}
|
key={i}
|
||||||
default={isSelected ? 'default' : ''}
|
default={isSelected ? 'default' : ''}
|
||||||
label={track.label}
|
label={track.label}
|
||||||
type='subtitles'
|
type='subtitles'
|
||||||
src={track.buffer} />
|
src={track.buffer}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the <audio> or <video> tag
|
// Create the <audio> or <video> tag
|
||||||
var MediaTagName = state.playing.type
|
const MediaTagName = state.playing.type
|
||||||
var mediaTag = (
|
const mediaTag = (
|
||||||
<MediaTagName
|
<MediaTagName
|
||||||
src={state.server.localURL}
|
src={Playlist.getCurrentLocalURL(state)}
|
||||||
onDoubleClick={dispatcher('toggleFullScreen')}
|
onDoubleClick={dispatcher('toggleFullScreen')}
|
||||||
onLoadedMetadata={onLoadedMetadata}
|
onLoadedMetadata={onLoadedMetadata}
|
||||||
onEnded={onEnded}
|
onEnded={onEnded}
|
||||||
@@ -116,7 +130,8 @@ function renderMedia (state) {
|
|||||||
onError={dispatcher('mediaError')}
|
onError={dispatcher('mediaError')}
|
||||||
onTimeUpdate={dispatcher('mediaTimeUpdate')}
|
onTimeUpdate={dispatcher('mediaTimeUpdate')}
|
||||||
onEncrypted={dispatcher('mediaEncrypted')}
|
onEncrypted={dispatcher('mediaEncrypted')}
|
||||||
onCanPlay={onCanPlay}>
|
onCanPlay={onCanPlay}
|
||||||
|
>
|
||||||
{trackTags}
|
{trackTags}
|
||||||
</MediaTagName>
|
</MediaTagName>
|
||||||
)
|
)
|
||||||
@@ -126,7 +141,8 @@ function renderMedia (state) {
|
|||||||
<div
|
<div
|
||||||
key='letterbox'
|
key='letterbox'
|
||||||
className='letterbox'
|
className='letterbox'
|
||||||
onMouseMove={dispatcher('mediaMouseMoved')}>
|
onMouseMove={dispatcher('mediaMouseMoved')}
|
||||||
|
>
|
||||||
{mediaTag}
|
{mediaTag}
|
||||||
{renderOverlay(state)}
|
{renderOverlay(state)}
|
||||||
</div>
|
</div>
|
||||||
@@ -135,21 +151,27 @@ function renderMedia (state) {
|
|||||||
// As soon as we know the video dimensions, resize the window
|
// As soon as we know the video dimensions, resize the window
|
||||||
function onLoadedMetadata (e) {
|
function onLoadedMetadata (e) {
|
||||||
if (state.playing.type !== 'video') return
|
if (state.playing.type !== 'video') return
|
||||||
var video = e.target
|
const video = e.target
|
||||||
var dimensions = {
|
const dimensions = {
|
||||||
width: video.videoWidth,
|
width: video.videoWidth,
|
||||||
height: video.videoHeight
|
height: video.videoHeight
|
||||||
}
|
}
|
||||||
dispatch('setDimensions', dimensions)
|
dispatch('setDimensions', dimensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the video completes, pause the video instead of looping
|
|
||||||
function onEnded (e) {
|
function onEnded (e) {
|
||||||
state.playing.isPaused = true
|
if (Playlist.hasNext(state)) {
|
||||||
|
dispatch('nextTrack')
|
||||||
|
} else {
|
||||||
|
// When the last video completes, pause the video instead of looping
|
||||||
|
state.playing.isPaused = true
|
||||||
|
if (state.window.isFullScreen) dispatch('toggleFullScreen')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCanPlay (e) {
|
function onCanPlay (e) {
|
||||||
var elem = e.target
|
const elem = e.target
|
||||||
|
if (elem.readyState < HTMLMediaElement.HAVE_FUTURE_DATA) return
|
||||||
if (state.playing.type === 'video' &&
|
if (state.playing.type === 'video' &&
|
||||||
elem.webkitVideoDecodedByteCount === 0) {
|
elem.webkitVideoDecodedByteCount === 0) {
|
||||||
dispatch('mediaError', 'Video codec unsupported')
|
dispatch('mediaError', 'Video codec unsupported')
|
||||||
@@ -163,15 +185,15 @@ function renderMedia (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderOverlay (state) {
|
function renderOverlay (state) {
|
||||||
var elems = []
|
const elems = []
|
||||||
var audioMetadataElem = renderAudioMetadata(state)
|
const audioMetadataElem = renderAudioMetadata(state)
|
||||||
var spinnerElem = renderLoadingSpinner(state)
|
const spinnerElem = renderLoadingSpinner(state)
|
||||||
if (audioMetadataElem) elems.push(audioMetadataElem)
|
if (audioMetadataElem) elems.push(audioMetadataElem)
|
||||||
if (spinnerElem) elems.push(spinnerElem)
|
if (spinnerElem) elems.push(spinnerElem)
|
||||||
|
|
||||||
// Video fills the window, centered with black bars if necessary
|
// Video fills the window, centered with black bars if necessary
|
||||||
// Audio gets a static poster image and a summary of the file metadata.
|
// Audio gets a static poster image and a summary of the file metadata.
|
||||||
var style
|
let style
|
||||||
if (state.playing.type === 'audio') {
|
if (state.playing.type === 'audio') {
|
||||||
style = { backgroundImage: cssBackgroundImagePoster(state) }
|
style = { backgroundImage: cssBackgroundImagePoster(state) }
|
||||||
} else if (elems.length !== 0) {
|
} else if (elems.length !== 0) {
|
||||||
@@ -188,28 +210,41 @@ function renderOverlay (state) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render track or disk number string
|
||||||
|
* @param common metadata.common part
|
||||||
|
* @param key should be either 'track' or 'disk'
|
||||||
|
* @return track or disk number metadata as JSX block
|
||||||
|
*/
|
||||||
|
function renderTrack (common, key) {
|
||||||
|
// Audio metadata: track-number
|
||||||
|
if (common[key] && common[key].no) {
|
||||||
|
let str = `${common[key].no}`
|
||||||
|
if (common[key].of) {
|
||||||
|
str += ` of ${common[key].of}`
|
||||||
|
}
|
||||||
|
const style = { textTransform: 'capitalize' }
|
||||||
|
return (
|
||||||
|
<div className={`audio-${key}`}>
|
||||||
|
<label style={style}>{key}</label> {str}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderAudioMetadata (state) {
|
function renderAudioMetadata (state) {
|
||||||
var fileSummary = state.getPlayingFileSummary()
|
const fileSummary = state.getPlayingFileSummary()
|
||||||
if (!fileSummary.audioInfo) return
|
if (!fileSummary.audioInfo) return
|
||||||
var info = fileSummary.audioInfo
|
const common = fileSummary.audioInfo.common || {}
|
||||||
|
|
||||||
// Get audio track info
|
// Get audio track info
|
||||||
var title = info.title
|
const title = common.title ? common.title : fileSummary.name
|
||||||
if (!title) {
|
|
||||||
title = fileSummary.name
|
|
||||||
}
|
|
||||||
var artist = info.artist && info.artist[0]
|
|
||||||
var album = info.album
|
|
||||||
if (album && info.year && !album.includes(info.year)) {
|
|
||||||
album += ' (' + info.year + ')'
|
|
||||||
}
|
|
||||||
var track
|
|
||||||
if (info.track && info.track.no && info.track.of) {
|
|
||||||
track = info.track.no + ' of ' + info.track.of
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show a small info box in the middle of the screen with title/album/etc
|
// Show a small info box in the middle of the screen with title/album/etc
|
||||||
var elems = []
|
const elems = []
|
||||||
|
|
||||||
|
// Audio metadata: artist(s)
|
||||||
|
const artist = common.artist || common.albumartist
|
||||||
if (artist) {
|
if (artist) {
|
||||||
elems.push((
|
elems.push((
|
||||||
<div key='artist' className='audio-artist'>
|
<div key='artist' className='audio-artist'>
|
||||||
@@ -217,23 +252,96 @@ function renderAudioMetadata (state) {
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (album) {
|
|
||||||
|
// Audio metadata: disk & track-number
|
||||||
|
const count = ['track', 'disk']
|
||||||
|
count.forEach(key => {
|
||||||
|
const nrElem = renderTrack(common, key)
|
||||||
|
if (nrElem) {
|
||||||
|
elems.push(nrElem)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Audio metadata: album
|
||||||
|
if (common.album) {
|
||||||
elems.push((
|
elems.push((
|
||||||
<div key='album' className='audio-album'>
|
<div key='album' className='audio-album'>
|
||||||
<label>Album</label>{album}
|
<label>Album</label>{common.album}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (track) {
|
|
||||||
|
// Audio metadata: year
|
||||||
|
if (common.year) {
|
||||||
elems.push((
|
elems.push((
|
||||||
<div key='track' className='audio-track'>
|
<div key='year' className='audio-year'>
|
||||||
<label>Track</label>{track}
|
<label>Year</label>{common.year}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio metadata: release information (label & catalog-number)
|
||||||
|
if (common.label || common.catalognumber) {
|
||||||
|
const releaseInfo = []
|
||||||
|
if (common.label && common.catalognumber &&
|
||||||
|
common.label.length === common.catalognumber.length) {
|
||||||
|
// Assume labels & catalog-numbers are pairs
|
||||||
|
for (let n = 0; n < common.label.length; ++n) {
|
||||||
|
releaseInfo.push(common.label[0] + ' / ' + common.catalognumber[n])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (common.label) {
|
||||||
|
releaseInfo.push(...common.label)
|
||||||
|
}
|
||||||
|
if (common.catalognumber) {
|
||||||
|
releaseInfo.push(...common.catalognumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elems.push((
|
||||||
|
<div key='release' className='audio-release'>
|
||||||
|
<label>Release</label>{releaseInfo.join(', ')}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio metadata: format
|
||||||
|
const format = []
|
||||||
|
fileSummary.audioInfo.format = fileSummary.audioInfo.format || ''
|
||||||
|
if (fileSummary.audioInfo.format.container) {
|
||||||
|
format.push(fileSummary.audioInfo.format.container)
|
||||||
|
}
|
||||||
|
if (fileSummary.audioInfo.format.codec &&
|
||||||
|
fileSummary.audioInfo.format.container !== fileSummary.audioInfo.format.codec) {
|
||||||
|
format.push(fileSummary.audioInfo.format.codec)
|
||||||
|
}
|
||||||
|
if (fileSummary.audioInfo.format.bitrate) {
|
||||||
|
format.push(Math.round(fileSummary.audioInfo.format.bitrate / 1000) + ' kbps') // 128 kbps
|
||||||
|
}
|
||||||
|
if (fileSummary.audioInfo.format.sampleRate) {
|
||||||
|
format.push(Math.round(fileSummary.audioInfo.format.sampleRate / 100) / 10 + ' kHz')
|
||||||
|
}
|
||||||
|
if (fileSummary.audioInfo.format.bitsPerSample) {
|
||||||
|
format.push(fileSummary.audioInfo.format.bitsPerSample + ' bit')
|
||||||
|
}
|
||||||
|
if (format.length > 0) {
|
||||||
|
elems.push((
|
||||||
|
<div key='format' className='audio-format'>
|
||||||
|
<label>Format</label>{format.join(', ')}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio metadata: comments
|
||||||
|
if (common.comment) {
|
||||||
|
elems.push((
|
||||||
|
<div key='comments' className='audio-comments'>
|
||||||
|
<label>Comments</label>{common.comment.join(' / ')}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Align the title with the other info, if available. Otherwise, center title
|
// Align the title with the other info, if available. Otherwise, center title
|
||||||
var emptyLabel = (<label />)
|
const emptyLabel = (<label />)
|
||||||
elems.unshift((
|
elems.unshift((
|
||||||
<div key='title' className='audio-title'>
|
<div key='title' className='audio-title'>
|
||||||
{elems.length ? emptyLabel : undefined}{title}
|
{elems.length ? emptyLabel : undefined}{title}
|
||||||
@@ -245,31 +353,31 @@ function renderAudioMetadata (state) {
|
|||||||
|
|
||||||
function renderLoadingSpinner (state) {
|
function renderLoadingSpinner (state) {
|
||||||
if (state.playing.isPaused) return
|
if (state.playing.isPaused) return
|
||||||
var isProbablyStalled = state.playing.isStalled ||
|
const isProbablyStalled = state.playing.isStalled ||
|
||||||
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
(new Date().getTime() - state.playing.lastTimeUpdate > 2000)
|
||||||
if (!isProbablyStalled) return
|
if (!isProbablyStalled) return
|
||||||
|
|
||||||
var prog = state.getPlayingTorrentSummary().progress || {}
|
const prog = state.getPlayingTorrentSummary().progress || {}
|
||||||
var fileProgress = 0
|
let fileProgress = 0
|
||||||
if (prog.files) {
|
if (prog.files) {
|
||||||
var file = prog.files[state.playing.fileIndex]
|
const file = prog.files[state.playing.fileIndex]
|
||||||
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
|
fileProgress = Math.floor(100 * file.numPiecesPresent / file.numPieces)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='loading' className='media-stalled'>
|
<div key='loading' className='media-stalled'>
|
||||||
<div key='loading-spinner' className='loading-spinner'> </div>
|
<div key='loading-spinner' className='loading-spinner' />
|
||||||
<div key='loading-progress' className='loading-status ellipsis'>
|
<div key='loading-progress' className='loading-status ellipsis'>
|
||||||
<span className='progress'>{fileProgress}%</span> downloaded,
|
<span><span className='progress'>{fileProgress}%</span> downloaded</span>
|
||||||
<span>↓ {prettyBytes(prog.downloadSpeed || 0)}/s</span>
|
<span> ↓ {prettyBytes(prog.downloadSpeed || 0)}/s</span>
|
||||||
<span>↑ {prettyBytes(prog.uploadSpeed || 0)}/s</span>
|
<span> ↑ {prettyBytes(prog.uploadSpeed || 0)}/s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCastScreen (state) {
|
function renderCastScreen (state) {
|
||||||
var castIcon, castType, isCast
|
let castIcon, castType, isCast
|
||||||
if (state.playing.location.startsWith('chromecast')) {
|
if (state.playing.location.startsWith('chromecast')) {
|
||||||
castIcon = 'cast_connected'
|
castIcon = 'cast_connected'
|
||||||
castType = 'Chromecast'
|
castType = 'Chromecast'
|
||||||
@@ -283,27 +391,24 @@ function renderCastScreen (state) {
|
|||||||
castType = 'DLNA'
|
castType = 'DLNA'
|
||||||
isCast = true
|
isCast = true
|
||||||
} else if (state.playing.location === 'external') {
|
} else if (state.playing.location === 'external') {
|
||||||
// TODO: get the player name in a more reliable way
|
|
||||||
var playerPath = state.saved.prefs.externalPlayerPath
|
|
||||||
var playerName = playerPath ? path.basename(playerPath).split('.')[0] : 'VLC'
|
|
||||||
castIcon = 'tv'
|
castIcon = 'tv'
|
||||||
castType = playerName
|
castType = state.getExternalPlayerName()
|
||||||
isCast = false
|
isCast = false
|
||||||
} else if (state.playing.location === 'error') {
|
} else if (state.playing.location === 'error') {
|
||||||
castIcon = 'error_outline'
|
castIcon = 'error_outline'
|
||||||
castType = 'Error'
|
castType = 'Unable to Play'
|
||||||
isCast = false
|
isCast = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var isStarting = state.playing.location.endsWith('-pending')
|
const isStarting = state.playing.location.endsWith('-pending')
|
||||||
var castName = state.playing.castName
|
const castName = state.playing.castName
|
||||||
var 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 = ''
|
||||||
|
|
||||||
// Show a nice title image, if possible
|
// Show a nice title image, if possible
|
||||||
var style = {
|
const style = {
|
||||||
backgroundImage: cssBackgroundImagePoster(state)
|
backgroundImage: cssBackgroundImagePoster(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,15 +426,16 @@ function renderCastScreen (state) {
|
|||||||
function renderCastOptions (state) {
|
function renderCastOptions (state) {
|
||||||
if (!state.devices.castMenu) return
|
if (!state.devices.castMenu) return
|
||||||
|
|
||||||
var {location, devices} = state.devices.castMenu
|
const { location, devices } = state.devices.castMenu
|
||||||
var player = state.devices[location]
|
const player = state.devices[location]
|
||||||
|
|
||||||
var items = devices.map(function (device, ix) {
|
const items = devices.map(function (device, ix) {
|
||||||
var isSelected = player.device === device
|
const isSelected = player.device === device
|
||||||
var name = device.name
|
const name = device.name
|
||||||
return (
|
return (
|
||||||
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
|
<li key={ix} onClick={dispatcher('selectCastDevice', ix)}>
|
||||||
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
<i className='icon'>{isSelected ? 'radio_button_checked' : 'radio_button_unchecked'}</i>
|
||||||
|
{' '}
|
||||||
{name}
|
{name}
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
@@ -343,11 +449,11 @@ function renderCastOptions (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSubtitleOptions (state) {
|
function renderSubtitleOptions (state) {
|
||||||
var subtitles = state.playing.subtitles
|
const subtitles = state.playing.subtitles
|
||||||
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
if (!subtitles.tracks.length || !subtitles.showMenu) return
|
||||||
|
|
||||||
var items = subtitles.tracks.map(function (track, ix) {
|
const items = subtitles.tracks.map(function (track, ix) {
|
||||||
var 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)}>
|
||||||
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
<i className='icon'>{'radio_button_' + (isSelected ? 'checked' : 'unchecked')}</i>
|
||||||
@@ -356,8 +462,8 @@ function renderSubtitleOptions (state) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
var noneSelected = state.playing.subtitles.selectedIndex === -1
|
const noneSelected = state.playing.subtitles.selectedIndex === -1
|
||||||
var noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
const noneClass = 'radio_button_' + (noneSelected ? 'checked' : 'unchecked')
|
||||||
return (
|
return (
|
||||||
<ul key='subtitle-options' className='options-list'>
|
<ul key='subtitle-options' className='options-list'>
|
||||||
{items}
|
{items}
|
||||||
@@ -370,15 +476,17 @@ function renderSubtitleOptions (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderPlayerControls (state) {
|
function renderPlayerControls (state) {
|
||||||
var positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
const positionPercent = 100 * state.playing.currentTime / state.playing.duration
|
||||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
const playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 3px)' }
|
||||||
var captionsClass = state.playing.subtitles.tracks.length === 0
|
const captionsClass = state.playing.subtitles.tracks.length === 0
|
||||||
? 'disabled'
|
? 'disabled'
|
||||||
: state.playing.subtitles.selectedIndex >= 0
|
: state.playing.subtitles.selectedIndex >= 0
|
||||||
? 'active'
|
? 'active'
|
||||||
: ''
|
: ''
|
||||||
|
const prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
|
||||||
|
const nextClass = Playlist.hasNext(state) ? '' : 'disabled'
|
||||||
|
|
||||||
var elements = [
|
const elements = [
|
||||||
<div key='playback-bar' className='playback-bar'>
|
<div key='playback-bar' className='playback-bar'>
|
||||||
{renderLoadingBar(state)}
|
{renderLoadingBar(state)}
|
||||||
<div
|
<div
|
||||||
@@ -396,52 +504,71 @@ function renderPlayerControls (state) {
|
|||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
|
|
||||||
|
<i
|
||||||
|
key='skip-previous'
|
||||||
|
className={'icon skip-previous float-left ' + prevClass}
|
||||||
|
onClick={dispatcher('previousTrack')}
|
||||||
|
>
|
||||||
|
skip_previous
|
||||||
|
</i>,
|
||||||
|
|
||||||
<i
|
<i
|
||||||
key='play'
|
key='play'
|
||||||
className='icon play-pause float-left'
|
className='icon play-pause float-left'
|
||||||
onClick={dispatcher('playPause')}>
|
onClick={dispatcher('playPause')}
|
||||||
|
>
|
||||||
{state.playing.isPaused ? 'play_arrow' : 'pause'}
|
{state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||||
</i>,
|
</i>,
|
||||||
|
|
||||||
|
<i
|
||||||
|
key='skip-next'
|
||||||
|
className={'icon skip-next float-left ' + nextClass}
|
||||||
|
onClick={dispatcher('nextTrack')}
|
||||||
|
>
|
||||||
|
skip_next
|
||||||
|
</i>,
|
||||||
|
|
||||||
<i
|
<i
|
||||||
key='fullscreen'
|
key='fullscreen'
|
||||||
className='icon fullscreen float-right'
|
className='icon fullscreen float-right'
|
||||||
onClick={dispatcher('toggleFullScreen')}>
|
onClick={dispatcher('toggleFullScreen')}
|
||||||
|
>
|
||||||
{state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
{state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||||
</i>
|
</i>
|
||||||
]
|
]
|
||||||
|
|
||||||
if (state.playing.type === 'video') {
|
if (state.playing.type === 'video') {
|
||||||
// show closed captions icon
|
// Show closed captions icon
|
||||||
elements.push((
|
elements.push((
|
||||||
<i
|
<i
|
||||||
key='subtitles'
|
key='subtitles'
|
||||||
className={'icon closed-caption float-right ' + captionsClass}
|
className={'icon closed-caption float-right ' + captionsClass}
|
||||||
onClick={handleSubtitles}>
|
onClick={handleSubtitles}
|
||||||
|
>
|
||||||
closed_caption
|
closed_caption
|
||||||
</i>
|
</i>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||||
var castTypes = ['chromecast', 'airplay', 'dlna']
|
const castTypes = ['chromecast', 'airplay', 'dlna']
|
||||||
var isCastingAnywhere = castTypes.some(
|
const isCastingAnywhere = castTypes.some(
|
||||||
(castType) => state.playing.location.startsWith(castType))
|
(castType) => state.playing.location.startsWith(castType))
|
||||||
|
|
||||||
// Add the cast buttons. Icons for each cast type, connected/disconnected:
|
// Add the cast buttons. Icons for each cast type, connected/disconnected:
|
||||||
var buttonIcons = {
|
const buttonIcons = {
|
||||||
'chromecast': {true: 'cast_connected', false: 'cast'},
|
chromecast: { true: 'cast_connected', false: 'cast' },
|
||||||
'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(function (castType) {
|
||||||
// Do we show this button (eg. the Chromecast button) at all?
|
// Do we show this button (eg. the Chromecast button) at all?
|
||||||
var isCasting = state.playing.location.startsWith(castType)
|
const isCasting = state.playing.location.startsWith(castType)
|
||||||
var player = state.devices[castType]
|
const player = state.devices[castType]
|
||||||
if ((!player || player.getDevices().length === 0) && !isCasting) return
|
if ((!player || player.getDevices().length === 0) && !isCasting) return
|
||||||
|
|
||||||
// Show the button. Three options for eg the Chromecast button:
|
// Show the button. Three options for eg the Chromecast button:
|
||||||
var buttonClass, buttonHandler
|
let buttonClass, buttonHandler
|
||||||
if (isCasting) {
|
if (isCasting) {
|
||||||
// Option 1: we are currently connected to Chromecast. Button stops the cast.
|
// Option 1: we are currently connected to Chromecast. Button stops the cast.
|
||||||
buttonClass = 'active'
|
buttonClass = 'active'
|
||||||
@@ -455,38 +582,38 @@ function renderPlayerControls (state) {
|
|||||||
buttonClass = ''
|
buttonClass = ''
|
||||||
buttonHandler = dispatcher('toggleCastMenu', castType)
|
buttonHandler = dispatcher('toggleCastMenu', castType)
|
||||||
}
|
}
|
||||||
var buttonIcon = buttonIcons[castType][isCasting]
|
const buttonIcon = buttonIcons[castType][isCasting]
|
||||||
|
|
||||||
elements.push((
|
elements.push((
|
||||||
<i
|
<i
|
||||||
key={castType}
|
key={castType}
|
||||||
className={'icon device float-right ' + buttonClass}
|
className={'icon device float-right ' + buttonClass}
|
||||||
onClick={buttonHandler}>
|
onClick={buttonHandler}
|
||||||
|
>
|
||||||
{buttonIcon}
|
{buttonIcon}
|
||||||
</i>
|
</i>
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Render volume slider
|
// Render volume slider
|
||||||
var volume = state.playing.volume
|
const volume = state.playing.volume
|
||||||
var volumeIcon = 'volume_' + (
|
const volumeIcon = 'volume_' + (
|
||||||
volume === 0 ? 'off'
|
volume === 0 ? 'off'
|
||||||
: volume < 0.3 ? 'mute'
|
: volume < 0.3 ? 'mute'
|
||||||
: volume < 0.6 ? 'down'
|
: volume < 0.6 ? 'down'
|
||||||
: 'up')
|
: 'up')
|
||||||
var 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), ' +
|
||||||
'color-stop(' + (volume * 100) + '%, #727272))'
|
'color-stop(' + (volume * 100) + '%, #727272))'
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: dcposch change the range input to use value / onChanged instead of
|
|
||||||
// "readonly" / onMouse[Down,Move,Up]
|
|
||||||
elements.push((
|
elements.push((
|
||||||
<div key='volume' className='volume float-left'>
|
<div key='volume' className='volume float-left'>
|
||||||
<i
|
<i
|
||||||
className='icon volume-icon float-left'
|
className='icon volume-icon float-left'
|
||||||
onMouseDown={handleVolumeMute}>
|
onMouseDown={handleVolumeMute}
|
||||||
|
>
|
||||||
{volumeIcon}
|
{volumeIcon}
|
||||||
</i>
|
</i>
|
||||||
<input
|
<input
|
||||||
@@ -500,15 +627,15 @@ function renderPlayerControls (state) {
|
|||||||
))
|
))
|
||||||
|
|
||||||
// Show video playback progress
|
// Show video playback progress
|
||||||
var currentTimeStr = formatTime(state.playing.currentTime)
|
const currentTimeStr = formatTime(state.playing.currentTime, state.playing.duration)
|
||||||
var durationStr = formatTime(state.playing.duration)
|
const durationStr = formatTime(state.playing.duration, state.playing.duration)
|
||||||
elements.push((
|
elements.push((
|
||||||
<span key='time' className='time float-left'>
|
<span key='time' className='time float-left'>
|
||||||
{currentTimeStr} / {durationStr}
|
{currentTimeStr} / {durationStr}
|
||||||
</span>
|
</span>
|
||||||
))
|
))
|
||||||
|
|
||||||
// render playback rate
|
// Render playback rate
|
||||||
if (state.playing.playbackRate !== 1) {
|
if (state.playing.playbackRate !== 1) {
|
||||||
elements.push((
|
elements.push((
|
||||||
<span key='rate' className='rate float-left'>
|
<span key='rate' className='rate float-left'>
|
||||||
@@ -518,7 +645,11 @@ function renderPlayerControls (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='controls' className='controls'>
|
<div
|
||||||
|
key='controls' className='controls'
|
||||||
|
onMouseEnter={dispatcher('mediaControlsMouseEnter')}
|
||||||
|
onMouseLeave={dispatcher('mediaControlsMouseLeave')}
|
||||||
|
>
|
||||||
{elements}
|
{elements}
|
||||||
{renderCastOptions(state)}
|
{renderCastOptions(state)}
|
||||||
{renderSubtitleOptions(state)}
|
{renderSubtitleOptions(state)}
|
||||||
@@ -528,7 +659,7 @@ function renderPlayerControls (state) {
|
|||||||
function handleDragStart (e) {
|
function handleDragStart (e) {
|
||||||
// Prevent the cursor from changing, eg to a green + icon on Mac
|
// Prevent the cursor from changing, eg to a green + icon on Mac
|
||||||
if (e.dataTransfer) {
|
if (e.dataTransfer) {
|
||||||
var dt = e.dataTransfer
|
const dt = e.dataTransfer
|
||||||
dt.effectAllowed = 'none'
|
dt.effectAllowed = 'none'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,9 +668,9 @@ function renderPlayerControls (state) {
|
|||||||
function handleScrub (e) {
|
function handleScrub (e) {
|
||||||
if (!e.clientX) return
|
if (!e.clientX) return
|
||||||
dispatch('mediaMouseMoved')
|
dispatch('mediaMouseMoved')
|
||||||
var windowWidth = document.querySelector('body').clientWidth
|
const windowWidth = document.querySelector('body').clientWidth
|
||||||
var fraction = e.clientX / windowWidth
|
const fraction = e.clientX / windowWidth
|
||||||
var position = fraction * state.playing.duration /* seconds */
|
const position = fraction * state.playing.duration /* seconds */
|
||||||
dispatch('skipTo', position)
|
dispatch('skipTo', position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,20 +701,25 @@ function renderPlayerControls (state) {
|
|||||||
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
// Renders the loading bar. Shows which parts of the torrent are loaded, which
|
||||||
// can be 'spongey' / non-contiguous
|
// can be 'spongey' / non-contiguous
|
||||||
function renderLoadingBar (state) {
|
function renderLoadingBar (state) {
|
||||||
var torrentSummary = state.getPlayingTorrentSummary()
|
if (config.IS_TEST) return // Don't integration test the loading bar. Screenshots won't match.
|
||||||
|
|
||||||
|
const torrentSummary = state.getPlayingTorrentSummary()
|
||||||
if (!torrentSummary.progress) {
|
if (!torrentSummary.progress) {
|
||||||
return []
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all contiguous parts of the torrent which are loaded
|
// Find all contiguous parts of the torrent which are loaded
|
||||||
var prog = torrentSummary.progress
|
const prog = torrentSummary.progress
|
||||||
var fileProg = prog.files[state.playing.fileIndex]
|
const fileProg = prog.files[state.playing.fileIndex]
|
||||||
var parts = []
|
|
||||||
var lastPiecePresent = false
|
if (!fileProg) return null
|
||||||
for (var i = fileProg.startPiece; i <= fileProg.endPiece; i++) {
|
|
||||||
var partPresent = Bitfield.prototype.get.call(prog.bitfield, i)
|
const parts = []
|
||||||
|
let lastPiecePresent = false
|
||||||
|
for (let i = fileProg.startPiece; i <= fileProg.endPiece; 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) {
|
||||||
parts[parts.length - 1].count++
|
parts[parts.length - 1].count++
|
||||||
}
|
}
|
||||||
@@ -591,23 +727,24 @@ 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
|
||||||
var loadingBarElems = parts.map(function (part, i) {
|
const loadingBarElems = parts.map(function (part, i) {
|
||||||
var 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) + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div key={i} className='loading-bar-part' style={style} />)
|
return (<div key={i} className='loading-bar-part' style={style} />)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
|
return (<div key='loading-bar' className='loading-bar'>{loadingBarElems}</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the CSS background-image string for a poster image + dark vignette
|
// Returns the CSS background-image string for a poster image + dark vignette
|
||||||
function cssBackgroundImagePoster (state) {
|
function cssBackgroundImagePoster (state) {
|
||||||
var torrentSummary = state.getPlayingTorrentSummary()
|
const torrentSummary = state.getPlayingTorrentSummary()
|
||||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
if (!posterPath) return ''
|
if (!posterPath) return ''
|
||||||
return cssBackgroundImageDarkGradient() + `, url(${posterPath})`
|
return cssBackgroundImageDarkGradient() + `, url('${posterPath}')`
|
||||||
}
|
}
|
||||||
|
|
||||||
function cssBackgroundImageDarkGradient () {
|
function cssBackgroundImageDarkGradient () {
|
||||||
@@ -615,17 +752,19 @@ function cssBackgroundImageDarkGradient () {
|
|||||||
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
'rgba(0,0,0,0.4) 0%, rgba(0,0,0,1) 100%)'
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime (time) {
|
function formatTime (time, total) {
|
||||||
if (typeof time !== 'number' || Number.isNaN(time)) {
|
if (typeof time !== 'number' || Number.isNaN(time)) {
|
||||||
return '0:00'
|
return '0:00'
|
||||||
}
|
}
|
||||||
|
|
||||||
var hours = Math.floor(time / 3600)
|
const totalHours = Math.floor(total / 3600)
|
||||||
var minutes = Math.floor(time % 3600 / 60)
|
const totalMinutes = Math.floor(total / 60)
|
||||||
if (hours > 0) {
|
const hours = Math.floor(time / 3600)
|
||||||
minutes = zeroFill(2, minutes)
|
let minutes = Math.floor(time % 3600 / 60)
|
||||||
|
if (totalMinutes > 9 && minutes < 10) {
|
||||||
|
minutes = '0' + minutes
|
||||||
}
|
}
|
||||||
var seconds = zeroFill(2, Math.floor(time % 60))
|
const seconds = `0${Math.floor(time % 60)}`.slice(-2)
|
||||||
|
|
||||||
return (hours > 0 ? hours + ':' : '') + minutes + ':' + seconds
|
return (totalHours > 0 ? hours + ':' : '') + minutes + ':' + seconds
|
||||||
}
|
}
|
||||||
290
src/renderer/pages/preferences-page.js
Normal file
290
src/renderer/pages/preferences-page.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
const React = require('react')
|
||||||
|
const PropTypes = require('prop-types')
|
||||||
|
|
||||||
|
const colors = require('material-ui/styles/colors')
|
||||||
|
const Checkbox = require('material-ui/Checkbox').default
|
||||||
|
const RaisedButton = require('material-ui/RaisedButton').default
|
||||||
|
const Heading = require('../components/heading')
|
||||||
|
const PathSelector = require('../components/path-selector')
|
||||||
|
|
||||||
|
const { dispatch } = require('../lib/dispatcher')
|
||||||
|
const config = require('../../config')
|
||||||
|
|
||||||
|
class PreferencesPage extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.handleDownloadPathChange =
|
||||||
|
this.handleDownloadPathChange.bind(this)
|
||||||
|
|
||||||
|
this.handleOpenExternalPlayerChange =
|
||||||
|
this.handleOpenExternalPlayerChange.bind(this)
|
||||||
|
|
||||||
|
this.handleExternalPlayerPathChange =
|
||||||
|
this.handleExternalPlayerPathChange.bind(this)
|
||||||
|
|
||||||
|
this.handleStartupChange =
|
||||||
|
this.handleStartupChange.bind(this)
|
||||||
|
|
||||||
|
this.handleSoundNotificationsChange =
|
||||||
|
this.handleSoundNotificationsChange.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadPathSelector () {
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<PathSelector
|
||||||
|
dialog={{
|
||||||
|
title: 'Select download directory',
|
||||||
|
properties: ['openDirectory']
|
||||||
|
}}
|
||||||
|
onChange={this.handleDownloadPathChange}
|
||||||
|
title='Download location'
|
||||||
|
value={this.props.state.saved.prefs.downloadPath}
|
||||||
|
/>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDownloadPathChange (filePath) {
|
||||||
|
dispatch('updatePreferences', 'downloadPath', filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
openExternalPlayerCheckbox () {
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<Checkbox
|
||||||
|
className='control'
|
||||||
|
checked={!this.props.state.saved.prefs.openExternalPlayer}
|
||||||
|
label='Play torrent media files using WebTorrent'
|
||||||
|
onCheck={this.handleOpenExternalPlayerChange}
|
||||||
|
/>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenExternalPlayerChange (e, isChecked) {
|
||||||
|
dispatch('updatePreferences', 'openExternalPlayer', !isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
highestPlaybackPriorityCheckbox () {
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<Checkbox
|
||||||
|
className='control'
|
||||||
|
checked={this.props.state.saved.prefs.highestPlaybackPriority}
|
||||||
|
label='Highest Playback Priority'
|
||||||
|
onCheck={this.handleHighestPlaybackPriorityChange}
|
||||||
|
/>
|
||||||
|
<p>Pauses all active torrents to allow playback to use all of the available bandwidth.</p>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHighestPlaybackPriorityChange (e, isChecked) {
|
||||||
|
dispatch('updatePreferences', 'highestPlaybackPriority', isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
externalPlayerPathSelector () {
|
||||||
|
const playerPath = this.props.state.saved.prefs.externalPlayerPath
|
||||||
|
const playerName = this.props.state.getExternalPlayerName()
|
||||||
|
|
||||||
|
const description = this.props.state.saved.prefs.openExternalPlayer
|
||||||
|
? `Torrent media files will always play in ${playerName}.`
|
||||||
|
: `Torrent media files will play in ${playerName} if WebTorrent cannot play them.`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<p>{description}</p>
|
||||||
|
<PathSelector
|
||||||
|
dialog={{
|
||||||
|
title: 'Select media player app',
|
||||||
|
properties: ['openFile']
|
||||||
|
}}
|
||||||
|
onChange={this.handleExternalPlayerPathChange}
|
||||||
|
title='External player'
|
||||||
|
value={playerPath}
|
||||||
|
/>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleExternalPlayerPathChange (filePath) {
|
||||||
|
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
autoAddTorrentsCheckbox () {
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<Checkbox
|
||||||
|
className='control'
|
||||||
|
checked={this.props.state.saved.prefs.autoAddTorrents}
|
||||||
|
label='Watch for new .torrent files and add them immediately'
|
||||||
|
onCheck={(e, value) => { this.handleAutoAddTorrentsChange(e, value) }}
|
||||||
|
/>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAutoAddTorrentsChange (e, isChecked) {
|
||||||
|
const torrentsFolderPath = this.props.state.saved.prefs.torrentsFolderPath
|
||||||
|
if (isChecked && !torrentsFolderPath) {
|
||||||
|
alert('Select a torrents folder first.') // eslint-disable-line
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch('updatePreferences', 'autoAddTorrents', isChecked)
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
dispatch('startFolderWatcher')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch('stopFolderWatcher')
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentsFolderPathSelector () {
|
||||||
|
const torrentsFolderPath = this.props.state.saved.prefs.torrentsFolderPath
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<PathSelector
|
||||||
|
dialog={{
|
||||||
|
title: 'Select folder to watch for new torrents',
|
||||||
|
properties: ['openDirectory']
|
||||||
|
}}
|
||||||
|
onChange={this.handleTorrentsFolderPathChange}
|
||||||
|
title='Folder to watch'
|
||||||
|
value={torrentsFolderPath}
|
||||||
|
/>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTorrentsFolderPathChange (filePath) {
|
||||||
|
dispatch('updatePreferences', 'torrentsFolderPath', filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultAppButton () {
|
||||||
|
const isFileHandler = this.props.state.saved.prefs.isFileHandler
|
||||||
|
if (isFileHandler) {
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<p>WebTorrent is your default torrent app. Hooray!</p>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<p>WebTorrent is not currently the default torrent app.</p>
|
||||||
|
<RaisedButton
|
||||||
|
className='control'
|
||||||
|
onClick={this.handleSetDefaultApp}
|
||||||
|
label='Make WebTorrent the default'
|
||||||
|
/>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStartupChange (e, isChecked) {
|
||||||
|
dispatch('updatePreferences', 'startup', isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
setStartupCheckbox () {
|
||||||
|
if (config.IS_PORTABLE) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<Checkbox
|
||||||
|
className='control'
|
||||||
|
checked={this.props.state.saved.prefs.startup}
|
||||||
|
label='Open WebTorrent on startup'
|
||||||
|
onCheck={this.handleStartupChange}
|
||||||
|
/>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
soundNotificationsCheckbox () {
|
||||||
|
return (
|
||||||
|
<Preference>
|
||||||
|
<Checkbox
|
||||||
|
className='control'
|
||||||
|
checked={this.props.state.saved.prefs.soundNotifications}
|
||||||
|
label='Enable sounds'
|
||||||
|
onCheck={this.handleSoundNotificationsChange}
|
||||||
|
/>
|
||||||
|
</Preference>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSoundNotificationsChange (e, isChecked) {
|
||||||
|
dispatch('updatePreferences', 'soundNotifications', isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSetDefaultApp () {
|
||||||
|
dispatch('updatePreferences', 'isFileHandler', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const style = {
|
||||||
|
color: colors.grey400,
|
||||||
|
marginLeft: 25,
|
||||||
|
marginRight: 25
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
<PreferencesSection title='Folders'>
|
||||||
|
{this.downloadPathSelector()}
|
||||||
|
{this.autoAddTorrentsCheckbox()}
|
||||||
|
{this.torrentsFolderPathSelector()}
|
||||||
|
</PreferencesSection>
|
||||||
|
<PreferencesSection title='Playback'>
|
||||||
|
{this.openExternalPlayerCheckbox()}
|
||||||
|
{this.externalPlayerPathSelector()}
|
||||||
|
{this.highestPlaybackPriorityCheckbox()}
|
||||||
|
</PreferencesSection>
|
||||||
|
<PreferencesSection title='Default torrent app'>
|
||||||
|
{this.setDefaultAppButton()}
|
||||||
|
</PreferencesSection>
|
||||||
|
<PreferencesSection title='General'>
|
||||||
|
{this.setStartupCheckbox()}
|
||||||
|
{this.soundNotificationsCheckbox()}
|
||||||
|
</PreferencesSection>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreferencesSection extends React.Component {
|
||||||
|
static get propTypes () {
|
||||||
|
return {
|
||||||
|
title: PropTypes.string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const style = {
|
||||||
|
marginBottom: 25,
|
||||||
|
marginTop: 25
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
<Heading level={2}>{this.props.title}</Heading>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Preference extends React.Component {
|
||||||
|
render () {
|
||||||
|
const style = { marginBottom: 10 }
|
||||||
|
return (<div style={style}>{this.props.children}</div>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PreferencesPage
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
const React = require('react')
|
const React = require('react')
|
||||||
const prettyBytes = require('prettier-bytes')
|
const prettyBytes = require('prettier-bytes')
|
||||||
|
|
||||||
|
const Checkbox = require('material-ui/Checkbox').default
|
||||||
|
const LinearProgress = require('material-ui/LinearProgress').default
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
const {dispatcher} = require('../lib/dispatcher')
|
const { dispatcher } = require('../lib/dispatcher')
|
||||||
|
|
||||||
module.exports = class TorrentList extends React.Component {
|
module.exports = class TorrentList extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
|
|
||||||
var contents = []
|
const contents = []
|
||||||
if (state.downloadPathStatus === 'missing') {
|
if (state.downloadPathStatus === 'missing') {
|
||||||
contents.push(
|
contents.push(
|
||||||
<div key='torrent-missing-path'>
|
<div key='torrent-missing-path'>
|
||||||
@@ -21,7 +24,7 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var torrentElems = state.saved.torrents.map(
|
const torrentElems = state.saved.torrents.map(
|
||||||
(torrentSummary) => this.renderTorrent(torrentSummary)
|
(torrentSummary) => this.renderTorrent(torrentSummary)
|
||||||
)
|
)
|
||||||
contents.push(...torrentElems)
|
contents.push(...torrentElems)
|
||||||
@@ -39,80 +42,127 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTorrent (torrentSummary) {
|
renderTorrent (torrentSummary) {
|
||||||
var state = this.props.state
|
const state = this.props.state
|
||||||
var infoHash = torrentSummary.infoHash
|
const infoHash = torrentSummary.infoHash
|
||||||
var isSelected = infoHash && state.selectedInfoHash === infoHash
|
const isSelected = infoHash && state.selectedInfoHash === infoHash
|
||||||
|
|
||||||
// Background image: show some nice visuals, like a frame from the movie, if possible
|
// Background image: show some nice visuals, like a frame from the movie, if possible
|
||||||
var style = {}
|
const style = {}
|
||||||
if (torrentSummary.posterFileName) {
|
if (torrentSummary.posterFileName) {
|
||||||
var gradient = isSelected
|
const gradient = 'linear-gradient(to bottom, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
||||||
? 'linear-gradient(to bottom, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.4) 100%)'
|
const posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
||||||
: 'linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%)'
|
style.backgroundImage = `${gradient}, url('${posterPath}')`
|
||||||
var posterPath = TorrentSummary.getPosterPath(torrentSummary)
|
|
||||||
style.backgroundImage = gradient + `, url('${posterPath}')`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Foreground: name of the torrent, basic info like size, play button,
|
// Foreground: name of the torrent, basic info like size, play button,
|
||||||
// cast buttons if available, and delete
|
// cast buttons if available, and delete
|
||||||
var classes = ['torrent']
|
const classes = ['torrent']
|
||||||
// playStatus turns the play button into a loading spinner or error icon
|
|
||||||
if (torrentSummary.playStatus) classes.push(torrentSummary.playStatus)
|
|
||||||
if (isSelected) classes.push('selected')
|
if (isSelected) classes.push('selected')
|
||||||
if (!infoHash) classes.push('disabled')
|
if (!infoHash) classes.push('disabled')
|
||||||
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
if (!torrentSummary.torrentKey) throw new Error('Missing torrentKey')
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
id={torrentSummary.testID && ('torrent-' + torrentSummary.testID)}
|
||||||
key={torrentSummary.torrentKey}
|
key={torrentSummary.torrentKey}
|
||||||
style={style}
|
style={style}
|
||||||
className={classes.join(' ')}
|
className={classes.join(' ')}
|
||||||
onContextMenu={infoHash && dispatcher('openTorrentContextMenu', infoHash)}
|
onContextMenu={infoHash && dispatcher('openTorrentContextMenu', infoHash)}
|
||||||
onClick={infoHash && dispatcher('toggleSelectTorrent', infoHash)}>
|
onClick={infoHash && dispatcher('toggleSelectTorrent', infoHash)}
|
||||||
|
>
|
||||||
{this.renderTorrentMetadata(torrentSummary)}
|
{this.renderTorrentMetadata(torrentSummary)}
|
||||||
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
|
{infoHash ? this.renderTorrentButtons(torrentSummary) : null}
|
||||||
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
|
{isSelected ? this.renderTorrentDetails(torrentSummary) : null}
|
||||||
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show name, download status, % complete
|
// Show name, download status, % complete
|
||||||
renderTorrentMetadata (torrentSummary) {
|
renderTorrentMetadata (torrentSummary) {
|
||||||
var name = torrentSummary.name || 'Loading torrent...'
|
const name = torrentSummary.name || 'Loading torrent...'
|
||||||
var elements = [(
|
const elements = [(
|
||||||
<div key='name' className='name ellipsis'>{name}</div>
|
<div key='name' className='name ellipsis'>{name}</div>
|
||||||
)]
|
)]
|
||||||
|
|
||||||
// If it's downloading/seeding then show progress info
|
// If it's downloading/seeding then show progress info
|
||||||
var prog = torrentSummary.progress
|
const prog = torrentSummary.progress
|
||||||
|
let progElems
|
||||||
if (torrentSummary.error) {
|
if (torrentSummary.error) {
|
||||||
elements.push(
|
progElems = [getErrorMessage(torrentSummary)]
|
||||||
<div key='progress-info' className='ellipsis'>
|
|
||||||
{getErrorMessage(torrentSummary)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
} else if (torrentSummary.status !== 'paused' && prog) {
|
} else if (torrentSummary.status !== 'paused' && prog) {
|
||||||
elements.push(
|
progElems = [
|
||||||
<div key='progress-info' className='ellipsis'>
|
renderDownloadCheckbox(),
|
||||||
{renderPercentProgress()}
|
renderTorrentStatus(),
|
||||||
{renderTotalProgress()}
|
renderProgressBar(),
|
||||||
{renderPeers()}
|
renderPercentProgress(),
|
||||||
{renderDownloadSpeed()}
|
renderTotalProgress(),
|
||||||
{renderUploadSpeed()}
|
renderPeers(),
|
||||||
{renderEta()}
|
renderSpeeds(),
|
||||||
|
renderEta()
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
progElems = [
|
||||||
|
renderDownloadCheckbox(),
|
||||||
|
renderTorrentStatus()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
elements.push(
|
||||||
|
<div key='progress-info' className='ellipsis'>
|
||||||
|
{progElems}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (<div key='metadata' className='metadata'>{elements}</div>)
|
||||||
|
|
||||||
|
function renderDownloadCheckbox () {
|
||||||
|
const infoHash = torrentSummary.infoHash
|
||||||
|
const isActive = ['downloading', 'seeding'].includes(torrentSummary.status)
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
key='download-button'
|
||||||
|
className={'control download ' + torrentSummary.status}
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
width: 32
|
||||||
|
}}
|
||||||
|
iconStyle={{
|
||||||
|
width: 20,
|
||||||
|
height: 20
|
||||||
|
}}
|
||||||
|
checked={isActive}
|
||||||
|
onClick={stopPropagation}
|
||||||
|
onCheck={dispatcher('toggleTorrent', infoHash)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProgressBar () {
|
||||||
|
const progress = Math.floor(100 * prog.progress)
|
||||||
|
const styles = {
|
||||||
|
wrapper: {
|
||||||
|
display: 'inline-block',
|
||||||
|
marginRight: 8
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
height: 8,
|
||||||
|
width: 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key='progress-bar' style={styles.wrapper}>
|
||||||
|
<LinearProgress style={styles.progress} mode='determinate' value={progress} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<div key='metadata' className='metadata'>{elements}</div>)
|
|
||||||
|
|
||||||
function renderPercentProgress () {
|
function renderPercentProgress () {
|
||||||
var progress = Math.floor(100 * prog.progress)
|
const progress = Math.floor(100 * prog.progress)
|
||||||
return (<span key='percent-progress'>{progress}%</span>)
|
return (<span key='percent-progress'>{progress}%</span>)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTotalProgress () {
|
function renderTotalProgress () {
|
||||||
var downloaded = prettyBytes(prog.downloaded)
|
const downloaded = prettyBytes(prog.downloaded)
|
||||||
var total = prettyBytes(prog.length || 0)
|
const total = prettyBytes(prog.length || 0)
|
||||||
if (downloaded === total) {
|
if (downloaded === total) {
|
||||||
return (<span key='total-progress'>{downloaded}</span>)
|
return (<span key='total-progress'>{downloaded}</span>)
|
||||||
} else {
|
} else {
|
||||||
@@ -122,116 +172,85 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
|
|
||||||
function renderPeers () {
|
function renderPeers () {
|
||||||
if (prog.numPeers === 0) return
|
if (prog.numPeers === 0) return
|
||||||
var count = prog.numPeers === 1 ? 'peer' : 'peers'
|
const count = prog.numPeers === 1 ? 'peer' : 'peers'
|
||||||
return (<span key='peers'>{prog.numPeers} {count}</span>)
|
return (<span key='peers'>{prog.numPeers} {count}</span>)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDownloadSpeed () {
|
function renderSpeeds () {
|
||||||
if (prog.downloadSpeed === 0) return
|
let str = ''
|
||||||
return (<span key='download'>↓ {prettyBytes(prog.downloadSpeed)}/s</span>)
|
if (prog.downloadSpeed > 0) str += ' ↓ ' + prettyBytes(prog.downloadSpeed) + '/s'
|
||||||
}
|
if (prog.uploadSpeed > 0) str += ' ↑ ' + prettyBytes(prog.uploadSpeed) + '/s'
|
||||||
|
if (str === '') return
|
||||||
function renderUploadSpeed () {
|
return (<span key='download'>{str}</span>)
|
||||||
if (prog.uploadSpeed === 0) return
|
|
||||||
return (<span key='upload'>↑ {prettyBytes(prog.uploadSpeed)}/s</span>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEta () {
|
function renderEta () {
|
||||||
var downloaded = prog.downloaded
|
const downloaded = prog.downloaded
|
||||||
var total = prog.length || 0
|
const total = prog.length || 0
|
||||||
var missing = total - downloaded
|
const missing = total - downloaded
|
||||||
var downloadSpeed = prog.downloadSpeed
|
const downloadSpeed = prog.downloadSpeed
|
||||||
if (downloadSpeed === 0 || missing === 0) return
|
if (downloadSpeed === 0 || missing === 0) return
|
||||||
|
|
||||||
var rawEta = missing / downloadSpeed
|
const rawEta = missing / downloadSpeed
|
||||||
var hours = Math.floor(rawEta / 3600) % 24
|
const hours = Math.floor(rawEta / 3600) % 24
|
||||||
var minutes = Math.floor(rawEta / 60) % 60
|
const minutes = Math.floor(rawEta / 60) % 60
|
||||||
var seconds = Math.floor(rawEta % 60)
|
const seconds = Math.floor(rawEta % 60)
|
||||||
|
|
||||||
// Only display hours and minutes if they are greater than 0 but always
|
// Only display hours and minutes if they are greater than 0 but always
|
||||||
// display minutes if hours is being displayed
|
// display minutes if hours is being displayed
|
||||||
var hoursStr = hours ? hours + 'h' : ''
|
const hoursStr = hours ? hours + 'h' : ''
|
||||||
var minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
const minutesStr = (hours || minutes) ? minutes + 'm' : ''
|
||||||
var secondsStr = seconds + 's'
|
const secondsStr = seconds + 's'
|
||||||
|
|
||||||
return (<span>ETA: {hoursStr} {minutesStr} {secondsStr}</span>)
|
return (<span key='eta'>{hoursStr} {minutesStr} {secondsStr} remaining</span>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTorrentStatus () {
|
||||||
|
let status
|
||||||
|
if (torrentSummary.status === 'paused') {
|
||||||
|
if (!torrentSummary.progress) status = ''
|
||||||
|
else if (torrentSummary.progress.progress === 1) status = 'Not seeding'
|
||||||
|
else status = 'Paused'
|
||||||
|
} else if (torrentSummary.status === 'downloading') {
|
||||||
|
status = 'Downloading'
|
||||||
|
} else if (torrentSummary.status === 'seeding') {
|
||||||
|
status = 'Seeding'
|
||||||
|
} else { // torrentSummary.status is 'new' or something unexpected
|
||||||
|
status = ''
|
||||||
|
}
|
||||||
|
return (<span key='torrent-status'>{status}</span>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download button toggles between torrenting (DL/seed) and paused
|
// Download button toggles between torrenting (DL/seed) and paused
|
||||||
// Play button starts streaming the torrent immediately, unpausing if needed
|
// Play button starts streaming the torrent immediately, unpausing if needed
|
||||||
renderTorrentButtons (torrentSummary) {
|
renderTorrentButtons (torrentSummary) {
|
||||||
var infoHash = torrentSummary.infoHash
|
const infoHash = torrentSummary.infoHash
|
||||||
|
|
||||||
var playIcon, playTooltip, playClass
|
// Only show the play/dowload buttons for torrents that contain playable media
|
||||||
if (torrentSummary.playStatus === 'timeout') {
|
let playButton
|
||||||
playIcon = 'warning'
|
if (!torrentSummary.error && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
||||||
playTooltip = 'Playback timed out. No seeds? No internet? Click to try again.'
|
|
||||||
} else {
|
|
||||||
playIcon = 'play_arrow'
|
|
||||||
playTooltip = 'Start streaming'
|
|
||||||
}
|
|
||||||
|
|
||||||
var downloadIcon, downloadTooltip
|
|
||||||
if (torrentSummary.status === 'seeding') {
|
|
||||||
downloadIcon = 'file_upload'
|
|
||||||
downloadTooltip = 'Seeding. Click to stop.'
|
|
||||||
} else if (torrentSummary.status === 'downloading') {
|
|
||||||
downloadIcon = 'file_download'
|
|
||||||
downloadTooltip = 'Torrenting. Click to stop.'
|
|
||||||
} else {
|
|
||||||
downloadIcon = 'file_download'
|
|
||||||
downloadTooltip = 'Click to start torrenting.'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do we have a saved position? Show it using a radial progress bar on top
|
|
||||||
// of the play button, unless already showing a spinner there:
|
|
||||||
var positionElem
|
|
||||||
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
|
||||||
var defaultFile = torrentSummary.files &&
|
|
||||||
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
|
|
||||||
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
|
|
||||||
var fraction = defaultFile.currentTime / defaultFile.duration
|
|
||||||
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
|
|
||||||
playClass = 'resume-position'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only show the play button for torrents that contain playable media
|
|
||||||
var playButton, downloadButton
|
|
||||||
var noErrors = !torrentSummary.error
|
|
||||||
if (noErrors && TorrentPlayer.isPlayableTorrentSummary(torrentSummary)) {
|
|
||||||
playButton = (
|
playButton = (
|
||||||
<i
|
<i
|
||||||
key='play-button'
|
key='play-button'
|
||||||
title={playTooltip}
|
title='Start streaming'
|
||||||
className={'button-round icon play ' + playClass}
|
className='icon play'
|
||||||
onClick={dispatcher('playFile', infoHash)}>
|
onClick={dispatcher('playFile', infoHash)}
|
||||||
{playIcon}
|
>
|
||||||
</i>
|
play_circle_outline
|
||||||
)
|
|
||||||
}
|
|
||||||
if (noErrors) {
|
|
||||||
downloadButton = (
|
|
||||||
<i
|
|
||||||
key='download-button'
|
|
||||||
className={'button-round icon download ' + torrentSummary.status}
|
|
||||||
title={downloadTooltip}
|
|
||||||
onClick={dispatcher('toggleTorrent', infoHash)}>
|
|
||||||
{downloadIcon}
|
|
||||||
</i>
|
</i>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='buttons' className='buttons'>
|
<div className='torrent-controls'>
|
||||||
{positionElem}
|
|
||||||
{playButton}
|
{playButton}
|
||||||
{downloadButton}
|
|
||||||
<i
|
<i
|
||||||
key='delete-button'
|
key='delete-button'
|
||||||
className='icon delete'
|
className='icon delete'
|
||||||
title='Remove torrent'
|
title='Remove torrent'
|
||||||
onClick={dispatcher('confirmDeleteTorrent', infoHash, false)}>
|
onClick={dispatcher('confirmDeleteTorrent', infoHash, false)}
|
||||||
|
>
|
||||||
close
|
close
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,9 +259,9 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
|
|
||||||
// Show files, per-file download status and play buttons, and so on
|
// Show files, per-file download status and play buttons, and so on
|
||||||
renderTorrentDetails (torrentSummary) {
|
renderTorrentDetails (torrentSummary) {
|
||||||
var filesElement
|
let filesElement
|
||||||
if (torrentSummary.error || !torrentSummary.files) {
|
if (torrentSummary.error || !torrentSummary.files) {
|
||||||
var message = ''
|
let message = ''
|
||||||
if (torrentSummary.error === 'path-missing') {
|
if (torrentSummary.error === 'path-missing') {
|
||||||
// Special case error: this torrent's download dir or file is missing
|
// Special case error: this torrent's download dir or file is missing
|
||||||
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
|
message = 'Missing path: ' + TorrentSummary.getFileOrFolder(torrentSummary)
|
||||||
@@ -263,14 +282,9 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// We do know the files. List them and show download stats for each one
|
// We do know the files. List them and show download stats for each one
|
||||||
var fileRows = torrentSummary.files
|
const fileRows = torrentSummary.files
|
||||||
.filter((file) => !file.path.includes('/.____padding_file/'))
|
.filter((file) => !file.path.includes('/.____padding_file/'))
|
||||||
.map((file, index) => ({ file, index }))
|
.map((file, index) => ({ file, index }))
|
||||||
.sort(function (a, b) {
|
|
||||||
if (a.file.name < b.file.name) return -1
|
|
||||||
if (b.file.name < a.file.name) return 1
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
|
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
|
||||||
|
|
||||||
filesElement = (
|
filesElement = (
|
||||||
@@ -295,36 +309,39 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
renderFileRow (torrentSummary, file, index) {
|
renderFileRow (torrentSummary, file, index) {
|
||||||
// First, find out how much of the file we've downloaded
|
// First, find out how much of the file we've downloaded
|
||||||
// Are we even torrenting it?
|
// Are we even torrenting it?
|
||||||
var isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
const isSelected = torrentSummary.selections && torrentSummary.selections[index]
|
||||||
var isDone = false // Are we finished torrenting it?
|
let isDone = false // Are we finished torrenting it?
|
||||||
var progress = ''
|
let progress = ''
|
||||||
if (torrentSummary.progress && torrentSummary.progress.files &&
|
if (torrentSummary.progress && torrentSummary.progress.files &&
|
||||||
torrentSummary.progress.files[index]) {
|
torrentSummary.progress.files[index]) {
|
||||||
var fileProg = torrentSummary.progress.files[index]
|
const fileProg = torrentSummary.progress.files[index]
|
||||||
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
isDone = fileProg.numPiecesPresent === fileProg.numPieces
|
||||||
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
progress = Math.round(100 * fileProg.numPiecesPresent / fileProg.numPieces) + '%'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second, for media files where we saved our position, show how far we got
|
// Second, for media files where we saved our position, show how far we got
|
||||||
var positionElem
|
let positionElem
|
||||||
if (file.currentTime) {
|
if (file.currentTime) {
|
||||||
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
|
// Radial progress bar. 0% = start from 0:00, 270% = 3/4 of the way thru
|
||||||
positionElem = this.renderRadialProgressBar(file.currentTime / file.duration)
|
positionElem = this.renderRadialProgressBar(file.currentTime / file.duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, render the file as a table row
|
// Finally, render the file as a table row
|
||||||
var isPlayable = TorrentPlayer.isPlayable(file)
|
const isPlayable = TorrentPlayer.isPlayable(file)
|
||||||
var infoHash = torrentSummary.infoHash
|
const infoHash = torrentSummary.infoHash
|
||||||
var icon
|
let icon
|
||||||
var handleClick
|
let handleClick
|
||||||
if (isPlayable) {
|
if (isPlayable) {
|
||||||
icon = 'play_arrow' /* playable? add option to play */
|
icon = 'play_arrow' /* playable? add option to play */
|
||||||
handleClick = dispatcher('playFile', infoHash, index)
|
handleClick = dispatcher('playFile', infoHash, index)
|
||||||
} else {
|
} else {
|
||||||
icon = 'description' /* file icon, opens in OS default app */
|
icon = 'description' /* file icon, opens in OS default app */
|
||||||
handleClick = dispatcher('openItem', infoHash, index)
|
handleClick = isDone
|
||||||
|
? dispatcher('openItem', infoHash, index)
|
||||||
|
: (e) => e.stopPropagation() // noop if file is not ready
|
||||||
}
|
}
|
||||||
var rowClass = ''
|
// TODO: add a css 'disabled' class to indicate that a file cannot be opened/streamed
|
||||||
|
let rowClass = ''
|
||||||
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
if (!isSelected) rowClass = 'disabled' // File deselected, not being torrented
|
||||||
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
if (!isDone && !isPlayable) rowClass = 'disabled' // Can't open yet, can't stream
|
||||||
return (
|
return (
|
||||||
@@ -342,41 +359,47 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
<td className={'col-size ' + rowClass}>
|
<td className={'col-size ' + rowClass}>
|
||||||
{prettyBytes(file.length)}
|
{prettyBytes(file.length)}
|
||||||
</td>
|
</td>
|
||||||
<td className='col-select'
|
<td
|
||||||
onClick={dispatcher('toggleTorrentFile', infoHash, index)}>
|
className='col-select'
|
||||||
<i className='icon'>{isSelected ? 'close' : 'add'}</i>
|
onClick={dispatcher('toggleTorrentFile', infoHash, index)}
|
||||||
|
>
|
||||||
|
<i className='icon deselect-file'>{isSelected ? 'close' : 'add'}</i>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRadialProgressBar (fraction, cssClass) {
|
renderRadialProgressBar (fraction, cssClass) {
|
||||||
var rotation = 360 * fraction
|
const rotation = 360 * fraction
|
||||||
var transformFill = {transform: 'rotate(' + (rotation / 2) + 'deg)'}
|
const transformFill = { transform: 'rotate(' + (rotation / 2) + 'deg)' }
|
||||||
var transformFix = {transform: 'rotate(' + rotation + 'deg)'}
|
const transformFix = { transform: 'rotate(' + rotation + 'deg)' }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key='radial-progress' className={'radial-progress ' + cssClass}>
|
<div key='radial-progress' className={'radial-progress ' + cssClass}>
|
||||||
<div key='circle' className='circle'>
|
<div className='circle'>
|
||||||
<div key='mask-full' className='mask full' style={transformFill}>
|
<div className='mask full' style={transformFill}>
|
||||||
<div key='fill' className='fill' style={transformFill} />
|
<div className='fill' style={transformFill} />
|
||||||
</div>
|
</div>
|
||||||
<div key='mask-half' className='mask half'>
|
<div className='mask half'>
|
||||||
<div key='fill' className='fill' style={transformFill} />
|
<div className='fill' style={transformFill} />
|
||||||
<div key='fill-fix' className='fill fix' style={transformFix} />
|
<div className='fill fix' style={transformFix} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div key='inset' className='inset' />
|
<div className='inset' />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopPropagation (e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
function getErrorMessage (torrentSummary) {
|
function getErrorMessage (torrentSummary) {
|
||||||
var err = torrentSummary.error
|
const err = torrentSummary.error
|
||||||
if (err === 'path-missing') {
|
if (err === 'path-missing') {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span key='path-missing'>
|
||||||
Path missing.<br />
|
Path missing.<br />
|
||||||
Fix and restart the app, or delete the torrent.
|
Fix and restart the app, or delete the torrent.
|
||||||
</span>
|
</span>
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
const React = require('react')
|
|
||||||
|
|
||||||
const Header = require('./header')
|
|
||||||
|
|
||||||
const Views = {
|
|
||||||
'home': require('./torrent-list'),
|
|
||||||
'player': require('./player'),
|
|
||||||
'create-torrent': require('./create-torrent'),
|
|
||||||
'preferences': require('./preferences')
|
|
||||||
}
|
|
||||||
|
|
||||||
const Modals = {
|
|
||||||
'open-torrent-address-modal': require('./open-torrent-address-modal'),
|
|
||||||
'remove-torrent-modal': require('./remove-torrent-modal'),
|
|
||||||
'update-available-modal': require('./update-available-modal'),
|
|
||||||
'unsupported-media-modal': require('./unsupported-media-modal')
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = class App extends React.Component {
|
|
||||||
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
this.state = props.state
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
var state = this.state
|
|
||||||
|
|
||||||
// Hide player controls while playing video, if the mouse stays still for a while
|
|
||||||
// Never hide the controls when:
|
|
||||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
|
||||||
// * The video is paused
|
|
||||||
// * The video is playing remotely on Chromecast or Airplay
|
|
||||||
var hideControls = state.location.url() === 'player' &&
|
|
||||||
state.playing.mouseStationarySince !== 0 &&
|
|
||||||
new Date().getTime() - state.playing.mouseStationarySince > 2000 &&
|
|
||||||
!state.playing.isPaused &&
|
|
||||||
state.playing.location === 'local' &&
|
|
||||||
state.playing.playbackRate === 1
|
|
||||||
|
|
||||||
var cls = [
|
|
||||||
'view-' + state.location.url(), /* e.g. view-home, view-player */
|
|
||||||
'is-' + process.platform /* e.g. is-darwin, is-win32, is-linux */
|
|
||||||
]
|
|
||||||
if (state.window.isFullScreen) cls.push('is-fullscreen')
|
|
||||||
if (state.window.isFocused) cls.push('is-focused')
|
|
||||||
if (hideControls) cls.push('hide-video-controls')
|
|
||||||
|
|
||||||
var vdom = (
|
|
||||||
<div className={'app ' + cls.join(' ')}>
|
|
||||||
<Header state={state} />
|
|
||||||
{this.getErrorPopover()}
|
|
||||||
<div key='content' className='content'>{this.getView()}</div>
|
|
||||||
{this.getModal()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return vdom
|
|
||||||
}
|
|
||||||
|
|
||||||
getErrorPopover () {
|
|
||||||
var now = new Date().getTime()
|
|
||||||
var recentErrors = this.state.errors.filter((x) => now - x.time < 5000)
|
|
||||||
var hasErrors = recentErrors.length > 0
|
|
||||||
|
|
||||||
var errorElems = recentErrors.map(function (error, i) {
|
|
||||||
return (<div key={i} className='error'>{error.message}</div>)
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div key='errors'
|
|
||||||
className={'error-popover ' + (hasErrors ? 'visible' : 'hidden')}>
|
|
||||||
<div key='title' className='title'>Error</div>
|
|
||||||
{errorElems}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getModal () {
|
|
||||||
var state = this.state
|
|
||||||
if (!state.modal) return
|
|
||||||
var ModalContents = Modals[state.modal.id]
|
|
||||||
return (
|
|
||||||
<div key='modal' className='modal'>
|
|
||||||
<div key='modal-background' className='modal-background' />
|
|
||||||
<div key='modal-content' className='modal-content'>
|
|
||||||
<ModalContents state={state} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getView () {
|
|
||||||
var state = this.state
|
|
||||||
var View = Views[state.location.url()]
|
|
||||||
return (<View state={state} />)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
const React = require('react')
|
|
||||||
const createTorrent = require('create-torrent')
|
|
||||||
const path = require('path')
|
|
||||||
const prettyBytes = require('prettier-bytes')
|
|
||||||
|
|
||||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
|
||||||
const CreateTorrentErrorPage = require('./create-torrent-error-page')
|
|
||||||
|
|
||||||
module.exports = class CreateTorrentPage extends React.Component {
|
|
||||||
render () {
|
|
||||||
var state = this.props.state
|
|
||||||
var info = state.location.current()
|
|
||||||
|
|
||||||
// Preprocess: exclude .DS_Store and other dotfiles
|
|
||||||
var files = info.files
|
|
||||||
.filter((f) => !f.name.startsWith('.'))
|
|
||||||
.map((f) => ({name: f.name, path: f.path, size: f.size}))
|
|
||||||
if (files.length === 0) return (<CreateTorrentErrorPage state={state} />)
|
|
||||||
|
|
||||||
// First, extract the base folder that the files are all in
|
|
||||||
var pathPrefix = info.folderPath
|
|
||||||
if (!pathPrefix) {
|
|
||||||
pathPrefix = files.map((x) => x.path).reduce(findCommonPrefix)
|
|
||||||
if (!pathPrefix.endsWith('/') && !pathPrefix.endsWith('\\')) {
|
|
||||||
pathPrefix = path.dirname(pathPrefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity check: show the number of files and total size
|
|
||||||
var numFiles = files.length
|
|
||||||
var totalBytes = files
|
|
||||||
.map((f) => f.size)
|
|
||||||
.reduce((a, b) => a + b, 0)
|
|
||||||
var torrentInfo = `${numFiles} files, ${prettyBytes(totalBytes)}`
|
|
||||||
|
|
||||||
// Then, use the name of the base folder (or sole file, for a single file torrent)
|
|
||||||
// as the default name. Show all files relative to the base folder.
|
|
||||||
var defaultName, basePath
|
|
||||||
if (files.length === 1) {
|
|
||||||
// Single file torrent: /a/b/foo.jpg -> torrent name 'foo.jpg', path '/a/b'
|
|
||||||
defaultName = files[0].name
|
|
||||||
basePath = pathPrefix
|
|
||||||
} else {
|
|
||||||
// Multi file torrent: /a/b/{foo, bar}.jpg -> torrent name 'b', path '/a'
|
|
||||||
defaultName = path.basename(pathPrefix)
|
|
||||||
basePath = path.dirname(pathPrefix)
|
|
||||||
}
|
|
||||||
var maxFileElems = 100
|
|
||||||
var fileElems = files.slice(0, maxFileElems).map(function (file, i) {
|
|
||||||
var relativePath = files.length === 0 ? file.name : path.relative(pathPrefix, file.path)
|
|
||||||
return (<div key={i}>{relativePath}</div>)
|
|
||||||
})
|
|
||||||
if (files.length > maxFileElems) {
|
|
||||||
fileElems.push(<div key='more'>+ {maxFileElems - files.length} more</div>)
|
|
||||||
}
|
|
||||||
var trackers = createTorrent.announceList.join('\n')
|
|
||||||
var collapsedClass = info.showAdvanced ? 'expanded' : 'collapsed'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='create-torrent'>
|
|
||||||
<h2>Create torrent {defaultName}</h2>
|
|
||||||
<div key='info' className='torrent-info'>
|
|
||||||
{torrentInfo}
|
|
||||||
</div>
|
|
||||||
<div key='path-prefix' className='torrent-attribute'>
|
|
||||||
<label>Path:</label>
|
|
||||||
<div className='torrent-attribute'>{pathPrefix}</div>
|
|
||||||
</div>
|
|
||||||
<div key='toggle' className={'expand-collapse ' + collapsedClass}
|
|
||||||
onClick={dispatcher('toggleCreateTorrentAdvanced')}>
|
|
||||||
{info.showAdvanced ? 'Basic' : 'Advanced'}
|
|
||||||
</div>
|
|
||||||
<div key='advanced' className={'create-torrent-advanced ' + collapsedClass}>
|
|
||||||
<div key='comment' className='torrent-attribute'>
|
|
||||||
<label>Comment:</label>
|
|
||||||
<textarea className='torrent-attribute torrent-comment' />
|
|
||||||
</div>
|
|
||||||
<div key='trackers' className='torrent-attribute'>
|
|
||||||
<label>Trackers:</label>
|
|
||||||
<textarea className='torrent-attribute torrent-trackers' defaultValue={trackers} />
|
|
||||||
</div>
|
|
||||||
<div key='private' className='torrent-attribute'>
|
|
||||||
<label>Private:</label>
|
|
||||||
<input type='checkbox' className='torrent-is-private' value='torrent-is-private' />
|
|
||||||
</div>
|
|
||||||
<div key='files' className='torrent-attribute'>
|
|
||||||
<label>Files:</label>
|
|
||||||
<div>{fileElems}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div key='buttons' className='float-right'>
|
|
||||||
<button key='cancel' className='button-flat light' onClick={dispatcher('cancel')}>Cancel</button>
|
|
||||||
<button key='create' className='button-raised' onClick={handleOK}>Create Torrent</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
function handleOK () {
|
|
||||||
// TODO: dcposch use React refs instead
|
|
||||||
var announceList = document.querySelector('.torrent-trackers').value
|
|
||||||
.split('\n')
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter((s) => s !== '')
|
|
||||||
var isPrivate = document.querySelector('.torrent-is-private').checked
|
|
||||||
var comment = document.querySelector('.torrent-comment').value.trim()
|
|
||||||
var options = {
|
|
||||||
// We can't let the user choose their own name if we want WebTorrent
|
|
||||||
// to use the files in place rather than creating a new folder.
|
|
||||||
// If we ever want to add support for that:
|
|
||||||
// name: document.querySelector('.torrent-name').value
|
|
||||||
name: defaultName,
|
|
||||||
path: basePath,
|
|
||||||
files: files,
|
|
||||||
announce: announceList,
|
|
||||||
private: isPrivate,
|
|
||||||
comment: comment
|
|
||||||
}
|
|
||||||
dispatch('createTorrent', options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finds the longest common prefix
|
|
||||||
function findCommonPrefix (a, b) {
|
|
||||||
for (var i = 0; i < a.length && i < b.length; i++) {
|
|
||||||
if (a.charCodeAt(i) !== b.charCodeAt(i)) break
|
|
||||||
}
|
|
||||||
if (i === a.length) return a
|
|
||||||
if (i === b.length) return b
|
|
||||||
return a.substring(0, i)
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
const React = require('react')
|
|
||||||
|
|
||||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
|
||||||
|
|
||||||
module.exports = class OpenTorrentAddressModal extends React.Component {
|
|
||||||
render () {
|
|
||||||
// TODO: dcposch remove janky inline <script>
|
|
||||||
return (
|
|
||||||
<div className='open-torrent-address-modal'>
|
|
||||||
<p><label>Enter torrent address or magnet link</label></p>
|
|
||||||
<p>
|
|
||||||
<input id='add-torrent-url' type='text' onKeyPress={handleKeyPress} />
|
|
||||||
</p>
|
|
||||||
<p className='float-right'>
|
|
||||||
<button className='button button-flat' onClick={dispatcher('exitModal')}>Cancel</button>
|
|
||||||
<button className='button button-raised' onClick={handleOK}>OK</button>
|
|
||||||
</p>
|
|
||||||
<script>document.querySelector('#add-torrent-url').focus()</script>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeyPress (e) {
|
|
||||||
if (e.which === 13) handleOK() /* hit Enter to submit */
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOK () {
|
|
||||||
dispatch('exitModal')
|
|
||||||
// TODO: dcposch use React refs instead
|
|
||||||
dispatch('addTorrent', document.querySelector('#add-torrent-url').value)
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
const React = require('react')
|
|
||||||
const remote = require('electron').remote
|
|
||||||
const dialog = remote.dialog
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
|
||||||
|
|
||||||
module.exports = class Preferences extends React.Component {
|
|
||||||
render () {
|
|
||||||
var state = this.props.state
|
|
||||||
return (
|
|
||||||
<div className='preferences'>
|
|
||||||
{renderGeneralSection(state)}
|
|
||||||
{renderPlaybackSection(state)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGeneralSection (state) {
|
|
||||||
return renderSection({
|
|
||||||
key: 'general',
|
|
||||||
title: 'General',
|
|
||||||
description: '',
|
|
||||||
icon: 'settings'
|
|
||||||
}, [
|
|
||||||
renderDownloadPathSelector(state),
|
|
||||||
renderFileHandlers(state)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPlaybackSection (state) {
|
|
||||||
return renderSection({
|
|
||||||
title: 'Playback',
|
|
||||||
description: '',
|
|
||||||
icon: 'settings'
|
|
||||||
}, [
|
|
||||||
renderOpenExternalPlayerSelector(state),
|
|
||||||
renderExternalPlayerSelector(state)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDownloadPathSelector (state) {
|
|
||||||
return renderFileSelector({
|
|
||||||
key: 'download-path',
|
|
||||||
label: 'Download Path',
|
|
||||||
description: 'Data from torrents will be saved here',
|
|
||||||
property: 'downloadPath',
|
|
||||||
options: {
|
|
||||||
title: 'Select download directory',
|
|
||||||
properties: [ 'openDirectory' ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
state.unsaved.prefs.downloadPath,
|
|
||||||
function (filePath) {
|
|
||||||
dispatch('updatePreferences', 'downloadPath', filePath)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFileHandlers (state) {
|
|
||||||
var definition = {
|
|
||||||
key: 'file-handlers',
|
|
||||||
label: 'Handle Torrent Files'
|
|
||||||
}
|
|
||||||
var buttonText = state.unsaved.prefs.isFileHandler
|
|
||||||
? 'Remove default app for torrent files'
|
|
||||||
: 'Make WebTorrent the default app for torrent files'
|
|
||||||
var controls = [(
|
|
||||||
<button key='toggle-handlers'
|
|
||||||
className='btn'
|
|
||||||
onClick={toggleFileHandlers}>
|
|
||||||
{buttonText}
|
|
||||||
</button>
|
|
||||||
)]
|
|
||||||
return renderControlGroup(definition, controls)
|
|
||||||
|
|
||||||
function toggleFileHandlers () {
|
|
||||||
var isFileHandler = state.unsaved.prefs.isFileHandler
|
|
||||||
dispatch('updatePreferences', 'isFileHandler', !isFileHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderExternalPlayerSelector (state) {
|
|
||||||
return renderFileSelector({
|
|
||||||
label: 'External Media Player',
|
|
||||||
description: 'Progam that will be used to play media externally',
|
|
||||||
property: 'externalPlayerPath',
|
|
||||||
options: {
|
|
||||||
title: 'Select media player executable',
|
|
||||||
properties: [ 'openFile' ]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
state.unsaved.prefs.externalPlayerPath || '<VLC>',
|
|
||||||
function (filePath) {
|
|
||||||
if (path.extname(filePath) === '.app') {
|
|
||||||
// Get executable in packaged mac app
|
|
||||||
var name = path.basename(filePath, '.app')
|
|
||||||
filePath += '/Contents/MacOS/' + name
|
|
||||||
}
|
|
||||||
dispatch('updatePreferences', 'externalPlayerPath', filePath)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderOpenExternalPlayerSelector (state) {
|
|
||||||
return renderCheckbox({
|
|
||||||
key: 'open-external-player',
|
|
||||||
label: 'Play in External Player',
|
|
||||||
description: 'Media will play in external player',
|
|
||||||
property: 'openExternalPlayer',
|
|
||||||
value: state.saved.prefs.openExternalPlayer
|
|
||||||
},
|
|
||||||
state.unsaved.prefs.openExternalPlayer,
|
|
||||||
function (value) {
|
|
||||||
dispatch('updatePreferences', 'openExternalPlayer', value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renders a prefs section.
|
|
||||||
// - definition should be {icon, title, description}
|
|
||||||
// - controls should be an array of vdom elements
|
|
||||||
function renderSection (definition, controls) {
|
|
||||||
var helpElem = !definition.description ? null : (
|
|
||||||
<div key='help' className='help text'>
|
|
||||||
<i className='icon'>help_outline</i>{definition.description}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<section key={definition.key} className='section preferences-panel'>
|
|
||||||
<div className='section-container'>
|
|
||||||
<div key='heading' className='section-heading'>
|
|
||||||
<i className='icon'>{definition.icon}</i>{definition.title}
|
|
||||||
</div>
|
|
||||||
{helpElem}
|
|
||||||
<div key='body' className='section-body'>
|
|
||||||
{controls}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCheckbox (definition, value, callback) {
|
|
||||||
var iconClass = 'icon clickable'
|
|
||||||
if (value) iconClass += ' enabled'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key='{definition.key}' className='control-group'>
|
|
||||||
<div className='controls'>
|
|
||||||
<label className='control-label'>
|
|
||||||
<div className='preference-title'>{definition.label}</div>
|
|
||||||
</label>
|
|
||||||
<div className='controls'>
|
|
||||||
<label className='clickable' onClick={handleClick}>
|
|
||||||
<i
|
|
||||||
className={iconClass}
|
|
||||||
id='{definition.property}'
|
|
||||||
>
|
|
||||||
check_circle
|
|
||||||
</i>
|
|
||||||
<span className='checkbox-label'>{definition.description}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
function handleClick () {
|
|
||||||
callback(!value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a file chooser
|
|
||||||
// - defition should be {label, description, options}
|
|
||||||
// options are passed to dialog.showOpenDialog
|
|
||||||
// - value should be the current pref, a file or folder path
|
|
||||||
// - callback takes a new file or folder path
|
|
||||||
function renderFileSelector (definition, value, callback) {
|
|
||||||
var controls = [(
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
className='file-picker-text'
|
|
||||||
key={definition.property}
|
|
||||||
id={definition.property}
|
|
||||||
disabled='disabled'
|
|
||||||
value={value} />
|
|
||||||
), (
|
|
||||||
<button
|
|
||||||
key={definition.property + '-btn'}
|
|
||||||
className='btn'
|
|
||||||
onClick={handleClick}>
|
|
||||||
<i className='icon'>folder_open</i>
|
|
||||||
</button>
|
|
||||||
)]
|
|
||||||
return renderControlGroup(definition, controls)
|
|
||||||
|
|
||||||
function handleClick () {
|
|
||||||
dialog.showOpenDialog(remote.getCurrentWindow(), definition.options, function (filenames) {
|
|
||||||
if (!Array.isArray(filenames)) return
|
|
||||||
callback(filenames[0])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderControlGroup (definition, controls) {
|
|
||||||
return (
|
|
||||||
<div key={definition.key} className='control-group'>
|
|
||||||
<div className='controls'>
|
|
||||||
<label className='control-label'>
|
|
||||||
<div className='preference-title'>{definition.label}</div>
|
|
||||||
<div className='preference-description'>{definition.description}</div>
|
|
||||||
</label>
|
|
||||||
<div className='controls'>
|
|
||||||
{controls}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const React = require('react')
|
|
||||||
const electron = require('electron')
|
|
||||||
const path = require('path')
|
|
||||||
|
|
||||||
const {dispatcher} = require('../lib/dispatcher')
|
|
||||||
|
|
||||||
module.exports = class UnsupportedMediaModal extends React.Component {
|
|
||||||
render () {
|
|
||||||
var state = this.props.state
|
|
||||||
var err = state.modal.error
|
|
||||||
var message = (err && err.getMessage)
|
|
||||||
? err.getMessage()
|
|
||||||
: err
|
|
||||||
var playerPath = state.saved.prefs.externalPlayerPath
|
|
||||||
var playerName = playerPath
|
|
||||||
? path.basename(playerPath).split('.')[0]
|
|
||||||
: 'VLC'
|
|
||||||
var actionButton = state.modal.externalPlayerInstalled
|
|
||||||
? (<button className='button-raised' onClick={dispatcher('openExternalPlayer')}>Play in {playerName}</button>)
|
|
||||||
: (<button className='button-raised' onClick={() => this.onInstall}>Install VLC</button>)
|
|
||||||
var playerMessage = state.modal.externalPlayerNotFound
|
|
||||||
? 'Couldn\'t run external player. Please make sure it\'s installed.'
|
|
||||||
: ''
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p><strong>Sorry, we can't play that file.</strong></p>
|
|
||||||
<p>{message}</p>
|
|
||||||
<p className='float-right'>
|
|
||||||
<button className='button-flat' onClick={dispatcher('backToList')}>Cancel</button>
|
|
||||||
{actionButton}
|
|
||||||
</p>
|
|
||||||
<p className='error-text'>{playerMessage}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
onInstall () {
|
|
||||||
electron.shell.openExternal('http://www.videolan.org/vlc/')
|
|
||||||
|
|
||||||
// TODO: dcposch send a dispatch rather than modifying state directly
|
|
||||||
var state = this.props.state
|
|
||||||
state.modal.externalPlayerInstalled = true // Assume they'll install it successfully
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const React = require('react')
|
|
||||||
const electron = require('electron')
|
|
||||||
|
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
|
||||||
|
|
||||||
module.exports = class UpdateAvailableModal extends React.Component {
|
|
||||||
render () {
|
|
||||||
var state = this.props.state
|
|
||||||
return (
|
|
||||||
<div className='update-available-modal'>
|
|
||||||
<p><strong>A new version of WebTorrent is available: v{state.modal.version}</strong></p>
|
|
||||||
<p>We have an auto-updater for Windows and Mac. We don't have one for Linux yet, so you'll have to download the new version manually.</p>
|
|
||||||
<p className='float-right'>
|
|
||||||
<button className='button button-flat' onClick={handleSkip}>Skip This Release</button>
|
|
||||||
<button className='button button-raised' onClick={handleShow}>Show Download Page</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
function handleShow () {
|
|
||||||
electron.shell.openExternal('https://github.com/feross/webtorrent-desktop/releases')
|
|
||||||
dispatch('exitModal')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSkip () {
|
|
||||||
dispatch('skipVersion', state.modal.version)
|
|
||||||
dispatch('exitModal')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,26 +2,27 @@
|
|||||||
// process from the main window.
|
// process from the main window.
|
||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
var deepEqual = require('deep-equal')
|
const crypto = require('crypto')
|
||||||
var defaultAnnounceList = require('create-torrent').announceList
|
const deepEqual = require('deep-equal')
|
||||||
var electron = require('electron')
|
const defaultAnnounceList = require('create-torrent').announceList
|
||||||
var fs = require('fs-extra')
|
const electron = require('electron')
|
||||||
var hat = require('hat')
|
const fs = require('fs')
|
||||||
var musicmetadata = require('musicmetadata')
|
const mkdirp = require('mkdirp')
|
||||||
var networkAddress = require('network-address')
|
const mm = require('music-metadata')
|
||||||
var path = require('path')
|
const networkAddress = require('network-address')
|
||||||
var WebTorrent = require('webtorrent')
|
const path = require('path')
|
||||||
var zeroFill = require('zero-fill')
|
const WebTorrent = require('webtorrent')
|
||||||
|
|
||||||
var crashReporter = require('../crash-reporter')
|
const crashReporter = require('../crash-reporter')
|
||||||
var config = require('../config')
|
const config = require('../config')
|
||||||
var torrentPoster = require('./lib/torrent-poster')
|
const { TorrentKeyNotFoundError } = require('./lib/errors')
|
||||||
|
const torrentPoster = require('./lib/torrent-poster')
|
||||||
|
|
||||||
// Report when the process crashes
|
// Report when the process crashes
|
||||||
crashReporter.init()
|
crashReporter.init()
|
||||||
|
|
||||||
// Send & receive messages from the main window
|
// Send & receive messages from the main window
|
||||||
var ipc = electron.ipcRenderer
|
const ipc = electron.ipcRenderer
|
||||||
|
|
||||||
// Force use of webtorrent trackers on all torrents
|
// Force use of webtorrent trackers on all torrents
|
||||||
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
||||||
@@ -31,7 +32,7 @@ global.WEBTORRENT_ANNOUNCE = defaultAnnounceList
|
|||||||
/**
|
/**
|
||||||
* WebTorrent version.
|
* WebTorrent version.
|
||||||
*/
|
*/
|
||||||
var VERSION = require('../../package.json').version
|
const VERSION = require('../../package.json').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.
|
||||||
@@ -39,10 +40,9 @@ var VERSION = require('../../package.json').version
|
|||||||
* '0.16.1' -> '0016'
|
* '0.16.1' -> '0016'
|
||||||
* '1.2.5' -> '0102'
|
* '1.2.5' -> '0102'
|
||||||
*/
|
*/
|
||||||
var VERSION_STR = VERSION.match(/([0-9]+)/g)
|
const VERSION_STR = VERSION
|
||||||
.slice(0, 2)
|
.replace(/\d*./g, v => `0${v % 100}`.slice(-2))
|
||||||
.map((v) => zeroFill(2, v))
|
.slice(0, 4)
|
||||||
.join('')
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version prefix string (used in peer ID). WebTorrent uses the Azureus-style
|
* Version prefix string (used in peer ID). WebTorrent uses the Azureus-style
|
||||||
@@ -51,25 +51,27 @@ var VERSION_STR = VERSION.match(/([0-9]+)/g)
|
|||||||
* For example:
|
* For example:
|
||||||
* '-WW0102-'...
|
* '-WW0102-'...
|
||||||
*/
|
*/
|
||||||
var VERSION_PREFIX = '-WD' + VERSION_STR + '-'
|
const VERSION_PREFIX = '-WD' + VERSION_STR + '-'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate an ephemeral peer ID each time.
|
||||||
|
*/
|
||||||
|
const PEER_ID = Buffer.from(VERSION_PREFIX + crypto.randomBytes(9).toString('base64'))
|
||||||
|
|
||||||
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
// Connect to the WebTorrent and BitTorrent networks. WebTorrent Desktop is a hybrid
|
||||||
// client, as explained here: https://webtorrent.io/faq
|
// client, as explained here: https://webtorrent.io/faq
|
||||||
var client = window.client = new WebTorrent({
|
let client = window.client = new WebTorrent({ peerId: PEER_ID })
|
||||||
peerId: Buffer.from(VERSION_PREFIX + hat(48))
|
|
||||||
})
|
|
||||||
|
|
||||||
// WebTorrent-to-HTTP streaming sever
|
// WebTorrent-to-HTTP streaming sever
|
||||||
var server = null
|
let server = null
|
||||||
|
|
||||||
// Used for diffing, so we only send progress updates when necessary
|
// Used for diffing, so we only send progress updates when necessary
|
||||||
var prevProgress = null
|
let prevProgress = null
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
listenToClientEvents()
|
||||||
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
|
||||||
|
|
||||||
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
ipc.on('wt-start-torrenting', (e, torrentKey, torrentID, path, fileModtimes, selections) =>
|
||||||
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
startTorrenting(torrentKey, torrentID, path, fileModtimes, selections))
|
||||||
@@ -83,8 +85,8 @@ function init () {
|
|||||||
generateTorrentPoster(torrentKey))
|
generateTorrentPoster(torrentKey))
|
||||||
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
||||||
getAudioMetadata(infoHash, index))
|
getAudioMetadata(infoHash, index))
|
||||||
ipc.on('wt-start-server', (e, infoHash, index) =>
|
ipc.on('wt-start-server', (e, infoHash) =>
|
||||||
startServer(infoHash, index))
|
startServer(infoHash))
|
||||||
ipc.on('wt-stop-server', (e) =>
|
ipc.on('wt-stop-server', (e) =>
|
||||||
stopServer())
|
stopServer())
|
||||||
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
||||||
@@ -93,18 +95,24 @@ function init () {
|
|||||||
ipc.send('ipcReadyWebTorrent')
|
ipc.send('ipcReadyWebTorrent')
|
||||||
|
|
||||||
window.addEventListener('error', (e) =>
|
window.addEventListener('error', (e) =>
|
||||||
ipc.send('wt-uncaught-error', {message: e.error.message, stack: e.error.stack}),
|
ipc.send('wt-uncaught-error', { message: e.error.message, stack: e.error.stack }),
|
||||||
true)
|
true)
|
||||||
|
|
||||||
setInterval(updateTorrentProgress, 1000)
|
setInterval(updateTorrentProgress, 1000)
|
||||||
|
console.timeEnd('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starts a given TorrentID, which can be an infohash, magnet URI, etc. Returns WebTorrent object
|
function listenToClientEvents () {
|
||||||
// See https://github.com/feross/webtorrent/blob/master/docs/api.md#clientaddtorrentid-opts-function-ontorrent-torrent-
|
client.on('warning', (err) => ipc.send('wt-warning', null, err.message))
|
||||||
|
client.on('error', (err) => ipc.send('wt-error', null, err.message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts a given TorrentID, which can be an infohash, magnet URI, etc.
|
||||||
|
// Returns a WebTorrent object. See https://git.io/vik9M
|
||||||
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections) {
|
||||||
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
console.log('starting torrent %s: %s', torrentKey, torrentID)
|
||||||
|
|
||||||
var torrent = client.add(torrentID, {
|
const torrent = client.add(torrentID, {
|
||||||
path: path,
|
path: path,
|
||||||
fileModtimes: fileModtimes
|
fileModtimes: fileModtimes
|
||||||
})
|
})
|
||||||
@@ -115,20 +123,19 @@ function startTorrenting (torrentKey, torrentID, path, fileModtimes, selections)
|
|||||||
|
|
||||||
// Only download the files the user wants, not necessarily all files
|
// Only download the files the user wants, not necessarily all files
|
||||||
torrent.once('ready', () => selectFiles(torrent, selections))
|
torrent.once('ready', () => selectFiles(torrent, selections))
|
||||||
|
|
||||||
return torrent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopTorrenting (infoHash) {
|
function stopTorrenting (infoHash) {
|
||||||
var torrent = client.get(infoHash)
|
console.log('--- STOP TORRENTING: ', infoHash)
|
||||||
|
const torrent = client.get(infoHash)
|
||||||
if (torrent) torrent.destroy()
|
if (torrent) torrent.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new torrent, start seeding
|
// Create a new torrent, start seeding
|
||||||
function createTorrent (torrentKey, options) {
|
function createTorrent (torrentKey, options) {
|
||||||
console.log('creating torrent', torrentKey, options)
|
console.log('creating torrent', torrentKey, options)
|
||||||
var paths = options.files.map((f) => f.path)
|
const paths = options.files.map((f) => f.path)
|
||||||
var 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')
|
ipc.send('wt-new-torrent')
|
||||||
@@ -146,22 +153,22 @@ function addTorrentEvents (torrent) {
|
|||||||
torrent.on('done', torrentDone)
|
torrent.on('done', torrentDone)
|
||||||
|
|
||||||
function torrentMetadata () {
|
function torrentMetadata () {
|
||||||
var info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipc.send('wt-metadata', torrent.key, info)
|
ipc.send('wt-metadata', torrent.key, info)
|
||||||
|
|
||||||
updateTorrentProgress()
|
updateTorrentProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentReady () {
|
function torrentReady () {
|
||||||
var info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipc.send('wt-ready', torrent.key, info)
|
ipc.send('wt-ready', torrent.key, info)
|
||||||
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info) // TODO: hack
|
ipc.send('wt-ready-' + torrent.infoHash, torrent.key, info)
|
||||||
|
|
||||||
updateTorrentProgress()
|
updateTorrentProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
function torrentDone () {
|
function torrentDone () {
|
||||||
var info = getTorrentInfo(torrent)
|
const info = getTorrentInfo(torrent)
|
||||||
ipc.send('wt-done', torrent.key, info)
|
ipc.send('wt-done', torrent.key, info)
|
||||||
|
|
||||||
updateTorrentProgress()
|
updateTorrentProgress()
|
||||||
@@ -194,19 +201,22 @@ function getTorrentFileInfo (file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every time we resolve a magnet URI, save the torrent file so that we never
|
// Every time we resolve a magnet URI, save the torrent file so that we can use
|
||||||
// have to download it again. Never ask the DHT the same question twice.
|
// it on next startup. Starting with the full torrent metadata will be faster
|
||||||
|
// than re-fetching it from peers using ut_metadata.
|
||||||
function saveTorrentFile (torrentKey) {
|
function saveTorrentFile (torrentKey) {
|
||||||
var torrent = getTorrent(torrentKey)
|
const torrent = getTorrent(torrentKey)
|
||||||
checkIfTorrentFileExists(torrent.infoHash, function (torrentPath, exists) {
|
const torrentPath = path.join(config.TORRENT_PATH, torrent.infoHash + '.torrent')
|
||||||
var fileName = torrent.infoHash + '.torrent'
|
|
||||||
if (exists) {
|
fs.access(torrentPath, fs.constants.R_OK, function (err) {
|
||||||
|
const fileName = torrent.infoHash + '.torrent'
|
||||||
|
if (!err) {
|
||||||
// We've already saved the file
|
// We've already saved the file
|
||||||
return ipc.send('wt-file-saved', torrentKey, fileName)
|
return ipc.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, function (_) {
|
mkdirp(config.TORRENT_PATH, function (_) {
|
||||||
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
fs.writeFile(torrentPath, torrent.torrentFile, function (err) {
|
||||||
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
if (err) return console.log('error saving torrent file %s: %o', torrentPath, err)
|
||||||
console.log('saved torrent file %s', torrentPath)
|
console.log('saved torrent file %s', torrentPath)
|
||||||
@@ -216,26 +226,17 @@ function saveTorrentFile (torrentKey) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks whether we've already resolved a given infohash to a torrent file
|
|
||||||
// Calls back with (torrentPath, exists). Logs, does not call back on error
|
|
||||||
function checkIfTorrentFileExists (infoHash, cb) {
|
|
||||||
var torrentPath = path.join(config.TORRENT_PATH, infoHash + '.torrent')
|
|
||||||
fs.exists(torrentPath, function (exists) {
|
|
||||||
cb(torrentPath, exists)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save a JPG that represents a torrent.
|
// Save a JPG that represents a torrent.
|
||||||
// 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) {
|
||||||
var torrent = getTorrent(torrentKey)
|
const torrent = getTorrent(torrentKey)
|
||||||
torrentPoster(torrent, function (err, buf, extension) {
|
torrentPoster(torrent, function (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.mkdirp(config.POSTER_PATH, function (err) {
|
mkdirp(config.POSTER_PATH, function (err) {
|
||||||
if (err) return console.log('error creating poster dir: %o', err)
|
if (err) return console.log('error creating poster dir: %o', err)
|
||||||
var posterFileName = torrent.infoHash + extension
|
const posterFileName = torrent.infoHash + extension
|
||||||
var 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, function (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
|
||||||
@@ -246,9 +247,9 @@ function generateTorrentPoster (torrentKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateTorrentProgress () {
|
function updateTorrentProgress () {
|
||||||
var progress = getTorrentProgress()
|
const progress = getTorrentProgress()
|
||||||
// TODO: diff torrent-by-torrent, not once for the whole update
|
// TODO: diff torrent-by-torrent, not once for the whole update
|
||||||
if (prevProgress && deepEqual(progress, prevProgress, {strict: true})) {
|
if (prevProgress && deepEqual(progress, prevProgress, { strict: true })) {
|
||||||
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)
|
ipc.send('wt-progress', progress)
|
||||||
@@ -257,19 +258,19 @@ function updateTorrentProgress () {
|
|||||||
|
|
||||||
function getTorrentProgress () {
|
function getTorrentProgress () {
|
||||||
// First, track overall progress
|
// First, track overall progress
|
||||||
var progress = client.progress
|
const progress = client.progress
|
||||||
var hasActiveTorrents = client.torrents.some(function (torrent) {
|
const hasActiveTorrents = client.torrents.some(function (torrent) {
|
||||||
return 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
|
||||||
var torrentProg = client.torrents.map(function (torrent) {
|
const torrentProg = client.torrents.map(function (torrent) {
|
||||||
var fileProg = torrent.files && torrent.files.map(function (file, index) {
|
const fileProg = torrent.files && torrent.files.map(function (file, index) {
|
||||||
var numPieces = file._endPiece - file._startPiece + 1
|
const numPieces = file._endPiece - file._startPiece + 1
|
||||||
var numPiecesPresent = 0
|
let numPiecesPresent = 0
|
||||||
for (var piece = file._startPiece; piece <= file._endPiece; piece++) {
|
for (let piece = file._startPiece; piece <= file._endPiece; piece++) {
|
||||||
if (torrent.bitfield.get(piece)) numPiecesPresent++
|
if (torrent.bitfield.get(piece)) numPiecesPresent++
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -300,28 +301,29 @@ function getTorrentProgress () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServer (infoHash, index) {
|
function startServer (infoHash) {
|
||||||
var torrent = client.get(infoHash)
|
const torrent = client.get(infoHash)
|
||||||
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
|
if (torrent.ready) startServerFromReadyTorrent(torrent)
|
||||||
else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
|
else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServerFromReadyTorrent (torrent, index, cb) {
|
function startServerFromReadyTorrent (torrent, cb) {
|
||||||
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, function () {
|
||||||
var port = server.address().port
|
const port = server.address().port
|
||||||
var urlSuffix = ':' + port + '/' + index
|
const urlSuffix = ':' + port
|
||||||
var info = {
|
const info = {
|
||||||
torrentKey: torrent.key,
|
torrentKey: torrent.key,
|
||||||
localURL: 'http://localhost' + urlSuffix,
|
localURL: 'http://localhost' + urlSuffix,
|
||||||
networkURL: 'http://' + networkAddress() + urlSuffix
|
networkURL: 'http://' + networkAddress() + urlSuffix,
|
||||||
|
networkAddress: networkAddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
ipc.send('wt-server-running', info)
|
ipc.send('wt-server-running', info)
|
||||||
ipc.send('wt-server-' + torrent.infoHash, info) // TODO: hack
|
ipc.send('wt-server-' + torrent.infoHash, info)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,24 +333,49 @@ function stopServer () {
|
|||||||
server = null
|
server = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Initializing...')
|
||||||
|
|
||||||
function getAudioMetadata (infoHash, index) {
|
function getAudioMetadata (infoHash, index) {
|
||||||
var torrent = client.get(infoHash)
|
const torrent = client.get(infoHash)
|
||||||
var file = torrent.files[index]
|
const file = torrent.files[index]
|
||||||
musicmetadata(file.createReadStream(), function (err, info) {
|
|
||||||
if (err) return
|
// Set initial matadata to display the filename first.
|
||||||
console.log('got audio metadata for %s: %o', file.name, info)
|
const metadata = { title: file.name }
|
||||||
ipc.send('wt-audio-metadata', infoHash, index, info)
|
ipc.send('wt-audio-metadata', infoHash, index, metadata)
|
||||||
})
|
|
||||||
|
const options = {
|
||||||
|
native: false,
|
||||||
|
skipCovers: true,
|
||||||
|
fileSize: file.length,
|
||||||
|
observer: event => {
|
||||||
|
ipc.send('wt-audio-metadata', infoHash, index, event.metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const onMetaData = file.done
|
||||||
|
// If completed; use direct file access
|
||||||
|
? mm.parseFile(path.join(torrent.path, file.path), options)
|
||||||
|
// otherwise stream
|
||||||
|
: mm.parseStream(file.createReadStream(), file.name, options)
|
||||||
|
|
||||||
|
onMetaData
|
||||||
|
.then(() => {
|
||||||
|
console.log(`metadata for file='${file.name}' completed.`)
|
||||||
|
}).catch(function (err) {
|
||||||
|
return console.log('error getting audio metadata for ' + infoHash + ':' + index, err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectFiles (torrentOrInfoHash, selections) {
|
function selectFiles (torrentOrInfoHash, selections) {
|
||||||
// Get the torrent object
|
// Get the torrent object
|
||||||
var torrent
|
let torrent
|
||||||
if (typeof torrentOrInfoHash === 'string') {
|
if (typeof torrentOrInfoHash === 'string') {
|
||||||
torrent = client.get(torrentOrInfoHash)
|
torrent = client.get(torrentOrInfoHash)
|
||||||
} else {
|
} else {
|
||||||
torrent = torrentOrInfoHash
|
torrent = torrentOrInfoHash
|
||||||
}
|
}
|
||||||
|
if (!torrent) {
|
||||||
|
throw new Error('selectFiles: missing torrent ' + torrentOrInfoHash)
|
||||||
|
}
|
||||||
|
|
||||||
// Selections not specified?
|
// Selections not specified?
|
||||||
// Load all files. We still need to replace the default whole-torrent
|
// Load all files. We still need to replace the default whole-torrent
|
||||||
@@ -368,8 +395,8 @@ 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 (var i = 0; i < selections.length; i++) {
|
for (let i = 0; i < selections.length; i++) {
|
||||||
var file = torrent.files[i]
|
const file = torrent.files[i]
|
||||||
if (selections[i]) {
|
if (selections[i]) {
|
||||||
file.select()
|
file.select()
|
||||||
} else {
|
} else {
|
||||||
@@ -382,11 +409,25 @@ function selectFiles (torrentOrInfoHash, selections) {
|
|||||||
// Gets a WebTorrent handle by torrentKey
|
// Gets a WebTorrent handle by torrentKey
|
||||||
// Throws an Error if we're not currently torrenting anything w/ that key
|
// Throws an Error if we're not currently torrenting anything w/ that key
|
||||||
function getTorrent (torrentKey) {
|
function getTorrent (torrentKey) {
|
||||||
var ret = client.torrents.find((x) => x.key === torrentKey)
|
const ret = client.torrents.find((x) => x.key === torrentKey)
|
||||||
if (!ret) throw new Error('missing torrent key ' + torrentKey)
|
if (!ret) throw new TorrentKeyNotFoundError(torrentKey)
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError (err) {
|
function onError (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove this once the following bugs are fixed:
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=490143
|
||||||
|
// https://github.com/electron/electron/issues/7212
|
||||||
|
window.testOfflineMode = function () {
|
||||||
|
console.log('Test, going OFFLINE')
|
||||||
|
client = window.client = new WebTorrent({
|
||||||
|
peerId: PEER_ID,
|
||||||
|
tracker: false,
|
||||||
|
dht: false,
|
||||||
|
webSeeds: false
|
||||||
|
})
|
||||||
|
listenToClientEvents()
|
||||||
|
}
|
||||||
|
|||||||
0
static/MaterialIcons-Regular.woff2
Normal file → Executable file
0
static/MaterialIcons-Regular.woff2
Normal file → Executable file
BIN
static/NextTrackThumbnailBarButton.png
Normal file
BIN
static/NextTrackThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user