mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-07 13:39:58 -05:00
Compare commits
739 Commits
phantom-mo
...
phantom-di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94f8e38d5a | ||
|
|
031371652b | ||
|
|
02104aff34 | ||
|
|
5bd1c47ca7 | ||
|
|
f846e0c031 | ||
|
|
72b0f640f4 | ||
|
|
430af0401c | ||
|
|
0ff08dbe8d | ||
|
|
57cca9fcdc | ||
|
|
b3daa280c5 | ||
|
|
449c1caf55 | ||
|
|
0c05236bee | ||
|
|
9f54ff8169 | ||
|
|
f2e1b4e435 | ||
|
|
1e80361c3a | ||
|
|
3564be19a8 | ||
|
|
14c9b6aaf4 | ||
|
|
069fc5cd33 | ||
|
|
3586d7042b | ||
|
|
a32b6276bd | ||
|
|
b0e31629b5 | ||
|
|
e2644c3847 | ||
|
|
7ea45bb714 | ||
|
|
f524fcd3e4 | ||
|
|
6c418302f8 | ||
|
|
b28d329654 | ||
|
|
ebe2ad1520 | ||
|
|
41dfb677e7 | ||
|
|
c646bef369 | ||
|
|
910de6d94a | ||
|
|
5951992bd5 | ||
|
|
cb9d78064a | ||
|
|
fd608fd411 | ||
|
|
d3bd90e4b9 | ||
|
|
4988655568 | ||
|
|
098db08ede | ||
|
|
93e3e92bba | ||
|
|
bdfdd28d6a | ||
|
|
a75e10c4c9 | ||
|
|
5251db7224 | ||
|
|
4d1a4d4241 | ||
|
|
d3a22459ac | ||
|
|
4f7e00bdc4 | ||
|
|
1199ae4e4f | ||
|
|
36088ef49d | ||
|
|
7ffb2eb440 | ||
|
|
0716d0931a | ||
|
|
66ee28d0a9 | ||
|
|
1487f54749 | ||
|
|
013c46d266 | ||
|
|
c8d2fcb223 | ||
|
|
5288b61378 | ||
|
|
a2679f64ee | ||
|
|
5d9dfee3c0 | ||
|
|
98f9323b42 | ||
|
|
f282ae8aae | ||
|
|
0b1e99991e | ||
|
|
75be036a87 | ||
|
|
23dc7794f1 | ||
|
|
b8e2f3d716 | ||
|
|
686a14cdff | ||
|
|
b4405b0600 | ||
|
|
d4bcf28d08 | ||
|
|
4bacc35605 | ||
|
|
417340c2c6 | ||
|
|
79d8a9d44b | ||
|
|
68440bba4d | ||
|
|
0719c83da4 | ||
|
|
5a7dec34cc | ||
|
|
9d766cfed5 | ||
|
|
1498f4e361 | ||
|
|
05dd17aacb | ||
|
|
200aee52f7 | ||
|
|
d6dd13a6be | ||
|
|
be3b3df903 | ||
|
|
f2a56b29d9 | ||
|
|
27d98868b8 | ||
|
|
7f28ab895a | ||
|
|
97ec184754 | ||
|
|
42343d5283 | ||
|
|
479baf06a7 | ||
|
|
7f7d196e44 | ||
|
|
c862fd9ff6 | ||
|
|
770b89c2b3 | ||
|
|
576275b6da | ||
|
|
776191b3bd | ||
|
|
d369d85699 | ||
|
|
552fac0466 | ||
|
|
a348d98dd9 | ||
|
|
ccdfdd1049 | ||
|
|
f0ca636654 | ||
|
|
b5e734b9e5 | ||
|
|
e1639d35a2 | ||
|
|
9b99ad27cd | ||
|
|
bba57bb434 | ||
|
|
8c24cd9864 | ||
|
|
91de7ff11c | ||
|
|
9702d2e5ad | ||
|
|
638066db03 | ||
|
|
1b3839ac0d | ||
|
|
219494ea9d | ||
|
|
642f75761f | ||
|
|
ed28f94f02 | ||
|
|
618c611a59 | ||
|
|
00821b7ad6 | ||
|
|
84b9488cfb | ||
|
|
37ad801065 | ||
|
|
4219cdb364 | ||
|
|
e23a879669 | ||
|
|
4ddf4a22a3 | ||
|
|
72afb28c30 | ||
|
|
eb51a42f60 | ||
|
|
bc01384cc7 | ||
|
|
00c922875f | ||
|
|
8c93d73b42 | ||
|
|
3b6d60e904 | ||
|
|
a965b8e7b2 | ||
|
|
25abf52b3f | ||
|
|
19764014be | ||
|
|
c91a5c80d3 | ||
|
|
e7b88c313d | ||
|
|
9ac0864b61 | ||
|
|
fcdd0f21c7 | ||
|
|
5497b68a98 | ||
|
|
50886ac928 | ||
|
|
e2ff089232 | ||
|
|
ae7f8926f8 | ||
|
|
0bbc4e8c1b | ||
|
|
295fdad750 | ||
|
|
63e01aff8c | ||
|
|
e05ceb226c | ||
|
|
1c699841c1 | ||
|
|
385c7fb0ce | ||
|
|
15d84046db | ||
|
|
3ad396a9c2 | ||
|
|
77f886ceef | ||
|
|
8adb788205 | ||
|
|
d731317c81 | ||
|
|
a824ce691b | ||
|
|
506023b0f3 | ||
|
|
52e5d4d0f1 | ||
|
|
00edffc0f4 | ||
|
|
92f1f3e73a | ||
|
|
1d339ad4f1 | ||
|
|
99728a604d | ||
|
|
c07a67ae3c | ||
|
|
be11789a86 | ||
|
|
b8ce274fa5 | ||
|
|
d7967e3e1b | ||
|
|
746da69070 | ||
|
|
b05b7ec4ad | ||
|
|
9abdaca079 | ||
|
|
5a79b8502e | ||
|
|
466d4fba9e | ||
|
|
108f6fe393 | ||
|
|
792896c46b | ||
|
|
43d04cd54e | ||
|
|
283f905d79 | ||
|
|
dd8d1b673e | ||
|
|
9ef64660ce | ||
|
|
88b1c8fc3e | ||
|
|
bcc8b655f7 | ||
|
|
438d9eb717 | ||
|
|
2c0a0175ef | ||
|
|
e51f1b5e16 | ||
|
|
544108df37 | ||
|
|
a23639e62e | ||
|
|
cde5a6d1a4 | ||
|
|
b601c8bcfe | ||
|
|
023c8260f2 | ||
|
|
51e2e084af | ||
|
|
fc5dd8137f | ||
|
|
268fc46ef7 | ||
|
|
010c65af9c | ||
|
|
db42256dc3 | ||
|
|
e9b537b6e6 | ||
|
|
c615ef476a | ||
|
|
b93e8da235 | ||
|
|
74a0a57468 | ||
|
|
b19d665817 | ||
|
|
10dc884fa8 | ||
|
|
d8446c2d5a | ||
|
|
d3cd46bb51 | ||
|
|
bc0da03caf | ||
|
|
c0a356261b | ||
|
|
fa4060b7fe | ||
|
|
29117fc222 | ||
|
|
24ba5e5bda | ||
|
|
2d94857369 | ||
|
|
c6ea7d7e63 | ||
|
|
3916495329 | ||
|
|
4e965e59a9 | ||
|
|
0acb3aa32b | ||
|
|
9189d8bf4d | ||
|
|
ec0c96bde4 | ||
|
|
562c8c4afe | ||
|
|
fd6d4493c4 | ||
|
|
1a2419e096 | ||
|
|
b86cfd49ef | ||
|
|
d421ff9736 | ||
|
|
92c61701f2 | ||
|
|
d45d9e356c | ||
|
|
098f9a2675 | ||
|
|
0347dab82e | ||
|
|
9aa89a0df9 | ||
|
|
556bd11725 | ||
|
|
07f5c21a07 | ||
|
|
93b20960b8 | ||
|
|
3cbdd6bfd3 | ||
|
|
c3c38880e6 | ||
|
|
415bbf5b3b | ||
|
|
186cb02748 | ||
|
|
4aaccb909f | ||
|
|
2daf7dd01a | ||
|
|
ab9ed73e55 | ||
|
|
a4a33fe167 | ||
|
|
e6fbd10031 | ||
|
|
9868d96fec | ||
|
|
0d1c2ac40c | ||
|
|
a6d0dddaf7 | ||
|
|
06d57e8f32 | ||
|
|
70a40edc5d | ||
|
|
95d64208d0 | ||
|
|
e28b2e8328 | ||
|
|
4123745a6b | ||
|
|
70bb4d71e6 | ||
|
|
dd314e1741 | ||
|
|
3cbb489ac6 | ||
|
|
101df4cbf1 | ||
|
|
42263a0ec0 | ||
|
|
d402f7514e | ||
|
|
84e6674e23 | ||
|
|
afcfaace19 | ||
|
|
5a3bd8cfe5 | ||
|
|
f0c90a4744 | ||
|
|
3baed292e1 | ||
|
|
d41a2cad73 | ||
|
|
ffccc3be38 | ||
|
|
ef1e8d7ef3 | ||
|
|
7af891216d | ||
|
|
aa80500b35 | ||
|
|
b72fbe06f7 | ||
|
|
41a63a5418 | ||
|
|
e8ce7898c1 | ||
|
|
687a45c564 | ||
|
|
3ac3dd3ca5 | ||
|
|
f2efebf7d9 | ||
|
|
7b68ce49d5 | ||
|
|
8a2a41fab0 | ||
|
|
ceaaec5378 | ||
|
|
e49a3e7206 | ||
|
|
dc7986dbad | ||
|
|
2dfba130f5 | ||
|
|
155c7c409b | ||
|
|
aacb8970f8 | ||
|
|
be66a0520d | ||
|
|
3fa3c45794 | ||
|
|
0f6da1873e | ||
|
|
1564208e83 | ||
|
|
e724e8db60 | ||
|
|
5a092a83cd | ||
|
|
ffefe5e8aa | ||
|
|
631fdd8a26 | ||
|
|
53d7ef4014 | ||
|
|
5c3ac79043 | ||
|
|
90fb1646e0 | ||
|
|
54604e45e0 | ||
|
|
9ed0f9eee8 | ||
|
|
b764c44318 | ||
|
|
adbd519061 | ||
|
|
b0415299ca | ||
|
|
e96d05149c | ||
|
|
354ddcfee5 | ||
|
|
6d232778e2 | ||
|
|
95ee7daf21 | ||
|
|
2238ac5d17 | ||
|
|
c209c1c034 | ||
|
|
b1eec16333 | ||
|
|
e126c45fb3 | ||
|
|
c89ff93be4 | ||
|
|
c82c27a5c5 | ||
|
|
b3e84f407a | ||
|
|
72902c8984 | ||
|
|
2c47c5eb99 | ||
|
|
398129f3e1 | ||
|
|
d74ab12d9e | ||
|
|
679c0599dd | ||
|
|
4d04ad5632 | ||
|
|
3fdc50b354 | ||
|
|
7eeff32185 | ||
|
|
d40f2cb852 | ||
|
|
f9dc2fb6d5 | ||
|
|
de31dfb11e | ||
|
|
ef6a648189 | ||
|
|
09953e2af8 | ||
|
|
be240119e8 | ||
|
|
2b7893c834 | ||
|
|
896e824ca1 | ||
|
|
7a94725808 | ||
|
|
a66fb76e9a | ||
|
|
b453d48fee | ||
|
|
a7f2c07998 | ||
|
|
3cff878f74 | ||
|
|
665d536481 | ||
|
|
ec6d407fbb | ||
|
|
72bc7ed6d4 | ||
|
|
ac407ca2c0 | ||
|
|
6af5f2b528 | ||
|
|
8fd4a98fbe | ||
|
|
07d553fae3 | ||
|
|
34e0eea173 | ||
|
|
73e6db9a12 | ||
|
|
0df464ac03 | ||
|
|
78ee6afbae | ||
|
|
31be74e6d3 | ||
|
|
767a09894a | ||
|
|
0d410d107d | ||
|
|
7829b18b3c | ||
|
|
b2267a55ce | ||
|
|
eca016fe61 | ||
|
|
81723f7fa9 | ||
|
|
44c91fb90c | ||
|
|
7cb5bd9c95 | ||
|
|
8196f6b9db | ||
|
|
d72b16531b | ||
|
|
f333196efe | ||
|
|
6ea047dcb4 | ||
|
|
ea65867b23 | ||
|
|
c41200e762 | ||
|
|
4c70afbb53 | ||
|
|
0c1ce66053 | ||
|
|
fa8b8cebf9 | ||
|
|
72fa89ba76 | ||
|
|
dc7b4cebf2 | ||
|
|
3c1dd94915 | ||
|
|
3decbbac3a | ||
|
|
27f43569f5 | ||
|
|
bd9bded73b | ||
|
|
0ce81e1ab6 | ||
|
|
2926201694 | ||
|
|
059be2c853 | ||
|
|
dd09f31abb | ||
|
|
1da20da3ff | ||
|
|
341773830b | ||
|
|
c65452bb01 | ||
|
|
34d81356a3 | ||
|
|
c9b84a5202 | ||
|
|
5394cc2dc9 | ||
|
|
63141f339f | ||
|
|
079a0b56c3 | ||
|
|
66721affe7 | ||
|
|
d8ab23e9ba | ||
|
|
11b0a5c9cc | ||
|
|
d273a72cb3 | ||
|
|
08641a6694 | ||
|
|
9ac5c0d886 | ||
|
|
8a57d33223 | ||
|
|
1945b53e43 | ||
|
|
161e1820a8 | ||
|
|
22b5ac8622 | ||
|
|
f75a70e42b | ||
|
|
c9076606be | ||
|
|
853d22b947 | ||
|
|
82d6719d91 | ||
|
|
3f02c150e3 | ||
|
|
d7d46a93a7 | ||
|
|
dfd51635a6 | ||
|
|
c04366d505 | ||
|
|
fd89e88d40 | ||
|
|
894de923b9 | ||
|
|
c47e7cd91d | ||
|
|
e359347a3b | ||
|
|
d320017e3c | ||
|
|
e6c34f4311 | ||
|
|
9b0c945086 | ||
|
|
bc85f5de1d | ||
|
|
4df219161c | ||
|
|
06f157e634 | ||
|
|
54addbdd28 | ||
|
|
0e721917e7 | ||
|
|
4dc7089f89 | ||
|
|
30b5a35db2 | ||
|
|
dc8f81b536 | ||
|
|
a018770a18 | ||
|
|
5e4f7c5d8e | ||
|
|
1d9d665ed0 | ||
|
|
68477c09a7 | ||
|
|
574b9086d4 | ||
|
|
d74c323c66 | ||
|
|
8e85a1b84e | ||
|
|
6ce1cb4325 | ||
|
|
fbbd85d8b2 | ||
|
|
e611dc43c5 | ||
|
|
c86309cfc0 | ||
|
|
ec74e9bce0 | ||
|
|
f371e8a523 | ||
|
|
57a059eecb | ||
|
|
2cb149c647 | ||
|
|
ee5371b582 | ||
|
|
70e4dbe3bd | ||
|
|
18ead9a64f | ||
|
|
d2764cee2a | ||
|
|
082c098420 | ||
|
|
46a42e2901 | ||
|
|
c21cacd309 | ||
|
|
81ac359f71 | ||
|
|
f5b91c90bc | ||
|
|
0a92a3012e | ||
|
|
ff8fc237e2 | ||
|
|
b99d943b4d | ||
|
|
3199fe08e8 | ||
|
|
7503ce62af | ||
|
|
df8ca250aa | ||
|
|
c71b4bde86 | ||
|
|
3f67802e3d | ||
|
|
093ed23140 | ||
|
|
0f8dee7011 | ||
|
|
0cb557b716 | ||
|
|
8137a776b6 | ||
|
|
1af3e0bd93 | ||
|
|
d3662f2302 | ||
|
|
5fe34cb593 | ||
|
|
af5166e95d | ||
|
|
13907d6711 | ||
|
|
c4c9f0e368 | ||
|
|
394932b57f | ||
|
|
7dff9bc696 | ||
|
|
4713eaffdb | ||
|
|
3a7992b1c8 | ||
|
|
5275aa72fb | ||
|
|
9e45b9e808 | ||
|
|
88dfa14046 | ||
|
|
dd4216d432 | ||
|
|
628ab85de4 | ||
|
|
39ea2dd32f | ||
|
|
1d77c40d0e | ||
|
|
a3cbb4158c | ||
|
|
52aa84e9f9 | ||
|
|
f08fc7493d | ||
|
|
a80b1bbcb3 | ||
|
|
7ce8bac3ea | ||
|
|
5c258797ec | ||
|
|
0dccc7e91e | ||
|
|
9e68653949 | ||
|
|
83c09b4540 | ||
|
|
5cf2672469 | ||
|
|
50144721d7 | ||
|
|
0fe7da80ab | ||
|
|
068eb33bf6 | ||
|
|
98b1a7681b | ||
|
|
488967c6ef | ||
|
|
a5aab95ecf | ||
|
|
345de3654a | ||
|
|
6ea7e785e3 | ||
|
|
0c2331f638 | ||
|
|
5fe1ce1eff | ||
|
|
6a6d6f9e0d | ||
|
|
30a512c880 | ||
|
|
2b4519a4ad | ||
|
|
3e25d41c0f | ||
|
|
d8baa93289 | ||
|
|
d8c2640959 | ||
|
|
2ae4337d0d | ||
|
|
0416060643 | ||
|
|
2b1fd77ad7 | ||
|
|
43567a3119 | ||
|
|
d463c2fbc5 | ||
|
|
940cba5f90 | ||
|
|
7321075631 | ||
|
|
a06cbc44cd | ||
|
|
088b4af795 | ||
|
|
fc0b1e8941 | ||
|
|
9ad3b12403 | ||
|
|
88ecec2f9a | ||
|
|
4ea5e9ce9b | ||
|
|
9b617af713 | ||
|
|
e70d92f670 | ||
|
|
1b3acb52f1 | ||
|
|
052ddc11b7 | ||
|
|
4e3a5a8823 | ||
|
|
05e17b70b5 | ||
|
|
949d764638 | ||
|
|
5c2cb4de80 | ||
|
|
f68c5cb4f7 | ||
|
|
06c1f376bc | ||
|
|
7c7a6a4514 | ||
|
|
5293349785 | ||
|
|
2ee0ae1f9e | ||
|
|
8143237d25 | ||
|
|
a426068273 | ||
|
|
599b19102e | ||
|
|
e7a979d6b4 | ||
|
|
7991ed0154 | ||
|
|
a9a3e50179 | ||
|
|
7642fe046b | ||
|
|
ccc2c39d43 | ||
|
|
26228e546e | ||
|
|
6036bc17c5 | ||
|
|
ca4b03f48a | ||
|
|
b0f59ad988 | ||
|
|
c9bdf43a0d | ||
|
|
dadab50f3b | ||
|
|
0b49eba77a | ||
|
|
004b7391c6 | ||
|
|
45c221a3b2 | ||
|
|
364f074be1 | ||
|
|
e82eded1e9 | ||
|
|
b298f84f51 | ||
|
|
26ff28aae6 | ||
|
|
588eb6f691 | ||
|
|
c10448af0b | ||
|
|
0eb7973ab0 | ||
|
|
150a87f2ea | ||
|
|
2505a19a88 | ||
|
|
beea02cea9 | ||
|
|
600b5cfa8e | ||
|
|
8055b5e5da | ||
|
|
4933a75d15 | ||
|
|
478e13b0fd | ||
|
|
9c26da70da | ||
|
|
872a8d983b | ||
|
|
47d3fe1de5 | ||
|
|
9421af2c3f | ||
|
|
90626e353f | ||
|
|
b3e019e7a0 | ||
|
|
84a0a0743b | ||
|
|
377bd6e2b7 | ||
|
|
dc42c6a1df | ||
|
|
0ca70a3197 | ||
|
|
889f92268e | ||
|
|
24fba7a36d | ||
|
|
c90672a5ab | ||
|
|
81c8fc0381 | ||
|
|
7922a3856e | ||
|
|
9aea452829 | ||
|
|
e797b759b7 | ||
|
|
b60d5f837e | ||
|
|
877235c8a2 | ||
|
|
558bbeaa34 | ||
|
|
0911abcfc0 | ||
|
|
04850331ce | ||
|
|
b58336f4c8 | ||
|
|
4665b4fb37 | ||
|
|
49197a2ae0 | ||
|
|
c718db21da | ||
|
|
35f28bdc71 | ||
|
|
bf86b09f4e | ||
|
|
cdde8cfdbe | ||
|
|
cd26b8f728 | ||
|
|
8abfc7609e | ||
|
|
7d06e5d684 | ||
|
|
439870a546 | ||
|
|
82b35f095e | ||
|
|
b40d7d89a1 | ||
|
|
8f3dbbc356 | ||
|
|
f748891b4b | ||
|
|
e325a5c27e | ||
|
|
7d131fbb3d | ||
|
|
2b63110323 | ||
|
|
a63ca0f0e9 | ||
|
|
5b2d0a2ade | ||
|
|
1a984df11a | ||
|
|
fdc7a19628 | ||
|
|
8087996c8e | ||
|
|
e5f264a510 | ||
|
|
0a2eb74c26 | ||
|
|
d5bb3bd799 | ||
|
|
cfa9d88be6 | ||
|
|
ac4617ae51 | ||
|
|
f6bcadfeec | ||
|
|
5272078ea3 | ||
|
|
b6257400ec | ||
|
|
c3a6e01040 | ||
|
|
b63cbbdaaa | ||
|
|
81d3f35034 | ||
|
|
1fc2866032 | ||
|
|
eb2e7b9c79 | ||
|
|
cab900f656 | ||
|
|
e2b91e5dc4 | ||
|
|
00a66e9030 | ||
|
|
775c8780a6 | ||
|
|
50a968ace8 | ||
|
|
db41104d9b | ||
|
|
95bb73c5ac | ||
|
|
7ed2776476 | ||
|
|
341dfb934d | ||
|
|
ecebe73c33 | ||
|
|
1b0c6b919f | ||
|
|
06b876cf8b | ||
|
|
de0d0a3526 | ||
|
|
7f99ac0efa | ||
|
|
d2980c58ec | ||
|
|
d244ed6c64 | ||
|
|
a30d9a1af2 | ||
|
|
7acd6a4d3c | ||
|
|
a6fdcb7493 | ||
|
|
0cc2437a9d | ||
|
|
8102cb63ae | ||
|
|
5062d74041 | ||
|
|
feebb349d5 | ||
|
|
1036813b97 | ||
|
|
6b405700ec | ||
|
|
29b4a83d93 | ||
|
|
7b159c1e63 | ||
|
|
02c64ad3a5 | ||
|
|
13c625d7c0 | ||
|
|
9a3f49bf9c | ||
|
|
6698ca400c | ||
|
|
6bb649bac5 | ||
|
|
b72b74b6c6 | ||
|
|
e7bfea8c69 | ||
|
|
de7e805718 | ||
|
|
7b5e8646eb | ||
|
|
966e147a20 | ||
|
|
269a5bd914 | ||
|
|
4f1f56f653 | ||
|
|
6b517d7ffd | ||
|
|
4eb9a1cfa5 | ||
|
|
db3aeb9ab5 | ||
|
|
984b7bbeea | ||
|
|
582beed977 | ||
|
|
7fa8bd5613 | ||
|
|
4474172c40 | ||
|
|
1dde397e2b | ||
|
|
7ea3b6ca15 | ||
|
|
e52fcf843c | ||
|
|
08ba273089 | ||
|
|
956e7b564c | ||
|
|
08990dd58a | ||
|
|
faa2d632e5 | ||
|
|
1b939ebf4b | ||
|
|
aa46216117 | ||
|
|
ca32434535 | ||
|
|
ebf4f17f17 | ||
|
|
22778091f9 | ||
|
|
4d389ae5ce | ||
|
|
39e3120058 | ||
|
|
6b15d7e260 | ||
|
|
277025775d | ||
|
|
dbd649bfb5 | ||
|
|
df4ddba1ab | ||
|
|
21985d0814 | ||
|
|
f7031dcb7f | ||
|
|
0219e62979 | ||
|
|
e66725047a | ||
|
|
84fa99a126 | ||
|
|
78b3c9552b | ||
|
|
1222aeaab6 | ||
|
|
cbbfc5b58c | ||
|
|
11164ab838 | ||
|
|
8339f7fdb3 | ||
|
|
220cd84ef5 | ||
|
|
e5fa446159 | ||
|
|
70c320e98b | ||
|
|
8e486da928 | ||
|
|
7ae906863d | ||
|
|
fb4483fdcf | ||
|
|
e32e6e0bec | ||
|
|
05e7b90aab | ||
|
|
ee59f91ba2 | ||
|
|
ece3241041 | ||
|
|
fcb1bcb91b | ||
|
|
728c0e8272 | ||
|
|
9b212d11f0 | ||
|
|
223209e1eb | ||
|
|
5b741a10db | ||
|
|
1606ea19a8 | ||
|
|
e5632019db | ||
|
|
ff994d594a | ||
|
|
0214ced8f0 | ||
|
|
813e5e1db8 | ||
|
|
69627911b3 | ||
|
|
29f905b942 | ||
|
|
551fa7fe10 | ||
|
|
2ce3cd4c2a | ||
|
|
8ea24a6b09 | ||
|
|
00ca91399a | ||
|
|
cf327077e9 | ||
|
|
f215ba9bac | ||
|
|
c3c6b3d166 | ||
|
|
b319cb9525 | ||
|
|
95cd327022 | ||
|
|
807cfebf76 | ||
|
|
a29b1259b5 | ||
|
|
102b8afd66 | ||
|
|
561fdef815 | ||
|
|
47db1db861 | ||
|
|
de3d7e925a | ||
|
|
b04c286efc | ||
|
|
b8b82189f7 | ||
|
|
08b65a954d | ||
|
|
c6c998dc9f | ||
|
|
b3ff91608e | ||
|
|
1d862db7c9 | ||
|
|
070cbeebbe | ||
|
|
2c95f07cb2 | ||
|
|
4a2277b424 | ||
|
|
a1f02916d4 | ||
|
|
900dfd92d0 | ||
|
|
d6997b0588 | ||
|
|
86c74b3ee0 | ||
|
|
b1a8c70d20 | ||
|
|
9107d1678c | ||
|
|
095234babc | ||
|
|
8aecec507e | ||
|
|
70fb1551af | ||
|
|
fb67e123f4 | ||
|
|
ae2efbc116 | ||
|
|
4bc0ffa74d | ||
|
|
6fed932a61 | ||
|
|
939ebcf897 | ||
|
|
1239fa874d | ||
|
|
779ab39f50 | ||
|
|
00283e3d6e | ||
|
|
2b4429f8b7 | ||
|
|
2446c4185a | ||
|
|
04900e5f90 | ||
|
|
8abdb8bf51 | ||
|
|
e217068dbd | ||
|
|
e3a9f753d2 | ||
|
|
fc2a586453 | ||
|
|
979ea449bd | ||
|
|
ba5e2cfc45 | ||
|
|
3b565d8bb1 | ||
|
|
21a92b62fd | ||
|
|
7e33261ccc | ||
|
|
6a489a0b8f | ||
|
|
44e9c77568 | ||
|
|
77816aebac | ||
|
|
9dd967f2aa | ||
|
|
ef7a08879f | ||
|
|
647e444a07 | ||
|
|
c417239652 | ||
|
|
e10c92878d | ||
|
|
fc376bfe3f | ||
|
|
36fe4eaa49 | ||
|
|
a3baab9671 | ||
|
|
edd6c0bd4c | ||
|
|
ce59db528b |
@@ -9,7 +9,7 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{js,html,js,hbs,less}]
|
||||
[*.{js,html,js,hbs,less,css}]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
24
.gitattributes
vendored
24
.gitattributes
vendored
@@ -1,22 +1,12 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
*text eol=lf
|
||||
* text=auto
|
||||
|
||||
# Explicitly set bash scripts to have unix endings
|
||||
# when checked out on windows
|
||||
*.sh text eol=lf
|
||||
distribution/debian/* text eol=lf
|
||||
macOS/Sonarr text eol=lf
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
*.sln merge=union
|
||||
*.csproj merge=union
|
||||
*.vbproj merge=union
|
||||
*.fsproj merge=union
|
||||
*.dbproj merge=union
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -45,6 +45,10 @@ _dotCover*
|
||||
# DevExpress CodeRush
|
||||
src/.cr/
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
\#*\#
|
||||
|
||||
# NCrunch
|
||||
*.ncrunch*
|
||||
.*crunch*.local.xml
|
||||
@@ -115,7 +119,9 @@ node_modules/
|
||||
_output*
|
||||
_rawPackage/
|
||||
_dotTrace*
|
||||
_tests/
|
||||
_tests*
|
||||
_publish*
|
||||
_temp*
|
||||
*.Result.xml
|
||||
setup/Output/
|
||||
*.~is
|
||||
@@ -133,6 +139,5 @@ output/*
|
||||
.DS_Store
|
||||
|
||||
_start
|
||||
_temp_*/**/*
|
||||
|
||||
src/.idea/
|
||||
|
||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
||||
[submodule "src/ExternalModules/CurlSharp"]
|
||||
path = src/ExternalModules/CurlSharp
|
||||
url = https://github.com/Sonarr/CurlSharp.git
|
||||
branch = master
|
||||
3
.yarnrc
3
.yarnrc
@@ -1 +1,2 @@
|
||||
save-prefix ""
|
||||
save-exact true
|
||||
registry "https://registry.yarnpkg.com"
|
||||
|
||||
@@ -7,21 +7,7 @@ Setup guides, FAQ, the more information we have on the wiki the better.
|
||||
|
||||
## Development ##
|
||||
|
||||
### Tools required ###
|
||||
- Visual Studio 2015
|
||||
- HTML/Javascript editor of choice (Sublime Text/Webstorm/Atom/etc)
|
||||
- npm (node package manager)
|
||||
- git
|
||||
|
||||
### Getting started ###
|
||||
|
||||
1. Fork Sonarr
|
||||
2. Clone (develop branch) *you may need pull in submodules separately if you client doesn't clone them automatically (CurlSharp)*
|
||||
3. Run `npm install`
|
||||
4. Run `npm start` - Used to compile the UI components and copy them.
|
||||
Leave this window open.
|
||||
If you have gulp globally installed you can use `gulp watch` instead
|
||||
5. Compile in Visual Studio
|
||||
See the readme for information on setting up your development environment.
|
||||
|
||||
### Contributing Code ###
|
||||
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
|
||||
|
||||
85
README.md
85
README.md
@@ -1,52 +1,73 @@
|
||||
# Sonarr
|
||||
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
||||
|
||||
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||
|
||||
## Major Features Include:
|
||||
## Getting Started
|
||||
|
||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
* Automatically detects new episodes
|
||||
* Can scan your existing library and download any missing episodes
|
||||
* Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
||||
* Automatic failed download handling will try another release if one fails
|
||||
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||
* Fully configurable episode renaming
|
||||
* Full integration with SABnzbd and NZBGet
|
||||
* Full integration with Kodi, Plex (notification, library update, metadata)
|
||||
* Full support for specials and multi-episode releases
|
||||
* And a beautiful UI
|
||||
- [Download](https://sonarr.tv/#download) (Linux, MacOS, Windows, Docker, etc.)
|
||||
- [Installation](https://github.com/Sonarr/Sonarr/wiki/Installation)
|
||||
- [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
|
||||
- [Wiki](https://github.com/Sonarr/Sonarr/wiki)
|
||||
- [API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
|
||||
|
||||
## Support
|
||||
|
||||
- [Donate](https://sonarr.tv/donate)
|
||||
- [Discord](https://discord.gg/M6BvZn5)
|
||||
- [Reddit](https://www.reddit.com/r/sonarr)
|
||||
|
||||
## Features
|
||||
|
||||
### Current Features
|
||||
|
||||
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
- Automatically detects new episodes
|
||||
- Can scan your existing library and download any missing episodes
|
||||
- Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
||||
- Automatic failed download handling will try another release if one fails
|
||||
- Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||
- Fully configurable episode renaming
|
||||
- Full integration with SABnzbd and NZBGet
|
||||
- Full integration with Kodi, Plex (notification, library update, metadata)
|
||||
- Full support for specials and multi-episode releases
|
||||
- And a beautiful UI
|
||||
|
||||
## Configuring Development Environment:
|
||||
|
||||
### Requirements
|
||||
|
||||
* Visual Studio 2015 (https://www.visualstudio.com/vs/)
|
||||
* [Git](https://git-scm.com/downloads)
|
||||
* [NodeJS](https://nodejs.org/en/download/)
|
||||
- [Visual Studio 2017](https://www.visualstudio.com/vs)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS](https://nodejs.org/en/download)
|
||||
- [Yarn](https://yarnpkg.com)
|
||||
|
||||
### Setup
|
||||
|
||||
* Make sure all the required software mentioned above are installed.
|
||||
* Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
|
||||
* Grab the submodules `git submodule init && git submodule update`
|
||||
* Install the required Node Packages `npm install`
|
||||
* Start gulp to monitor your dev environment for any changes that need post processing using `npm start` command.
|
||||
- Make sure all the required software mentioned above are installed
|
||||
- Clone the repository recursively to get Sonarr and it's submodules
|
||||
- You can do this by running `git clone --recursive https://github.com/Sonarr/Sonarr.git`
|
||||
- Install the required Node Packages using `yarn`
|
||||
|
||||
*Please note gulp must be running at all times while you are working with Sonarr client source files.*
|
||||
### Backend Development
|
||||
|
||||
### Development
|
||||
- Run `yarn build` to build the UI
|
||||
- Open `Sonarr.sln` in Visual Studio
|
||||
- Make sure `Sonarr.Console` is set as the startup project
|
||||
- Build `Sonarr.Windows` and `Sonarr.Mono` projects
|
||||
- Build Solution
|
||||
|
||||
* Open `NzbDrone.sln` in Visual Studio
|
||||
* Make sure `NzbDrone.Console` is set as the startup project
|
||||
### UI Development
|
||||
|
||||
### License
|
||||
- Run `yarn watch` to build UI and rebuild automatically when changes are detected
|
||||
- Run Sonarr.Console.exe (or debug in Visual Studio)
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2017
|
||||
### Licenses
|
||||
|
||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
- Copyright 2010-2020
|
||||
|
||||
### Sponsors
|
||||
|
||||
* [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
||||
* [ReSharper](http://www.jetbrains.com/resharper/)
|
||||
* [WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
* [TeamCity](http://www.jetbrains.com/teamcity/)
|
||||
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
||||
- [ReSharper](http://www.jetbrains.com/resharper/)
|
||||
- [TeamCity](http://www.jetbrains.com/teamcity/)
|
||||
|
||||
185
build.sh
185
build.sh
@@ -1,15 +1,16 @@
|
||||
#! /bin/bash
|
||||
msBuildVersion='15.0'
|
||||
outputFolder='./_output'
|
||||
outputFolderWindows='./_output_windows'
|
||||
outputFolderLinux='./_output_linux'
|
||||
outputFolderMacOS='./_output_macos'
|
||||
outputFolderMacOSApp='./_output_macos_app'
|
||||
testPackageFolder='./_tests/'
|
||||
testSearchPattern='*.Test/bin/x86/Release'
|
||||
testPackageFolder='./_tests'
|
||||
testPackageFolderWindows='./_tests_windows'
|
||||
testPackageFolderLinux='./_tests_linux'
|
||||
sourceFolder='./src'
|
||||
slnFile=$sourceFolder/Sonarr.sln
|
||||
updateFolder=$outputFolder/Sonarr.Update
|
||||
updateFolderMono=$outputFolderLinux/Sonarr.Update
|
||||
updateSubFolder=Sonarr.Update
|
||||
|
||||
nuget='tools/nuget/nuget.exe';
|
||||
vswhere='tools/vswhere/vswhere.exe';
|
||||
@@ -47,7 +48,8 @@ UpdateVersionNumber()
|
||||
verBuild=`echo "${BUILD_NUMBER}" | cut -d. -f4`
|
||||
BUILD_NUMBER=$verMajorMinorRevision.$verBuild
|
||||
echo "##teamcity[buildNumber '$BUILD_NUMBER']"
|
||||
sed -i "s/^[[]assembly: Assembly\(File\|Informational\)\?Version[(]\"[0-9.*]\+\"[)]/[assembly: Assembly\1Version(\"$BUILD_NUMBER\")/g" ./src/NzbDrone*/Properties/AssemblyInfo.cs ./src/Sonarr*/Properties/AssemblyInfo.cs ./src/ServiceHelpers/*/Properties/AssemblyInfo.cs ./src/Common/CommonVersionInfo.cs
|
||||
sed -i "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$BUILD_NUMBER<\/AssemblyVersion>/g" ./src/Directory.Build.props
|
||||
sed -i "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BRANCH:-dev}<\/AssemblyConfiguration>/g" ./src/Directory.Build.props
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -86,13 +88,14 @@ CleanFolder()
|
||||
|
||||
BuildWithMSBuild()
|
||||
{
|
||||
installationPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -property installationPath`
|
||||
installationPath=${installationPath/C:\\/\/c\/}
|
||||
installationPath=${installationPath//\\/\/}
|
||||
msBuild="$installationPath/MSBuild/$msBuildVersion/Bin"
|
||||
echo $msBuild
|
||||
msBuildPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -find MSBuild\\\\\*\*\\\\Bin\\\\MSBuild.exe`
|
||||
msBuildPath=${msBuildPath/C:\\/\/c\/}
|
||||
msBuildPath=${msBuildPath//\\/\/}
|
||||
msBuildDir=$(dirname "$msBuildPath")
|
||||
|
||||
export PATH=$msBuild:$PATH
|
||||
echo $msBuildDir
|
||||
|
||||
export PATH=$msBuildDir:$PATH
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Clean //m
|
||||
$nuget restore $slnFile
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
|
||||
@@ -101,15 +104,15 @@ BuildWithMSBuild()
|
||||
BuildWithXbuild()
|
||||
{
|
||||
export MONO_IOMAP=case
|
||||
CheckExitCode xbuild /t:Clean $slnFile
|
||||
CheckExitCode msbuild /t:Clean $slnFile
|
||||
mono $nuget restore $slnFile
|
||||
CheckExitCode xbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
|
||||
CheckExitCode msbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
|
||||
}
|
||||
|
||||
LintUI()
|
||||
{
|
||||
ProgressStart 'ESLint'
|
||||
CheckExitCode yarn eslint
|
||||
CheckExitCode yarn lint
|
||||
ProgressEnd 'ESLint'
|
||||
|
||||
ProgressStart 'Stylelint'
|
||||
@@ -122,6 +125,7 @@ Build()
|
||||
ProgressStart 'Build'
|
||||
|
||||
rm -rf $outputFolder
|
||||
rm -rf $testPackageFolder
|
||||
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
BuildWithMSBuild
|
||||
@@ -167,6 +171,48 @@ CreateMdbs()
|
||||
fi
|
||||
}
|
||||
|
||||
PatchMono()
|
||||
{
|
||||
local path=$1
|
||||
|
||||
# Below we deal with some mono incompatibilities with windows-only dotnet core/standard libs
|
||||
# See: https://github.com/mono/mono/blob/master/tools/nuget-hash-extractor/download.sh
|
||||
# That list defines assemblies that are prohibited from being loaded from the appdir, instead loading from mono GAC.
|
||||
|
||||
# We have debian dependencies to get these installed or facades from mono 5.10+
|
||||
for assembly in System.IO.Compression System.Runtime.InteropServices.RuntimeInformation System.Net.Http System.Globalization.Extensions System.Text.Encoding.CodePages System.Threading.Overlapped
|
||||
do
|
||||
if [ -e $path/$assembly.dll ]; then
|
||||
if [ -e $sourceFolder/Libraries/Mono/$assembly.dll ]; then
|
||||
echo "Copy Mono-specific facade $assembly.dll (uses win32 interop)"
|
||||
cp $sourceFolder/Libraries/Mono/$assembly.dll $path/$assembly.dll
|
||||
else
|
||||
echo "Remove $assembly.dll (uses win32 interop)"
|
||||
rm $path/$assembly.dll
|
||||
fi
|
||||
|
||||
fi
|
||||
done
|
||||
|
||||
# Copy more stable version of Vectors for mono <5.12
|
||||
if [ -e $path/System.Numerics.Vectors.dll ]; then
|
||||
packageDir="$HOME/.nuget/packages/system.numerics.vectors/4.5.0"
|
||||
|
||||
if [ ! -d "$HOME/.nuget/packages/system.numerics.vectors/4.5.0" ]; then
|
||||
# May reside in the NuGetFallback folder, which is harder to find
|
||||
# Download somewhere to get the real cache populated
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
$nuget install System.Numerics.Vectors -Version 4.5.0 -Output ./_temp/System.Numerics.Vectors
|
||||
else
|
||||
mono $nuget install System.Numerics.Vectors -Version 4.5.0 -Output ./_temp/System.Numerics.Vectors
|
||||
fi
|
||||
rm -rf ./_temp/System.Numerics.Vectors
|
||||
fi
|
||||
# Copy the netstandard2.0 version rather than net46
|
||||
cp "$packageDir/lib/netstandard2.0/System.Numerics.Vectors.dll" $path/
|
||||
fi
|
||||
}
|
||||
|
||||
PackageMono()
|
||||
{
|
||||
ProgressStart 'Creating Mono Package'
|
||||
@@ -190,15 +236,14 @@ PackageMono()
|
||||
rm -f $outputFolderLinux/sqlite3.*
|
||||
rm -f $outputFolderLinux/MediaInfo.*
|
||||
|
||||
PatchMono $outputFolderLinux
|
||||
|
||||
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $outputFolderLinux
|
||||
|
||||
echo "Adding CurlSharp.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $outputFolderLinux
|
||||
|
||||
echo "Adding unix System.Runtime.InteropServices.RuntimeInformation.dll (for SharpRaven)"
|
||||
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux
|
||||
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux/Sonarr.Update
|
||||
# Remove Http binding redirect by renaming it
|
||||
# We don't need this anymore once our minimum mono version is 5.10
|
||||
sed -i "s/System.Net.Http/System.Net.Http.Mono/g" $outputFolderLinux/Sonarr.Console.exe.config
|
||||
|
||||
echo "Renaming Sonarr.Console.exe to Sonarr.exe"
|
||||
rm $outputFolderLinux/Sonarr.exe*
|
||||
@@ -210,7 +255,7 @@ PackageMono()
|
||||
rm $outputFolderLinux/Sonarr.Windows.*
|
||||
|
||||
echo "Adding Sonarr.Mono to UpdatePackage"
|
||||
cp $outputFolderLinux/Sonarr.Mono.* $updateFolderMono
|
||||
cp $outputFolderLinux/Sonarr.Mono.* $outputFolderLinux/$updateSubFolder/
|
||||
|
||||
ProgressEnd 'Creating Mono Package'
|
||||
}
|
||||
@@ -266,54 +311,92 @@ PackageMacOSApp()
|
||||
ProgressEnd 'Creating macOS App Package'
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
PackageTestsMono()
|
||||
{
|
||||
ProgressStart 'Creating Test Package'
|
||||
ProgressStart 'Creating Mono Test Package'
|
||||
|
||||
rm -rf $testPackageFolder
|
||||
mkdir $testPackageFolder
|
||||
rm -rf $testPackageFolderLinux
|
||||
|
||||
find $sourceFolder -path $testSearchPattern -exec cp -r -u -T "{}" $testPackageFolder \;
|
||||
echo "Copying Binaries"
|
||||
cp -r $testPackageFolder $testPackageFolderLinux
|
||||
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
$nuget install NUnit.ConsoleRunner -Version 3.2.0 -Output $testPackageFolder
|
||||
$nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderLinux
|
||||
else
|
||||
mono $nuget install NUnit.ConsoleRunner -Version 3.2.0 -Output $testPackageFolder
|
||||
mono $nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderLinux
|
||||
fi
|
||||
|
||||
cp $outputFolder/*.dll $testPackageFolder
|
||||
cp ./test.sh $testPackageFolder
|
||||
echo "Creating MDBs"
|
||||
CreateMdbs $testPackageFolderLinux
|
||||
|
||||
echo "Creating MDBs for tests"
|
||||
CreateMdbs $testPackageFolder
|
||||
echo "Removing PDBs"
|
||||
find $testPackageFolderLinux -name "*.pdb" -exec rm "{}" \;
|
||||
|
||||
rm -f $testPackageFolder/*.log.config
|
||||
|
||||
CleanFolder $testPackageFolder true
|
||||
PatchMono $testPackageFolderLinux
|
||||
|
||||
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $testPackageFolder
|
||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $testPackageFolderLinux
|
||||
|
||||
echo "Adding CurlSharp.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $testPackageFolder
|
||||
# Remove Http binding redirect by renaming it
|
||||
# We don't need this anymore once our minimum mono version is 5.10
|
||||
sed -i "s/System.Net.Http/System.Net.Http.Mono/g" $testPackageFolderLinux/Sonarr.Common.Test.dll.config
|
||||
|
||||
echo "Copying CurlSharp libraries"
|
||||
cp $sourceFolder/ExternalModules/CurlSharp/libs/i386/* $testPackageFolder
|
||||
cp ./test.sh $testPackageFolderLinux/
|
||||
dos2unix $testPackageFolderLinux/test.sh
|
||||
|
||||
ProgressEnd 'Creating Test Package'
|
||||
echo "Removing Sonarr.Windows"
|
||||
rm $testPackageFolderLinux/Sonarr.Windows.*
|
||||
|
||||
rm -f $testPackageFolderLinux/*.log.config
|
||||
|
||||
CleanFolder $testPackageFolderLinux true
|
||||
|
||||
ProgressEnd 'Creating Linux Test Package'
|
||||
}
|
||||
|
||||
CleanupWindowsPackage()
|
||||
PackageTestsWindows()
|
||||
{
|
||||
ProgressStart 'Cleaning Windows Package'
|
||||
ProgressStart 'Creating Windows Test Package'
|
||||
|
||||
rm -rf $testPackageFolderWindows
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $testPackageFolder $testPackageFolderWindows
|
||||
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
$nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderWindows
|
||||
else
|
||||
mono $nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderWindows
|
||||
fi
|
||||
|
||||
cp ./test.sh $testPackageFolderWindows
|
||||
|
||||
echo "Removing Sonarr.Mono"
|
||||
rm -f $outputFolder/Sonarr.Mono.*
|
||||
rm -f $testPackageFolderWindows/Sonarr.Mono.*
|
||||
|
||||
rm -f $testPackageFolderWindows/*.log.config
|
||||
|
||||
CleanFolder $testPackageFolderWindows true
|
||||
|
||||
ProgressEnd 'Creating Windows Test Package'
|
||||
}
|
||||
|
||||
PackageWindows()
|
||||
{
|
||||
ProgressStart 'Creating Windows Package'
|
||||
|
||||
rm -rf $outputFolderWindows
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $outputFolder $outputFolderWindows
|
||||
|
||||
echo "Removing Sonarr.Mono"
|
||||
rm -f $outputFolderWindows/Sonarr.Mono.*
|
||||
|
||||
echo "Adding Sonarr.Windows to UpdatePackage"
|
||||
cp $outputFolder/Sonarr.Windows.* $updateFolder
|
||||
cp $outputFolderWindows/Sonarr.Windows.* $outputFolderWindows/$updateSubFolder/
|
||||
|
||||
ProgressEnd 'Cleaning Windows Package'
|
||||
ProgressEnd 'Creating Windows Package'
|
||||
}
|
||||
|
||||
PublishArtifacts()
|
||||
@@ -321,10 +404,11 @@ PublishArtifacts()
|
||||
ProgressStart 'Publishing Artifacts'
|
||||
|
||||
# Tests
|
||||
echo "##teamcity[publishArtifacts '_tests/** => tests.zip']"
|
||||
echo "##teamcity[publishArtifacts '$testPackageFolderWindows/** => tests.windows.zip']"
|
||||
echo "##teamcity[publishArtifacts '$testPackageFolderLinux/** => tests.linux.zip']"
|
||||
|
||||
# Releases
|
||||
echo "##teamcity[publishArtifacts '$outputFolder/** => Sonarr.$BRANCH.$BUILD_NUMBER.windows.zip!Sonarr']"
|
||||
echo "##teamcity[publishArtifacts '$outputFolderWindows/** => Sonarr.$BRANCH.$BUILD_NUMBER.windows.zip!Sonarr']"
|
||||
echo "##teamcity[publishArtifacts '$outputFolderLinux/** => Sonarr.$BRANCH.$BUILD_NUMBER.linux.tar.gz!Sonarr']"
|
||||
echo "##teamcity[publishArtifacts '$outputFolderMacOS/** => Sonarr.$BRANCH.$BUILD_NUMBER.macos.tar.gz!Sonarr']"
|
||||
echo "##teamcity[publishArtifacts '$outputFolderMacOSApp/** => Sonarr.$BRANCH.$BUILD_NUMBER.macos.zip']"
|
||||
@@ -354,6 +438,7 @@ RunGulp
|
||||
PackageMono
|
||||
PackageMacOS
|
||||
PackageMacOSApp
|
||||
PackageTests
|
||||
CleanupWindowsPackage
|
||||
PackageTestsMono
|
||||
PackageTestsWindows
|
||||
PackageWindows
|
||||
PublishArtifacts
|
||||
|
||||
7
distribution/build-package.sh
Normal file
7
distribution/build-package.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
# Note, this script is only used for local dev tests, this is not the script used for building the official sonarr package
|
||||
|
||||
mkdir -p /${PWD}/../_output_debian
|
||||
|
||||
docker build -f docker-build/Dockerfile -t sonarr-packager ./docker-build
|
||||
|
||||
docker run --rm -v /${PWD}/../_output_linux:/data/sonarr_bin:ro -v /${PWD}:/data/build -v /${PWD}/../_output_debian:/data/output sonarr-packager
|
||||
@@ -19,7 +19,11 @@ sed -i '/#BEGIN BUILTIN UPDATER/,/#END BUILTIN UPDATER/d' debian/preinst debian/
|
||||
echo "# Do Not Edit\nPackageVersion=$BuildVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$PackageUpdater\nBranch=$BuildBranch" > package_info
|
||||
|
||||
echo Running debuild for $BuildVersion
|
||||
debuild -b
|
||||
if [ -z "${TEST_OUTPUT}" ]; then
|
||||
debuild -b
|
||||
else
|
||||
debuild -us -uc -b
|
||||
fi
|
||||
|
||||
# Restore debian directory to the original files
|
||||
rm -rf ./debian
|
||||
@@ -32,16 +36,28 @@ sed -i '/#BEGIN BUILTIN UPDATER/d; /#END BUILTIN UPDATER/d' debian/preinst debia
|
||||
echo "# Do Not Edit\nPackageVersion=$BootstrapVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$BootstrapUpdater\nBranch=$BuildBranch" > package_info
|
||||
|
||||
echo Running debuild for $BootstrapVersion
|
||||
debuild -b
|
||||
if [ -z "${TEST_OUTPUT}" ]; then
|
||||
debuild -b
|
||||
else
|
||||
debuild -us -uc -b
|
||||
fi
|
||||
|
||||
echo Moving stuff around
|
||||
mv ../sonarr_*.deb ./
|
||||
mv ../sonarr_*.changes ./
|
||||
rm ../sonarr_*.build
|
||||
|
||||
echo Signing Package
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BuildVersion}_all.deb"
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BootstrapVersion}_all.deb"
|
||||
if [ -z "${TEST_OUTPUT}" ]; then
|
||||
echo Signing Package
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BuildVersion}_all.deb"
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BootstrapVersion}_all.deb"
|
||||
|
||||
echo running alien
|
||||
alien -r -v ./*.deb
|
||||
echo running alien
|
||||
alien -r -v ./*.deb
|
||||
else
|
||||
echo "Exporting packages to ${TEST_OUTPUT}"
|
||||
dpkg -e "sonarr_${BuildVersion}_all.deb" ${TEST_OUTPUT}/sonarr-build
|
||||
dpkg -e "sonarr_${BootstrapVersion}_all.deb" ${TEST_OUTPUT}/sonarr-release
|
||||
|
||||
cp *.deb ${TEST_OUTPUT}/
|
||||
fi
|
||||
|
||||
@@ -7,15 +7,16 @@ Vcs-Git: git@github.com:Sonarr/Sonarr.git
|
||||
Vcs-Browser: https://github.com/Sonarr/Sonarr
|
||||
Build-Depends: debhelper (>= 9),
|
||||
dh-systemd (>= 1.5),
|
||||
mono-devel (>= 4.6),
|
||||
libmono-cil-dev (>= 4.6),
|
||||
cli-common-dev (>= 0.5.7)
|
||||
mono-devel (>= 5.18),
|
||||
libmono-cil-dev (>= 5.18),
|
||||
cli-common-dev (>= 0.9+xamarin5)
|
||||
|
||||
Package: sonarr
|
||||
Architecture: all
|
||||
Provides: nzbdrone
|
||||
Conflicts: nzbdrone
|
||||
Replaces: nzbdrone
|
||||
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52), mono-runtime (>= 5.4), ${cli:Depends}, ${misc:Depends}
|
||||
Recommends: sqlite3 (>= 3.7), mediainfo (>= 0.7.52), ${cli:Recommends}
|
||||
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52) | libmediainfo0 (>= 0.7.52), mono-runtime (>= 5.18), ca-certificates-mono, libmono-system-net-http4.0-cil (>= 4.0.0~alpha1), ${cli:Depends}, ${misc:Depends}
|
||||
Recommends: libmediainfo0v5 (>= 18.03) | libmediainfo0 (>= 18.03)
|
||||
Suggests: sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
|
||||
Description: Internet PVR
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
sonarr_bin/* /usr/lib/sonarr/bin
|
||||
package_info /usr/lib/sonarr
|
||||
sonarr_bin/* usr/lib/sonarr/bin
|
||||
package_info usr/lib/sonarr
|
||||
|
||||
@@ -95,7 +95,7 @@ chown -R $USER:$GROUP /usr/lib/sonarr
|
||||
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
|
||||
|
||||
#BEGIN BUILTIN UPDATER
|
||||
if [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ]; then
|
||||
if [ "$UPDATER" = "BuiltIn" ]; then
|
||||
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
|
||||
touch $CONFDIR/update_required
|
||||
chown $USER:$GROUP $CONFDIR/update_required
|
||||
@@ -104,4 +104,4 @@ fi
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
exit 0
|
||||
|
||||
@@ -22,10 +22,25 @@ if [ $1 = "install" ]; then
|
||||
fi
|
||||
|
||||
if [ "$psNzbDroneUnit" != "-" ] && [ -d /run/systemd/system ]; then
|
||||
# The user used a systemd auto-startup for NzbDrone, we can deal with that.
|
||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and disabling..."
|
||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||
deb-systemd-invoke mask $psNzbDroneUnit >/dev/null
|
||||
if [ "$psNzbDroneUnit" = "sonarr.service" ]; then
|
||||
# Conflicts with our new sonarr.service so we have to remove it
|
||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and removing..."
|
||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||
if [ -f "/etc/systemd/system/$psNzbDroneUnit" ]; then
|
||||
rm /etc/systemd/system/$psNzbDroneUnit
|
||||
fi
|
||||
if [ -f "/usr/lib/systemd/system/$psNzbDroneUnit" ]; then
|
||||
rm /usr/lib/systemd/system/$psNzbDroneUnit
|
||||
fi
|
||||
deb-systemd-helper purge $psNzbDroneUnit >/dev/null
|
||||
deb-systemd-helper unmask $psNzbDroneUnit >/dev/null
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
else
|
||||
# Just disable it, so the user can revisit the settings later
|
||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and disabling..."
|
||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||
deb-systemd-invoke mask $psNzbDroneUnit >/dev/null
|
||||
fi
|
||||
else
|
||||
# We don't support auto migration for other startup methods, so bail.
|
||||
# This leaves the sonarr package in an incomplete state.
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
# Sample debian/rules that uses debhelper.
|
||||
# This file was originally written by Joey Hess and Craig Small.
|
||||
# As a special exception, when this file is copied by dh-make into a
|
||||
# dh-make output file, you may use that output file without restriction.
|
||||
# This special exception was added by Craig Small in version 0.37 of dh-make.
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
# Note: System.Native is a dependency of System.Runtime.InteropServices.RuntimeInformation used by SharpRaven,
|
||||
# but SharpRaven doesn't use any functions that need System.Native
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi System.Native
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr mscorlib mscoree.dll Microsoft.DiaSymReader.Native.x86.dll Microsoft.DiaSymReader.Native.amd64.dll
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd --with=cli
|
||||
@@ -20,7 +12,7 @@ EXCLUDE_MODULEREFS = crypt32 httpapi System.Native
|
||||
override_dh_installinit:
|
||||
true
|
||||
|
||||
# Sonarr like debug symbols for logging
|
||||
# Sonarr likes debug symbols for logging
|
||||
override_dh_clistrip:
|
||||
|
||||
override_dh_makeclilibs:
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
recommends libcurl3
|
||||
ignores msbuild
|
||||
ignores msbuild
|
||||
ignores libmediainfo0v5
|
||||
|
||||
23
distribution/docker-build/Dockerfile
Normal file
23
distribution/docker-build/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM ubuntu:xenial AS builder
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV MONO_VERSION 5.18
|
||||
|
||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
||||
echo "deb http://download.mono-project.com/repo/debian stable-xenial/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||
apt-get update && apt-get install -y \
|
||||
devscripts build-essential tofrodos \
|
||||
dh-make dh-systemd \
|
||||
cli-common-dev \
|
||||
mono-complete \
|
||||
sqlite3 libcurl3 mediainfo
|
||||
|
||||
RUN apt-cache policy mono-complete
|
||||
RUN apt-cache policy cli-common-dev
|
||||
|
||||
COPY debian-start.sh /debian-start.sh
|
||||
RUN fromdos /debian-start.sh
|
||||
|
||||
WORKDIR /data
|
||||
VOLUME [ "/data/sonarr_bin", "/data/build", "/data/output" ]
|
||||
CMD /debian-start.sh
|
||||
18
distribution/docker-build/debian-start.sh
Normal file
18
distribution/docker-build/debian-start.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
echo "Debian Build Dev bootstrap..."
|
||||
|
||||
export TEST_OUTPUT=/data/output
|
||||
|
||||
mkdir ${TEST_OUTPUT}
|
||||
|
||||
mkdir /data/temp
|
||||
|
||||
cp -rf /data/build/debian.sh /data/temp
|
||||
cp -rf /data/build/debian /data/temp
|
||||
cp -rf /data/sonarr_bin /data/temp/sonarr_bin
|
||||
|
||||
cd /data/temp
|
||||
|
||||
ls -al .
|
||||
|
||||
fromdos debian.sh
|
||||
sh debian.sh
|
||||
22
docker/tests/mono/complete/Dockerfile
Normal file
22
docker/tests/mono/complete/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM ubuntu:xenial
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ARG MONO_VERSION=5.20
|
||||
ARG MONO_URL=stable-xenial/snapshots/$MONO_VERSION
|
||||
|
||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
||||
echo "deb http://download.mono-project.com/repo/debian $MONO_URL main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||
apt-get update && apt-get install -y \
|
||||
tofrodos tzdata \
|
||||
mono-complete \
|
||||
sqlite3 mediainfo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY startup.sh /startup.sh
|
||||
RUN fromdos /startup.sh
|
||||
|
||||
WORKDIR /data/
|
||||
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
||||
|
||||
CMD bash /startup.sh
|
||||
|
||||
29
docker/tests/mono/sonarr/Dockerfile
Normal file
29
docker/tests/mono/sonarr/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
FROM ubuntu:xenial
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ARG MONO_VERSION=5.20
|
||||
ARG MONO_URL=stable-xenial/snapshots/$MONO_VERSION
|
||||
|
||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
||||
echo "deb http://download.mono-project.com/repo/debian $MONO_URL main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||
apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 2009837CBFFD68F45BC180471F4F90DE2A9B4BF8 && \
|
||||
echo "deb http://apt.sonarr.tv/ubuntu xenial main" > /etc/apt/sources.list.d/sonarr.list && \
|
||||
apt-get update && apt-get install -y \
|
||||
tofrodos tzdata \
|
||||
sonarr \
|
||||
sqlite3 mediainfo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libmono-system-runtime4.0-cil \
|
||||
libmono-system-net-http4.0-cil \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY startup.sh /startup.sh
|
||||
RUN fromdos /startup.sh
|
||||
|
||||
WORKDIR /data/
|
||||
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
||||
|
||||
CMD bash /startup.sh
|
||||
|
||||
15
docker/tests/mono/startup.sh
Normal file
15
docker/tests/mono/startup.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
echo "Preparing Test..."
|
||||
mkdir -p /data/test
|
||||
cp -r /data/_tests_linux/* /data/test/
|
||||
cp -r /data/_output_linux /data/test/bin
|
||||
|
||||
cd /data/test
|
||||
|
||||
runTest()
|
||||
{
|
||||
bash test.sh Linux $1
|
||||
cp TestResult.xml /data/_tests_results/TestResult_$1.xml
|
||||
}
|
||||
|
||||
runTest Integration
|
||||
runTest Unit
|
||||
121
docker/tests/run-all.sh
Normal file
121
docker/tests/run-all.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
|
||||
opt_parallel=
|
||||
opt_version=
|
||||
opt_mode=both
|
||||
while getopts 'pv:m:r?h' c
|
||||
do
|
||||
case $c in
|
||||
p) opt_parallel=1 ;;
|
||||
v) opt_version=$OPTARG ;;
|
||||
m) opt_mode=$OPTARG ;;
|
||||
r) opt_report=1 ;;
|
||||
?|h) printf "Usage: %s [-p] [-v mono-ver] [-m sonarr|complete]\n" $0
|
||||
printf " -p run parallel\n"
|
||||
printf " -v run specified mono version\n"
|
||||
printf " -m run only mono-'complete' or 'sonarr' package variants\n"
|
||||
printf " -r only report\n"
|
||||
exit 2
|
||||
esac
|
||||
done
|
||||
# NOTE:
|
||||
# each container has a 1gb tmpfs mounted since it greatly speeds up the normally intensive db operations
|
||||
# make sure that the docker host has enough memory to handle about ~300 MB per container, so 2-3 GB total
|
||||
# excess goes to the swap and will slow down the entire system
|
||||
|
||||
MONO_VERSIONS=""
|
||||
|
||||
# Future versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 6.10=preview-xenial"
|
||||
|
||||
# Semi-Supported versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 6.8 6.6 6.4 6.0"
|
||||
|
||||
# Supported versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 5.20 5.18"
|
||||
|
||||
# Legacy unsupported versions (but appear to work)
|
||||
MONO_VERSIONS="$MONO_VERSIONS 5.16 5.14 5.12"
|
||||
|
||||
# Legacy unsupported versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 5.10 5.8 5.4 5.0"
|
||||
#MONO_VERSIONS="$MONO_VERSIONS 4.8=stable-wheezy/snapshots/4.8"
|
||||
|
||||
if [ "$opt_version" != "" ]; then
|
||||
MONO_VERSIONS="$opt_version"
|
||||
fi
|
||||
|
||||
mkdir -p ${PWD}/../../_tests_results
|
||||
|
||||
prepOne() {
|
||||
local MONO_VERSION_PAIR=$1
|
||||
|
||||
MONO_VERSION_SPLIT=(${MONO_VERSION_PAIR//=/ })
|
||||
MONO_VERSION=${MONO_VERSION_SPLIT[0]}
|
||||
MONO_URL=${MONO_VERSION_SPLIT[1]:-"stable-xenial/snapshots/$MONO_VERSION"}
|
||||
|
||||
echo "Building Test Docker for mono $MONO_VERSION"
|
||||
|
||||
if [ "$opt_mode" != "sonarr" ]; then
|
||||
docker build -t sonarr-test-$MONO_VERSION --build-arg MONO_VERSION=$MONO_VERSION --build-arg MONO_URL=$MONO_URL --file mono/complete/Dockerfile mono
|
||||
fi
|
||||
|
||||
if [ "$opt_mode" != "complete" ] && [ "$MONO_VERSION" != "5.0" ]; then
|
||||
docker build -t sonarr-test-$MONO_VERSION-sonarr --build-arg MONO_VERSION=$MONO_VERSION --build-arg MONO_URL=$MONO_URL --file mono/sonarr/Dockerfile mono
|
||||
fi
|
||||
}
|
||||
|
||||
runOne() {
|
||||
local MONO_VERSION_PAIR=$1
|
||||
|
||||
MONO_VERSION_SPLIT=(${MONO_VERSION_PAIR//=/ })
|
||||
MONO_VERSION=${MONO_VERSION_SPLIT[0]}
|
||||
|
||||
echo "Running Test Docker for mono $MONO_VERSION"
|
||||
|
||||
if [ "$opt_mode" != "sonarr" ]; then
|
||||
dockerArgs="--rm"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_linux:/data/_tests_linux:ro"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_output_linux:/data/_output_linux:ro"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_results/mono-$MONO_VERSION:/data/_tests_results"
|
||||
dockerArgs="$dockerArgs --mount type=tmpfs,destination=//data/test,tmpfs-size=1g"
|
||||
docker run $dockerArgs sonarr-test-$MONO_VERSION
|
||||
fi
|
||||
|
||||
if [ "$opt_mode" != "complete" ] && [ "$MONO_VERSION" != "5.0" ]; then
|
||||
dockerArgs="--rm"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_linux:/data/_tests_linux:ro"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_output_linux:/data/_output_linux:ro"
|
||||
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_results/mono-$MONO_VERSION-sonarr:/data/_tests_results"
|
||||
dockerArgs="$dockerArgs --mount type=tmpfs,destination=//data/test,tmpfs-size=1g"
|
||||
docker run $dockerArgs sonarr-test-$MONO_VERSION-sonarr
|
||||
fi
|
||||
|
||||
echo "Finished Test Docker for mono $MONO_VERSION"
|
||||
}
|
||||
|
||||
if [ "$opt_report" != "1" ]; then
|
||||
|
||||
if [ "$opt_parallel" == "1" ]; then
|
||||
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
|
||||
prepOne "$MONO_VERSION_PAIR"
|
||||
done
|
||||
fi
|
||||
|
||||
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
|
||||
if [ "$opt_parallel" == "1" ]; then
|
||||
runOne "$MONO_VERSION_PAIR" &
|
||||
else
|
||||
prepOne "$MONO_VERSION_PAIR"
|
||||
runOne "$MONO_VERSION_PAIR"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$opt_parallel" == "1" ]; then
|
||||
echo "Waiting for all runs to finish"
|
||||
wait
|
||||
echo "Finished all runs"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
grep "<test-run" ../../_tests_results/**/*.xml | sed -r 's/.*?mono-([0-9.]+(-s)?).*?_([IU]).*?\.xml.*?failed="([0-9]*)".*/\1\t\3:\tfailed \4/g' | sort -V -t.
|
||||
@@ -28,6 +28,12 @@
|
||||
"react"
|
||||
],
|
||||
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"filenames/match-exported": ["error"],
|
||||
|
||||
@@ -209,7 +215,6 @@
|
||||
"lines-around-comment": ["error", { "beforeBlockComment": true, "afterBlockComment": false }],
|
||||
"max-depth": ["error", {"maximum": 5}],
|
||||
"max-nested-callbacks": ["error", 4],
|
||||
"max-params": ["error", 6],
|
||||
"max-statements": "off",
|
||||
"max-statements-per-line": ["error", { "max": 1 }],
|
||||
"new-cap": ["error", {"capIsNewExceptions": ["$.Deferred", "DragDropContext", "DragLayer", "DragSource", "DropTarget"]}],
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"ignoreAtRules": [
|
||||
"/^add\\-mixin$/",
|
||||
"/^define\\-mixin$/"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"at-rule-no-vendor-prefix": true,
|
||||
|
||||
35
frontend/babel.config.js
Normal file
35
frontend/babel.config.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const loose = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-proposal-optional-chaining', { loose }],
|
||||
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
|
||||
|
||||
// Stage 2
|
||||
'@babel/plugin-proposal-export-namespace-from',
|
||||
|
||||
// Stage 3
|
||||
['@babel/plugin-proposal-class-properties', { loose }],
|
||||
'@babel/plugin-syntax-dynamic-import'
|
||||
],
|
||||
env: {
|
||||
development: {
|
||||
presets: [
|
||||
['@babel/preset-react', { development: true }]
|
||||
],
|
||||
plugins: [
|
||||
'babel-plugin-inline-classnames'
|
||||
]
|
||||
},
|
||||
production: {
|
||||
presets: [
|
||||
'@babel/preset-react'
|
||||
],
|
||||
plugins: [
|
||||
'babel-plugin-transform-react-remove-prop-types'
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,15 +1,17 @@
|
||||
const gulp = require('gulp');
|
||||
const runSequence = require('run-sequence');
|
||||
|
||||
require('./clean');
|
||||
require('./copy');
|
||||
require('./webpack');
|
||||
|
||||
gulp.task('build',
|
||||
gulp.series('clean',
|
||||
gulp.parallel(
|
||||
'webpack',
|
||||
'copyHtml',
|
||||
'copyFonts',
|
||||
'copyImages'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('build', () => {
|
||||
return runSequence('clean', [
|
||||
'webpack',
|
||||
'copyHtml',
|
||||
'copyFonts',
|
||||
'copyImages',
|
||||
'copyJs'
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
var path = require('path');
|
||||
var gulp = require('gulp');
|
||||
var print = require('gulp-print').default;
|
||||
var cache = require('gulp-cached');
|
||||
var livereload = require('gulp-livereload');
|
||||
var paths = require('./helpers/paths.js');
|
||||
|
||||
gulp.task('copyJs', () => {
|
||||
return gulp.src(
|
||||
[
|
||||
path.join(paths.src.root, 'polyfills.js')
|
||||
])
|
||||
.pipe(cache('copyJs'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const print = require('gulp-print').default;
|
||||
const cache = require('gulp-cached');
|
||||
const livereload = require('gulp-livereload');
|
||||
const paths = require('./helpers/paths.js');
|
||||
|
||||
gulp.task('copyHtml', () => {
|
||||
return gulp.src(paths.src.html)
|
||||
return gulp.src(paths.src.html, { base: paths.src.root })
|
||||
.pipe(cache('copyHtml'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
@@ -26,20 +15,20 @@ gulp.task('copyHtml', () => {
|
||||
|
||||
gulp.task('copyFonts', () => {
|
||||
return gulp.src(
|
||||
path.join(paths.src.fonts, '**', '*.*')
|
||||
path.join(paths.src.fonts, '**', '*.*'), { base: paths.src.root }
|
||||
)
|
||||
.pipe(cache('copyFonts'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.fonts))
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('copyImages', () => {
|
||||
return gulp.src(
|
||||
path.join(paths.src.images, '**', '*.*')
|
||||
path.join(paths.src.images, '**', '*.*'), { base: paths.src.root }
|
||||
)
|
||||
.pipe(cache('copyImages'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.images))
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
require('./build.js');
|
||||
require('./clean.js');
|
||||
require('./copy.js');
|
||||
require('./imageMin.js');
|
||||
require('./start.js');
|
||||
require('./stripBom.js');
|
||||
require('./watch.js');
|
||||
require('./webpack.js');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const gulpUtil = require('gulp-util');
|
||||
const colors = require('ansi-colors');
|
||||
|
||||
module.exports = function errorHandler(error) {
|
||||
gulpUtil.log(gulpUtil.colors.red(`Error (${error.plugin}): ${error.message}`));
|
||||
console.log(colors.red(`Error (${error.plugin}): ${error.message}`));
|
||||
this.emit('end');
|
||||
};
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
const path = require('path');
|
||||
const rootPath = path.resolve(__dirname + '/../../src/');
|
||||
module.exports = function(source) {
|
||||
if (this.cacheable) {
|
||||
this.cacheable();
|
||||
}
|
||||
|
||||
const resourcePath = this.resourcePath.replace(rootPath, '');
|
||||
const wrappedSource =`
|
||||
<!-- begin ${resourcePath} -->
|
||||
${source}
|
||||
<!-- end ${resourcePath} -->`;
|
||||
|
||||
return wrappedSource;
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
const root = './frontend/src/';
|
||||
const root = './frontend/src';
|
||||
|
||||
const paths = {
|
||||
src: {
|
||||
root,
|
||||
html: root + '*.html',
|
||||
scripts: root + '**/*.js',
|
||||
content: root + 'Content/',
|
||||
fonts: root + 'Content/Fonts/',
|
||||
images: root + 'Content/Images/',
|
||||
html: `${root}/*.html`,
|
||||
scripts: `${root}/**/*.js`,
|
||||
content: `${root}/Content/`,
|
||||
fonts: `${root}/Content/Fonts/`,
|
||||
images: `${root}/Content/Images/`,
|
||||
exclude: {
|
||||
libs: `!${root}JsLibraries/**`
|
||||
libs: `!${root}/JsLibraries/**`
|
||||
}
|
||||
},
|
||||
dest: {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
var gulp = require('gulp');
|
||||
var print = require('gulp-print').default;
|
||||
var paths = require('./helpers/paths.js');
|
||||
|
||||
gulp.task('imageMin', () => {
|
||||
var imagemin = require('gulp-imagemin');
|
||||
return gulp.src(paths.src.images)
|
||||
.pipe(imagemin({
|
||||
progressive: false,
|
||||
optimizationLevel: 4,
|
||||
svgoPlugins: [{ removeViewBox: false }]
|
||||
}))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.src.content + 'Images/'));
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
// will download and run sonarr (server) in a non-windows enviroment
|
||||
// you can use this if you don't care about the server code and just want to work
|
||||
// with the web code.
|
||||
|
||||
var http = require('http');
|
||||
var gulp = require('gulp');
|
||||
var fs = require('fs');
|
||||
var targz = require('tar.gz');
|
||||
var del = require('del');
|
||||
var spawn = require('child_process').spawn;
|
||||
|
||||
function download(url, dest, cb) {
|
||||
console.log('Downloading ' + url + ' to ' + dest);
|
||||
var file = fs.createWriteStream(dest);
|
||||
http.get(url, function(response) {
|
||||
response.pipe(file);
|
||||
file.on('finish', function() {
|
||||
console.log('Download completed');
|
||||
file.close(cb);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLatest(cb) {
|
||||
var branch = 'develop';
|
||||
process.argv.forEach(function(val) {
|
||||
var branchMatch = /branch=([\S]*)/.exec(val);
|
||||
if (branchMatch && branchMatch.length > 1) {
|
||||
branch = branchMatch[1];
|
||||
}
|
||||
});
|
||||
|
||||
var url = 'http://services.sonarr.tv/v1/update/' + branch + '?os=osx';
|
||||
|
||||
console.log('Checking for latest version:', url);
|
||||
|
||||
http.get(url, function(res) {
|
||||
var data = '';
|
||||
|
||||
res.on('data', function(chunk) {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', function() {
|
||||
var updatePackage = JSON.parse(data).updatePackage;
|
||||
console.log('Latest version available: ' + updatePackage.version + ' Release Date: ' + updatePackage.releaseDate);
|
||||
cb(updatePackage);
|
||||
});
|
||||
}).on('error', function(e) {
|
||||
console.log('problem with request: ' + e.message);
|
||||
});
|
||||
}
|
||||
|
||||
function extract(source, dest, cb) {
|
||||
console.log('extracting download page to ' + dest);
|
||||
new targz().extract(source, dest, function(err) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
}
|
||||
console.log('Update package extracted.');
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
gulp.task('getSonarr', function() {
|
||||
try {
|
||||
fs.mkdirSync('./_start/');
|
||||
} catch (e) {
|
||||
if (e.code !== 'EEXIST') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
getLatest(function(updatePackage) {
|
||||
var packagePath = './_start/' + updatePackage.filename;
|
||||
var dirName = './_start/' + updatePackage.version;
|
||||
download(updatePackage.url, packagePath, function() {
|
||||
extract(packagePath, dirName, function() {
|
||||
// clean old binaries
|
||||
console.log('Cleaning old binaries');
|
||||
del.sync(['./_output/*', '!./_output/UI/']);
|
||||
console.log('copying binaries to target');
|
||||
gulp.src(dirName + '/NzbDrone/*.*')
|
||||
.pipe(gulp.dest('./_output/'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('startSonarr', function() {
|
||||
var ls = spawn('mono', ['--debug', './_output/NzbDrone.exe']);
|
||||
|
||||
ls.stdout.on('data', function(data) {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
ls.stderr.on('data', function(data) {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
ls.on('close', function(code) {
|
||||
console.log('child process exited with code ' + code);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const paths = require('./helpers/paths.js');
|
||||
const stripbom = require('gulp-stripbom');
|
||||
|
||||
function stripBom(dest) {
|
||||
gulp.src([paths.src.scripts, paths.src.exclude.libs])
|
||||
.pipe(stripbom({ showLog: false }))
|
||||
.pipe(gulp.dest(dest));
|
||||
}
|
||||
|
||||
gulp.task('stripBom', () => {
|
||||
stripBom(paths.src.root);
|
||||
});
|
||||
@@ -1,27 +1,18 @@
|
||||
const gulp = require('gulp');
|
||||
const livereload = require('gulp-livereload');
|
||||
const watch = require('gulp-watch');
|
||||
const gulpWatch = require('gulp-watch');
|
||||
const paths = require('./helpers/paths.js');
|
||||
|
||||
require('./copy.js');
|
||||
require('./webpack.js');
|
||||
|
||||
function watchTask(glob, task) {
|
||||
const options = {
|
||||
name: `watch: ${task}`,
|
||||
verbose: true
|
||||
};
|
||||
return watch(glob, options, () => {
|
||||
gulp.start(task);
|
||||
});
|
||||
}
|
||||
|
||||
gulp.task('watch', ['copyHtml', 'copyFonts', 'copyImages', 'copyJs'], () => {
|
||||
function watch() {
|
||||
livereload.listen({ start: true });
|
||||
|
||||
gulp.start('webpackWatch');
|
||||
gulp.task('webpackWatch')();
|
||||
gulpWatch(paths.src.html, gulp.series('copyHtml'));
|
||||
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
|
||||
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
|
||||
}
|
||||
|
||||
watchTask(paths.src.html, 'copyHtml');
|
||||
watchTask(`${paths.src.fonts}**/*.*`, 'copyFonts');
|
||||
watchTask(`${paths.src.images}**/*.*`, 'copyImages');
|
||||
});
|
||||
gulp.task('watch', gulp.series('build', watch));
|
||||
|
||||
@@ -4,57 +4,66 @@ const livereload = require('gulp-livereload');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const errorHandler = require('./helpers/errorHandler');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
const uiFolder = 'UI';
|
||||
const root = path.join(__dirname, '..', 'src');
|
||||
const frontendFolder = path.join(__dirname, '..');
|
||||
const srcFolder = path.join(frontendFolder, 'src');
|
||||
const isProduction = process.argv.indexOf('--production') > -1;
|
||||
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
|
||||
const inlineWebWorkers = true;
|
||||
|
||||
console.log('ROOT:', root);
|
||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||
|
||||
console.log('Source Folder:', srcFolder);
|
||||
console.log('Output Folder:', distFolder);
|
||||
console.log('isProduction:', isProduction);
|
||||
console.log('isProfiling:', isProfiling);
|
||||
|
||||
const cssVarsFiles = [
|
||||
'../src/Styles/Variables/colors',
|
||||
'../src/Styles/Variables/dimensions',
|
||||
'../src/Styles/Variables/fonts',
|
||||
'../src/Styles/Variables/animations'
|
||||
'../src/Styles/Variables/animations',
|
||||
'../src/Styles/Variables/zIndexes'
|
||||
].map(require.resolve);
|
||||
|
||||
const extractCSSPlugin = new ExtractTextPlugin({
|
||||
filename: path.join('_output', uiFolder, 'Content', 'styles.css'),
|
||||
allChunks: true,
|
||||
disable: false,
|
||||
ignoreOrder: true
|
||||
});
|
||||
// Override the way HtmlWebpackPlugin injects the scripts
|
||||
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
|
||||
const head = assetTags.head.map((v) => {
|
||||
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` };
|
||||
return this.createHtmlTag(v);
|
||||
});
|
||||
const body = assetTags.body.map((v) => {
|
||||
v.attributes = { src: `/${v.attributes.src}` };
|
||||
return this.createHtmlTag(v);
|
||||
});
|
||||
|
||||
return html
|
||||
.replace('<!-- webpack bundles head -->', head.join('\r\n '))
|
||||
.replace('<!-- webpack bundles body -->', body.join('\r\n '));
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
extractCSSPlugin,
|
||||
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'vendor'
|
||||
}),
|
||||
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProduction,
|
||||
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: path.join('Content', 'styles.css')
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'frontend/src/index.html',
|
||||
filename: 'index.html'
|
||||
})
|
||||
];
|
||||
|
||||
if (isProduction) {
|
||||
plugins.push(new UglifyJSPlugin({
|
||||
sourceMap: true,
|
||||
uglifyOptions: {
|
||||
mangle: false,
|
||||
output: {
|
||||
comments: false,
|
||||
beautify: true
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: '#source-map',
|
||||
|
||||
stats: {
|
||||
@@ -66,15 +75,13 @@ const config = {
|
||||
},
|
||||
|
||||
entry: {
|
||||
preload: 'preload.js',
|
||||
vendor: 'vendor.js',
|
||||
index: 'index.js'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
modules: [
|
||||
root,
|
||||
path.join(root, 'Shims'),
|
||||
srcFolder,
|
||||
path.join(srcFolder, 'Shims'),
|
||||
'node_modules'
|
||||
],
|
||||
alias: {
|
||||
@@ -83,10 +90,22 @@ const config = {
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: path.join('_output', uiFolder, '[name].js'),
|
||||
path: distFolder,
|
||||
filename: '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
chunkIds: 'named',
|
||||
splitChunks: {
|
||||
chunks: 'initial'
|
||||
}
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
|
||||
plugins,
|
||||
|
||||
resolveLoader: {
|
||||
@@ -99,55 +118,75 @@ const config = {
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js?$/,
|
||||
exclude: /(node_modules|JsLibraries)/,
|
||||
loader: 'babel-loader',
|
||||
query: {
|
||||
plugins: ['transform-class-properties'],
|
||||
presets: ['es2015', 'decorators-legacy', 'react', 'stage-2'],
|
||||
env: {
|
||||
development: {
|
||||
plugins: ['transform-react-jsx-source']
|
||||
}
|
||||
test: /\.worker\.js$/,
|
||||
issuer: {
|
||||
// monaco-editor includes the editor.worker.js in other language workers,
|
||||
// don't use worker-loader in that case
|
||||
exclude: /monaco-editor/
|
||||
},
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
name: '[name].js',
|
||||
inline: inlineWebWorkers,
|
||||
fallback: !inlineWebWorkers
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js?$/,
|
||||
exclude: /(node_modules|JsLibraries)/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
configFile: `${frontendFolder}/babel.config.js`,
|
||||
envName: isProduction ? 'production' : 'development',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// CSS Modules
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /(node_modules|globals.css)/,
|
||||
use: extractCSSPlugin.extract({
|
||||
fallback: 'style-loader',
|
||||
use: [
|
||||
{
|
||||
loader: 'css-variables-loader',
|
||||
options: {
|
||||
cssVarsFiles
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName: '[name]-[local]-[hash:base64:5]',
|
||||
sourceMap: true
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
config: {
|
||||
ctx: {
|
||||
cssVarsFiles
|
||||
},
|
||||
path: 'frontend/postcss.config.js'
|
||||
}
|
||||
use: [
|
||||
{ loader: MiniCssExtractPlugin.loader },
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
ident: 'postcss',
|
||||
config: {
|
||||
ctx: {
|
||||
cssVarsFiles
|
||||
},
|
||||
path: 'frontend/postcss.config.js'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Global styles
|
||||
@@ -194,18 +233,35 @@ const config = {
|
||||
}
|
||||
};
|
||||
|
||||
if (isProfiling) {
|
||||
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
|
||||
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
|
||||
|
||||
config.optimization.minimizer = [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true, // Must be set to true if using source-maps in production
|
||||
terserOptions: {
|
||||
mangle: false,
|
||||
keep_classnames: true,
|
||||
keep_fnames: true
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
gulp.task('webpack', () => {
|
||||
return gulp.src('index.js')
|
||||
.pipe(webpackStream(config))
|
||||
.pipe(gulp.dest(''));
|
||||
return webpackStream(config)
|
||||
.pipe(gulp.dest('_output/UI'));
|
||||
});
|
||||
|
||||
gulp.task('webpackWatch', () => {
|
||||
config.watch = true;
|
||||
return gulp.src('')
|
||||
.pipe(webpackStream(config))
|
||||
|
||||
return webpackStream(config)
|
||||
.on('error', errorHandler)
|
||||
.pipe(gulp.dest(''))
|
||||
.pipe(gulp.dest('_output/UI'))
|
||||
.on('error', errorHandler)
|
||||
.pipe(livereload())
|
||||
.on('error', errorHandler);
|
||||
|
||||
20
frontend/jsconfig.json
Normal file
20
frontend/jsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"checkJs": false,
|
||||
"baseUrl": "src",
|
||||
"jsx": "react",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"*": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
]
|
||||
}
|
||||
@@ -14,18 +14,8 @@ module.exports = (ctx, configPath, options) => {
|
||||
return Object.assign(acc, reload(vars));
|
||||
}, {})
|
||||
},
|
||||
'postcss-nested': {},
|
||||
autoprefixer: {
|
||||
browsers: [
|
||||
'Chrome >= 30',
|
||||
'Firefox >= 30',
|
||||
'Safari >= 6',
|
||||
'Edge >= 12',
|
||||
'Explorer >= 11',
|
||||
'iOS >= 7',
|
||||
'Android >= 4.4'
|
||||
]
|
||||
}
|
||||
'postcss-color-function': {},
|
||||
'postcss-nested': {}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
@@ -41,9 +42,21 @@ class Blacklist extends Component {
|
||||
onPress={onClearBlacklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -90,7 +103,7 @@ class Blacklist extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,14 @@ class BlacklistConnector extends Component {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlacklistTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -126,6 +134,7 @@ class BlacklistConnector extends Component {
|
||||
}
|
||||
|
||||
BlacklistConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchBlacklist: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
.language,
|
||||
.quality {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.description {
|
||||
composes: title from 'Components/DescriptionList/DescriptionListItemDescription.css';
|
||||
composes: description from '~Components/DescriptionList/DescriptionListItemDescription.css';
|
||||
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -231,6 +231,40 @@ function HistoryDetails(props) {
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'downloadIgnored') {
|
||||
const {
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Message"
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
HistoryDetails.propTypes = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.markAsFailedButton {
|
||||
composes: button from 'Components/Link/Button.css';
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ function getHeaderTitle(eventType) {
|
||||
return 'Episode File Deleted';
|
||||
case 'episodeFileRenamed':
|
||||
return 'Episode File Renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Download Ignored';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
@@ -75,6 +76,16 @@ class History extends Component {
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
@@ -85,7 +96,7 @@ class History extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetchingAny && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -136,7 +147,7 @@ class History extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
|
||||
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||
import History from './History';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -28,7 +29,8 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
...historyActions,
|
||||
fetchEpisodes,
|
||||
clearEpisodes
|
||||
clearEpisodes,
|
||||
clearEpisodeFiles
|
||||
};
|
||||
|
||||
class HistoryConnector extends Component {
|
||||
@@ -68,6 +70,7 @@ class HistoryConnector extends Component {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearHistory();
|
||||
this.props.clearEpisodes();
|
||||
this.props.clearEpisodeFiles();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -137,6 +140,7 @@ class HistoryConnector extends Component {
|
||||
}
|
||||
|
||||
HistoryConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchHistory: PropTypes.func.isRequired,
|
||||
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
||||
@@ -149,7 +153,8 @@ HistoryConnector.propTypes = {
|
||||
setHistoryTableOption: PropTypes.func.isRequired,
|
||||
clearHistory: PropTypes.func.isRequired,
|
||||
fetchEpisodes: PropTypes.func.isRequired,
|
||||
clearEpisodes: PropTypes.func.isRequired
|
||||
clearEpisodes: PropTypes.func.isRequired,
|
||||
clearEpisodeFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.cell {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 35px;
|
||||
text-align: center;
|
||||
|
||||
@@ -19,6 +19,8 @@ function getIconName(eventType) {
|
||||
return icons.DELETE;
|
||||
case 'episodeFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'downloadIgnored':
|
||||
return icons.IGNORE;
|
||||
default:
|
||||
return icons.UNKNOWN;
|
||||
}
|
||||
@@ -47,6 +49,8 @@ function getTooltip(eventType, data) {
|
||||
return 'Episode file deleted';
|
||||
case 'episodeFileRenamed':
|
||||
return 'Episode file renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Episode Download Ignored';
|
||||
default:
|
||||
return 'Unknown event';
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
.downloadClient {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.releaseGroup {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.details {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
.torrent {
|
||||
composes: label from 'Components/Label.css';
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
border-color: $torrentColor;
|
||||
background-color: $torrentColor;
|
||||
}
|
||||
|
||||
.usenet {
|
||||
composes: label from 'Components/Label.css';
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
border-color: $usenetColor;
|
||||
background-color: $usenetColor;
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
@@ -34,37 +37,34 @@ class Queue extends Component {
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isPendingSelected: false,
|
||||
isConfirmRemoveModalOpen: false
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// Don't update when fetching has completed if items have changed,
|
||||
// before episodes start fetching or when episodes start fetching.
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isEpisodesFetching
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(
|
||||
this.props.isFetching &&
|
||||
nextProps.isPopulated &&
|
||||
hasDifferentItems(this.props.items, nextProps.items)
|
||||
) ||
|
||||
(!this.props.isEpisodesFetching && nextProps.isEpisodesFetching)
|
||||
(!isEpisodesFetching && prevProps.isEpisodesFetching) ||
|
||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.episodeId))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState({ selectedState: {} });
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay';
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
});
|
||||
|
||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||
@@ -100,8 +100,8 @@ class Queue extends Component {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveSelectedConfirmed = (blacklist) => {
|
||||
this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist);
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
@@ -117,7 +117,6 @@ class Queue extends Component {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
isEpisodesFetching,
|
||||
isEpisodesPopulated,
|
||||
episodesError,
|
||||
@@ -125,7 +124,7 @@ class Queue extends Component {
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
onRefreshPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -135,13 +134,15 @@ class Queue extends Component {
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isPendingSelected
|
||||
isPendingSelected,
|
||||
items
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
|
||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
|
||||
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
|
||||
const hasError = error || episodesError;
|
||||
const selectedCount = this.getSelectedIds().length;
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedCount = selectedIds.length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
return (
|
||||
@@ -173,9 +174,24 @@ class Queue extends Component {
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isRefreshing && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -232,11 +248,18 @@ class Queue extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.seriesId && item.episodeId);
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
@@ -257,7 +280,7 @@ Queue.propTypes = {
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
|
||||
@@ -18,13 +18,13 @@ function createMapStateToProps() {
|
||||
(state) => state.episodes,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
|
||||
(episodes, options, queue, isCheckForFinishedDownloadExecuting) => {
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
||||
return {
|
||||
isEpisodesFetching: episodes.isFetching,
|
||||
isEpisodesPopulated: episodes.isPopulated,
|
||||
episodesError: episodes.error,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
...options,
|
||||
...queue
|
||||
};
|
||||
@@ -129,7 +129,7 @@ class QueueConnector extends Component {
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,8 +137,8 @@ class QueueConnector extends Component {
|
||||
this.props.grabQueueItems({ ids });
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = (ids, blacklist) => {
|
||||
this.props.removeQueueItems({ ids, blacklist });
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -164,6 +164,8 @@ class QueueConnector extends Component {
|
||||
}
|
||||
|
||||
QueueConnector.propTypes = {
|
||||
includeUnknownSeriesItems: PropTypes.bool.isRequired,
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchQueue: PropTypes.func.isRequired,
|
||||
gotoQueueFirstPage: PropTypes.func.isRequired,
|
||||
|
||||
@@ -10,13 +10,13 @@ function QueueDetails(props) {
|
||||
size,
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status: queueStatus,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
errorMessage,
|
||||
progressBar
|
||||
} = props;
|
||||
|
||||
const status = queueStatus.toLowerCase();
|
||||
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
|
||||
if (status === 'pending') {
|
||||
@@ -39,7 +39,35 @@ function QueueDetails(props) {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: show an icon when download is complete, but not imported yet?
|
||||
if (trackedDownloadStatus === 'warning') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.WARNING}
|
||||
title={'Downloaded - Unable to Import: check logs for details'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.PURPLE}
|
||||
title={'Downloaded - Waiting to Import'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.PURPLE}
|
||||
title={'Downloaded - Importing'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
@@ -90,6 +118,8 @@ QueueDetails.propTypes = {
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
progressBar: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
.quality {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.protocol {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
@@ -67,13 +68,16 @@ class QueueRow extends Component {
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
series,
|
||||
episode,
|
||||
language,
|
||||
quality,
|
||||
protocol,
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
@@ -97,8 +101,8 @@ class QueueRow extends Component {
|
||||
} = this.state;
|
||||
|
||||
const progress = 100 - (sizeleft / size * 100);
|
||||
const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning';
|
||||
const isPending = status === 'Delay' || status === 'DownloadClientUnavailable';
|
||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
@@ -126,6 +130,7 @@ class QueueRow extends Component {
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
@@ -204,12 +209,26 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'language') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeLanguage
|
||||
language={language}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/>
|
||||
{
|
||||
quality ?
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -240,6 +259,22 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'title') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{title}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'outputPath') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{outputPath}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'estimatedCompletionTime') {
|
||||
return (
|
||||
<TimeleftCell
|
||||
@@ -321,6 +356,7 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!series}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
@@ -336,13 +372,16 @@ QueueRow.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadState: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
series: PropTypes.object,
|
||||
episode: PropTypes.object,
|
||||
language: PropTypes.object.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -15,11 +14,11 @@ function createMapStateToProps() {
|
||||
createEpisodeSelector(),
|
||||
createUISettingsSelector(),
|
||||
(series, episode, uiSettings) => {
|
||||
const result = _.pick(uiSettings, [
|
||||
'showRelativeDates',
|
||||
'shortDateFormat',
|
||||
'timeFormat'
|
||||
]);
|
||||
const result = {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
|
||||
result.series = series;
|
||||
result.episode = episode;
|
||||
@@ -43,8 +42,8 @@ class QueueRowConnector extends Component {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
}
|
||||
|
||||
onRemoveQueueItemPress = (blacklist) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, blacklist });
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.status {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
@@ -37,63 +37,79 @@ function QueueStatusCell(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus = 'Ok',
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'Warning';
|
||||
const hasError = trackedDownloadStatus === 'Error';
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let title = 'Downloading';
|
||||
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'Paused') {
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = 'Paused';
|
||||
}
|
||||
|
||||
if (status === 'Queued') {
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = 'Queued';
|
||||
}
|
||||
|
||||
if (status === 'Completed') {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = 'Downloaded';
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ' - Waiting to Import';
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ' - Importing';
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ' - Waiting to Process';
|
||||
iconKind = kinds.DANGER;
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'Delay') {
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = 'Pending';
|
||||
}
|
||||
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = 'Pending - Download client is unavailable';
|
||||
}
|
||||
|
||||
if (status === 'Failed') {
|
||||
if (status === 'failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
}
|
||||
|
||||
if (status === 'Warning') {
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'Completed') {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = `Import failed: ${sourceTitle}`;
|
||||
@@ -116,6 +132,7 @@ function QueueStatusCell(props) {
|
||||
title={title}
|
||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||
position={tooltipPositions.RIGHT}
|
||||
canFlip={false}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
@@ -124,9 +141,15 @@ function QueueStatusCell(props) {
|
||||
QueueStatusCell.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
QueueStatusCell.defaultProps = {
|
||||
trackedDownloadStatus: 'Ok',
|
||||
trackedDownloadState: 'Downloading'
|
||||
};
|
||||
|
||||
export default QueueStatusCell;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.message {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
@@ -21,26 +20,41 @@ class RemoveQueueItemModal extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ blacklist: false });
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
@@ -50,10 +64,11 @@ class RemoveQueueItemModal extends Component {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle
|
||||
sourceTitle,
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const blacklist = this.state.blacklist;
|
||||
const { remove, blacklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -69,17 +84,31 @@ class RemoveQueueItemModal extends Component {
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
<div>
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText="Prevents Sonarr from automatically grabbing this episode again"
|
||||
helpText="Starts a search for this episode again and prevents this release from being grabbed again"
|
||||
onChange={this.onBlacklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -93,7 +122,7 @@ class RemoveQueueItemModal extends Component {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@@ -107,6 +136,7 @@ class RemoveQueueItemModal extends Component {
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -21,26 +21,41 @@ class RemoveQueueItemsModal extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ blacklist: false });
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
@@ -50,10 +65,11 @@ class RemoveQueueItemsModal extends Component {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount
|
||||
selectedCount,
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const blacklist = this.state.blacklist;
|
||||
const { remove, blacklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -74,7 +90,23 @@ class RemoveQueueItemsModal extends Component {
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Blacklist Release{selectedCount > 1 ? 's' : ''}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
@@ -93,7 +125,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@@ -107,6 +139,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -12,8 +12,12 @@ function createMapStateToProps() {
|
||||
(state) => state.queue.options.includeUnknownSeriesItems,
|
||||
(app, status, includeUnknownSeriesItems) => {
|
||||
const {
|
||||
errors,
|
||||
warnings,
|
||||
unknownErrors,
|
||||
unknownWarnings,
|
||||
count,
|
||||
unknownCount
|
||||
totalCount
|
||||
} = status.item;
|
||||
|
||||
return {
|
||||
@@ -21,7 +25,9 @@ function createMapStateToProps() {
|
||||
isReconnecting: app.isReconnecting,
|
||||
isPopulated: status.isPopulated,
|
||||
...status.item,
|
||||
count: includeUnknownSeriesItems ? count : count - unknownCount
|
||||
count: includeUnknownSeriesItems ? totalCount : count,
|
||||
errors: includeUnknownSeriesItems ? errors || unknownErrors : errors,
|
||||
warnings: includeUnknownSeriesItems ? warnings || unknownWarnings : warnings
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.timeleft {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ function TimeleftCell(props) {
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (status === 'Delay') {
|
||||
if (status === 'delay') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
@@ -33,7 +33,7 @@ function TimeleftCell(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
@@ -47,7 +47,7 @@ function TimeleftCell(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!timeleft) {
|
||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
-
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
composes: input from 'Components/Form/TextInput.css';
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
height: 46px;
|
||||
border-radius: 0;
|
||||
@@ -35,14 +35,20 @@
|
||||
.message {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.noSeriesText {
|
||||
margin-top: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 300;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
|
||||
import styles from './AddNewSeries.css';
|
||||
|
||||
@@ -78,7 +79,8 @@ class AddNewSeries extends Component {
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
items
|
||||
items,
|
||||
hasExistingSeries
|
||||
} = this.props;
|
||||
|
||||
const term = this.state.term;
|
||||
@@ -86,7 +88,7 @@ class AddNewSeries extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title="Add New Series">
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<Icon
|
||||
@@ -100,6 +102,7 @@ class AddNewSeries extends Component {
|
||||
name="seriesLookup"
|
||||
value={term}
|
||||
placeholder="eg. Breaking Bad, tvdb:####"
|
||||
autoFocus={true}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
|
||||
@@ -120,8 +123,13 @@ class AddNewSeries extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Failed to load search results, please try again.</div>
|
||||
!isFetching && !!error ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
Failed to load search results, please try again.
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -154,15 +162,36 @@ class AddNewSeries extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!term &&
|
||||
term ?
|
||||
null :
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>It's easy to add a new series, just start typing the name the series you want to add.</div>
|
||||
<div className={styles.helpText}>
|
||||
It's easy to add a new series, just start typing the name the series you want to add.
|
||||
</div>
|
||||
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!term && !hasExistingSeries ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.noSeriesText}>
|
||||
You haven't added any series yet, do you want to import some or all of your series first?
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
to="/add/import"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
Import Existing Series
|
||||
</Button>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -175,6 +204,7 @@ AddNewSeries.propTypes = {
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasExistingSeries: PropTypes.bool.isRequired,
|
||||
onSeriesLookupChange: PropTypes.func.isRequired,
|
||||
onClearSeriesLookup: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -10,13 +10,15 @@ import AddNewSeries from './AddNewSeries';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addSeries,
|
||||
(state) => state.routing.location,
|
||||
(addSeries, location) => {
|
||||
(state) => state.series.items.length,
|
||||
(state) => state.router.location,
|
||||
(addSeries, existingSeriesCount, location) => {
|
||||
const { params } = parseUrl(location.search);
|
||||
|
||||
return {
|
||||
...addSeries,
|
||||
term: params.term,
|
||||
...addSeries
|
||||
hasExistingSeries: existingSeriesCount > 0
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -36,28 +36,28 @@
|
||||
}
|
||||
|
||||
.searchForMissingEpisodesContainer {
|
||||
composes: container from 'Components/Form/CheckInput.css';
|
||||
composes: container from '~Components/Form/CheckInput.css';
|
||||
|
||||
flex: 0 1 0;
|
||||
}
|
||||
|
||||
.searchForMissingEpisodesInput {
|
||||
composes: input from 'Components/Form/CheckInput.css';
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
composes: modalFooter from 'Components/Modal/ModalFooter.css';
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
}
|
||||
|
||||
.addButton {
|
||||
@add-mixin truncate;
|
||||
composes: button from 'Components/Link/SpinnerButton.css';
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
}
|
||||
|
||||
.hideLanguageProfile {
|
||||
composes: group from 'Components/Form/FormGroup.css';
|
||||
composes: group from '~Components/Form/FormGroup.css';
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import SeriesPoster from 'Series/SeriesPoster';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
||||
import styles from './AddNewSeriesModalContent.css';
|
||||
@@ -27,10 +28,19 @@ class AddNewSeriesModalContent extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
|
||||
props.seriesType.value :
|
||||
props.initialSeriesType,
|
||||
searchForMissingEpisodes: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.seriesType.value !== prevProps.seriesType.value) {
|
||||
this.setState({ seriesType: this.props.seriesType.value });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -47,7 +57,12 @@ class AddNewSeriesModalContent extends Component {
|
||||
}
|
||||
|
||||
onAddSeriesPress = () => {
|
||||
this.props.onAddSeriesPress(this.state.searchForMissingEpisodes);
|
||||
const {
|
||||
searchForMissingEpisodes,
|
||||
seriesType
|
||||
} = this.state;
|
||||
|
||||
this.props.onAddSeriesPress(searchForMissingEpisodes, seriesType);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -66,11 +81,14 @@ class AddNewSeriesModalContent extends Component {
|
||||
languageProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
folder,
|
||||
tags,
|
||||
showLanguageProfile,
|
||||
isSmallScreen,
|
||||
isWindows,
|
||||
onModalClose,
|
||||
onInputChange
|
||||
onInputChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -87,7 +105,8 @@ class AddNewSeriesModalContent extends Component {
|
||||
<ModalBody>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
isSmallScreen ?
|
||||
null :
|
||||
<div className={styles.poster}>
|
||||
<SeriesPoster
|
||||
className={styles.poster}
|
||||
@@ -98,17 +117,30 @@ class AddNewSeriesModalContent extends Component {
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.overview}>
|
||||
{overview}
|
||||
</div>
|
||||
{
|
||||
overview ?
|
||||
<div className={styles.overview}>
|
||||
{overview}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<Form>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Root Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
valueOptions={{
|
||||
seriesFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
selectedValueOptions={{
|
||||
seriesFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
helpText={`'${folder}' subfolder will be created automatically`}
|
||||
onChange={onInputChange}
|
||||
{...rootFolderPath}
|
||||
/>
|
||||
@@ -183,6 +215,7 @@ class AddNewSeriesModalContent extends Component {
|
||||
name="seriesType"
|
||||
onChange={onInputChange}
|
||||
{...seriesType}
|
||||
value={this.state.seriesType}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -245,6 +278,7 @@ AddNewSeriesModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
overview: PropTypes.string,
|
||||
initialSeriesType: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
@@ -254,9 +288,11 @@ AddNewSeriesModalContent.propTypes = {
|
||||
languageProfileId: PropTypes.object,
|
||||
seriesType: PropTypes.object.isRequired,
|
||||
seasonFolder: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onAddSeriesPress: PropTypes.func.isRequired
|
||||
|
||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setAddSeriesDefault, addSeries } from 'Store/Actions/addSeriesActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import AddNewSeriesModalContent from './AddNewSeriesModalContent';
|
||||
|
||||
@@ -12,7 +13,8 @@ function createMapStateToProps() {
|
||||
(state) => state.addSeries,
|
||||
(state) => state.settings.languageProfiles,
|
||||
createDimensionsSelector(),
|
||||
(addSeriesState, languageProfiles, dimensions) => {
|
||||
createSystemStatusSelector(),
|
||||
(addSeriesState, languageProfiles, dimensions, systemStatus) => {
|
||||
const {
|
||||
isAdding,
|
||||
addError,
|
||||
@@ -32,6 +34,7 @@ function createMapStateToProps() {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
isWindows: systemStatus.isWindows,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
@@ -52,14 +55,13 @@ class AddNewSeriesModalContentConnector extends Component {
|
||||
this.props.setAddSeriesDefault({ [name]: value });
|
||||
}
|
||||
|
||||
onAddSeriesPress = (searchForMissingEpisodes) => {
|
||||
onAddSeriesPress = (searchForMissingEpisodes, seriesType) => {
|
||||
const {
|
||||
tvdbId,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
languageProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
tags
|
||||
} = this.props;
|
||||
@@ -70,7 +72,7 @@ class AddNewSeriesModalContentConnector extends Component {
|
||||
monitor: monitor.value,
|
||||
qualityProfileId: qualityProfileId.value,
|
||||
languageProfileId: languageProfileId.value,
|
||||
seriesType: seriesType.value,
|
||||
seriesType,
|
||||
seasonFolder: seasonFolder.value,
|
||||
tags: tags.value,
|
||||
searchForMissingEpisodes
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
.searchResult {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
@add-mixin cover;
|
||||
|
||||
background-color: $white;
|
||||
transition: background 500ms;
|
||||
|
||||
&:hover {
|
||||
@@ -14,13 +19,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@add-mixin linkOverlay;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 0 1 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
@@ -30,9 +47,22 @@
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.tvdbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin-top: -4px;
|
||||
margin-left: auto;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.tvdbLinkIcon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.alreadyExistsIcon {
|
||||
margin-left: 10px;
|
||||
color: #37bc9b;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.overview {
|
||||
|
||||
@@ -24,7 +24,7 @@ class AddNewSeriesSearchResult extends Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.isExistingSeries && this.props.isExistingSeries) {
|
||||
this.onAddSerisModalClose();
|
||||
this.onAddSeriesModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,14 @@ class AddNewSeriesSearchResult extends Component {
|
||||
this.setState({ isNewAddSeriesModalOpen: true });
|
||||
}
|
||||
|
||||
onAddSerisModalClose = () => {
|
||||
onAddSeriesModalClose = () => {
|
||||
this.setState({ isNewAddSeriesModalOpen: false });
|
||||
}
|
||||
|
||||
onTVDBLinkPress = (event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -53,6 +57,8 @@ class AddNewSeriesSearchResult extends Component {
|
||||
overview,
|
||||
statistics,
|
||||
ratings,
|
||||
folder,
|
||||
seriesType,
|
||||
images,
|
||||
isExistingSeries,
|
||||
isSmallScreen
|
||||
@@ -72,38 +78,58 @@ class AddNewSeriesSearchResult extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.searchResult}>
|
||||
<Link
|
||||
className={styles.searchResult}
|
||||
className={styles.underlay}
|
||||
{...linkProps}
|
||||
>
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<SeriesPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
isSmallScreen ?
|
||||
null :
|
||||
<SeriesPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
overflow={true}
|
||||
/>
|
||||
}
|
||||
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
|
||||
{
|
||||
!title.contains(year) && !!year &&
|
||||
<span className={styles.year}>({year})</span>
|
||||
!title.contains(year) && year ?
|
||||
<span className={styles.year}>
|
||||
({year})
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isExistingSeries &&
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title="Already in your library"
|
||||
/>
|
||||
isExistingSeries ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title="Already in your library"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.tvdbLink}
|
||||
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||
onPress={this.onTVDBLinkPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.tvdbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={28}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -115,27 +141,41 @@ class AddNewSeriesSearchResult extends Component {
|
||||
</Label>
|
||||
|
||||
{
|
||||
!!network &&
|
||||
<Label size={sizes.LARGE}>
|
||||
{network}
|
||||
</Label>
|
||||
network ?
|
||||
<Label size={sizes.LARGE}>
|
||||
{network}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!seasonCount &&
|
||||
<Label size={sizes.LARGE}>
|
||||
{seasons}
|
||||
</Label>
|
||||
seasonCount ?
|
||||
<Label size={sizes.LARGE}>
|
||||
{seasons}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<Label
|
||||
kind={kinds.DANGER}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
status === 'ended' ?
|
||||
<Label
|
||||
kind={kinds.DANGER}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Ended
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
status === 'upcoming' ?
|
||||
<Label
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Upcoming
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -143,7 +183,7 @@ class AddNewSeriesSearchResult extends Component {
|
||||
{overview}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<AddNewSeriesModal
|
||||
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
|
||||
@@ -151,8 +191,10 @@ class AddNewSeriesSearchResult extends Component {
|
||||
title={title}
|
||||
year={year}
|
||||
overview={overview}
|
||||
folder={folder}
|
||||
initialSeriesType={seriesType}
|
||||
images={images}
|
||||
onModalClose={this.onAddSerisModalClose}
|
||||
onModalClose={this.onAddSeriesModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -169,6 +211,8 @@ AddNewSeriesSearchResult.propTypes = {
|
||||
overview: PropTypes.string,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
seriesType: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExistingSeries: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
|
||||
@@ -5,7 +5,7 @@ import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
|
||||
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
|
||||
|
||||
@@ -21,17 +21,15 @@ class ImportSeries extends Component {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
contentBody: null,
|
||||
scrollTop: 0
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setContentBodyRef = (ref) => {
|
||||
this.setState({ contentBody: ref });
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -94,13 +92,13 @@ class ImportSeries extends Component {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
contentBody
|
||||
scroller
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title="Import Series">
|
||||
<PageContentBodyConnector
|
||||
ref={this.setContentBodyRef}
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{
|
||||
@@ -121,23 +119,21 @@ class ImportSeries extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
|
||||
<ImportSeriesTableConnector
|
||||
rootFolderId={rootFolderId}
|
||||
unmappedFolders={unmappedFolders}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
selectedState={selectedState}
|
||||
contentBody={contentBody}
|
||||
scroller={scroller}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
scrollTop={this.state.scrollTop}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||
onScroll={this.onScroll}
|
||||
/>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
.importButton {
|
||||
composes: button from 'Components/Link/SpinnerButton.css';
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
height: 35px;
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
.loading {
|
||||
composes: loading from 'Components/Loading/LoadingIndicator.css';
|
||||
composes: loading from '~Components/Loading/LoadingIndicator.css';
|
||||
|
||||
margin: 0 10px 0 12px;
|
||||
text-align: left;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.folder {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 200px;
|
||||
}
|
||||
|
||||
.monitor {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 185px;
|
||||
@@ -13,28 +13,28 @@
|
||||
|
||||
.qualityProfile,
|
||||
.languageProfile {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 250px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.seriesType {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.seasonFolder {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 150px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.series {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 400px;
|
||||
min-width: 300px;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
.selectInput {
|
||||
composes: input from 'Components/Form/CheckInput.css';
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
}
|
||||
|
||||
.folder {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 1 0 200px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.monitor {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 185px;
|
||||
@@ -18,35 +18,35 @@
|
||||
|
||||
.qualityProfile,
|
||||
.languageProfile {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 250px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.seriesType {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.seasonFolder {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 150px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.series {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 400px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.hideLanguageProfile {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector';
|
||||
@@ -10,7 +9,6 @@ import styles from './ImportSeriesRow.css';
|
||||
|
||||
function ImportSeriesRow(props) {
|
||||
const {
|
||||
style,
|
||||
id,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
@@ -26,7 +24,7 @@ function ImportSeriesRow(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableRow style={style}>
|
||||
<>
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.selectInput}
|
||||
id={id}
|
||||
@@ -90,14 +88,14 @@ function ImportSeriesRow(props) {
|
||||
<ImportSeriesSelectSeriesConnector
|
||||
id={id}
|
||||
isExistingSeries={isExistingSeries}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
</VirtualTableRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ImportSeriesRow.propTypes = {
|
||||
style: PropTypes.object.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.input {
|
||||
composes: input from 'Components/Form/CheckInput.css';
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import ImportSeriesHeader from './ImportSeriesHeader';
|
||||
import ImportSeriesRowConnector from './ImportSeriesRowConnector';
|
||||
|
||||
@@ -112,15 +113,19 @@ class ImportSeriesTable extends Component {
|
||||
const item = items[rowIndex];
|
||||
|
||||
return (
|
||||
<ImportSeriesRowConnector
|
||||
<VirtualTableRow
|
||||
key={key}
|
||||
style={style}
|
||||
rootFolderId={rootFolderId}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
id={item.id}
|
||||
/>
|
||||
>
|
||||
<ImportSeriesRowConnector
|
||||
key={item.id}
|
||||
rootFolderId={rootFolderId}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
id={item.id}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,12 +138,10 @@ class ImportSeriesTable extends Component {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
isSmallScreen,
|
||||
contentBody,
|
||||
scroller,
|
||||
showLanguageProfile,
|
||||
scrollTop,
|
||||
selectedState,
|
||||
onSelectAllChange,
|
||||
onScroll
|
||||
onSelectAllChange
|
||||
} = this.props;
|
||||
|
||||
if (!items.length) {
|
||||
@@ -148,10 +151,9 @@ class ImportSeriesTable extends Component {
|
||||
return (
|
||||
<VirtualTable
|
||||
items={items}
|
||||
contentBody={contentBody}
|
||||
scroller={scroller}
|
||||
isSmallScreen={isSmallScreen}
|
||||
rowHeight={52}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
@@ -163,7 +165,6 @@ class ImportSeriesTable extends Component {
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -183,15 +184,13 @@ ImportSeriesTable.propTypes = {
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
allSeries: PropTypes.arrayOf(PropTypes.object),
|
||||
contentBody: PropTypes.object.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onRemoveSelectedStateItem: PropTypes.func.isRequired,
|
||||
onSeriesLookup: PropTypes.func.isRequired,
|
||||
onSetImportSeriesValue: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
onSetImportSeriesValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesTable;
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
.tether {
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
@@ -36,9 +31,10 @@
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
z-index: $popperZIndex;
|
||||
margin-top: 4px;
|
||||
padding: 0 8px;
|
||||
width: 400px;
|
||||
/* 400px container witdh with 8px padding on each side */
|
||||
width: 384px;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -65,7 +61,7 @@
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
composes: input from 'Components/Form/TextInput.css';
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import TetherComponent from 'react-tether';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Portal from 'Components/Portal';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -12,19 +13,6 @@ import ImportSeriesSearchResultConnector from './ImportSeriesSearchResultConnect
|
||||
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||
import styles from './ImportSeriesSelectSeries.css';
|
||||
|
||||
const tetherOptions = {
|
||||
skipMoveElement: true,
|
||||
constraints: [
|
||||
{
|
||||
to: 'window',
|
||||
attachment: 'together',
|
||||
pin: true
|
||||
}
|
||||
],
|
||||
attachment: 'top center',
|
||||
targetAttachment: 'bottom center'
|
||||
};
|
||||
|
||||
class ImportSeriesSelectSeries extends Component {
|
||||
|
||||
//
|
||||
@@ -34,6 +22,9 @@ class ImportSeriesSelectSeries extends Component {
|
||||
super(props, context);
|
||||
|
||||
this._seriesLookupTimeout = null;
|
||||
this._scheduleUpdate = null;
|
||||
this._buttonId = getUniqueElememtId();
|
||||
this._contentId = getUniqueElememtId();
|
||||
|
||||
this.state = {
|
||||
term: props.id,
|
||||
@@ -41,17 +32,15 @@ class ImportSeriesSelectSeries extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this._scheduleUpdate) {
|
||||
this._scheduleUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_setButtonRef = (ref) => {
|
||||
this._buttonRef = ref;
|
||||
}
|
||||
|
||||
_setContentRef = (ref) => {
|
||||
this._contentRef = ref;
|
||||
}
|
||||
|
||||
_addListener() {
|
||||
window.addEventListener('click', this.onWindowClick);
|
||||
}
|
||||
@@ -64,14 +53,18 @@ class ImportSeriesSelectSeries extends Component {
|
||||
// Listeners
|
||||
|
||||
onWindowClick = (event) => {
|
||||
const button = ReactDOM.findDOMNode(this._buttonRef);
|
||||
const content = ReactDOM.findDOMNode(this._contentRef);
|
||||
const button = document.getElementById(this._buttonId);
|
||||
const content = document.getElementById(this._contentId);
|
||||
|
||||
if (!button) {
|
||||
if (!button || !content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) {
|
||||
if (
|
||||
!button.contains(event.target) &&
|
||||
!content.contains(event.target) &&
|
||||
this.state.isOpen
|
||||
) {
|
||||
this.setState({ isOpen: false });
|
||||
this._removeListener();
|
||||
}
|
||||
@@ -129,129 +122,158 @@ class ImportSeriesSelectSeries extends Component {
|
||||
error.responseJSON.message;
|
||||
|
||||
return (
|
||||
<TetherComponent
|
||||
classes={{
|
||||
element: styles.tether
|
||||
}}
|
||||
{...tetherOptions}
|
||||
>
|
||||
<Link
|
||||
ref={this._setButtonRef}
|
||||
className={styles.button}
|
||||
component="div"
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
isLookingUpSeries && isQueued && !isPopulated &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div
|
||||
ref={ref}
|
||||
id={this._buttonId}
|
||||
>
|
||||
<Link
|
||||
ref={ref}
|
||||
className={styles.button}
|
||||
component="div"
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
isLookingUpSeries && isQueued && !isPopulated ?
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && selectedSeries && isExistingSeries &&
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
}
|
||||
{
|
||||
isPopulated && selectedSeries && isExistingSeries ?
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && selectedSeries &&
|
||||
<ImportSeriesTitle
|
||||
title={selectedSeries.title}
|
||||
year={selectedSeries.year}
|
||||
network={selectedSeries.network}
|
||||
isExistingSeries={isExistingSeries}
|
||||
/>
|
||||
}
|
||||
{
|
||||
isPopulated && selectedSeries ?
|
||||
<ImportSeriesTitle
|
||||
title={selectedSeries.title}
|
||||
year={selectedSeries.year}
|
||||
network={selectedSeries.network}
|
||||
isExistingSeries={isExistingSeries}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !selectedSeries &&
|
||||
<div className={styles.noMatches}>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
{
|
||||
isPopulated && !selectedSeries ?
|
||||
<div className={styles.noMatches}>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
No match found!
|
||||
</div>
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
title={errorMessage}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
{
|
||||
!isFetching && !!error ?
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
title={errorMessage}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
Search failed, please try again later.
|
||||
</div>
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className={styles.dropdownArrowContainer}>
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{
|
||||
this.state.isOpen &&
|
||||
<div
|
||||
ref={this._setContentRef}
|
||||
className={styles.contentContainer}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<Icon name={icons.SEARCH} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
name={`${name}_textInput`}
|
||||
value={this.state.term}
|
||||
onChange={this.onSearchInputChange}
|
||||
<div className={styles.dropdownArrowContainer}>
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
|
||||
<FormInputButton
|
||||
kind={kinds.DEFAULT}
|
||||
spinnerIcon={icons.REFRESH}
|
||||
canSpin={true}
|
||||
isSpinning={isFetching}
|
||||
onPress={this.onRefreshPress}
|
||||
>
|
||||
<Icon name={icons.REFRESH} />
|
||||
</FormInputButton>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
|
||||
<div className={styles.results}>
|
||||
<Portal>
|
||||
<Popper
|
||||
placement="bottom"
|
||||
modifiers={{
|
||||
preventOverflow: {
|
||||
boundariesElement: 'viewport'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ ref, style, scheduleUpdate }) => {
|
||||
this._scheduleUpdate = scheduleUpdate;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id={this._contentId}
|
||||
className={styles.contentContainer}
|
||||
style={style}
|
||||
>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ImportSeriesSearchResultConnector
|
||||
key={item.tvdbId}
|
||||
tvdbId={item.tvdbId}
|
||||
title={item.title}
|
||||
year={item.year}
|
||||
network={item.network}
|
||||
onPress={this.onSeriesSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
this.state.isOpen ?
|
||||
<div className={styles.content}>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<Icon name={icons.SEARCH} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
name={`${name}_textInput`}
|
||||
value={this.state.term}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
|
||||
<FormInputButton
|
||||
kind={kinds.DEFAULT}
|
||||
spinnerIcon={icons.REFRESH}
|
||||
canSpin={true}
|
||||
isSpinning={isFetching}
|
||||
onPress={this.onRefreshPress}
|
||||
>
|
||||
<Icon name={icons.REFRESH} />
|
||||
</FormInputButton>
|
||||
</div>
|
||||
|
||||
<div className={styles.results}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ImportSeriesSearchResultConnector
|
||||
key={item.tvdbId}
|
||||
tvdbId={item.tvdbId}
|
||||
title={item.title}
|
||||
year={item.year}
|
||||
network={item.network}
|
||||
onPress={this.onSeriesSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</TetherComponent>
|
||||
);
|
||||
}}
|
||||
</Popper>
|
||||
</Portal>
|
||||
</Manager>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||
import createImportSeriesItemSelector from 'Store/Selectors/createImportSeriesItemSelector';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import ImportSeriesSelectSeries from './ImportSeriesSelectSeries';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -41,13 +41,23 @@ class ImportSeriesSelectSeriesConnector extends Component {
|
||||
onSeriesSelect = (tvdbId) => {
|
||||
const {
|
||||
id,
|
||||
items
|
||||
items,
|
||||
onInputChange
|
||||
} = this.props;
|
||||
|
||||
const selectedSeries = items.find((item) => item.tvdbId === tvdbId);
|
||||
|
||||
this.props.setImportSeriesValue({
|
||||
id,
|
||||
selectedSeries: _.find(items, { tvdbId })
|
||||
selectedSeries
|
||||
});
|
||||
|
||||
if (selectedSeries.seriesType !== seriesTypes.STANDARD) {
|
||||
onInputChange({
|
||||
name: 'seriesType',
|
||||
value: selectedSeries.seriesType
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -69,6 +79,7 @@ ImportSeriesSelectSeriesConnector.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
selectedSeries: PropTypes.object,
|
||||
isSelected: PropTypes.bool,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
queueLookupSeries: PropTypes.func.isRequired,
|
||||
setImportSeriesValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
.link {
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
.freeSpace,
|
||||
.unmappedFolders {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 45px;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import ImportSeriesRootFolderRow from './ImportSeriesRootFolderRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
() => {
|
||||
return {
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
deleteRootFolder
|
||||
};
|
||||
|
||||
class ImportSeriesRootFolderRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDeletePress = () => {
|
||||
this.props.deleteRootFolder({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportSeriesRootFolderRow
|
||||
{...this.props}
|
||||
onDeletePress={this.onDeletePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesRootFolderRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
deleteRootFolder: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesRootFolderRowConnector);
|
||||
@@ -2,39 +2,15 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import ImportSeriesRootFolderRowConnector from './ImportSeriesRootFolderRowConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import RootFolders from 'RootFolder/RootFolders';
|
||||
import styles from './ImportSeriesSelectFolder.css';
|
||||
|
||||
const rootFolderColumns = [
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'freeSpace',
|
||||
label: 'Free Space',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'unmappedFolders',
|
||||
label: 'Unmapped Folders',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class ImportSeriesSelectFolder extends Component {
|
||||
|
||||
//
|
||||
@@ -77,7 +53,7 @@ class ImportSeriesSelectFolder extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title="Import Series">
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -99,10 +75,10 @@ class ImportSeriesSelectFolder extends Component {
|
||||
Some tips to ensure the import goes smoothly:
|
||||
<ul>
|
||||
<li className={styles.tip}>
|
||||
Make sure your files include the quality in the name. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
|
||||
Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
|
||||
</li>
|
||||
<li className={styles.tip}>
|
||||
Point Sonarr to the folder containing all of your tv shows not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span>
|
||||
Point Sonarr to the folder containing all of your tv shows, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -110,26 +86,13 @@ class ImportSeriesSelectFolder extends Component {
|
||||
{
|
||||
items.length > 0 ?
|
||||
<div className={styles.recentFolders}>
|
||||
<FieldSet legend="Recent Folders">
|
||||
<Table
|
||||
columns={rootFolderColumns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((rootFolder) => {
|
||||
return (
|
||||
<ImportSeriesRootFolderRowConnector
|
||||
key={rootFolder.id}
|
||||
id={rootFolder.id}
|
||||
path={rootFolder.path}
|
||||
freeSpace={rootFolder.freeSpace}
|
||||
unmappedFolders={rootFolder.unmappedFolders}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<FieldSet legend="Root Folders">
|
||||
<RootFolders
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
error={error}
|
||||
items={items}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<Button
|
||||
@@ -169,7 +132,7 @@ class ImportSeriesSelectFolder extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -181,8 +144,7 @@ ImportSeriesSelectFolder.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired,
|
||||
onDeleteRootFolderPress: PropTypes.func.isRequired
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesSelectFolder;
|
||||
|
||||
@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { push } from 'react-router-redux';
|
||||
import { push } from 'connected-react-router';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import { fetchRootFolders, addRootFolder, deleteRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchRootFolders, addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import ImportSeriesSelectFolder from './ImportSeriesSelectFolder';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -24,7 +24,6 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
fetchRootFolders,
|
||||
addRootFolder,
|
||||
deleteRootFolder,
|
||||
push
|
||||
};
|
||||
|
||||
@@ -60,10 +59,6 @@ class ImportSeriesSelectFolderConnector extends Component {
|
||||
this.props.addRootFolder({ path });
|
||||
}
|
||||
|
||||
onDeleteRootFolderPress = (id) => {
|
||||
this.props.deleteRootFolder({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -72,7 +67,6 @@ class ImportSeriesSelectFolderConnector extends Component {
|
||||
<ImportSeriesSelectFolder
|
||||
{...this.props}
|
||||
onNewRootFolderSelect={this.onNewRootFolderSelect}
|
||||
onDeleteRootFolderPress={this.onDeleteRootFolderPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -84,7 +78,6 @@ ImportSeriesSelectFolderConnector.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
addRootFolder: PropTypes.func.isRequired,
|
||||
deleteRootFolder: PropTypes.func.isRequired,
|
||||
push: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import DocumentTitle from 'react-document-title';
|
||||
import { Provider } from 'react-redux';
|
||||
import { ConnectedRouter } from 'react-router-redux';
|
||||
import { ConnectedRouter } from 'connected-react-router';
|
||||
import PageConnector from 'Components/Page/PageConnector';
|
||||
import AppRoutes from './AppRoutes';
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import Diagnostic from 'Diagnostic/Diagnostic';
|
||||
|
||||
function AppRoutes(props) {
|
||||
const {
|
||||
@@ -229,6 +230,15 @@ function AppRoutes(props) {
|
||||
component={Logs}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Diagnostics
|
||||
*/}
|
||||
|
||||
<Route
|
||||
path="/diag"
|
||||
component={Diagnostic}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Not Found
|
||||
*/}
|
||||
|
||||
6
frontend/src/App/ColorImpairedContext.js
Normal file
6
frontend/src/App/ColorImpairedContext.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
const ColorImpairedContext = React.createContext(false);
|
||||
export const ColorImpairedConsumer = ColorImpairedContext.Consumer;
|
||||
|
||||
export default ColorImpairedContext;
|
||||
@@ -63,27 +63,27 @@
|
||||
*/
|
||||
|
||||
.downloaded {
|
||||
composes: downloaded from 'Calendar/Events/CalendarEvent.css';
|
||||
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.downloading {
|
||||
composes: downloading from 'Calendar/Events/CalendarEvent.css';
|
||||
composes: downloading from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.unmonitored {
|
||||
composes: unmonitored from 'Calendar/Events/CalendarEvent.css';
|
||||
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.onAir {
|
||||
composes: onAir from 'Calendar/Events/CalendarEvent.css';
|
||||
composes: onAir from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.missing {
|
||||
composes: missing from 'Calendar/Events/CalendarEvent.css';
|
||||
composes: missing from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.premiere {
|
||||
composes: premiere from 'Calendar/Events/CalendarEvent.css';
|
||||
composes: premiere from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
|
||||
@@ -175,6 +175,7 @@ class CalendarConnector extends Component {
|
||||
}
|
||||
|
||||
CalendarConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
time: PropTypes.string,
|
||||
view: PropTypes.string.isRequired,
|
||||
firstDayOfWeek: PropTypes.number.isRequired,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.calendarPageBody {
|
||||
composes: contentBody from 'Components/Page/PageContentBody.css';
|
||||
composes: contentBody from '~Components/Page/PageContentBody.css';
|
||||
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.calendarInnerPageBody {
|
||||
composes: innerContentBody from 'Components/Page/PageContentBody.css';
|
||||
composes: innerContentBody from '~Components/Page/PageContentBody.css';
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -3,9 +3,10 @@ import React, { Component } from 'react';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import Measure from 'Components/Measure';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import NoSeries from 'Series/NoSeries';
|
||||
@@ -76,8 +77,10 @@ class CalendarPage extends Component {
|
||||
filters,
|
||||
hasSeries,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing,
|
||||
useCurrentPage,
|
||||
onRssSyncPress,
|
||||
onFilterSelect
|
||||
} = this.props;
|
||||
|
||||
@@ -99,6 +102,15 @@ class CalendarPage extends Component {
|
||||
onPress={this.onGetCalendarLinkPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="RSS Sync"
|
||||
iconName={icons.RSS}
|
||||
isSpinning={isRssSyncExecuting}
|
||||
onPress={onRssSyncPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Search for Missing"
|
||||
iconName={icons.SEARCH}
|
||||
@@ -126,7 +138,7 @@ class CalendarPage extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector
|
||||
<PageContentBody
|
||||
className={styles.calendarPageBody}
|
||||
innerClassName={styles.calendarInnerPageBody}
|
||||
>
|
||||
@@ -147,7 +159,7 @@ class CalendarPage extends Component {
|
||||
hasSeries &&
|
||||
<LegendConnector />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<CalendarLinkModal
|
||||
isOpen={isCalendarLinkModalOpen}
|
||||
@@ -168,10 +180,12 @@ CalendarPage.propTypes = {
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasSeries: PropTypes.bool.isRequired,
|
||||
missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||
isSearchingForMissing: PropTypes.bool.isRequired,
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
onSearchMissingPress: PropTypes.func.isRequired,
|
||||
onDaysCountChange: PropTypes.func.isRequired,
|
||||
onRssSyncPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ import { createSelector } from 'reselect';
|
||||
import moment from 'moment';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
import isBefore from 'Utilities/Date/isBefore';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import CalendarPage from './CalendarPage';
|
||||
|
||||
function createMissingEpisodeIdsSelector() {
|
||||
@@ -25,7 +28,7 @@ function createMissingEpisodeIdsSelector() {
|
||||
moment(airDateUtc).isAfter(start) &&
|
||||
moment(airDateUtc).isBefore(end) &&
|
||||
isBefore(episode.airDateUtc) &&
|
||||
!queueDetails.some((details) => details.episode.id === episode.id)
|
||||
!queueDetails.some((details) => !!details.episode && details.episode.id === episode.id)
|
||||
) {
|
||||
acc.push(episode.id);
|
||||
}
|
||||
@@ -59,6 +62,7 @@ function createMapStateToProps() {
|
||||
createSeriesCountSelector(),
|
||||
createUISettingsSelector(),
|
||||
createMissingEpisodeIdsSelector(),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createIsSearchingSelector(),
|
||||
(
|
||||
selectedFilterKey,
|
||||
@@ -66,6 +70,7 @@ function createMapStateToProps() {
|
||||
seriesCount,
|
||||
uiSettings,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing
|
||||
) => {
|
||||
return {
|
||||
@@ -74,6 +79,7 @@ function createMapStateToProps() {
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||
hasSeries: !!seriesCount,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing
|
||||
};
|
||||
}
|
||||
@@ -82,6 +88,12 @@ function createMapStateToProps() {
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRssSyncPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.RSS_SYNC
|
||||
}));
|
||||
},
|
||||
|
||||
onSearchMissingPress(episodeIds) {
|
||||
dispatch(searchMissing({ episodeIds }));
|
||||
},
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
flex: 1 0 14.28%;
|
||||
overflow: hidden;
|
||||
min-height: 70px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
border-left: 1px solid $borderColor;
|
||||
border-bottom: 1px solid $calendarBorderColor;
|
||||
border-left: 1px solid $calendarBorderColor;
|
||||
}
|
||||
|
||||
.isSingleDay {
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
.dayOfMonth {
|
||||
padding-right: 5px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
border-bottom: 1px solid $calendarBorderColor;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.days {
|
||||
display: flex;
|
||||
border-right: 1px solid $borderColor;
|
||||
border-right: 1px solid $calendarBorderColor;
|
||||
}
|
||||
|
||||
.day,
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
border-bottom: 1px solid $borderColor;
|
||||
border-left: 4px solid $borderColor;
|
||||
font-size: 12px;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
border-left-width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.info,
|
||||
@@ -12,10 +16,13 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.episodeInfo {
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
.seriesTitle,
|
||||
.episodeTitle {
|
||||
@add-mixin truncate;
|
||||
|
||||
flex: 1 0 1px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -33,12 +40,20 @@
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.airTime {
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
/*
|
||||
* Status
|
||||
*/
|
||||
|
||||
.downloaded {
|
||||
border-left-color: $successColor !important;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
border-left-color: color($successColor, saturation(+15%)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.downloading {
|
||||
@@ -49,7 +64,7 @@
|
||||
border-left-color: $gray !important;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(45deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
|
||||
background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +72,7 @@
|
||||
border-left-color: $warningColor !important;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
|
||||
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +80,8 @@
|
||||
border-left-color: $dangerColor !important;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
|
||||
border-left-color: color($dangerColor saturation(+15%)) !important;
|
||||
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +89,6 @@
|
||||
border-left-color: $primaryColor !important;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
|
||||
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ class CalendarEvent extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<div className={styles.airTime}>
|
||||
{formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user