mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
827 Commits
zeus
...
v5.12.2.93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
889d071004 | ||
|
|
0049922ab6 | ||
|
|
3c995a0fff | ||
|
|
430719baac | ||
|
|
9928d711a3 | ||
|
|
f90b43b3e1 | ||
|
|
64122b4cfb | ||
|
|
7912a942f7 | ||
|
|
0a7607bb62 | ||
|
|
beeb5204b8 | ||
|
|
ab13fb6e99 | ||
|
|
2a3d595a66 | ||
|
|
958a863d8f | ||
|
|
8b7884deb0 | ||
|
|
9a22e1c791 | ||
|
|
f0f828491b | ||
|
|
7f3d107eda | ||
|
|
ce4477eeac | ||
|
|
8b64f873f4 | ||
|
|
38bd060960 | ||
|
|
7c243cb6e8 | ||
|
|
b29dee63f4 | ||
|
|
f6542bab0a | ||
|
|
da1b53b7e2 | ||
|
|
0deae95782 | ||
|
|
75c7a3cfc6 | ||
|
|
cfdb7a15de | ||
|
|
63a7d33e7e | ||
|
|
c9836f997c | ||
|
|
d37e71415f | ||
|
|
9a5f4bef63 | ||
|
|
40551ba5a3 | ||
|
|
6e04dc894b | ||
|
|
ac767ed386 | ||
|
|
42fbb79017 | ||
|
|
c43bd77dae | ||
|
|
68dfa55b35 | ||
|
|
fa190c85a3 | ||
|
|
172dcf6f8d | ||
|
|
0736fc955f | ||
|
|
9d0b8d974d | ||
|
|
9a3e89f283 | ||
|
|
e33e45ec73 | ||
|
|
5893d88058 | ||
|
|
a81d27acda | ||
|
|
d2b279a6be | ||
|
|
6686fa0600 | ||
|
|
1d286df85d | ||
|
|
be2e1e4fdb | ||
|
|
08868e5d01 | ||
|
|
7b43c2e345 | ||
|
|
dc599b6531 | ||
|
|
1421179654 | ||
|
|
41dcf32e24 | ||
|
|
7a813a44b6 | ||
|
|
54a5059080 | ||
|
|
adaf7444d3 | ||
|
|
49d11e59b3 | ||
|
|
a7eb4a4a04 | ||
|
|
66a6a663ba | ||
|
|
f735e31835 | ||
|
|
b8f1286abb | ||
|
|
9df45199d0 | ||
|
|
a692c35b03 | ||
|
|
ddcad270c3 | ||
|
|
b06f1d7c12 | ||
|
|
480bb50b85 | ||
|
|
dbc94dbe4e | ||
|
|
b89271fc01 | ||
|
|
66fcde7325 | ||
|
|
463741da1f | ||
|
|
3388fae1a5 | ||
|
|
72b2cfe8be | ||
|
|
d5dd5e08ca | ||
|
|
fabd40cbae | ||
|
|
3ca327f611 | ||
|
|
c804140896 | ||
|
|
bb43d0c796 | ||
|
|
5757fa797f | ||
|
|
2fc32189d8 | ||
|
|
5975be3690 | ||
|
|
6095819005 | ||
|
|
7528882adf | ||
|
|
c1f1307345 | ||
|
|
348060351a | ||
|
|
ca31cdd33a | ||
|
|
36e278aa82 | ||
|
|
927e84654f | ||
|
|
96e60906c5 | ||
|
|
7a55b563c0 | ||
|
|
b4bbb71a9b | ||
|
|
0361299a73 | ||
|
|
e11339fb83 | ||
|
|
fbdd3129f5 | ||
|
|
2843647e23 | ||
|
|
b6b7d30fc1 | ||
|
|
e688dfadf7 | ||
|
|
f3ce0ac620 | ||
|
|
08c250dfe3 | ||
|
|
a5c1025efd | ||
|
|
f0019d622a | ||
|
|
032e8aa920 | ||
|
|
51cd7c70ba | ||
|
|
12c814ed78 | ||
|
|
d7bdc2c46c | ||
|
|
6b4c0bd24c | ||
|
|
c332c38890 | ||
|
|
d910611630 | ||
|
|
c6feb27962 | ||
|
|
0f7721ef11 | ||
|
|
89731bdc41 | ||
|
|
126b6eba00 | ||
|
|
56fece293c | ||
|
|
4503c3d36e | ||
|
|
0d6ce5ea49 | ||
|
|
b4b5ad9567 | ||
|
|
3ae6347532 | ||
|
|
a826ffdbc9 | ||
|
|
dc3bf9acb0 | ||
|
|
eca95826c2 | ||
|
|
593b943cb0 | ||
|
|
a3faa9ed5f | ||
|
|
229e87f398 | ||
|
|
fcb758bf67 | ||
|
|
0a9ae45ed1 | ||
|
|
b062a46cbd | ||
|
|
ac4669dfc1 | ||
|
|
36d80387c6 | ||
|
|
9fe4793606 | ||
|
|
fa80608394 | ||
|
|
6e81d5917e | ||
|
|
8d189523c4 | ||
|
|
4d589422e6 | ||
|
|
675612e7c6 | ||
|
|
be3916f67d | ||
|
|
453f216e0d | ||
|
|
2d4846e5be | ||
|
|
2700a6cf8a | ||
|
|
674e414111 | ||
|
|
21bd21b70c | ||
|
|
fde87a38f9 | ||
|
|
0d6ba200d3 | ||
|
|
93298645e3 | ||
|
|
58f544e9e0 | ||
|
|
cf952d5c0b | ||
|
|
f6d630bdd3 | ||
|
|
657ced4772 | ||
|
|
d3a0c83f98 | ||
|
|
5833d5d4c4 | ||
|
|
2ba4562f49 | ||
|
|
d79db69644 | ||
|
|
7532dfb03c | ||
|
|
a47528aa81 | ||
|
|
a812d9f39f | ||
|
|
fc97f05850 | ||
|
|
644876123d | ||
|
|
540659a799 | ||
|
|
288668f7e6 | ||
|
|
fcf3be42d5 | ||
|
|
16e218501e | ||
|
|
caf2d33c11 | ||
|
|
bc918ed3b5 | ||
|
|
df77474314 | ||
|
|
bf84471509 | ||
|
|
d346d969de | ||
|
|
14b125ccd9 | ||
|
|
da5323a08f | ||
|
|
672b351497 | ||
|
|
fc4f4ab211 | ||
|
|
333e8281ea | ||
|
|
c278ffd8a0 | ||
|
|
5898eea3d0 | ||
|
|
5b78a1297a | ||
|
|
14e3e1fa35 | ||
|
|
c0e76544ef | ||
|
|
8c16677875 | ||
|
|
401e19547c | ||
|
|
c9f28fdc4f | ||
|
|
0ad4d7ea9a | ||
|
|
e8bb3df68e | ||
|
|
9442f1fb04 | ||
|
|
9ad6b3a611 | ||
|
|
fa1d6ad109 | ||
|
|
ccbc8f591b | ||
|
|
a4301f8db0 | ||
|
|
fe00825f2b | ||
|
|
17a9b0f7b0 | ||
|
|
62bdb66d0f | ||
|
|
7c1fedb8ce | ||
|
|
333351da45 | ||
|
|
fbbe7f7b5d | ||
|
|
edec201a6c | ||
|
|
1e783bfe07 | ||
|
|
7d5236de21 | ||
|
|
1efe7db5f3 | ||
|
|
b37cc42805 | ||
|
|
fa19f45171 | ||
|
|
4ae382cea7 | ||
|
|
37c09ba1f8 | ||
|
|
322df78f5a | ||
|
|
3a4446cc8e | ||
|
|
6c456e57d8 | ||
|
|
abc7efabea | ||
|
|
ace692aca6 | ||
|
|
882bde713f | ||
|
|
2575e3647f | ||
|
|
5cac5b6068 | ||
|
|
4628868dfa | ||
|
|
25685314bc | ||
|
|
41b1ea553e | ||
|
|
5d17f8e84d | ||
|
|
7490fc7040 | ||
|
|
f4e1f51a9c | ||
|
|
8e1016572b | ||
|
|
caabb032f3 | ||
|
|
ce9c5d4d97 | ||
|
|
967bed3161 | ||
|
|
8d9f1697ee | ||
|
|
3be2c6b0be | ||
|
|
b6d9c73a17 | ||
|
|
b1a7652753 | ||
|
|
f76c97c3ce | ||
|
|
1f5a84d202 | ||
|
|
d25bcdb043 | ||
|
|
f75497f57d | ||
|
|
2f413c68d9 | ||
|
|
68c20713e5 | ||
|
|
6eeed96d12 | ||
|
|
6f306a22e5 | ||
|
|
29ef75960d | ||
|
|
364a42424a | ||
|
|
a5b315ba83 | ||
|
|
e80e96de0e | ||
|
|
44c7c71226 | ||
|
|
04c5e6c2a6 | ||
|
|
5533528b56 | ||
|
|
74246df881 | ||
|
|
88127298ae | ||
|
|
5559fa5fa5 | ||
|
|
d503e01747 | ||
|
|
ae89ae175f | ||
|
|
df35e78e1f | ||
|
|
a3b3fee06b | ||
|
|
ae377d97a5 | ||
|
|
270df9d1dd | ||
|
|
6ed3045433 | ||
|
|
ddb7d5690b | ||
|
|
a1104b8263 | ||
|
|
358ff0c130 | ||
|
|
ff0a04c331 | ||
|
|
c12f01f919 | ||
|
|
93d661242a | ||
|
|
324dac8db3 | ||
|
|
bba69d8b22 | ||
|
|
1366f6e8b4 | ||
|
|
f79712951b | ||
|
|
101b046753 | ||
|
|
cd713e7252 | ||
|
|
a54f54eb6e | ||
|
|
f2af7a1b72 | ||
|
|
a5b48153a6 | ||
|
|
1804e486d6 | ||
|
|
b490177a77 | ||
|
|
7a90b4a6b2 | ||
|
|
558043f1b2 | ||
|
|
1423ad6aa4 | ||
|
|
087f9e12aa | ||
|
|
c63d08e7a0 | ||
|
|
85b310c81c | ||
|
|
3c737c2c17 | ||
|
|
8ee70288c9 | ||
|
|
588e87e4be | ||
|
|
792b8182b2 | ||
|
|
4cec41324b | ||
|
|
10bb270da8 | ||
|
|
b5e6a36878 | ||
|
|
126a5b118e | ||
|
|
0f1cf21c39 | ||
|
|
92a19a1a81 | ||
|
|
54965cfa6f | ||
|
|
14f27cf2b6 | ||
|
|
a607f167f4 | ||
|
|
29449e83f9 | ||
|
|
bb4e185644 | ||
|
|
085b1db77f | ||
|
|
7bdb3e437d | ||
|
|
fcb0d8a930 | ||
|
|
7dc64c595c | ||
|
|
9a2b4bc81d | ||
|
|
f228841dc7 | ||
|
|
02be9cf825 | ||
|
|
8809c207bb | ||
|
|
1be2cded74 | ||
|
|
0a189d00ef | ||
|
|
5fc63ecb3f | ||
|
|
3a74393d05 | ||
|
|
4cbf5cfc57 | ||
|
|
797142d6f3 | ||
|
|
2a472c50c1 | ||
|
|
a12ff68fbd | ||
|
|
194926c7dd | ||
|
|
7dee5bb689 | ||
|
|
9b24dab71b | ||
|
|
62e1c02fe2 | ||
|
|
99b3d61862 | ||
|
|
bd905567de | ||
|
|
a8eea20d69 | ||
|
|
69ad0caf40 | ||
|
|
8a5c0ffd18 | ||
|
|
c8b409ed0b | ||
|
|
c5bcb13f63 | ||
|
|
80de711654 | ||
|
|
3fb558411e | ||
|
|
98384ab390 | ||
|
|
0c654377f4 | ||
|
|
e8c925274a | ||
|
|
320bfeec16 | ||
|
|
638f92495c | ||
|
|
077b041d3f | ||
|
|
ff3dd3ae42 | ||
|
|
2e3beddcbc | ||
|
|
dc068bbf3d | ||
|
|
7a303c1ebf | ||
|
|
152f50a1ef | ||
|
|
9798202589 | ||
|
|
7969776339 | ||
|
|
288982d7bd | ||
|
|
d39a3ade5b | ||
|
|
1fc6e88bc4 | ||
|
|
e8e1841e6c | ||
|
|
d17eb4f33f | ||
|
|
685f462959 | ||
|
|
7be8a34130 | ||
|
|
886711b496 | ||
|
|
5185e037da | ||
|
|
38e7e37d57 | ||
|
|
190c4c5893 | ||
|
|
0ec18ce4b3 | ||
|
|
a08575b7bc | ||
|
|
556cc885ec | ||
|
|
586c0c6e13 | ||
|
|
cec569461d | ||
|
|
8b79b5afbf | ||
|
|
cd4552ce6f | ||
|
|
256439304b | ||
|
|
bb44fbc362 | ||
|
|
cd401f72f5 | ||
|
|
c9624e7550 | ||
|
|
649702eaca | ||
|
|
1c52f0f5bd | ||
|
|
dff85dc1f3 | ||
|
|
1090aeff75 | ||
|
|
086a0addba | ||
|
|
8b6cf34ce4 | ||
|
|
7f03a916f1 | ||
|
|
3a6d603a9e | ||
|
|
cd2c7dc7fb | ||
|
|
f1d76c3483 | ||
|
|
39eac4b5ad | ||
|
|
71e1003358 | ||
|
|
89b6a5d51f | ||
|
|
711637c448 | ||
|
|
2677d25980 | ||
|
|
56639bcd42 | ||
|
|
1ed62b9ced | ||
|
|
a596dda253 | ||
|
|
c0b354039d | ||
|
|
3b5078d117 | ||
|
|
db1fee8d8a | ||
|
|
0d0575f3a9 | ||
|
|
2d82347a66 | ||
|
|
25838df550 | ||
|
|
b3a8b99f9a | ||
|
|
93a852841f | ||
|
|
ead1ec43be | ||
|
|
04b6dd44cb | ||
|
|
3db78079f3 | ||
|
|
c8a6b9f565 | ||
|
|
811cafd9ae | ||
|
|
ac7039d651 | ||
|
|
a2d11cf684 | ||
|
|
cc32635f6f | ||
|
|
10f9cb64ac | ||
|
|
f77e27bace | ||
|
|
8ea6d59d59 | ||
|
|
98668d0d25 | ||
|
|
649d57a234 | ||
|
|
dc7c8bf800 | ||
|
|
8d90c7678f | ||
|
|
02518e2116 | ||
|
|
3191a883dc | ||
|
|
31a714e6b3 | ||
|
|
f7ca0b8b06 | ||
|
|
56be9502af | ||
|
|
77381d3f72 | ||
|
|
198e6324e0 | ||
|
|
81c9537e5a | ||
|
|
d3cbb9be8d | ||
|
|
2e043c0cf7 | ||
|
|
ada33dc065 | ||
|
|
badb68b817 | ||
|
|
3bd1b3e972 | ||
|
|
6851de42a7 | ||
|
|
dd0b7c91f9 | ||
|
|
45ac69e2d9 | ||
|
|
9ccf0ecdb1 | ||
|
|
48a3467572 | ||
|
|
d0a10379f9 | ||
|
|
caab5e3614 | ||
|
|
4e47695f89 | ||
|
|
83bd4d0686 | ||
|
|
a75619c8ef | ||
|
|
28689006fb | ||
|
|
43b0589bea | ||
|
|
c4aad5800c | ||
|
|
0c998dac5c | ||
|
|
d41c0f0ab7 | ||
|
|
85b13b7e41 | ||
|
|
2a545a84b4 | ||
|
|
280083f4d7 | ||
|
|
d6dcae3d6a | ||
|
|
ebde4d3bc8 | ||
|
|
1ee30290ef | ||
|
|
d303eae7c6 | ||
|
|
584910514a | ||
|
|
a253181d7d | ||
|
|
7ea6918327 | ||
|
|
953d3ad3fb | ||
|
|
b9f4073514 | ||
|
|
86a17e7984 | ||
|
|
f38545f852 | ||
|
|
a7720e829d | ||
|
|
3a4eac4d59 | ||
|
|
04f792c55a | ||
|
|
ada326e4dd | ||
|
|
cae58d620b | ||
|
|
e84df18e8d | ||
|
|
a51ae70938 | ||
|
|
7cc04245ec | ||
|
|
2caf3c6725 | ||
|
|
41ff9352b9 | ||
|
|
d7b9b2ccb2 | ||
|
|
e90a50a3aa | ||
|
|
a0dd26c353 | ||
|
|
2286055d6a | ||
|
|
0a5a4e0a6f | ||
|
|
619c38c493 | ||
|
|
0b8694c627 | ||
|
|
e2793e56e9 | ||
|
|
68f61da321 | ||
|
|
8edb541e21 | ||
|
|
d441becc74 | ||
|
|
a97b2ee2ed | ||
|
|
e70c61e24e | ||
|
|
d1f96746e0 | ||
|
|
35893697bd | ||
|
|
540c150b93 | ||
|
|
48f819caee | ||
|
|
4ad7b60d9d | ||
|
|
7e4231fc0e | ||
|
|
94287d9427 | ||
|
|
8ec6b5dd4d | ||
|
|
4be43c9f2b | ||
|
|
c388cf968b | ||
|
|
b6b809f473 | ||
|
|
9dd31be7b3 | ||
|
|
25ab396a2c | ||
|
|
145cd74969 | ||
|
|
b9c76d9bed | ||
|
|
63f16924b1 | ||
|
|
a91a9f7fd9 | ||
|
|
4c8e9f204e | ||
|
|
d64ee6681f | ||
|
|
2ecc57cd31 | ||
|
|
9620207503 | ||
|
|
0b090e5f39 | ||
|
|
51cb0920ed | ||
|
|
a90d6682d3 | ||
|
|
db62eddf5a | ||
|
|
ac2b2e6215 | ||
|
|
9581dd9764 | ||
|
|
6c459c744a | ||
|
|
4676ecfce9 | ||
|
|
ef92af9dd8 | ||
|
|
b144482d68 | ||
|
|
173b1d6a4c | ||
|
|
5f624a147b | ||
|
|
af066da4ff | ||
|
|
937ebcdac3 | ||
|
|
67f5199667 | ||
|
|
38cd130da5 | ||
|
|
ed340be2b1 | ||
|
|
34cfb58b39 | ||
|
|
3d0f22ca7c | ||
|
|
2510f44c25 | ||
|
|
c0bf75cae3 | ||
|
|
a63ab1ddd6 | ||
|
|
41cb020ff0 | ||
|
|
d660309b5a | ||
|
|
222c19e4b3 | ||
|
|
b08981dee0 | ||
|
|
4a9c0b2240 | ||
|
|
8970b1276f | ||
|
|
e868dbf911 | ||
|
|
e38b31a220 | ||
|
|
9b1dac4b57 | ||
|
|
20ac0bb0e1 | ||
|
|
9ffa1cc2b9 | ||
|
|
422db874f0 | ||
|
|
adf647f3e1 | ||
|
|
dc81f51d40 | ||
|
|
c9da7ee0c9 | ||
|
|
7198aa24a6 | ||
|
|
35c6fef2d1 | ||
|
|
deac2bdf5c | ||
|
|
8837473ed8 | ||
|
|
4ac538682d | ||
|
|
0277b2b201 | ||
|
|
e73015010e | ||
|
|
f704ab1512 | ||
|
|
2f1e077e0d | ||
|
|
cd3397a7a1 | ||
|
|
b3517c14de | ||
|
|
2d05708fa9 | ||
|
|
2ca581f2b6 | ||
|
|
8289b8978f | ||
|
|
54c1f54b13 | ||
|
|
918fcfd86e | ||
|
|
f55206537c | ||
|
|
d2d9ac8b9d | ||
|
|
ca1a40723b | ||
|
|
bfff736cfc | ||
|
|
c2d28dd41b | ||
|
|
0e8a1ca522 | ||
|
|
1ba7bfe585 | ||
|
|
0be449033f | ||
|
|
3b1d4460ad | ||
|
|
4eb4128a89 | ||
|
|
f90cdbb112 | ||
|
|
a8dbc97921 | ||
|
|
f93e136386 | ||
|
|
a70fa0fcfe | ||
|
|
c8931784a7 | ||
|
|
f601448a65 | ||
|
|
64125a31b6 | ||
|
|
2f4da90d8a | ||
|
|
20d9db2cde | ||
|
|
5b7c0a94fb | ||
|
|
1416f7898e | ||
|
|
f9cd9f3204 | ||
|
|
99ab65f790 | ||
|
|
82fb355930 | ||
|
|
83d437cbb3 | ||
|
|
4beb5b328b | ||
|
|
23830f50ac | ||
|
|
b808a92cdf | ||
|
|
3185c73659 | ||
|
|
7dc9ec03a5 | ||
|
|
33228335e3 | ||
|
|
833340f8bd | ||
|
|
0ecb1d0706 | ||
|
|
25b77eb4a2 | ||
|
|
b946173c05 | ||
|
|
e5ccc32a37 | ||
|
|
3aeb52c3fd | ||
|
|
c717989034 | ||
|
|
806b89abbe | ||
|
|
cc7104a814 | ||
|
|
84c2d7f69d | ||
|
|
fcd187970c | ||
|
|
34eb59dde4 | ||
|
|
31b66c6673 | ||
|
|
06a96ef2d1 | ||
|
|
c77ce2459c | ||
|
|
083989d151 | ||
|
|
c003fe16de | ||
|
|
bc9b2cd283 | ||
|
|
d0e400c55a | ||
|
|
77863dc2cf | ||
|
|
18dc6f60b0 | ||
|
|
49501a55ae | ||
|
|
d5d77a4f1a | ||
|
|
0ae8952b38 | ||
|
|
6292ff76b0 | ||
|
|
646d271e81 | ||
|
|
3d2ca830bc | ||
|
|
da02ec3b04 | ||
|
|
cc9a443473 | ||
|
|
81b6bf521d | ||
|
|
7edb892eb4 | ||
|
|
3b36921787 | ||
|
|
c2d8bc85d0 | ||
|
|
3e55b1cf25 | ||
|
|
0b0c93081d | ||
|
|
91fbad72c0 | ||
|
|
35651ac59b | ||
|
|
1932aec131 | ||
|
|
ea470b4ee9 | ||
|
|
1bb404a912 | ||
|
|
374d20634d | ||
|
|
60d9aacac6 | ||
|
|
c5992ed944 | ||
|
|
4c4073ce1c | ||
|
|
d72f78d979 | ||
|
|
dca9d69aaa | ||
|
|
5a64826868 | ||
|
|
cda40312e0 | ||
|
|
907779b4ce | ||
|
|
cc03651af5 | ||
|
|
1ae98d618c | ||
|
|
f5914da2f9 | ||
|
|
f7816aa5cd | ||
|
|
a652ce50a9 | ||
|
|
58b726a292 | ||
|
|
1d8cf6a7f5 | ||
|
|
2c3ad380ef | ||
|
|
0e7874aacf | ||
|
|
8638d82ad3 | ||
|
|
f3d6a1f99d | ||
|
|
fa036f5807 | ||
|
|
a931f8a69f | ||
|
|
a491c9a4a0 | ||
|
|
2aafb6369c | ||
|
|
ef8253044e | ||
|
|
c1feeb72ee | ||
|
|
21560cd6cc | ||
|
|
bda2b9b0b8 | ||
|
|
4630de9616 | ||
|
|
7e83180e50 | ||
|
|
e60eed49c7 | ||
|
|
74cfc94b4c | ||
|
|
213c55c7af | ||
|
|
c066fa5e27 | ||
|
|
2741ecb968 | ||
|
|
7965c29425 | ||
|
|
d2cbab70a9 | ||
|
|
16381a1aef | ||
|
|
b92e08b850 | ||
|
|
eab470c67f | ||
|
|
7f11659d95 | ||
|
|
03dec07cbe | ||
|
|
554c696ee6 | ||
|
|
093f8a39fe | ||
|
|
8a1663f136 | ||
|
|
251d2dde97 | ||
|
|
996542a4a5 | ||
|
|
0914d6250c | ||
|
|
3ff8e511b5 | ||
|
|
3a7b27fb45 | ||
|
|
c81d2c97f5 | ||
|
|
dae46524c4 | ||
|
|
3c6386f318 | ||
|
|
1400a8806d | ||
|
|
e3f33f5a61 | ||
|
|
e6f4b88cf3 | ||
|
|
b788464487 | ||
|
|
e29717ec6c | ||
|
|
5d7e23092f | ||
|
|
9921d51451 | ||
|
|
213620cb29 | ||
|
|
bdc4aade0f | ||
|
|
b2300dbf41 | ||
|
|
44289d30f9 | ||
|
|
260fb88f85 | ||
|
|
119cdf6f09 | ||
|
|
c8d30fd214 | ||
|
|
7e9e528d3b | ||
|
|
8554c0d9cb | ||
|
|
22cc34b4fe | ||
|
|
990785ebfc | ||
|
|
957be99401 | ||
|
|
4bcde25e29 | ||
|
|
1d70f36e7d | ||
|
|
cc0a448bc8 | ||
|
|
c9e977baea | ||
|
|
6cb9a46cd4 | ||
|
|
eef379277a | ||
|
|
41fef47684 | ||
|
|
fcda6faf3d | ||
|
|
79bbf9c50b | ||
|
|
43d2f2804b | ||
|
|
fa62f3f66a | ||
|
|
229d91fe40 | ||
|
|
2673d1eee4 | ||
|
|
e59fd1118f | ||
|
|
c1fd33b152 | ||
|
|
2f58c8676f | ||
|
|
defc448304 | ||
|
|
3ec3358728 | ||
|
|
d4072cdfe2 | ||
|
|
136a030c07 | ||
|
|
6d89ae89a4 | ||
|
|
98e4273b7a | ||
|
|
ecf9983ea6 | ||
|
|
a059a700eb | ||
|
|
ced624c2ff | ||
|
|
7c32061e17 | ||
|
|
bc4847cdc7 | ||
|
|
65d79dd078 | ||
|
|
238ddbbe1f | ||
|
|
3f444406da | ||
|
|
d7aaa1cdc2 | ||
|
|
263534717d | ||
|
|
073d15160d | ||
|
|
c5075e5d49 | ||
|
|
fc345047ee | ||
|
|
bffab87da7 | ||
|
|
a8a9d3b833 | ||
|
|
ff1987be84 | ||
|
|
cb08c0767d | ||
|
|
5f1d7ddc11 | ||
|
|
0ba3c08ea6 | ||
|
|
6b9a378eaf | ||
|
|
b4562e6236 | ||
|
|
bbffff78ed | ||
|
|
740f0f1e5f | ||
|
|
45b38b44c1 | ||
|
|
318d59bb99 | ||
|
|
ed54d071c4 | ||
|
|
cff15de4fc | ||
|
|
88c0e24c58 | ||
|
|
8e0645670b | ||
|
|
40eeb31a21 | ||
|
|
3e534cf8bf | ||
|
|
c96b3c4b0b | ||
|
|
78bc9f9b4b | ||
|
|
b737f05a83 | ||
|
|
8d96fd2387 | ||
|
|
6c487ead00 | ||
|
|
22e3cf844c | ||
|
|
14b4b5e122 | ||
|
|
10f5f3c5c8 | ||
|
|
c687b552f0 | ||
|
|
92c8c8a7f5 | ||
|
|
86a16c3c0c | ||
|
|
e8c280db34 | ||
|
|
ea65e2174c | ||
|
|
4aa2466693 | ||
|
|
4df0f0f721 | ||
|
|
d7bee375e8 | ||
|
|
906295466d | ||
|
|
f86060eca2 | ||
|
|
bf9a0b62f2 | ||
|
|
ccc62f0450 | ||
|
|
524657ad78 | ||
|
|
7a394ff864 | ||
|
|
d8862eedd3 | ||
|
|
71f700e240 | ||
|
|
ae5dd84e0a | ||
|
|
17b398cf62 | ||
|
|
d00678c1ba | ||
|
|
03df9b7f07 | ||
|
|
3442a0ecca | ||
|
|
3376a467ca | ||
|
|
1650ce17fb | ||
|
|
2f2004faa2 | ||
|
|
437e2f4597 | ||
|
|
17b8605751 | ||
|
|
b2a52e52b6 | ||
|
|
0f5fabdfcd | ||
|
|
6362ee9b7d | ||
|
|
50465fd482 | ||
|
|
54d447d55f | ||
|
|
50f48277e5 | ||
|
|
c2e206b7ac | ||
|
|
7a46de602f | ||
|
|
89820c1ff7 | ||
|
|
67e6e129ff | ||
|
|
b6001238e5 | ||
|
|
7bbdcc81bb | ||
|
|
3e8cbc497e | ||
|
|
60d2df043b | ||
|
|
da41cb8840 | ||
|
|
a4b7c99d91 | ||
|
|
8fb21e073b | ||
|
|
dbf424d454 | ||
|
|
a6dda70c0a | ||
|
|
e6fa14b1e6 | ||
|
|
b5c0d515ee | ||
|
|
b7aee25d0d | ||
|
|
233b85aaf3 | ||
|
|
80db9a7dd4 | ||
|
|
660d3d7643 | ||
|
|
d999aea36f | ||
|
|
5d45f1de89 | ||
|
|
3e5089719c | ||
|
|
ec69dfaabb | ||
|
|
aa13a40bad | ||
|
|
9b458812f1 | ||
|
|
1bdc48a889 | ||
|
|
e5d479a162 | ||
|
|
9a50fcb82a | ||
|
|
f2357e0b60 | ||
|
|
0591d05c3b | ||
|
|
299d50d56c | ||
|
|
7d3c01114b | ||
|
|
70376af70b | ||
|
|
9ef031bd9e | ||
|
|
3a9b276c43 | ||
|
|
aabf209a07 | ||
|
|
79c03f2fe6 | ||
|
|
9b36404071 | ||
|
|
ecfaea3885 | ||
|
|
bfbeb4c62e | ||
|
|
4b98d27f31 | ||
|
|
604d74270d | ||
|
|
15bb9139d1 | ||
|
|
32722eb704 | ||
|
|
e0c8a8f0d6 | ||
|
|
a3bb0541f0 | ||
|
|
e78bc34514 | ||
|
|
35c4538288 | ||
|
|
3981e816cd | ||
|
|
9354031571 | ||
|
|
a01328dc8c | ||
|
|
8cb6295ddc | ||
|
|
99f7d8bcf5 | ||
|
|
f13d479b88 | ||
|
|
23eb637bc3 | ||
|
|
3a786d0b9d | ||
|
|
6fb127235c | ||
|
|
5517e578b6 | ||
|
|
bced2e7b2e | ||
|
|
f7313369b5 | ||
|
|
b14e93e11f | ||
|
|
f5692d6cf1 | ||
|
|
a2d505c795 |
13
.devcontainer/Radarr.code-workspace
Normal file
13
.devcontainer/Radarr.code-workspace
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||||
|
// the frontend has vscode settings that are distinct from the backend
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../frontend"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||||
|
{
|
||||||
|
"name": "Radarr",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"nodeGypDependencies": true,
|
||||||
|
"version": "16",
|
||||||
|
"nvmVersion": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [7878],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -118,6 +118,7 @@ src/UI/.idea/*
|
|||||||
node_modules/
|
node_modules/
|
||||||
_output*
|
_output*
|
||||||
_artifacts
|
_artifacts
|
||||||
|
_temp*
|
||||||
_rawPackage/
|
_rawPackage/
|
||||||
_dotTrace*
|
_dotTrace*
|
||||||
_tests/
|
_tests/
|
||||||
@@ -126,6 +127,7 @@ coverage*.xml
|
|||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
|
.mono
|
||||||
|
|
||||||
# VS outout folders
|
# VS outout folders
|
||||||
bin
|
bin
|
||||||
|
|||||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||||
|
"name": "Run Radarr",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build dotnet",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/_output/net6.0/Radarr",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
44
.vscode/tasks.json
vendored
Normal file
44
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build dotnet",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"-restore",
|
||||||
|
"${workspaceFolder}/src/Radarr.sln",
|
||||||
|
"-p:GenerateFullPaths=true",
|
||||||
|
"-p:Configuration=Debug",
|
||||||
|
"-p:Platform=Posix",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/src/Radarr.sln",
|
||||||
|
"-property:GenerateFullPaths=true",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/src/Radarr.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
# Radarr
|
# Radarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
||||||
[](https://wiki.servarr.com/radarr/installation#docker)
|
[](https://wiki.servarr.com/radarr/installation/docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
[](#sponsors)
|
[](#sponsors)
|
||||||
[](#mega-sponsors)
|
[](#mega-sponsors)
|
||||||
|
|
||||||
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
|
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
|
||||||
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
|
Note that only one type of a given movie is supported. If you want both a 4k version and 1080p version of a given movie you will need multiple instances.
|
||||||
|
|
||||||
## Major Features Include
|
## Major Features Include
|
||||||
|
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '6.0.1'
|
majorVersion: '5.12.2'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.413'
|
dotnetVersion: '6.0.424'
|
||||||
nodeVersion: '16.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-13'
|
macImage: 'macOS-13'
|
||||||
@@ -27,7 +27,6 @@ trigger:
|
|||||||
include:
|
include:
|
||||||
- develop
|
- develop
|
||||||
- master
|
- master
|
||||||
- zeus
|
|
||||||
paths:
|
paths:
|
||||||
exclude:
|
exclude:
|
||||||
- .github
|
- .github
|
||||||
@@ -167,10 +166,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1090,10 +1089,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1117,7 +1116,7 @@ stages:
|
|||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@1
|
- task: SonarCloudPrepare@2
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
@@ -1129,7 +1128,7 @@ stages:
|
|||||||
cliProjectName: 'RadarrUI'
|
cliProjectName: 'RadarrUI'
|
||||||
cliProjectVersion: '$(radarrVersion)'
|
cliProjectVersion: '$(radarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@1
|
- task: SonarCloudAnalyze@2
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
@@ -1206,7 +1205,7 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@1
|
- task: SonarCloudPrepare@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
@@ -1224,25 +1223,21 @@ stages:
|
|||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@1
|
- task: SonarCloudAnalyze@2
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@4
|
- task: reportgenerator@5
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||||
- task: PublishCodeCoverageResults@1
|
publishCodeCoverageResults: true
|
||||||
displayName: Publish Coverage Report
|
|
||||||
inputs:
|
|
||||||
codeCoverageTool: 'cobertura'
|
|
||||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
|
||||||
reportDirectory: './CoverageResults/combined/'
|
|
||||||
|
|
||||||
- stage: Report_Out
|
- stage: Report_Out
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- Analyze
|
- Analyze
|
||||||
|
- Installer
|
||||||
- Unit_Test
|
- Unit_Test
|
||||||
- Integration
|
- Integration
|
||||||
- Automation
|
- Automation
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -254,7 +254,7 @@ InstallInno()
|
|||||||
ProgressStart "Installing portable Inno Setup"
|
ProgressStart "Installing portable Inno Setup"
|
||||||
|
|
||||||
rm -rf _inno
|
rm -rf _inno
|
||||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
|
||||||
mkdir _inno
|
mkdir _inno
|
||||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||||
rm innosetup.exe
|
rm innosetup.exe
|
||||||
|
|||||||
10
docs.sh
10
docs.sh
@@ -21,15 +21,21 @@ slnFile=src/Radarr.sln
|
|||||||
|
|
||||||
platform=Posix
|
platform=Posix
|
||||||
|
|
||||||
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
|
application=Radarr.Console.dll
|
||||||
|
else
|
||||||
|
application=Radarr.dll
|
||||||
|
fi
|
||||||
|
|
||||||
dotnet clean $slnFile -c Debug
|
dotnet clean $slnFile -c Debug
|
||||||
dotnet clean $slnFile -c Release
|
dotnet clean $slnFile -c Release
|
||||||
|
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||||
|
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
|
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|||||||
@@ -359,11 +359,16 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||||
'no-shadow': 'off',
|
'@typescript-eslint/no-unused-vars': [
|
||||||
// These should be enabled after cleaning things up
|
'error',
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
{
|
||||||
|
args: 'after-used',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true
|
||||||
|
}
|
||||||
|
],
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'react/prop-types': 'off',
|
'no-shadow': 'off',
|
||||||
'prettier/prettier': 'error',
|
'prettier/prettier': 'error',
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
'error',
|
'error',
|
||||||
@@ -376,7 +381,41 @@ module.exports = {
|
|||||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
|
||||||
|
// React Hooks
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
|
|
||||||
|
// React
|
||||||
|
'react/function-component-definition': 'error',
|
||||||
|
'react/hook-use-state': 'error',
|
||||||
|
'react/jsx-boolean-value': ['error', 'always'],
|
||||||
|
'react/jsx-curly-brace-presence': [
|
||||||
|
'error',
|
||||||
|
{ props: 'never', children: 'never' }
|
||||||
|
],
|
||||||
|
'react/jsx-fragments': 'error',
|
||||||
|
'react/jsx-handler-names': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
eventHandlerPrefix: 'on',
|
||||||
|
eventHandlerPropPrefix: 'on'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||||
|
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||||
|
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||||
|
'react/jsx-sort-props': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
callbacksLast: true,
|
||||||
|
noSortAlphabetically: true,
|
||||||
|
reservedFirst: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/self-closing-comp': 'error'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const loose = true;
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'@babel/plugin-transform-logical-assignment-operators',
|
||||||
|
|
||||||
// Stage 1
|
// Stage 1
|
||||||
'@babel/plugin-proposal-export-default-from',
|
'@babel/plugin-proposal-export-default-from',
|
||||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ module.exports = (env) => {
|
|||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: '[name]-[contenthash].js',
|
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ module.exports = (env) => {
|
|||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css',
|
filename: 'Content/styles.css',
|
||||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
@@ -201,7 +201,7 @@ module.exports = (env) => {
|
|||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const mixinsFiles = [
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'autoprefixer',
|
||||||
['postcss-mixins', {
|
['postcss-mixins', {
|
||||||
mixinsFiles
|
mixinsFiles
|
||||||
}],
|
}],
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
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 { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
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 BlocklistRowConnector from './BlocklistRowConnector';
|
|
||||||
|
|
||||||
class Blocklist extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: false,
|
|
||||||
lastToggled: null,
|
|
||||||
selectedState: {},
|
|
||||||
isConfirmRemoveModalOpen: false,
|
|
||||||
items: props.items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (hasDifferentItems(prevProps.items, items)) {
|
|
||||||
this.setState((state) => {
|
|
||||||
return {
|
|
||||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
|
||||||
items
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
getSelectedIds = () => {
|
|
||||||
return getSelectedIds(this.state.selectedState);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSelectAllChange = ({ value }) => {
|
|
||||||
this.setState(selectAll(this.state.selectedState, value));
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
|
||||||
this.setState((state) => {
|
|
||||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedPress = () => {
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedConfirmed = () => {
|
|
||||||
this.props.onRemoveSelected(this.getSelectedIds());
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmRemoveModalClose = () => {
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
totalRecords,
|
|
||||||
isRemoving,
|
|
||||||
isClearingBlocklistExecuting,
|
|
||||||
onClearBlocklistPress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
selectedState,
|
|
||||||
isConfirmRemoveModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const selectedIds = this.getSelectedIds();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Blocklist')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RemoveSelected')}
|
|
||||||
iconName={icons.REMOVE}
|
|
||||||
isDisabled={!selectedIds.length}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={this.onRemoveSelectedPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Clear')}
|
|
||||||
iconName={icons.CLEAR}
|
|
||||||
isSpinning={isClearingBlocklistExecuting}
|
|
||||||
onPress={onClearBlocklistPress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
{...otherProps}
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('BlocklistLoadError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error && !items.length &&
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{translate('NoHistoryBlocklist')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error && !!items.length &&
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
selectAll={true}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
columns={columns}
|
|
||||||
{...otherProps}
|
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<BlocklistRowConnector
|
|
||||||
key={item.id}
|
|
||||||
isSelected={selectedState[item.id] || false}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
onSelectedChange={this.onSelectedChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isFetching}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('RemoveSelected')}
|
|
||||||
message={translate('RemoveSelectedBlocklistMessageText')}
|
|
||||||
confirmLabel={translate('RemoveSelected')}
|
|
||||||
onConfirm={this.onRemoveSelectedConfirmed}
|
|
||||||
onCancel={this.onConfirmRemoveModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Blocklist.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
|
||||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
|
||||||
onRemoveSelected: PropTypes.func.isRequired,
|
|
||||||
onClearBlocklistPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Blocklist;
|
|
||||||
329
frontend/src/Activity/Blocklist/Blocklist.tsx
Normal file
329
frontend/src/Activity/Blocklist/Blocklist.tsx
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { SelectProvider } from 'App/SelectContext';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
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 usePaging from 'Components/Table/usePaging';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
clearBlocklist,
|
||||||
|
fetchBlocklist,
|
||||||
|
gotoBlocklistPage,
|
||||||
|
removeBlocklistItems,
|
||||||
|
setBlocklistFilter,
|
||||||
|
setBlocklistSort,
|
||||||
|
setBlocklistTableOption,
|
||||||
|
} from 'Store/Actions/blocklistActions';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||||
|
import BlocklistRow from './BlocklistRow';
|
||||||
|
|
||||||
|
function Blocklist() {
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalRecords,
|
||||||
|
isRemoving,
|
||||||
|
} = useSelector((state: AppState) => state.blocklist);
|
||||||
|
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
|
||||||
|
const isClearingBlocklistExecuting = useSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
|
||||||
|
);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [isConfirmClearModalOpen, setIsConfirmClearModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const wasClearingBlocklistExecuting = usePrevious(
|
||||||
|
isClearingBlocklistExecuting
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectAllChange = useCallback(
|
||||||
|
({ value }: CheckInputChanged) => {
|
||||||
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectedChange = useCallback(
|
||||||
|
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||||
|
setSelectState({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
items,
|
||||||
|
id,
|
||||||
|
isSelected: value,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveSelectedPress = useCallback(() => {
|
||||||
|
setIsConfirmRemoveModalOpen(true);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const handleRemoveSelectedConfirmed = useCallback(() => {
|
||||||
|
dispatch(removeBlocklistItems({ ids: selectedIds }));
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
}, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]);
|
||||||
|
|
||||||
|
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const handleClearBlocklistPress = useCallback(() => {
|
||||||
|
setIsConfirmClearModalOpen(true);
|
||||||
|
}, [setIsConfirmClearModalOpen]);
|
||||||
|
|
||||||
|
const handleClearBlocklistConfirmed = useCallback(() => {
|
||||||
|
dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST }));
|
||||||
|
setIsConfirmClearModalOpen(false);
|
||||||
|
}, [setIsConfirmClearModalOpen, dispatch]);
|
||||||
|
|
||||||
|
const handleConfirmClearModalClose = useCallback(() => {
|
||||||
|
setIsConfirmClearModalOpen(false);
|
||||||
|
}, [setIsConfirmClearModalOpen]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
} = usePaging({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
gotoPage: gotoBlocklistPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(selectedFilterKey: string) => {
|
||||||
|
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortPress = useCallback(
|
||||||
|
(sortKey: string) => {
|
||||||
|
dispatch(setBlocklistSort({ sortKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableOptionChange = useCallback(
|
||||||
|
(payload: TableOptionsChangePayload) => {
|
||||||
|
dispatch(setBlocklistTableOption(payload));
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchBlocklist());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearBlocklist());
|
||||||
|
};
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchBlocklist());
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) {
|
||||||
|
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
}, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectProvider items={items}>
|
||||||
|
<PageContent title={translate('Blocklist')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RemoveSelected')}
|
||||||
|
iconName={icons.REMOVE}
|
||||||
|
isDisabled={!selectedIds.length}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={handleRemoveSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Clear')}
|
||||||
|
iconName={icons.CLEAR}
|
||||||
|
isDisabled={!items.length}
|
||||||
|
isSpinning={isClearingBlocklistExecuting}
|
||||||
|
onPress={handleClearBlocklistPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={BlocklistFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && !!error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !items.length ? (
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{selectedFilterKey === 'all'
|
||||||
|
? translate('NoBlocklistItems')
|
||||||
|
: translate('BlocklistFilterHasNoItems')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !!items.length ? (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
onSelectAllChange={handleSelectAllChange}
|
||||||
|
onSortPress={handleSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<BlocklistRow
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id] || false}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={handleSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<TablePager
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('RemoveSelected')}
|
||||||
|
message={translate('RemoveSelectedBlocklistMessageText')}
|
||||||
|
confirmLabel={translate('RemoveSelected')}
|
||||||
|
onConfirm={handleRemoveSelectedConfirmed}
|
||||||
|
onCancel={handleConfirmRemoveModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmClearModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('ClearBlocklist')}
|
||||||
|
message={translate('ClearBlocklistMessageText')}
|
||||||
|
confirmLabel={translate('Clear')}
|
||||||
|
onConfirm={handleClearBlocklistConfirmed}
|
||||||
|
onCancel={handleConfirmClearModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
</SelectProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blocklist;
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import Blocklist from './Blocklist';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.blocklist,
|
|
||||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
|
||||||
(blocklist, isClearingBlocklistExecuting) => {
|
|
||||||
return {
|
|
||||||
isClearingBlocklistExecuting,
|
|
||||||
...blocklist
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...blocklistActions,
|
|
||||||
executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class BlocklistConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchBlocklist,
|
|
||||||
gotoBlocklistFirstPage
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchBlocklist();
|
|
||||||
} else {
|
|
||||||
gotoBlocklistFirstPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
|
|
||||||
this.props.gotoBlocklistFirstPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.clearBlocklist();
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
this.props.fetchBlocklist();
|
|
||||||
};
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFirstPagePress = () => {
|
|
||||||
this.props.gotoBlocklistFirstPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPagePress = () => {
|
|
||||||
this.props.gotoBlocklistPreviousPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPagePress = () => {
|
|
||||||
this.props.gotoBlocklistNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onLastPagePress = () => {
|
|
||||||
this.props.gotoBlocklistLastPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelect = (page) => {
|
|
||||||
this.props.gotoBlocklistPage({ page });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelected = (ids) => {
|
|
||||||
this.props.removeBlocklistItems({ ids });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
|
||||||
this.props.setBlocklistSort({ sortKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
|
||||||
this.props.setBlocklistTableOption(payload);
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
this.props.gotoBlocklistFirstPage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearBlocklistPress = () => {
|
|
||||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Blocklist
|
|
||||||
onFirstPagePress={this.onFirstPagePress}
|
|
||||||
onPreviousPagePress={this.onPreviousPagePress}
|
|
||||||
onNextPagePress={this.onNextPagePress}
|
|
||||||
onLastPagePress={this.onLastPagePress}
|
|
||||||
onPageSelect={this.onPageSelect}
|
|
||||||
onRemoveSelected={this.onRemoveSelected}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BlocklistConnector.propTypes = {
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
fetchBlocklist: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistFirstPage: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistNextPage: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistLastPage: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
|
||||||
removeBlocklistItems: PropTypes.func.isRequired,
|
|
||||||
setBlocklistSort: PropTypes.func.isRequired,
|
|
||||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
|
||||||
clearBlocklist: PropTypes.func.isRequired,
|
|
||||||
executeCommand: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
|
|
||||||
);
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class BlocklistDetailsModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
sourceTitle,
|
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
message,
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
Details
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Protocol')}
|
|
||||||
data={protocol}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
!!message &&
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!message &&
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BlocklistDetailsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
indexer: PropTypes.string,
|
|
||||||
message: PropTypes.string,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlocklistDetailsModal;
|
|
||||||
64
frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
Normal file
64
frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
interface BlocklistDetailsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
sourceTitle: string;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
indexer?: string;
|
||||||
|
message?: string;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
|
||||||
|
const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>Details</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Protocol')}
|
||||||
|
data={protocol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Indexer')}
|
||||||
|
data={indexer}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Message')}
|
||||||
|
data={message}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlocklistDetailsModal;
|
||||||
54
frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx
Normal file
54
frontend/src/Activity/Blocklist/BlocklistFilterModal.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setBlocklistFilter } from 'Store/Actions/blocklistActions';
|
||||||
|
|
||||||
|
function createBlocklistSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.blocklist.items,
|
||||||
|
(blocklistItems) => {
|
||||||
|
return blocklistItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.blocklist.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlocklistFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createBlocklistSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'blocklist';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setBlocklistFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
.language,
|
.languages,
|
||||||
.quality {
|
.quality {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'indexer': string;
|
'indexer': string;
|
||||||
'language': string;
|
'languages': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
|
||||||
import styles from './BlocklistRow.css';
|
|
||||||
|
|
||||||
class BlocklistRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isDetailsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onDetailsPress = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDetailsModalClose = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
movie,
|
|
||||||
sourceTitle,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
languages,
|
|
||||||
date,
|
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
message,
|
|
||||||
isSelected,
|
|
||||||
columns,
|
|
||||||
onSelectedChange,
|
|
||||||
onRemovePress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!movie) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableSelectCell
|
|
||||||
id={id}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'movieMetadata.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieTitleLink
|
|
||||||
titleSlug={movie.titleSlug}
|
|
||||||
title={movie.title}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sourceTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{sourceTitle}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieLanguage
|
|
||||||
languages={languages}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.quality}
|
|
||||||
>
|
|
||||||
<MovieQuality
|
|
||||||
quality={quality}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'date') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={date}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.indexer}
|
|
||||||
>
|
|
||||||
{indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.actions}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
name={icons.INFO}
|
|
||||||
onPress={this.onDetailsPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
title={translate('RemoveFromBlocklist')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={onRemovePress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<BlocklistDetailsModal
|
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
protocol={protocol}
|
|
||||||
indexer={indexer}
|
|
||||||
message={message}
|
|
||||||
onModalClose={this.onDetailsModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
BlocklistRow.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
movie: PropTypes.object.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
indexer: PropTypes.string,
|
|
||||||
message: PropTypes.string,
|
|
||||||
isSelected: PropTypes.bool.isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlocklistRow;
|
|
||||||
160
frontend/src/Activity/Blocklist/BlocklistRow.tsx
Normal file
160
frontend/src/Activity/Blocklist/BlocklistRow.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
|
import MovieLanguages from 'Movie/MovieLanguages';
|
||||||
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import useMovie from 'Movie/useMovie';
|
||||||
|
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||||
|
import Blocklist from 'typings/Blocklist';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||||
|
import styles from './BlocklistRow.css';
|
||||||
|
|
||||||
|
interface BlocklistRowProps extends Blocklist {
|
||||||
|
isSelected: boolean;
|
||||||
|
columns: Column[];
|
||||||
|
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlocklistRow(props: BlocklistRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
movieId,
|
||||||
|
sourceTitle,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
customFormats,
|
||||||
|
date,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
message,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const movie = useMovie(movieId);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleDetailsPress = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(true);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleDetailsModalClose = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(false);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleRemovePress = useCallback(() => {
|
||||||
|
dispatch(removeBlocklistItem({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.languages}>
|
||||||
|
<MovieLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.quality}>
|
||||||
|
<MovieQuality quality={quality} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'date') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore ts(2739)
|
||||||
|
return <RelativeDateCell key={name} date={date} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.indexer}>
|
||||||
|
{indexer}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.actions}>
|
||||||
|
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
title={translate('RemoveFromBlocklist')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={handleRemovePress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<BlocklistDetailsModal
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
protocol={protocol}
|
||||||
|
indexer={indexer}
|
||||||
|
message={message}
|
||||||
|
onModalClose={handleDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlocklistRow;
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import BlocklistRow from './BlocklistRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieSelector(),
|
|
||||||
(movie) => {
|
|
||||||
return {
|
|
||||||
movie
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onRemovePress() {
|
|
||||||
dispatch(removeBlocklistItem({ id: props.id }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
|
|
||||||
@@ -14,6 +14,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
|||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import HistoryFilterModal from './HistoryFilterModal';
|
||||||
import HistoryRowConnector from './HistoryRowConnector';
|
import HistoryRowConnector from './HistoryRowConnector';
|
||||||
|
|
||||||
class History extends Component {
|
class History extends Component {
|
||||||
@@ -33,6 +34,7 @@ class History extends Component {
|
|||||||
columns,
|
columns,
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
|
customFilters,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
onFirstPagePress,
|
onFirstPagePress,
|
||||||
@@ -70,7 +72,8 @@ class History extends Component {
|
|||||||
alignMenu={align.RIGHT}
|
alignMenu={align.RIGHT}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={[]}
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={HistoryFilterModal}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
@@ -144,8 +147,9 @@ History.propTypes = {
|
|||||||
moviesError: PropTypes.object,
|
moviesError: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onFirstPagePress: PropTypes.func.isRequired
|
onFirstPagePress: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import * as historyActions from 'Store/Actions/historyActions';
|
import * as historyActions from 'Store/Actions/historyActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import History from './History';
|
import History from './History';
|
||||||
|
|
||||||
@@ -11,11 +12,13 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.history,
|
(state) => state.history,
|
||||||
(state) => state.movies,
|
(state) => state.movies,
|
||||||
(history, movies) => {
|
createCustomFiltersSelector('history'),
|
||||||
|
(history, movies, customFilters) => {
|
||||||
return {
|
return {
|
||||||
isMoviesFetching: movies.isFetching,
|
isMoviesFetching: movies.isFetching,
|
||||||
isMoviesPopulated: movies.isPopulated,
|
isMoviesPopulated: movies.isPopulated,
|
||||||
moviesError: movies.error,
|
moviesError: movies.error,
|
||||||
|
customFilters,
|
||||||
...history
|
...history
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType) {
|
function getIconName(eventType, data) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
@@ -17,7 +17,7 @@ function getIconName(eventType) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return icons.DELETE;
|
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return translate('MovieDownloadFailedTooltip');
|
return translate('MovieDownloadFailedTooltip');
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return translate('MovieFileDeletedTooltip');
|
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return translate('MovieFileRenamedTooltip');
|
return translate('MovieFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, data }) {
|
function HistoryEventTypeCell({ eventType, data }) {
|
||||||
const iconName = getIconName(eventType);
|
const iconName = getIconName(eventType, data);
|
||||||
const iconKind = getIconKind(eventType);
|
const iconKind = getIconKind(eventType);
|
||||||
const tooltip = getTooltip(eventType, data);
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
|
|||||||
54
frontend/src/Activity/History/HistoryFilterModal.tsx
Normal file
54
frontend/src/Activity/History/HistoryFilterModal.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||||
|
|
||||||
|
function createHistorySelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.history.items,
|
||||||
|
(queueItems) => {
|
||||||
|
return queueItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.history.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createHistorySelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'history';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setHistoryFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguages from 'Movie/MovieLanguages';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
@@ -113,7 +113,7 @@ class HistoryRow extends Component {
|
|||||||
if (name === 'languages') {
|
if (name === 'languages') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
<MovieLanguage
|
<MovieLanguages
|
||||||
languages={languages}
|
languages={languages}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
@@ -143,7 +143,7 @@ class HistoryRow extends Component {
|
|||||||
|
|
||||||
if (name === 'date') {
|
if (name === 'date') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={date}
|
date={date}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,3 +11,7 @@
|
|||||||
border-color: var(--usenetColor);
|
border-color: var(--usenetColor);
|
||||||
background-color: var(--usenetColor);
|
background-color: var(--usenetColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unknown {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'torrent': string;
|
'torrent': string;
|
||||||
|
'unknown': string;
|
||||||
'usenet': string;
|
'usenet': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import styles from './ProtocolLabel.css';
|
|
||||||
|
|
||||||
function ProtocolLabel({ protocol }) {
|
|
||||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label className={styles[protocol]}>
|
|
||||||
{protocolName}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProtocolLabel.propTypes = {
|
|
||||||
protocol: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProtocolLabel;
|
|
||||||
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import styles from './ProtocolLabel.css';
|
||||||
|
|
||||||
|
interface ProtocolLabelProps {
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
|
||||||
|
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||||
|
|
||||||
|
return <Label className={styles[protocol]}>{protocolName}</Label>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProtocolLabel;
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|
||||||
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 { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
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 QueueOptionsConnector from './QueueOptionsConnector';
|
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
|
||||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
|
||||||
|
|
||||||
class Queue extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._shouldBlockRefresh = false;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: false,
|
|
||||||
lastToggled: null,
|
|
||||||
selectedState: {},
|
|
||||||
isPendingSelected: false,
|
|
||||||
isConfirmRemoveModalOpen: false,
|
|
||||||
items: props.items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
if (this._shouldBlockRefresh) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
isFetching,
|
|
||||||
isMoviesFetching
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(!isMoviesFetching && prevProps.isMoviesFetching) ||
|
|
||||||
(!isFetching && prevProps.isFetching) ||
|
|
||||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.movieId))
|
|
||||||
) {
|
|
||||||
this.setState((state) => {
|
|
||||||
return {
|
|
||||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
|
||||||
items
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextState = {};
|
|
||||||
|
|
||||||
if (prevProps.items !== items) {
|
|
||||||
nextState.items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedIds = this.getSelectedIds();
|
|
||||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
|
||||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
|
||||||
nextState.isPendingSelected = isPendingSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(nextState)) {
|
|
||||||
this.setState(nextState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
getSelectedIds = () => {
|
|
||||||
return getSelectedIds(this.state.selectedState);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
|
||||||
this._shouldBlockRefresh = isOpen;
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectAllChange = ({ value }) => {
|
|
||||||
this.setState(selectAll(this.state.selectedState, value));
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
|
||||||
this.setState((state) => {
|
|
||||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onGrabSelectedPress = () => {
|
|
||||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedPress = () => {
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
|
||||||
this._shouldBlockRefresh = true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedConfirmed = (payload) => {
|
|
||||||
this._shouldBlockRefresh = false;
|
|
||||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmRemoveModalClose = () => {
|
|
||||||
this._shouldBlockRefresh = false;
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isMoviesFetching,
|
|
||||||
isMoviesPopulated,
|
|
||||||
moviesError,
|
|
||||||
columns,
|
|
||||||
totalRecords,
|
|
||||||
isGrabbing,
|
|
||||||
isRemoving,
|
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
|
||||||
onRefreshPress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
selectedState,
|
|
||||||
isConfirmRemoveModalOpen,
|
|
||||||
isPendingSelected,
|
|
||||||
items
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isRefreshing = isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
|
||||||
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId));
|
|
||||||
const hasError = error || moviesError;
|
|
||||||
const selectedIds = this.getSelectedIds();
|
|
||||||
const selectedCount = selectedIds.length;
|
|
||||||
const disableSelectedActions = selectedCount === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Queue')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Refresh')}
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isRefreshing}
|
|
||||||
onPress={onRefreshPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('GrabSelected')}
|
|
||||||
iconName={icons.DOWNLOAD}
|
|
||||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={this.onGrabSelectedPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RemoveSelected')}
|
|
||||||
iconName={icons.REMOVE}
|
|
||||||
isDisabled={disableSelectedActions}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={this.onRemoveSelectedPress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection
|
|
||||||
alignContent={align.RIGHT}
|
|
||||||
>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
columns={columns}
|
|
||||||
{...otherProps}
|
|
||||||
optionsComponent={QueueOptionsConnector}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isRefreshing && !isAllPopulated ?
|
|
||||||
<LoadingIndicator /> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isRefreshing && hasError ?
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('QueueLoadError')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !items.length ?
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{translate('QueueIsEmpty')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !!items.length ?
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
selectAll={true}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
{...otherProps}
|
|
||||||
optionsComponent={QueueOptionsConnector}
|
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<QueueRowConnector
|
|
||||||
key={item.id}
|
|
||||||
movieId={item.movieId}
|
|
||||||
isSelected={selectedState[item.id]}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
onSelectedChange={this.onSelectedChange}
|
|
||||||
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isRefreshing}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
<RemoveQueueItemsModal
|
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
|
||||||
selectedCount={selectedCount}
|
|
||||||
canIgnore={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
return !!(item && item.movieId);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
allPending={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
|
||||||
onModalClose={this.onConfirmRemoveModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Queue.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
isMoviesFetching: PropTypes.bool.isRequired,
|
|
||||||
isMoviesPopulated: PropTypes.bool.isRequired,
|
|
||||||
moviesError: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
|
||||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
|
||||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
|
||||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Queue;
|
|
||||||
400
frontend/src/Activity/Queue/Queue.tsx
Normal file
400
frontend/src/Activity/Queue/Queue.tsx
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
import React, {
|
||||||
|
ReactElement,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
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 usePaging from 'Components/Table/usePaging';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import {
|
||||||
|
clearQueue,
|
||||||
|
fetchQueue,
|
||||||
|
gotoQueuePage,
|
||||||
|
grabQueueItems,
|
||||||
|
removeQueueItems,
|
||||||
|
setQueueFilter,
|
||||||
|
setQueueSort,
|
||||||
|
setQueueTableOption,
|
||||||
|
} from 'Store/Actions/queueActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import QueueFilterModal from './QueueFilterModal';
|
||||||
|
import QueueOptions from './QueueOptions';
|
||||||
|
import QueueRow from './QueueRow';
|
||||||
|
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||||
|
import createQueueStatusSelector from './Status/createQueueStatusSelector';
|
||||||
|
|
||||||
|
function Queue() {
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalRecords,
|
||||||
|
isGrabbing,
|
||||||
|
isRemoving,
|
||||||
|
} = useSelector((state: AppState) => state.queue.paged);
|
||||||
|
|
||||||
|
const { count } = useSelector(createQueueStatusSelector());
|
||||||
|
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
|
||||||
|
createMoviesFetchingSelector()
|
||||||
|
);
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('queue'));
|
||||||
|
|
||||||
|
const isRefreshMonitoredDownloadsExecuting = useSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldBlockRefresh = useRef(false);
|
||||||
|
const currentQueue = useRef<ReactElement | null>(null);
|
||||||
|
|
||||||
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const isPendingSelected = useMemo(() => {
|
||||||
|
return items.some((item) => {
|
||||||
|
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||||
|
});
|
||||||
|
}, [items, selectedIds]);
|
||||||
|
|
||||||
|
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const isRefreshing =
|
||||||
|
isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||||
|
const isAllPopulated =
|
||||||
|
isPopulated &&
|
||||||
|
(isMoviesPopulated || !items.length || items.every((m) => !m.movieId));
|
||||||
|
const hasError = error || moviesError;
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
const disableSelectedActions = selectedCount === 0;
|
||||||
|
|
||||||
|
const handleSelectAllChange = useCallback(
|
||||||
|
({ value }: CheckInputChanged) => {
|
||||||
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectedChange = useCallback(
|
||||||
|
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||||
|
setSelectState({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
items,
|
||||||
|
id,
|
||||||
|
isSelected: value,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRefreshPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: commandNames.REFRESH_MONITORED_DOWNLOADS,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => {
|
||||||
|
shouldBlockRefresh.current = isOpen;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGrabSelectedPress = useCallback(() => {
|
||||||
|
dispatch(grabQueueItems({ ids: selectedIds }));
|
||||||
|
}, [selectedIds, dispatch]);
|
||||||
|
|
||||||
|
const handleRemoveSelectedPress = useCallback(() => {
|
||||||
|
shouldBlockRefresh.current = true;
|
||||||
|
setIsConfirmRemoveModalOpen(true);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const handleRemoveSelectedConfirmed = useCallback(
|
||||||
|
(payload: RemovePressProps) => {
|
||||||
|
shouldBlockRefresh.current = false;
|
||||||
|
dispatch(removeQueueItems({ ids: selectedIds, ...payload }));
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
},
|
||||||
|
[selectedIds, setIsConfirmRemoveModalOpen, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||||
|
shouldBlockRefresh.current = false;
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
} = usePaging({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
gotoPage: gotoQueuePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(selectedFilterKey: string) => {
|
||||||
|
dispatch(setQueueFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortPress = useCallback(
|
||||||
|
(sortKey: string) => {
|
||||||
|
dispatch(setQueueSort({ sortKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableOptionChange = useCallback(
|
||||||
|
(payload: TableOptionsChangePayload) => {
|
||||||
|
dispatch(setQueueTableOption(payload));
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
dispatch(gotoQueuePage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchQueue());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoQueuePage({ page: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearQueue());
|
||||||
|
};
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchQueue());
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (!shouldBlockRefresh.current) {
|
||||||
|
currentQueue.current = (
|
||||||
|
<PageContentBody>
|
||||||
|
{isRefreshing && !isAllPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isRefreshing && hasError ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('QueueLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && !items.length ? (
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{selectedFilterKey !== 'all' && count > 0
|
||||||
|
? translate('QueueFilterHasNoItems')
|
||||||
|
: translate('QueueIsEmpty')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && !!items.length ? (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
optionsComponent={QueueOptions}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
onSelectAllChange={handleSelectAllChange}
|
||||||
|
onSortPress={handleSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<QueueRow
|
||||||
|
key={item.id}
|
||||||
|
movieId={item.movieId}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={handleSelectedChange}
|
||||||
|
onQueueRowModalOpenOrClose={
|
||||||
|
handleQueueRowModalOpenOrClose
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Queue')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Refresh"
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isRefreshing}
|
||||||
|
onPress={handleRefreshPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('GrabSelected')}
|
||||||
|
iconName={icons.DOWNLOAD}
|
||||||
|
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={handleGrabSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RemoveSelected')}
|
||||||
|
iconName={icons.REMOVE}
|
||||||
|
isDisabled={disableSelectedActions}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={handleRemoveSelectedPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
maxPageSize={200}
|
||||||
|
optionsComponent={QueueOptions}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={QueueFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
{currentQueue.current}
|
||||||
|
|
||||||
|
<RemoveQueueItemModal
|
||||||
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
|
selectedCount={selectedCount}
|
||||||
|
canChangeCategory={
|
||||||
|
isConfirmRemoveModalOpen &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.downloadClientHasPostImportCategory);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
canIgnore={
|
||||||
|
isConfirmRemoveModalOpen &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.movieId);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isPending={
|
||||||
|
isConfirmRemoveModalOpen &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
item.status === 'delay' ||
|
||||||
|
item.status === 'downloadClientUnavailable'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onRemovePress={handleRemoveSelectedConfirmed}
|
||||||
|
onModalClose={handleConfirmRemoveModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Queue;
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import * as queueActions from 'Store/Actions/queueActions';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import Queue from './Queue';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movies,
|
|
||||||
(state) => state.queue.options,
|
|
||||||
(state) => state.queue.paged,
|
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
|
||||||
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
|
||||||
return {
|
|
||||||
isMoviesFetching: movies.isFetching,
|
|
||||||
isMoviesPopulated: movies.isPopulated,
|
|
||||||
moviesError: movies.error,
|
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
|
||||||
...options,
|
|
||||||
...queue
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...queueActions,
|
|
||||||
executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueueConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchQueue,
|
|
||||||
fetchQueueStatus,
|
|
||||||
gotoQueueFirstPage
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchQueue();
|
|
||||||
} else {
|
|
||||||
gotoQueueFirstPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchQueueStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (
|
|
||||||
this.props.includeUnknownMovieItems !==
|
|
||||||
prevProps.includeUnknownMovieItems
|
|
||||||
) {
|
|
||||||
this.repopulate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
this.props.clearQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
this.props.fetchQueue();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFirstPagePress = () => {
|
|
||||||
this.props.gotoQueueFirstPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPagePress = () => {
|
|
||||||
this.props.gotoQueuePreviousPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPagePress = () => {
|
|
||||||
this.props.gotoQueueNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onLastPagePress = () => {
|
|
||||||
this.props.gotoQueueLastPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelect = (page) => {
|
|
||||||
this.props.gotoQueuePage({ page });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
|
||||||
this.props.setQueueSort({ sortKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
|
||||||
this.props.setQueueTableOption(payload);
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
this.props.gotoQueueFirstPage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onRefreshPress = () => {
|
|
||||||
this.props.executeCommand({
|
|
||||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onGrabSelectedPress = (ids) => {
|
|
||||||
this.props.grabQueueItems({ ids });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedPress = (payload) => {
|
|
||||||
this.props.removeQueueItems(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Queue
|
|
||||||
onFirstPagePress={this.onFirstPagePress}
|
|
||||||
onPreviousPagePress={this.onPreviousPagePress}
|
|
||||||
onNextPagePress={this.onNextPagePress}
|
|
||||||
onLastPagePress={this.onLastPagePress}
|
|
||||||
onPageSelect={this.onPageSelect}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
onRefreshPress={this.onRefreshPress}
|
|
||||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
|
||||||
onRemoveSelectedPress={this.onRemoveSelectedPress}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueConnector.propTypes = {
|
|
||||||
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
fetchQueue: PropTypes.func.isRequired,
|
|
||||||
fetchQueueStatus: PropTypes.func.isRequired,
|
|
||||||
gotoQueueFirstPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueueNextPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueuePage: PropTypes.func.isRequired,
|
|
||||||
setQueueSort: PropTypes.func.isRequired,
|
|
||||||
setQueueTableOption: PropTypes.func.isRequired,
|
|
||||||
clearQueue: PropTypes.func.isRequired,
|
|
||||||
grabQueueItems: PropTypes.func.isRequired,
|
|
||||||
removeQueueItems: PropTypes.func.isRequired,
|
|
||||||
executeCommand: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
|
||||||
);
|
|
||||||
@@ -1,36 +1,49 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QueueStatus from './QueueStatus';
|
import QueueStatus from './QueueStatus';
|
||||||
import styles from './QueueDetails.css';
|
import styles from './QueueDetails.css';
|
||||||
|
|
||||||
function QueueDetails(props) {
|
interface QueueDetailsProps {
|
||||||
|
title: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
progressBar: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueDetails(props: QueueDetailsProps) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
size,
|
size,
|
||||||
sizeleft,
|
sizeleft,
|
||||||
status,
|
status,
|
||||||
trackedDownloadState,
|
trackedDownloadState = 'downloading',
|
||||||
trackedDownloadStatus,
|
trackedDownloadStatus = 'ok',
|
||||||
statusMessages,
|
statusMessages,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
progressBar
|
progressBar,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
||||||
const isDownloading = status === 'downloading';
|
const isDownloading = status === 'downloading';
|
||||||
const isPaused = status === 'paused';
|
const isPaused = status === 'paused';
|
||||||
const hasWarning = trackedDownloadStatus === 'warning';
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
const hasError = trackedDownloadStatus === 'error';
|
const hasError = trackedDownloadStatus === 'error';
|
||||||
|
|
||||||
if (
|
if ((isDownloading || isPaused) && !hasWarning && !hasError) {
|
||||||
(isDownloading || isPaused) &&
|
|
||||||
!hasWarning &&
|
|
||||||
!hasError
|
|
||||||
) {
|
|
||||||
const state = isPaused ? translate('Paused') : translate('Downloading');
|
const state = isPaused ? translate('Paused') : translate('Downloading');
|
||||||
|
|
||||||
if (progress < 5) {
|
if (progress < 5) {
|
||||||
@@ -45,11 +58,9 @@ function QueueDetails(props) {
|
|||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
className={styles.progressBarContainer}
|
className={styles.progressBarContainer}
|
||||||
anchor={progressBar}
|
anchor={progressBar!}
|
||||||
title={`${state} - ${progress.toFixed(1)}%`}
|
title={`${state} - ${progress.toFixed(1)}%`}
|
||||||
body={
|
body={<div>{title}</div>}
|
||||||
<div>{title}</div>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -68,17 +79,4 @@ function QueueDetails(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueDetails.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
sizeleft: PropTypes.number.isRequired,
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
progressBar: PropTypes.node.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueDetails;
|
export default QueueDetails;
|
||||||
54
frontend/src/Activity/Queue/QueueFilterModal.tsx
Normal file
54
frontend/src/Activity/Queue/QueueFilterModal.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setQueueFilter } from 'Store/Actions/queueActions';
|
||||||
|
|
||||||
|
function createQueueSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.paged.items,
|
||||||
|
(queueItems) => {
|
||||||
|
return queueItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.paged.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createQueueSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'queue';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setQueueFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component, Fragment } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class QueueOptions extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
includeUnknownMovieItems: props.includeUnknownMovieItems
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
includeUnknownMovieItems
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (includeUnknownMovieItems !== prevProps.includeUnknownMovieItems) {
|
|
||||||
this.setState({
|
|
||||||
includeUnknownMovieItems
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onOptionChange = ({ name, value }) => {
|
|
||||||
this.setState({
|
|
||||||
[name]: value
|
|
||||||
}, () => {
|
|
||||||
this.props.onOptionChange({
|
|
||||||
[name]: value
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
includeUnknownMovieItems
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="includeUnknownMovieItems"
|
|
||||||
value={includeUnknownMovieItems}
|
|
||||||
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
|
||||||
onChange={this.onOptionChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueOptions.propTypes = {
|
|
||||||
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
|
||||||
onOptionChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueOptions;
|
|
||||||
48
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
48
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function QueueOptions() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { includeUnknownMovieItems } = useSelector(
|
||||||
|
(state: AppState) => state.queue.options
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOptionChange = useCallback(
|
||||||
|
({ name, value }: CheckInputChanged) => {
|
||||||
|
dispatch(
|
||||||
|
setQueueOption({
|
||||||
|
[name]: value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (name === 'includeUnknownMovieItems') {
|
||||||
|
dispatch(gotoQueuePage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeUnknownMovieItems"
|
||||||
|
value={includeUnknownMovieItems}
|
||||||
|
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
||||||
|
onChange={handleOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueOptions;
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { setQueueOption } from 'Store/Actions/queueActions';
|
|
||||||
import QueueOptions from './QueueOptions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.queue.options,
|
|
||||||
(options) => {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
onOptionChange: setQueueOption
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
|
||||||
@@ -26,4 +26,5 @@
|
|||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 70px;
|
width: 70px;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,420 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import ProgressBar from 'Components/ProgressBar';
|
|
||||||
// import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QueueStatusCell from './QueueStatusCell';
|
|
||||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
|
||||||
import TimeleftCell from './TimeleftCell';
|
|
||||||
import styles from './QueueRow.css';
|
|
||||||
|
|
||||||
class QueueRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isRemoveQueueItemModalOpen: false,
|
|
||||||
isInteractiveImportModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveQueueItemPress = () => {
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
|
|
||||||
const {
|
|
||||||
onRemoveQueueItemPress,
|
|
||||||
onQueueRowModalOpenOrClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onQueueRowModalOpenOrClose(false);
|
|
||||||
onRemoveQueueItemPress(blocklist, skipRedownload);
|
|
||||||
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveQueueItemModalClose = () => {
|
|
||||||
this.props.onQueueRowModalOpenOrClose(false);
|
|
||||||
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInteractiveImportPress = () => {
|
|
||||||
this.props.onQueueRowModalOpenOrClose(true);
|
|
||||||
|
|
||||||
this.setState({ isInteractiveImportModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInteractiveImportModalClose = () => {
|
|
||||||
this.props.onQueueRowModalOpenOrClose(false);
|
|
||||||
|
|
||||||
this.setState({ isInteractiveImportModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
downloadId,
|
|
||||||
title,
|
|
||||||
status,
|
|
||||||
trackedDownloadStatus,
|
|
||||||
trackedDownloadState,
|
|
||||||
statusMessages,
|
|
||||||
errorMessage,
|
|
||||||
movie,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
customFormatScore,
|
|
||||||
languages,
|
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
outputPath,
|
|
||||||
downloadClient,
|
|
||||||
estimatedCompletionTime,
|
|
||||||
timeleft,
|
|
||||||
size,
|
|
||||||
sizeleft,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
isGrabbing,
|
|
||||||
grabError,
|
|
||||||
isRemoving,
|
|
||||||
isSelected,
|
|
||||||
columns,
|
|
||||||
onSelectedChange,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isRemoveQueueItemModalOpen,
|
|
||||||
isInteractiveImportModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const progress = 100 - (sizeleft / size * 100);
|
|
||||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
|
||||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableSelectCell
|
|
||||||
id={id}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'status') {
|
|
||||||
return (
|
|
||||||
<QueueStatusCell
|
|
||||||
key={name}
|
|
||||||
sourceTitle={title}
|
|
||||||
status={status}
|
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
|
||||||
trackedDownloadState={trackedDownloadState}
|
|
||||||
statusMessages={statusMessages}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'movies.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
movie ?
|
|
||||||
<MovieTitleLink
|
|
||||||
titleSlug={movie.titleSlug}
|
|
||||||
title={movie.title}
|
|
||||||
/> :
|
|
||||||
title
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieLanguage
|
|
||||||
languages={languages}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
quality ?
|
|
||||||
<MovieQuality
|
|
||||||
quality={quality}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormatScore') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.customFormatScore}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
anchor={formatCustomFormatScore(
|
|
||||||
customFormatScore,
|
|
||||||
customFormats.length
|
|
||||||
)}
|
|
||||||
tooltip={<MovieFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'protocol') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{downloadClient}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'size') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{formatBytes(size)}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'year') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
movie ? movie.year : ''
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'title') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{title}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'outputPath') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{outputPath}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'estimatedCompletionTime') {
|
|
||||||
return (
|
|
||||||
<TimeleftCell
|
|
||||||
key={name}
|
|
||||||
status={status}
|
|
||||||
estimatedCompletionTime={estimatedCompletionTime}
|
|
||||||
timeleft={timeleft}
|
|
||||||
size={size}
|
|
||||||
sizeleft={sizeleft}
|
|
||||||
showRelativeDates={showRelativeDates}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'progress') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.progress}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
!!progress &&
|
|
||||||
<ProgressBar
|
|
||||||
progress={progress}
|
|
||||||
title={`${progress.toFixed(1)}%`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.actions}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
showInteractiveImport &&
|
|
||||||
<IconButton
|
|
||||||
name={icons.INTERACTIVE}
|
|
||||||
onPress={this.onInteractiveImportPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPending &&
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={icons.DOWNLOAD}
|
|
||||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={onGrabPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<SpinnerIconButton
|
|
||||||
title={translate('RemoveFromQueue')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={this.onRemoveQueueItemPress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<InteractiveImportModal
|
|
||||||
isOpen={isInteractiveImportModalOpen}
|
|
||||||
downloadId={downloadId}
|
|
||||||
title={title}
|
|
||||||
onModalClose={this.onInteractiveImportModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RemoveQueueItemModal
|
|
||||||
isOpen={isRemoveQueueItemModalOpen}
|
|
||||||
sourceTitle={title}
|
|
||||||
canIgnore={!!movie}
|
|
||||||
isPending={isPending}
|
|
||||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
|
||||||
onModalClose={this.onRemoveQueueItemModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueRow.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string,
|
|
||||||
trackedDownloadState: PropTypes.string,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
movie: PropTypes.object,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
indexer: PropTypes.string,
|
|
||||||
outputPath: PropTypes.string,
|
|
||||||
downloadClient: PropTypes.string,
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
timeleft: PropTypes.string,
|
|
||||||
size: PropTypes.number,
|
|
||||||
year: PropTypes.number,
|
|
||||||
sizeleft: PropTypes.number,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
grabError: PropTypes.object,
|
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
|
||||||
onRemoveQueueItemPress: PropTypes.func.isRequired,
|
|
||||||
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueRow.defaultProps = {
|
|
||||||
customFormats: [],
|
|
||||||
isGrabbing: false,
|
|
||||||
isRemoving: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueRow;
|
|
||||||
361
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
361
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||||
|
import { Error } from 'App/State/AppSectionState';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
|
import MovieLanguages from 'Movie/MovieLanguages';
|
||||||
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import useMovie from 'Movie/useMovie';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueueStatusCell from './QueueStatusCell';
|
||||||
|
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||||
|
import TimeleftCell from './TimeleftCell';
|
||||||
|
import styles from './QueueRow.css';
|
||||||
|
|
||||||
|
interface QueueRowProps {
|
||||||
|
id: number;
|
||||||
|
movieId?: number;
|
||||||
|
downloadId?: string;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats?: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
indexer?: string;
|
||||||
|
outputPath?: string;
|
||||||
|
downloadClient?: string;
|
||||||
|
downloadClientHasPostImportCategory?: boolean;
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
added?: string;
|
||||||
|
timeleft?: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
isGrabbing?: boolean;
|
||||||
|
grabError?: Error;
|
||||||
|
isRemoving?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
columns: Column[];
|
||||||
|
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||||
|
onQueueRowModalOpenOrClose: (isOpen: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueRow(props: QueueRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
movieId,
|
||||||
|
downloadId,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus,
|
||||||
|
trackedDownloadState,
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
customFormats = [],
|
||||||
|
customFormatScore,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
outputPath,
|
||||||
|
downloadClient,
|
||||||
|
downloadClientHasPostImportCategory,
|
||||||
|
estimatedCompletionTime,
|
||||||
|
added,
|
||||||
|
timeleft,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
isGrabbing = false,
|
||||||
|
grabError,
|
||||||
|
isRemoving = false,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange,
|
||||||
|
onQueueRowModalOpenOrClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const movie = useMovie(movieId);
|
||||||
|
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const handleGrabPress = useCallback(() => {
|
||||||
|
dispatch(grabQueueItem({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
const handleInteractiveImportPress = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(true);
|
||||||
|
setIsInteractiveImportModalOpen(true);
|
||||||
|
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const handleInteractiveImportModalClose = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
setIsInteractiveImportModalOpen(false);
|
||||||
|
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const handleRemoveQueueItemPress = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(true);
|
||||||
|
setIsRemoveQueueItemModalOpen(true);
|
||||||
|
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const handleRemoveQueueItemModalConfirmed = useCallback(
|
||||||
|
(payload: RemovePressProps) => {
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
dispatch(removeQueueItem({ id, ...payload }));
|
||||||
|
setIsRemoveQueueItemModalOpen(false);
|
||||||
|
},
|
||||||
|
[id, setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveQueueItemModalClose = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
setIsRemoveQueueItemModalOpen(false);
|
||||||
|
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
||||||
|
const showInteractiveImport =
|
||||||
|
status === 'completed' && trackedDownloadStatus === 'warning';
|
||||||
|
const isPending =
|
||||||
|
status === 'delay' || status === 'downloadClientUnavailable';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<QueueStatusCell
|
||||||
|
key={name}
|
||||||
|
sourceTitle={title}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movies.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{movie ? (
|
||||||
|
<MovieTitleLink
|
||||||
|
titleSlug={movie.titleSlug}
|
||||||
|
title={movie.title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'year') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>{movie ? movie.year : ''}</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{quality ? <MovieQuality quality={quality} /> : null}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<MovieFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'protocol') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<ProtocolLabel protocol={protocol} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return <TableRowCell key={name}>{indexer}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
return <TableRowCell key={name}>{downloadClient}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'title') {
|
||||||
|
return <TableRowCell key={name}>{title}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'size') {
|
||||||
|
return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'outputPath') {
|
||||||
|
return <TableRowCell key={name}>{outputPath}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'estimatedCompletionTime') {
|
||||||
|
return (
|
||||||
|
<TimeleftCell
|
||||||
|
key={name}
|
||||||
|
status={status}
|
||||||
|
estimatedCompletionTime={estimatedCompletionTime}
|
||||||
|
timeleft={timeleft}
|
||||||
|
size={size}
|
||||||
|
sizeleft={sizeleft}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'progress') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.progress}>
|
||||||
|
{!!progress && (
|
||||||
|
<ProgressBar
|
||||||
|
progress={progress}
|
||||||
|
title={`${progress.toFixed(1)}%`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'added') {
|
||||||
|
return <RelativeDateCell key={name} date={added} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.actions}>
|
||||||
|
{showInteractiveImport ? (
|
||||||
|
<IconButton
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
onPress={handleInteractiveImportPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPending ? (
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.DOWNLOAD}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={handleGrabPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SpinnerIconButton
|
||||||
|
title={translate('RemoveFromQueue')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={handleRemoveQueueItemPress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<InteractiveImportModal
|
||||||
|
isOpen={isInteractiveImportModalOpen}
|
||||||
|
downloadId={downloadId}
|
||||||
|
modalTitle={title}
|
||||||
|
onModalClose={handleInteractiveImportModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RemoveQueueItemModal
|
||||||
|
isOpen={isRemoveQueueItemModalOpen}
|
||||||
|
sourceTitle={title}
|
||||||
|
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||||
|
canIgnore={!!movie}
|
||||||
|
isPending={isPending}
|
||||||
|
onRemovePress={handleRemoveQueueItemModalConfirmed}
|
||||||
|
onModalClose={handleRemoveQueueItemModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueRow;
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import QueueRow from './QueueRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(movie, uiSettings) => {
|
|
||||||
const result = {
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
|
|
||||||
result.movie = movie;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
grabQueueItem,
|
|
||||||
removeQueueItem
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueueRowConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onGrabPress = () => {
|
|
||||||
this.props.grabQueueItem({ id: this.props.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveQueueItemPress = (payload) => {
|
|
||||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<QueueRow
|
|
||||||
{...this.props}
|
|
||||||
onGrabPress={this.onGrabPress}
|
|
||||||
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueRowConnector.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
movie: PropTypes.object,
|
|
||||||
grabQueueItem: PropTypes.func.isRequired,
|
|
||||||
removeQueueItem: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);
|
|
||||||
@@ -1,51 +1,59 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon, { IconProps } from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import TooltipPosition from 'Helpers/Props/TooltipPosition';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './QueueStatus.css';
|
import styles from './QueueStatus.css';
|
||||||
|
|
||||||
function getDetailedPopoverBody(statusMessages) {
|
function getDetailedPopoverBody(statusMessages: StatusMessage[]) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{
|
{statusMessages.map(({ title, messages }) => {
|
||||||
statusMessages.map(({ title, messages }) => {
|
return (
|
||||||
return (
|
<div
|
||||||
<div
|
key={title}
|
||||||
key={title}
|
className={messages.length ? undefined : styles.noMessages}
|
||||||
className={messages.length ? undefined: styles.noMessages}
|
>
|
||||||
>
|
{title}
|
||||||
{title}
|
<ul>
|
||||||
<ul>
|
{messages.map((message) => {
|
||||||
{
|
return <li key={message}>{message}</li>;
|
||||||
messages.map((message) => {
|
})}
|
||||||
return (
|
</ul>
|
||||||
<li key={message}>
|
</div>
|
||||||
{message}
|
);
|
||||||
</li>
|
})}
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function QueueStatus(props) {
|
interface QueueStatusProps {
|
||||||
|
sourceTitle: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
position: TooltipPosition;
|
||||||
|
canFlip?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueStatus(props: QueueStatusProps) {
|
||||||
const {
|
const {
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
status,
|
status,
|
||||||
trackedDownloadStatus,
|
trackedDownloadStatus = 'ok',
|
||||||
trackedDownloadState,
|
trackedDownloadState = 'downloading',
|
||||||
statusMessages,
|
statusMessages = [],
|
||||||
errorMessage,
|
errorMessage,
|
||||||
position,
|
position,
|
||||||
canFlip
|
canFlip = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const hasWarning = trackedDownloadStatus === 'warning';
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
@@ -53,7 +61,7 @@ function QueueStatus(props) {
|
|||||||
|
|
||||||
// status === 'downloading'
|
// status === 'downloading'
|
||||||
let iconName = icons.DOWNLOADING;
|
let iconName = icons.DOWNLOADING;
|
||||||
let iconKind = kinds.DEFAULT;
|
let iconKind: IconProps['kind'] = kinds.DEFAULT;
|
||||||
let title = translate('Downloading');
|
let title = translate('Downloading');
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
@@ -70,6 +78,11 @@ function QueueStatus(props) {
|
|||||||
iconName = icons.DOWNLOADED;
|
iconName = icons.DOWNLOADED;
|
||||||
title = translate('Downloaded');
|
title = translate('Downloaded');
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'importBlocked') {
|
||||||
|
title += ` - ${translate('UnableToImportAutomatically')}`;
|
||||||
|
iconKind = kinds.WARNING;
|
||||||
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'importPending') {
|
if (trackedDownloadState === 'importPending') {
|
||||||
title += ` - ${translate('WaitingToImport')}`;
|
title += ` - ${translate('WaitingToImport')}`;
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
@@ -110,7 +123,8 @@ function QueueStatus(props) {
|
|||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
const warningMessage =
|
||||||
|
errorMessage || translate('CheckDownloadClientForDetails');
|
||||||
title = translate('DownloadWarning', { warningMessage });
|
title = translate('DownloadWarning', { warningMessage });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,35 +142,17 @@ function QueueStatus(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={<Icon name={iconName} kind={iconKind} />}
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
kind={iconKind}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={title}
|
title={title}
|
||||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
body={
|
||||||
|
hasWarning || hasError
|
||||||
|
? getDetailedPopoverBody(statusMessages)
|
||||||
|
: sourceTitle
|
||||||
|
}
|
||||||
position={position}
|
position={position}
|
||||||
canFlip={canFlip}
|
canFlip={canFlip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueStatus.propTypes = {
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
|
|
||||||
canFlip: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueStatus.defaultProps = {
|
|
||||||
trackedDownloadStatus: 'ok',
|
|
||||||
trackedDownloadState: 'downloading',
|
|
||||||
canFlip: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueStatus;
|
export default QueueStatus;
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import { tooltipPositions } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QueueStatus from './QueueStatus';
|
|
||||||
import styles from './QueueStatusCell.css';
|
|
||||||
|
|
||||||
function QueueStatusCell(props) {
|
|
||||||
const {
|
|
||||||
sourceTitle,
|
|
||||||
status,
|
|
||||||
trackedDownloadStatus,
|
|
||||||
trackedDownloadState,
|
|
||||||
statusMessages,
|
|
||||||
errorMessage
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell className={styles.status}>
|
|
||||||
<QueueStatus
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
status={status}
|
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
|
||||||
trackedDownloadState={trackedDownloadState}
|
|
||||||
statusMessages={statusMessages}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
position={tooltipPositions.RIGHT}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueStatusCell.propTypes = {
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueStatusCell.defaultProps = {
|
|
||||||
trackedDownloadStatus: translate('Ok'),
|
|
||||||
trackedDownloadState: translate('Downloading')
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueStatusCell;
|
|
||||||
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
|
import QueueStatus from './QueueStatus';
|
||||||
|
import styles from './QueueStatusCell.css';
|
||||||
|
|
||||||
|
interface QueueStatusCellProps {
|
||||||
|
sourceTitle: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueStatusCell(props: QueueStatusCellProps) {
|
||||||
|
const {
|
||||||
|
sourceTitle,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus = 'ok',
|
||||||
|
trackedDownloadState = 'downloading',
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.status}>
|
||||||
|
<QueueStatus
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
position="right"
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueStatusCell;
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class RemoveQueueItemModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = function() {
|
|
||||||
this.setState({
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveChange = ({ value }) => {
|
|
||||||
this.setState({ remove: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlocklistChange = ({ value }) => {
|
|
||||||
this.setState({ blocklist: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSkipRedownloadChange = ({ value }) => {
|
|
||||||
this.setState({ skipRedownload: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
|
||||||
const state = this.state;
|
|
||||||
|
|
||||||
this.resetState();
|
|
||||||
this.props.onRemovePress(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.resetState();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
sourceTitle,
|
|
||||||
canIgnore,
|
|
||||||
isPending
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { remove, blocklist, skipRedownload } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('RemoveQueueItem', { sourceTitle })}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPending ?
|
|
||||||
null :
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="remove"
|
|
||||||
value={remove}
|
|
||||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
|
||||||
isDisabled={!canIgnore}
|
|
||||||
onChange={this.onRemoveChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="blocklist"
|
|
||||||
value={blocklist}
|
|
||||||
helpText={translate('BlocklistReleaseHelpText')}
|
|
||||||
onChange={this.onBlocklistChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
blocklist ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="skipRedownload"
|
|
||||||
value={skipRedownload}
|
|
||||||
helpText={translate('SkipRedownloadHelpText')}
|
|
||||||
onChange={this.onSkipRedownloadChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={this.onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={this.onRemoveConfirmed}
|
|
||||||
>
|
|
||||||
{translate('Remove')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveQueueItemModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
canIgnore: PropTypes.bool.isRequired,
|
|
||||||
isPending: PropTypes.bool.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RemoveQueueItemModal;
|
|
||||||
231
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
231
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './RemoveQueueItemModal.css';
|
||||||
|
|
||||||
|
export interface RemovePressProps {
|
||||||
|
remove: boolean;
|
||||||
|
changeCategory: boolean;
|
||||||
|
blocklist: boolean;
|
||||||
|
skipRedownload: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoveQueueItemModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
sourceTitle?: string;
|
||||||
|
canChangeCategory: boolean;
|
||||||
|
canIgnore: boolean;
|
||||||
|
isPending: boolean;
|
||||||
|
selectedCount?: number;
|
||||||
|
onRemovePress(props: RemovePressProps): void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||||
|
type BlocklistMethod =
|
||||||
|
| 'doNotBlocklist'
|
||||||
|
| 'blocklistAndSearch'
|
||||||
|
| 'blocklistOnly';
|
||||||
|
|
||||||
|
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
sourceTitle = '',
|
||||||
|
canIgnore,
|
||||||
|
canChangeCategory,
|
||||||
|
isPending,
|
||||||
|
selectedCount,
|
||||||
|
onRemovePress,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const multipleSelected = selectedCount && selectedCount > 1;
|
||||||
|
|
||||||
|
const [removalMethod, setRemovalMethod] =
|
||||||
|
useState<RemovalMethod>('removeFromClient');
|
||||||
|
const [blocklistMethod, setBlocklistMethod] =
|
||||||
|
useState<BlocklistMethod>('doNotBlocklist');
|
||||||
|
|
||||||
|
const { title, message } = useMemo(() => {
|
||||||
|
if (!selectedCount) {
|
||||||
|
return {
|
||||||
|
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||||
|
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCount === 1) {
|
||||||
|
return {
|
||||||
|
title: translate('RemoveSelectedItem'),
|
||||||
|
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: translate('RemoveSelectedItems'),
|
||||||
|
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||||
|
selectedCount,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, [sourceTitle, selectedCount]);
|
||||||
|
|
||||||
|
const removalMethodOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'removeFromClient',
|
||||||
|
value: translate('RemoveFromDownloadClient'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('RemoveMultipleFromDownloadClientHint')
|
||||||
|
: translate('RemoveFromDownloadClientHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'changeCategory',
|
||||||
|
value: translate('ChangeCategory'),
|
||||||
|
isDisabled: !canChangeCategory,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('ChangeCategoryMultipleHint')
|
||||||
|
: translate('ChangeCategoryHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ignore',
|
||||||
|
value: multipleSelected
|
||||||
|
? translate('IgnoreDownloads')
|
||||||
|
: translate('IgnoreDownload'),
|
||||||
|
isDisabled: !canIgnore,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('IgnoreDownloadsHint')
|
||||||
|
: translate('IgnoreDownloadHint'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||||
|
|
||||||
|
const blocklistMethodOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'doNotBlocklist',
|
||||||
|
value: translate('DoNotBlocklist'),
|
||||||
|
hint: translate('DoNotBlocklistHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocklistAndSearch',
|
||||||
|
value: translate('BlocklistAndSearch'),
|
||||||
|
isDisabled: isPending,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistAndSearchMultipleHint')
|
||||||
|
: translate('BlocklistAndSearchHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocklistOnly',
|
||||||
|
value: translate('BlocklistOnly'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistMultipleOnlyHint')
|
||||||
|
: translate('BlocklistOnlyHint'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [isPending, multipleSelected]);
|
||||||
|
|
||||||
|
const handleRemovalMethodChange = useCallback(
|
||||||
|
({ value }: { value: RemovalMethod }) => {
|
||||||
|
setRemovalMethod(value);
|
||||||
|
},
|
||||||
|
[setRemovalMethod]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlocklistMethodChange = useCallback(
|
||||||
|
({ value }: { value: BlocklistMethod }) => {
|
||||||
|
setBlocklistMethod(value);
|
||||||
|
},
|
||||||
|
[setBlocklistMethod]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirmRemove = useCallback(() => {
|
||||||
|
onRemovePress({
|
||||||
|
remove: removalMethod === 'removeFromClient',
|
||||||
|
changeCategory: removalMethod === 'changeCategory',
|
||||||
|
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||||
|
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemovalMethod('removeFromClient');
|
||||||
|
setBlocklistMethod('doNotBlocklist');
|
||||||
|
}, [
|
||||||
|
removalMethod,
|
||||||
|
blocklistMethod,
|
||||||
|
setRemovalMethod,
|
||||||
|
setBlocklistMethod,
|
||||||
|
onRemovePress,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
setRemovalMethod('removeFromClient');
|
||||||
|
setBlocklistMethod('doNotBlocklist');
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||||
|
<ModalContent onModalClose={handleModalClose}>
|
||||||
|
<ModalHeader>{title}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.message}>{message}</div>
|
||||||
|
|
||||||
|
{isPending ? null : (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="removalMethod"
|
||||||
|
value={removalMethod}
|
||||||
|
values={removalMethodOptions}
|
||||||
|
isDisabled={!canChangeCategory && !canIgnore}
|
||||||
|
helpTextWarning={translate(
|
||||||
|
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||||
|
)}
|
||||||
|
onChange={handleRemovalMethodChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{multipleSelected
|
||||||
|
? translate('BlocklistReleases')
|
||||||
|
: translate('BlocklistRelease')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="blocklistMethod"
|
||||||
|
value={blocklistMethod}
|
||||||
|
values={blocklistMethodOptions}
|
||||||
|
helpText={translate('BlocklistReleaseHelpText')}
|
||||||
|
onChange={handleBlocklistMethodChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||||
|
{translate('Remove')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoveQueueItemModal;
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './RemoveQueueItemsModal.css';
|
|
||||||
|
|
||||||
class RemoveQueueItemsModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = function() {
|
|
||||||
this.setState({
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveChange = ({ value }) => {
|
|
||||||
this.setState({ remove: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlocklistChange = ({ value }) => {
|
|
||||||
this.setState({ blocklist: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSkipRedownloadChange = ({ value }) => {
|
|
||||||
this.setState({ skipRedownload: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
|
||||||
const state = this.state;
|
|
||||||
|
|
||||||
this.resetState();
|
|
||||||
this.props.onRemovePress(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.resetState();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
selectedCount,
|
|
||||||
canIgnore,
|
|
||||||
allPending
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { remove, blocklist, skipRedownload } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div className={styles.message}>
|
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
allPending ?
|
|
||||||
null :
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="remove"
|
|
||||||
value={remove}
|
|
||||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
|
||||||
isDisabled={!canIgnore}
|
|
||||||
onChange={this.onRemoveChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="blocklist"
|
|
||||||
value={blocklist}
|
|
||||||
helpText={translate('BlocklistReleaseHelpText')}
|
|
||||||
onChange={this.onBlocklistChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
blocklist ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="skipRedownload"
|
|
||||||
value={skipRedownload}
|
|
||||||
helpText={translate('SkipRedownloadHelpText')}
|
|
||||||
onChange={this.onSkipRedownloadChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={this.onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={this.onRemoveConfirmed}
|
|
||||||
>
|
|
||||||
{translate('Remove')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveQueueItemsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
selectedCount: PropTypes.number.isRequired,
|
|
||||||
canIgnore: PropTypes.bool.isRequired,
|
|
||||||
allPending: PropTypes.bool.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RemoveQueueItemsModal;
|
|
||||||
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||||
|
import createQueueStatusSelector from './createQueueStatusSelector';
|
||||||
|
|
||||||
|
function QueueStatus() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isConnected, isReconnecting } = useSelector(
|
||||||
|
(state: AppState) => state.app
|
||||||
|
);
|
||||||
|
const { isPopulated, count, errors, warnings } = useSelector(
|
||||||
|
createQueueStatusSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasReconnecting = usePrevious(isReconnecting);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPopulated) {
|
||||||
|
dispatch(fetchQueueStatus());
|
||||||
|
}
|
||||||
|
}, [isPopulated, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isConnected && wasReconnecting) {
|
||||||
|
dispatch(fetchQueueStatus());
|
||||||
|
}
|
||||||
|
}, [isConnected, wasReconnecting, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSidebarStatus count={count} errors={errors} warnings={warnings} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueStatus;
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
|
||||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.app,
|
|
||||||
(state) => state.queue.status,
|
|
||||||
(state) => state.queue.options.includeUnknownMovieItems,
|
|
||||||
(app, status, includeUnknownMovieItems) => {
|
|
||||||
const {
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
unknownErrors,
|
|
||||||
unknownWarnings,
|
|
||||||
count,
|
|
||||||
totalCount
|
|
||||||
} = status.item;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isConnected: app.isConnected,
|
|
||||||
isReconnecting: app.isReconnecting,
|
|
||||||
isPopulated: status.isPopulated,
|
|
||||||
...status.item,
|
|
||||||
count: includeUnknownMovieItems ? totalCount : count,
|
|
||||||
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
|
|
||||||
warnings: includeUnknownMovieItems ? warnings || unknownWarnings : warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchQueueStatus
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueueStatusConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.props.isPopulated) {
|
|
||||||
this.props.fetchQueueStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this.props.isConnected && prevProps.isReconnecting) {
|
|
||||||
this.props.fetchQueueStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PageSidebarStatus
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueStatusConnector.propTypes = {
|
|
||||||
isConnected: PropTypes.bool.isRequired,
|
|
||||||
isReconnecting: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
fetchQueueStatus: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function createQueueStatusSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.status.isPopulated,
|
||||||
|
(state: AppState) => state.queue.status.item,
|
||||||
|
(state: AppState) => state.queue.options.includeUnknownMovieItems,
|
||||||
|
(isPopulated, status, includeUnknownMovieItems) => {
|
||||||
|
const {
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
unknownErrors,
|
||||||
|
unknownWarnings,
|
||||||
|
count,
|
||||||
|
totalCount,
|
||||||
|
} = status;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...status,
|
||||||
|
isPopulated,
|
||||||
|
count: includeUnknownMovieItems ? totalCount : count,
|
||||||
|
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
|
||||||
|
warnings: includeUnknownMovieItems
|
||||||
|
? warnings || unknownWarnings
|
||||||
|
: warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createQueueStatusSelector;
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import formatTime from 'Utilities/Date/formatTime';
|
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './TimeleftCell.css';
|
|
||||||
|
|
||||||
function TimeleftCell(props) {
|
|
||||||
const {
|
|
||||||
estimatedCompletionTime,
|
|
||||||
timeleft,
|
|
||||||
status,
|
|
||||||
size,
|
|
||||||
sizeleft,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (status === 'delay') {
|
|
||||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.timeleft}
|
|
||||||
title={translate('DelayingDownloadUntil', { date, time })}
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'downloadClientUnavailable') {
|
|
||||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.timeleft}
|
|
||||||
title={translate('RetryingDownloadOn', { date, time })}
|
|
||||||
>
|
|
||||||
-
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
|
||||||
return (
|
|
||||||
<TableRowCell className={styles.timeleft}>
|
|
||||||
-
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalSize = formatBytes(size);
|
|
||||||
const remainingSize = formatBytes(sizeleft);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.timeleft}
|
|
||||||
title={`${remainingSize} / ${totalSize}`}
|
|
||||||
>
|
|
||||||
{formatTimeSpan(timeleft)}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeleftCell.propTypes = {
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
timeleft: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
sizeleft: PropTypes.number.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TimeleftCell;
|
|
||||||
97
frontend/src/Activity/Queue/TimeleftCell.tsx
Normal file
97
frontend/src/Activity/Queue/TimeleftCell.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import formatTime from 'Utilities/Date/formatTime';
|
||||||
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './TimeleftCell.css';
|
||||||
|
|
||||||
|
interface TimeleftCellProps {
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
timeleft?: string;
|
||||||
|
status: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
showRelativeDates: boolean;
|
||||||
|
shortDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimeleftCell(props: TimeleftCellProps) {
|
||||||
|
const {
|
||||||
|
estimatedCompletionTime,
|
||||||
|
timeleft,
|
||||||
|
status,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (status === 'delay') {
|
||||||
|
const date = getRelativeDate({
|
||||||
|
date: estimatedCompletionTime,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||||
|
includeMinuteZero: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.timeleft}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={<Icon name={icons.INFO} />}
|
||||||
|
tooltip={translate('DelayingDownloadUntil', { date, time })}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'downloadClientUnavailable') {
|
||||||
|
const date = getRelativeDate({
|
||||||
|
date: estimatedCompletionTime,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||||
|
includeMinuteZero: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.timeleft}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={<Icon name={icons.INFO} />}
|
||||||
|
tooltip={translate('RetryingDownloadOn', { date, time })}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||||
|
return <TableRowCell className={styles.timeleft}>-</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = formatBytes(size);
|
||||||
|
const remainingSize = formatBytes(sizeleft);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.timeleft}
|
||||||
|
title={`${remainingSize} / ${totalSize}`}
|
||||||
|
>
|
||||||
|
{formatTimeSpan(timeleft)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimeleftCell;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
@@ -130,7 +131,14 @@ class AddNewMovie extends Component {
|
|||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('FailedLoadingSearchResults')}
|
{translate('FailedLoadingSearchResults')}
|
||||||
</div>
|
</div>
|
||||||
<div>{getErrorMessage(error)}</div>
|
|
||||||
|
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
|
||||||
|
{translate('WhySearchesCouldBeFailing')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
||||||
|
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
import parseUrl from 'Utilities/String/parseUrl';
|
import parseUrl from 'Utilities/String/parseUrl';
|
||||||
import AddNewMovie from './AddNewMovie';
|
import AddNewMovie from './AddNewMovie';
|
||||||
|
|
||||||
@@ -33,9 +35,10 @@ const mapDispatchToProps = {
|
|||||||
lookupMovie,
|
lookupMovie,
|
||||||
clearAddMovie,
|
clearAddMovie,
|
||||||
fetchRootFolders,
|
fetchRootFolders,
|
||||||
fetchImportExclusions,
|
|
||||||
fetchQueueDetails,
|
fetchQueueDetails,
|
||||||
clearQueueDetails
|
clearQueueDetails,
|
||||||
|
fetchMovieFiles,
|
||||||
|
clearMovieFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
class AddNewMovieConnector extends Component {
|
class AddNewMovieConnector extends Component {
|
||||||
@@ -51,10 +54,23 @@ class AddNewMovieConnector extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchRootFolders();
|
this.props.fetchRootFolders();
|
||||||
this.props.fetchImportExclusions();
|
|
||||||
this.props.fetchQueueDetails();
|
this.props.fetchQueueDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (hasDifferentItems(prevProps.items, items)) {
|
||||||
|
const movieIds = selectUniqueIds(items, 'internalId');
|
||||||
|
|
||||||
|
if (movieIds.length) {
|
||||||
|
this.props.fetchMovieFiles({ movieId: movieIds });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this._movieLookupTimeout) {
|
if (this._movieLookupTimeout) {
|
||||||
clearTimeout(this._movieLookupTimeout);
|
clearTimeout(this._movieLookupTimeout);
|
||||||
@@ -62,6 +78,7 @@ class AddNewMovieConnector extends Component {
|
|||||||
|
|
||||||
this.props.clearAddMovie();
|
this.props.clearAddMovie();
|
||||||
this.props.clearQueueDetails();
|
this.props.clearQueueDetails();
|
||||||
|
this.props.clearMovieFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -107,12 +124,14 @@ class AddNewMovieConnector extends Component {
|
|||||||
|
|
||||||
AddNewMovieConnector.propTypes = {
|
AddNewMovieConnector.propTypes = {
|
||||||
term: PropTypes.string,
|
term: PropTypes.string,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
lookupMovie: PropTypes.func.isRequired,
|
lookupMovie: PropTypes.func.isRequired,
|
||||||
clearAddMovie: PropTypes.func.isRequired,
|
clearAddMovie: PropTypes.func.isRequired,
|
||||||
fetchRootFolders: PropTypes.func.isRequired,
|
fetchRootFolders: PropTypes.func.isRequired,
|
||||||
fetchImportExclusions: PropTypes.func.isRequired,
|
|
||||||
fetchQueueDetails: PropTypes.func.isRequired,
|
fetchQueueDetails: PropTypes.func.isRequired,
|
||||||
clearQueueDetails: PropTypes.func.isRequired
|
clearQueueDetails: PropTypes.func.isRequired,
|
||||||
|
fetchMovieFiles: PropTypes.func.isRequired,
|
||||||
|
clearMovieFiles: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AddNewMovieModalContent.css';
|
import styles from './AddNewMovieModalContent.css';
|
||||||
@@ -20,6 +23,10 @@ class AddNewMovieModalContent extends Component {
|
|||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
onQualityProfileIdChange = ({ value }) => {
|
||||||
|
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||||
|
};
|
||||||
|
|
||||||
onAddMoviePress = () => {
|
onAddMoviePress = () => {
|
||||||
this.props.onAddMoviePress();
|
this.props.onAddMoviePress();
|
||||||
};
|
};
|
||||||
@@ -36,7 +43,7 @@ class AddNewMovieModalContent extends Component {
|
|||||||
isAdding,
|
isAdding,
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
monitor,
|
monitor,
|
||||||
qualityProfileIds,
|
qualityProfileId,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
searchForMovie,
|
searchForMovie,
|
||||||
folder,
|
folder,
|
||||||
@@ -111,13 +118,28 @@ class AddNewMovieModalContent extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...minimumAvailability}
|
{...minimumAvailability}
|
||||||
|
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
@@ -126,9 +148,9 @@ class AddNewMovieModalContent extends Component {
|
|||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
name="qualityProfileIds"
|
name="qualityProfileId"
|
||||||
onChange={onInputChange}
|
onChange={this.onQualityProfileIdChange}
|
||||||
{...qualityProfileIds}
|
{...qualityProfileId}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
@@ -185,7 +207,7 @@ AddNewMovieModalContent.propTypes = {
|
|||||||
addError: PropTypes.object,
|
addError: PropTypes.object,
|
||||||
rootFolderPath: PropTypes.object,
|
rootFolderPath: PropTypes.object,
|
||||||
monitor: PropTypes.object.isRequired,
|
monitor: PropTypes.object.isRequired,
|
||||||
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
|
qualityProfileId: PropTypes.object,
|
||||||
minimumAvailability: PropTypes.object.isRequired,
|
minimumAvailability: PropTypes.object.isRequired,
|
||||||
searchForMovie: PropTypes.object.isRequired,
|
searchForMovie: PropTypes.object.isRequired,
|
||||||
folder: PropTypes.string.isRequired,
|
folder: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class AddNewMovieModalContentConnector extends Component {
|
|||||||
tmdbId,
|
tmdbId,
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
monitor,
|
monitor,
|
||||||
qualityProfileIds,
|
qualityProfileId,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
searchForMovie,
|
searchForMovie,
|
||||||
tags
|
tags
|
||||||
@@ -68,7 +68,7 @@ class AddNewMovieModalContentConnector extends Component {
|
|||||||
tmdbId,
|
tmdbId,
|
||||||
rootFolderPath: rootFolderPath.value,
|
rootFolderPath: rootFolderPath.value,
|
||||||
monitor: monitor.value,
|
monitor: monitor.value,
|
||||||
qualityProfileIds: qualityProfileIds.value,
|
qualityProfileId: qualityProfileId.value,
|
||||||
minimumAvailability: minimumAvailability.value,
|
minimumAvailability: minimumAvailability.value,
|
||||||
searchForMovie: searchForMovie.value,
|
searchForMovie: searchForMovie.value,
|
||||||
tags: tags.value
|
tags: tags.value
|
||||||
@@ -93,7 +93,7 @@ AddNewMovieModalContentConnector.propTypes = {
|
|||||||
tmdbId: PropTypes.number.isRequired,
|
tmdbId: PropTypes.number.isRequired,
|
||||||
rootFolderPath: PropTypes.object,
|
rootFolderPath: PropTypes.object,
|
||||||
monitor: PropTypes.object.isRequired,
|
monitor: PropTypes.object.isRequired,
|
||||||
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
|
qualityProfileId: PropTypes.object,
|
||||||
minimumAvailability: PropTypes.object.isRequired,
|
minimumAvailability: PropTypes.object.isRequired,
|
||||||
searchForMovie: PropTypes.object.isRequired,
|
searchForMovie: PropTypes.object.isRequired,
|
||||||
tags: PropTypes.object.isRequired,
|
tags: PropTypes.object.isRequired,
|
||||||
|
|||||||
@@ -85,8 +85,14 @@
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.originalLanguage,
|
||||||
|
.studio,
|
||||||
|
.genres {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
margin-left: 8px;
|
margin-left: 5px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ interface CssExports {
|
|||||||
'certification': string;
|
'certification': string;
|
||||||
'content': string;
|
'content': string;
|
||||||
'exclusionIcon': string;
|
'exclusionIcon': string;
|
||||||
|
'genres': string;
|
||||||
'icons': string;
|
'icons': string;
|
||||||
'links': string;
|
'links': string;
|
||||||
|
'originalLanguage': string;
|
||||||
'overlay': string;
|
'overlay': string;
|
||||||
'overview': string;
|
'overview': string;
|
||||||
'poster': string;
|
'poster': string;
|
||||||
@@ -14,6 +16,7 @@ interface CssExports {
|
|||||||
'runtime': string;
|
'runtime': string;
|
||||||
'searchResult': string;
|
'searchResult': string;
|
||||||
'statusContainer': string;
|
'statusContainer': string;
|
||||||
|
'studio': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
'titleContainer': string;
|
'titleContainer': string;
|
||||||
'titleRow': string;
|
'titleRow': string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
@@ -61,34 +62,34 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
titleSlug,
|
titleSlug,
|
||||||
year,
|
year,
|
||||||
studio,
|
studio,
|
||||||
|
originalLanguage,
|
||||||
|
genres,
|
||||||
status,
|
status,
|
||||||
overview,
|
overview,
|
||||||
ratings,
|
ratings,
|
||||||
folder,
|
folder,
|
||||||
images,
|
images,
|
||||||
|
existingMovieId,
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
isExclusionMovie,
|
isExcluded,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
colorImpairedMode,
|
colorImpairedMode,
|
||||||
id,
|
id,
|
||||||
monitored,
|
monitored,
|
||||||
isAvailable,
|
isAvailable,
|
||||||
queueStatus,
|
movieFile,
|
||||||
queueState,
|
queueItem,
|
||||||
runtime,
|
runtime,
|
||||||
movieRuntimeFormat,
|
movieRuntimeFormat,
|
||||||
certification,
|
certification
|
||||||
statistics
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
|
||||||
movieFileCount
|
|
||||||
} = statistics;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isNewAddMovieModalOpen
|
isNewAddMovieModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const hasMovieFile = !!movieFile;
|
||||||
|
|
||||||
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
|
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
|
||||||
const posterWidth = 167;
|
const posterWidth = 167;
|
||||||
const posterHeight = 250;
|
const posterHeight = 250;
|
||||||
@@ -124,13 +125,13 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
{
|
{
|
||||||
isExistingMovie &&
|
isExistingMovie &&
|
||||||
<MovieIndexProgressBar
|
<MovieIndexProgressBar
|
||||||
|
movieId={existingMovieId}
|
||||||
|
movieFile={movieFile}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
hasFile={movieFileCount > 0}
|
hasFile={hasMovieFile}
|
||||||
status={status}
|
status={status}
|
||||||
width={posterWidth}
|
width={posterWidth}
|
||||||
detailedProgressBar={true}
|
detailedProgressBar={true}
|
||||||
queueStatus={queueStatus}
|
|
||||||
queueState={queueState}
|
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -154,26 +155,27 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.icons}>
|
<div className={styles.icons}>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
isExistingMovie &&
|
||||||
|
<Icon
|
||||||
|
className={styles.alreadyExistsIcon}
|
||||||
|
name={icons.CHECK_CIRCLE}
|
||||||
|
size={36}
|
||||||
|
title={translate('AlreadyInYourLibrary')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isExistingMovie &&
|
isExcluded &&
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.alreadyExistsIcon}
|
className={styles.exclusionIcon}
|
||||||
name={icons.CHECK_CIRCLE}
|
name={icons.DANGER}
|
||||||
size={36}
|
size={36}
|
||||||
title={translate('AlreadyInYourLibrary')}
|
title={translate('MovieIsOnImportExclusionList')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
{
|
|
||||||
isExclusionMovie &&
|
|
||||||
<Icon
|
|
||||||
className={styles.exclusionIcon}
|
|
||||||
name={icons.DANGER}
|
|
||||||
size={36}
|
|
||||||
title={translate('MovieIsOnImportExclusionList')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -202,10 +204,56 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{
|
{
|
||||||
!!studio &&
|
ratings.imdb ?
|
||||||
<Label size={sizes.LARGE}>
|
<Label size={sizes.LARGE}>
|
||||||
{studio}
|
<ImdbRating
|
||||||
</Label>
|
ratings={ratings}
|
||||||
|
iconSize={13}
|
||||||
|
/>
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
originalLanguage?.name ?
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<Icon
|
||||||
|
name={icons.LANGUAGE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.originalLanguage}>
|
||||||
|
{originalLanguage.name}
|
||||||
|
</span>
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
studio ?
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<Icon
|
||||||
|
name={icons.STUDIO}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.studio}>
|
||||||
|
{studio}
|
||||||
|
</span>
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
genres.length > 0 ?
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<Icon
|
||||||
|
name={icons.GENRE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.genres}>
|
||||||
|
{genres.slice(0, 3).join(', ')}
|
||||||
|
</span>
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -219,15 +267,15 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.links}>
|
<span className={styles.links}>
|
||||||
Links
|
{translate('Links')}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
tooltip={
|
tooltip={
|
||||||
<MovieDetailsLinks
|
<MovieDetailsLinks
|
||||||
tmdbId={tmdbId}
|
tmdbId={tmdbId}
|
||||||
youTubeTrailerId={youTubeTrailerId}
|
|
||||||
imdbId={imdbId}
|
imdbId={imdbId}
|
||||||
|
youTubeTrailerId={youTubeTrailerId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
canFlip={true}
|
canFlip={true}
|
||||||
@@ -238,9 +286,11 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
{
|
{
|
||||||
isExistingMovie && isSmallScreen &&
|
isExistingMovie && isSmallScreen &&
|
||||||
<MovieStatusLabel
|
<MovieStatusLabel
|
||||||
hasMovieFiles={movieFileCount > 0}
|
status={status}
|
||||||
|
hasMovieFiles={hasMovieFile}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
|
queueItem={queueItem}
|
||||||
id={id}
|
id={id}
|
||||||
useLabel={true}
|
useLabel={true}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
@@ -277,32 +327,31 @@ AddNewMovieSearchResult.propTypes = {
|
|||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
studio: PropTypes.string,
|
studio: PropTypes.string,
|
||||||
|
originalLanguage: PropTypes.object,
|
||||||
|
genres: PropTypes.arrayOf(PropTypes.string),
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
folder: PropTypes.string.isRequired,
|
folder: PropTypes.string.isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
existingMovieId: PropTypes.number,
|
||||||
isExistingMovie: PropTypes.bool.isRequired,
|
isExistingMovie: PropTypes.bool.isRequired,
|
||||||
isExclusionMovie: PropTypes.bool.isRequired,
|
isExcluded: PropTypes.bool,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
queueItems: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
|
movieFile: PropTypes.object,
|
||||||
|
queueItem: PropTypes.object,
|
||||||
colorImpairedMode: PropTypes.bool,
|
colorImpairedMode: PropTypes.bool,
|
||||||
queueStatus: PropTypes.string,
|
|
||||||
queueState: PropTypes.string,
|
|
||||||
runtime: PropTypes.number.isRequired,
|
runtime: PropTypes.number.isRequired,
|
||||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||||
certification: PropTypes.string,
|
certification: PropTypes.string
|
||||||
statistics: PropTypes.object
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AddNewMovieSearchResult.defaultProps = {
|
AddNewMovieSearchResult.defaultProps = {
|
||||||
statistics: {
|
genres: [],
|
||||||
movieFileCount: 0
|
isExcluded: false
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddNewMovieSearchResult;
|
export default AddNewMovieSearchResult;
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector';
|
|
||||||
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
|
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
|
||||||
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
|
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createExistingMovieSelector(),
|
createExistingMovieSelector(),
|
||||||
createExclusionMovieSelector(),
|
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(state) => state.queue.details.items,
|
(state) => state.queue.details.items,
|
||||||
|
(state) => state.movieFiles.items,
|
||||||
(state, { internalId }) => internalId,
|
(state, { internalId }) => internalId,
|
||||||
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId) => {
|
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||||
const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0);
|
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
||||||
|
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
||||||
|
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
existingMovieId: internalId,
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
isExclusionMovie,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
queueStatus: firstQueueItem ? firstQueueItem.status : null,
|
queueItem,
|
||||||
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
|
movieFile,
|
||||||
|
movieRuntimeFormat
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ class ImportMovieFooter extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
defaultMonitor,
|
defaultMonitor,
|
||||||
defaultQualityProfileIds,
|
defaultQualityProfileId,
|
||||||
defaultMinimumAvailability
|
defaultMinimumAvailability
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
monitor: defaultMonitor,
|
monitor: defaultMonitor,
|
||||||
qualityProfileIds: defaultQualityProfileIds,
|
qualityProfileId: defaultQualityProfileId,
|
||||||
minimumAvailability: defaultMinimumAvailability
|
minimumAvailability: defaultMinimumAvailability
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -39,16 +39,16 @@ class ImportMovieFooter extends Component {
|
|||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const {
|
const {
|
||||||
defaultMonitor,
|
defaultMonitor,
|
||||||
defaultQualityProfileIds,
|
defaultQualityProfileId,
|
||||||
defaultMinimumAvailability,
|
defaultMinimumAvailability,
|
||||||
isMonitorMixed,
|
isMonitorMixed,
|
||||||
isQualityProfileIdsMixed,
|
isQualityProfileIdMixed,
|
||||||
isMinimumAvailabilityMixed
|
isMinimumAvailabilityMixed
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
monitor,
|
monitor,
|
||||||
qualityProfileIds,
|
qualityProfileId,
|
||||||
minimumAvailability
|
minimumAvailability
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
@@ -60,10 +60,10 @@ class ImportMovieFooter extends Component {
|
|||||||
newState.monitor = defaultMonitor;
|
newState.monitor = defaultMonitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isQualityProfileIdsMixed && qualityProfileIds !== MIXED) {
|
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
|
||||||
newState.qualityProfileIds = MIXED;
|
newState.qualityProfileId = MIXED;
|
||||||
} else if (!isQualityProfileIdsMixed && qualityProfileIds !== defaultQualityProfileIds) {
|
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
|
||||||
newState.qualityProfileIds = defaultQualityProfileIds;
|
newState.qualityProfileId = defaultQualityProfileId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) {
|
if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) {
|
||||||
@@ -94,7 +94,7 @@ class ImportMovieFooter extends Component {
|
|||||||
isImporting,
|
isImporting,
|
||||||
isLookingUpMovie,
|
isLookingUpMovie,
|
||||||
isMonitorMixed,
|
isMonitorMixed,
|
||||||
isQualityProfileIdsMixed,
|
isQualityProfileIdMixed,
|
||||||
isMinimumAvailabilityMixed,
|
isMinimumAvailabilityMixed,
|
||||||
hasUnsearchedItems,
|
hasUnsearchedItems,
|
||||||
importError,
|
importError,
|
||||||
@@ -105,7 +105,7 @@ class ImportMovieFooter extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
monitor,
|
monitor,
|
||||||
qualityProfileIds,
|
qualityProfileId,
|
||||||
minimumAvailability
|
minimumAvailability
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
@@ -148,10 +148,10 @@ class ImportMovieFooter extends Component {
|
|||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
name="qualityProfileIds"
|
name="qualityProfileId"
|
||||||
value={qualityProfileIds}
|
value={qualityProfileId}
|
||||||
isDisabled={!selectedCount}
|
isDisabled={!selectedCount}
|
||||||
includeMixed={isQualityProfileIdsMixed}
|
includeMixed={isQualityProfileIdMixed}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,10 +257,10 @@ ImportMovieFooter.propTypes = {
|
|||||||
isImporting: PropTypes.bool.isRequired,
|
isImporting: PropTypes.bool.isRequired,
|
||||||
isLookingUpMovie: PropTypes.bool.isRequired,
|
isLookingUpMovie: PropTypes.bool.isRequired,
|
||||||
defaultMonitor: PropTypes.string.isRequired,
|
defaultMonitor: PropTypes.string.isRequired,
|
||||||
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
|
defaultQualityProfileId: PropTypes.number,
|
||||||
defaultMinimumAvailability: PropTypes.string,
|
defaultMinimumAvailability: PropTypes.string,
|
||||||
isMonitorMixed: PropTypes.bool.isRequired,
|
isMonitorMixed: PropTypes.bool.isRequired,
|
||||||
isQualityProfileIdsMixed: PropTypes.bool.isRequired,
|
isQualityProfileIdMixed: PropTypes.bool.isRequired,
|
||||||
isMinimumAvailabilityMixed: PropTypes.bool.isRequired,
|
isMinimumAvailabilityMixed: PropTypes.bool.isRequired,
|
||||||
hasUnsearchedItems: PropTypes.bool.isRequired,
|
hasUnsearchedItems: PropTypes.bool.isRequired,
|
||||||
importError: PropTypes.object,
|
importError: PropTypes.object,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
|||||||
(addMovie, importMovie, selectedIds) => {
|
(addMovie, importMovie, selectedIds) => {
|
||||||
const {
|
const {
|
||||||
monitor: defaultMonitor,
|
monitor: defaultMonitor,
|
||||||
qualityProfileIds: defaultQualityProfileIds,
|
qualityProfileId: defaultQualityProfileId,
|
||||||
minimumAvailability: defaultMinimumAvailability
|
minimumAvailability: defaultMinimumAvailability
|
||||||
} = addMovie.defaults;
|
} = addMovie.defaults;
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ function createMapStateToProps() {
|
|||||||
} = importMovie;
|
} = importMovie;
|
||||||
|
|
||||||
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
||||||
const isQualityProfileIdsMixed = isMixed(items, selectedIds, defaultQualityProfileIds, 'qualityProfileIds');
|
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
|
||||||
const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability');
|
const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability');
|
||||||
const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated);
|
const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated);
|
||||||
|
|
||||||
@@ -39,10 +39,10 @@ function createMapStateToProps() {
|
|||||||
isLookingUpMovie,
|
isLookingUpMovie,
|
||||||
isImporting,
|
isImporting,
|
||||||
defaultMonitor,
|
defaultMonitor,
|
||||||
defaultQualityProfileIds,
|
defaultQualityProfileId,
|
||||||
defaultMinimumAvailability,
|
defaultMinimumAvailability,
|
||||||
isMonitorMixed,
|
isMonitorMixed,
|
||||||
isQualityProfileIdsMixed,
|
isQualityProfileIdMixed,
|
||||||
isMinimumAvailabilityMixed,
|
isMinimumAvailabilityMixed,
|
||||||
importError,
|
importError,
|
||||||
hasUnsearchedItems
|
hasUnsearchedItems
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ImportMovieHeader.css';
|
import styles from './ImportMovieHeader.css';
|
||||||
|
|
||||||
@@ -46,7 +50,19 @@ function ImportMovieHeader(props) {
|
|||||||
className={styles.minimumAvailability}
|
className={styles.minimumAvailability}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
>
|
>
|
||||||
{translate('MinAvailability')}
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.detailsIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
</VirtualTableHeaderCell>
|
</VirtualTableHeaderCell>
|
||||||
|
|
||||||
<VirtualTableHeaderCell
|
<VirtualTableHeaderCell
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import styles from './ImportMovieRow.css';
|
|||||||
function ImportMovieRow(props) {
|
function ImportMovieRow(props) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
relativePath,
|
||||||
monitor,
|
monitor,
|
||||||
qualityProfileIds,
|
qualityProfileId,
|
||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
selectedMovie,
|
selectedMovie,
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
@@ -31,7 +32,7 @@ function ImportMovieRow(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.folder}>
|
<VirtualTableRowCell className={styles.folder}>
|
||||||
{id}
|
{relativePath}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.movie}>
|
<VirtualTableRowCell className={styles.movie}>
|
||||||
@@ -62,8 +63,8 @@ function ImportMovieRow(props) {
|
|||||||
<VirtualTableRowCell className={styles.qualityProfile}>
|
<VirtualTableRowCell className={styles.qualityProfile}>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
name="qualityProfileIds"
|
name="qualityProfileId"
|
||||||
value={qualityProfileIds}
|
value={qualityProfileId}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
@@ -73,8 +74,9 @@ function ImportMovieRow(props) {
|
|||||||
|
|
||||||
ImportMovieRow.propTypes = {
|
ImportMovieRow.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
relativePath: PropTypes.string.isRequired,
|
||||||
monitor: PropTypes.string.isRequired,
|
monitor: PropTypes.string.isRequired,
|
||||||
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
qualityProfileId: PropTypes.number.isRequired,
|
||||||
minimumAvailability: PropTypes.string.isRequired,
|
minimumAvailability: PropTypes.string.isRequired,
|
||||||
selectedMovie: PropTypes.object,
|
selectedMovie: PropTypes.object,
|
||||||
isExistingMovie: PropTypes.bool.isRequired,
|
isExistingMovie: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class ImportMovieTable extends Component {
|
|||||||
const {
|
const {
|
||||||
unmappedFolders,
|
unmappedFolders,
|
||||||
defaultMonitor,
|
defaultMonitor,
|
||||||
defaultQualityProfileIds,
|
defaultQualityProfileId,
|
||||||
defaultMinimumAvailability,
|
defaultMinimumAvailability,
|
||||||
onMovieLookup,
|
onMovieLookup,
|
||||||
onSetImportMovieValue
|
onSetImportMovieValue
|
||||||
@@ -23,14 +23,14 @@ class ImportMovieTable extends Component {
|
|||||||
|
|
||||||
const values = {
|
const values = {
|
||||||
monitor: defaultMonitor,
|
monitor: defaultMonitor,
|
||||||
qualityProfileIds: defaultQualityProfileIds,
|
qualityProfileId: defaultQualityProfileId,
|
||||||
minimumAvailability: defaultMinimumAvailability
|
minimumAvailability: defaultMinimumAvailability
|
||||||
};
|
};
|
||||||
|
|
||||||
unmappedFolders.forEach((unmappedFolder) => {
|
unmappedFolders.forEach((unmappedFolder) => {
|
||||||
const id = unmappedFolder.name;
|
const id = unmappedFolder.name;
|
||||||
|
|
||||||
onMovieLookup(id, unmappedFolder.path);
|
onMovieLookup(id, unmappedFolder.path, unmappedFolder.relativePath);
|
||||||
|
|
||||||
onSetImportMovieValue({
|
onSetImportMovieValue({
|
||||||
id,
|
id,
|
||||||
@@ -167,7 +167,7 @@ ImportMovieTable.propTypes = {
|
|||||||
items: PropTypes.arrayOf(PropTypes.object),
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||||
defaultMonitor: PropTypes.string.isRequired,
|
defaultMonitor: PropTypes.string.isRequired,
|
||||||
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
|
defaultQualityProfileId: PropTypes.number,
|
||||||
defaultMinimumAvailability: PropTypes.string,
|
defaultMinimumAvailability: PropTypes.string,
|
||||||
allSelected: PropTypes.bool.isRequired,
|
allSelected: PropTypes.bool.isRequired,
|
||||||
allUnselected: PropTypes.bool.isRequired,
|
allUnselected: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function createMapStateToProps() {
|
|||||||
(addMovie, importMovie, dimensions, allMovies) => {
|
(addMovie, importMovie, dimensions, allMovies) => {
|
||||||
return {
|
return {
|
||||||
defaultMonitor: addMovie.defaults.monitor,
|
defaultMonitor: addMovie.defaults.monitor,
|
||||||
defaultQualityProfileIds: addMovie.defaults.qualityProfileIds,
|
defaultQualityProfileId: addMovie.defaults.qualityProfileId,
|
||||||
defaultMinimumAvailability: addMovie.defaults.minimumAvailability,
|
defaultMinimumAvailability: addMovie.defaults.minimumAvailability,
|
||||||
items: importMovie.items,
|
items: importMovie.items,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
@@ -25,10 +25,11 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
return {
|
return {
|
||||||
onMovieLookup(name, path) {
|
onMovieLookup(name, path, relativePath) {
|
||||||
dispatch(queueLookupMovie({
|
dispatch(queueLookupMovie({
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
|
relativePath,
|
||||||
term: name
|
term: name
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
.contentContainer {
|
.contentContainer {
|
||||||
z-index: $popperZIndex;
|
z-index: $popperZIndex;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
/* 400px container witdh with 8px padding on each side */
|
/* 400px container width with 8px padding on each side */
|
||||||
width: 384px;
|
width: 384px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import FormInputButton from 'Components/Form/FormInputButton';
|
|||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Portal from 'Components/Portal';
|
import Portal from 'Components/Portal';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
@@ -243,7 +242,7 @@ class ImportMovieSelectMovie extends Component {
|
|||||||
<FormInputButton
|
<FormInputButton
|
||||||
kind={kinds.DEFAULT}
|
kind={kinds.DEFAULT}
|
||||||
spinnerIcon={icons.REFRESH}
|
spinnerIcon={icons.REFRESH}
|
||||||
ButtonComponent={SpinnerButton}
|
canSpin={true}
|
||||||
isSpinning={isFetching}
|
isSpinning={isFetching}
|
||||||
onPress={this.onRefreshPress}
|
onPress={this.onRefreshPress}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
|
|||||||
className={styles.addErrorAlert}
|
className={styles.addErrorAlert}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
>
|
>
|
||||||
{translate('UnableToAddRootFolder')}
|
{translate('AddRootFolderError')}
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
|
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.rootFolders,
|
createRootFoldersSelector(),
|
||||||
createSystemStatusSelector(),
|
createSystemStatusSelector(),
|
||||||
(rootFolders, systemStatus) => {
|
(rootFolders, systemStatus) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function MovieMinimumAvailabilityPopoverContent() {
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Announced')}
|
||||||
|
data={translate('AnnouncedMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('InCinemas')}
|
||||||
|
data={translate('InCinemasMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Released')}
|
||||||
|
data={translate('ReleasedMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieMinimumAvailabilityPopoverContent;
|
||||||
@@ -12,11 +12,10 @@ function App({ store, history }) {
|
|||||||
<DocumentTitle title={window.Radarr.instanceName}>
|
<DocumentTitle title={window.Radarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<ApplyTheme>
|
<ApplyTheme />
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
<AppRoutes app={App} />
|
<AppRoutes app={App} />
|
||||||
</PageConnector>
|
</PageConnector>
|
||||||
</ApplyTheme>
|
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
import Blocklist from 'Activity/Blocklist/Blocklist';
|
||||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
import Queue from 'Activity/Queue/Queue';
|
||||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||||
@@ -33,6 +33,8 @@ import Status from 'System/Status/Status';
|
|||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||||
|
|
||||||
function AppRoutes(props) {
|
function AppRoutes(props) {
|
||||||
const {
|
const {
|
||||||
@@ -113,12 +115,26 @@ function AppRoutes(props) {
|
|||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/activity/queue"
|
path="/activity/queue"
|
||||||
component={QueueConnector}
|
component={Queue}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/activity/blocklist"
|
path="/activity/blocklist"
|
||||||
component={BlocklistConnector}
|
component={Blocklist}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Wanted
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/wanted/missing"
|
||||||
|
component={MissingConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/wanted/cutoffunmet"
|
||||||
|
component={CutoffUnmetConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
|
|||||||
@@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('AppUpdated', { appName: 'Radarr' })}
|
{translate('AppUpdated')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Radarr', version })} blockClassName={styles.version} />
|
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import themes from 'Styles/Themes';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.ui.item.theme || window.Radarr.theme,
|
|
||||||
(
|
|
||||||
theme
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
theme
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApplyTheme({ theme, children }) {
|
|
||||||
// Update the CSS Variables
|
|
||||||
const updateCSSVariables = useCallback(() => {
|
|
||||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
|
||||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
|
||||||
|
|
||||||
// Loop through each array key and set the CSS Variables
|
|
||||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
|
||||||
// Based on our snippet from MDN
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
`--${cssVariableKey}`,
|
|
||||||
arrayOfVariableValues[index]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
// On Component Mount and Component Update
|
|
||||||
useEffect(() => {
|
|
||||||
updateCSSVariables(theme);
|
|
||||||
}, [updateCSSVariables, theme]);
|
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyTheme.propTypes = {
|
|
||||||
theme: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(ApplyTheme);
|
|
||||||
33
frontend/src/App/ApplyTheme.tsx
Normal file
33
frontend/src/App/ApplyTheme.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import themes from 'Styles/Themes';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
|
||||||
|
function createThemeSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.ui.item.theme || window.Radarr.theme,
|
||||||
|
(theme) => {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplyTheme() {
|
||||||
|
const theme = useSelector(createThemeSelector());
|
||||||
|
|
||||||
|
const updateCSSVariables = useCallback(() => {
|
||||||
|
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||||
|
document.documentElement.style.setProperty(`--${key}`, value);
|
||||||
|
});
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
// On Component Mount and Component Update
|
||||||
|
useEffect(() => {
|
||||||
|
updateCSSVariables();
|
||||||
|
}, [updateCSSVariables, theme]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApplyTheme;
|
||||||
@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
{translate('ConnectionLostToBackend', { appName: 'Radarr' })}
|
{translate('ConnectionLostToBackend')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.automatic}>
|
<div className={styles.automatic}>
|
||||||
{translate('ConnectionLostReconnect', { appName: 'Radarr' })}
|
{translate('ConnectionLostReconnect')}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import Column from 'Components/Table/Column';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
responseJSON: {
|
||||||
@@ -17,7 +19,19 @@ export interface AppSectionSaveState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PagedAppSectionState {
|
export interface PagedAppSectionState {
|
||||||
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalRecords?: number;
|
||||||
|
}
|
||||||
|
export interface TableAppSectionState {
|
||||||
|
columns: Column[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSectionFilterState<T> {
|
||||||
|
selectedFilterKey: string;
|
||||||
|
filters: PropertyFilter[];
|
||||||
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionSchemaState<T> {
|
export interface AppSectionSchemaState<T> {
|
||||||
@@ -33,6 +47,7 @@ export interface AppSectionItemState<T> {
|
|||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
error: Error;
|
error: Error;
|
||||||
|
pendingChanges: Partial<T>;
|
||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
|
import HistoryAppState from './HistoryAppState';
|
||||||
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
|
import MovieCreditAppState from './MovieCreditAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
@@ -43,11 +46,26 @@ export interface CustomFilter {
|
|||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionState {
|
||||||
|
isConnected: boolean;
|
||||||
|
isReconnecting: boolean;
|
||||||
|
version: string;
|
||||||
|
dimensions: {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
app: AppSectionState;
|
||||||
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
movieCollections: MovieCollectionAppState;
|
movieCollections: MovieCollectionAppState;
|
||||||
|
movieCredits: MovieCreditAppState;
|
||||||
movieFiles: MovieFilesAppState;
|
movieFiles: MovieFilesAppState;
|
||||||
movieIndex: MovieIndexAppState;
|
movieIndex: MovieIndexAppState;
|
||||||
movies: MoviesAppState;
|
movies: MoviesAppState;
|
||||||
|
|||||||
16
frontend/src/App/State/BlocklistAppState.ts
Normal file
16
frontend/src/App/State/BlocklistAppState.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import Blocklist from 'typings/Blocklist';
|
||||||
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
|
} from './AppSectionState';
|
||||||
|
|
||||||
|
interface BlocklistAppState
|
||||||
|
extends AppSectionState<Blocklist>,
|
||||||
|
AppSectionFilterState<Blocklist>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {
|
||||||
|
isRemoving: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlocklistAppState;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
import { FilterBuilderProp } from './AppState';
|
|
||||||
|
|
||||||
interface CalendarAppState extends AppSectionState<Movie> {
|
interface CalendarAppState
|
||||||
filterBuilderProps: FilterBuilderProp<Movie>[];
|
extends AppSectionState<Movie>,
|
||||||
}
|
AppSectionFilterState<Movie> {}
|
||||||
|
|
||||||
export default CalendarAppState;
|
export default CalendarAppState;
|
||||||
|
|||||||
10
frontend/src/App/State/HistoryAppState.ts
Normal file
10
frontend/src/App/State/HistoryAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import History from 'typings/History';
|
||||||
|
|
||||||
|
interface HistoryAppState
|
||||||
|
extends AppSectionState<History>,
|
||||||
|
AppSectionFilterState<History> {}
|
||||||
|
|
||||||
|
export default HistoryAppState;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
import MovieCollection from 'typings/MovieCollection';
|
import MovieCollection from 'typings/MovieCollection';
|
||||||
|
|
||||||
type MovieCollectionAppState = AppSectionState<MovieCollection>;
|
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
|
||||||
|
itemMap: Record<number, number>;
|
||||||
|
}
|
||||||
|
|
||||||
export default MovieCollectionAppState;
|
export default MovieCollectionAppState;
|
||||||
|
|||||||
6
frontend/src/App/State/MovieCreditAppState.ts
Normal file
6
frontend/src/App/State/MovieCreditAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
|
||||||
|
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
|
||||||
|
|
||||||
|
export default MovieCreditAppState;
|
||||||
@@ -20,11 +20,15 @@ export interface MovieIndexAppState {
|
|||||||
showTitle: boolean;
|
showTitle: boolean;
|
||||||
showMonitored: boolean;
|
showMonitored: boolean;
|
||||||
showQualityProfile: boolean;
|
showQualityProfile: boolean;
|
||||||
showReleaseDate: boolean;
|
|
||||||
showCinemaRelease: boolean;
|
showCinemaRelease: boolean;
|
||||||
|
showDigitalRelease: boolean;
|
||||||
|
showPhysicalRelease: boolean;
|
||||||
|
showReleaseDate: boolean;
|
||||||
showTmdbRating: boolean;
|
showTmdbRating: boolean;
|
||||||
showImdbRating: boolean;
|
showImdbRating: boolean;
|
||||||
showRottenTomatoesRating: boolean;
|
showRottenTomatoesRating: boolean;
|
||||||
|
showTraktRating: boolean;
|
||||||
|
showTags: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,6 +41,7 @@ export interface MovieIndexAppState {
|
|||||||
showAdded: boolean;
|
showAdded: boolean;
|
||||||
showPath: boolean;
|
showPath: boolean;
|
||||||
showSizeOnDisk: boolean;
|
showSizeOnDisk: boolean;
|
||||||
|
showTags: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,31 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import Queue from 'typings/Queue';
|
||||||
import Language from 'Language/Language';
|
import AppSectionState, {
|
||||||
import { QualityModel } from 'Quality/Quality';
|
AppSectionFilterState,
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
AppSectionItemState,
|
||||||
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
|
Error,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
|
} from './AppSectionState';
|
||||||
|
|
||||||
export interface StatusMessage {
|
export interface QueueStatus {
|
||||||
title: string;
|
totalCount: number;
|
||||||
messages: string[];
|
count: number;
|
||||||
}
|
unknownCount: number;
|
||||||
|
errors: boolean;
|
||||||
export interface Queue extends ModelBase {
|
warnings: boolean;
|
||||||
languages: Language[];
|
unknownErrors: boolean;
|
||||||
quality: QualityModel;
|
unknownWarnings: boolean;
|
||||||
customFormats: CustomFormat[];
|
|
||||||
size: number;
|
|
||||||
title: string;
|
|
||||||
sizeleft: number;
|
|
||||||
timeleft: string;
|
|
||||||
estimatedCompletionTime: string;
|
|
||||||
status: string;
|
|
||||||
trackedDownloadStatus: string;
|
|
||||||
trackedDownloadState: string;
|
|
||||||
statusMessages: StatusMessage[];
|
|
||||||
errorMessage: string;
|
|
||||||
downloadId: string;
|
|
||||||
protocol: string;
|
|
||||||
downloadClient: string;
|
|
||||||
outputPath: string;
|
|
||||||
movieHasFile: boolean;
|
|
||||||
movieId?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||||
params: unknown;
|
params: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueuePagedAppState extends AppSectionState<Queue> {
|
export interface QueuePagedAppState
|
||||||
|
extends AppSectionState<Queue>,
|
||||||
|
AppSectionFilterState<Queue>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {
|
||||||
isGrabbing: boolean;
|
isGrabbing: boolean;
|
||||||
grabError: Error;
|
grabError: Error;
|
||||||
isRemoving: boolean;
|
isRemoving: boolean;
|
||||||
@@ -43,9 +33,12 @@ export interface QueuePagedAppState extends AppSectionState<Queue> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface QueueAppState {
|
interface QueueAppState {
|
||||||
status: AppSectionItemState<Queue>;
|
status: AppSectionItemState<QueueStatus>;
|
||||||
details: QueueDetailsAppState;
|
details: QueueDetailsAppState;
|
||||||
paged: QueuePagedAppState;
|
paged: QueuePagedAppState;
|
||||||
|
options: {
|
||||||
|
includeUnknownMovieItems: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default QueueAppState;
|
export default QueueAppState;
|
||||||
|
|||||||
@@ -3,21 +3,42 @@ import AppSectionState, {
|
|||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
|
PagedAppSectionState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
|
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||||
|
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
import IndexerFlag from 'typings/IndexerFlag';
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import General from 'typings/Settings/General';
|
||||||
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {
|
||||||
|
isTestingAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneralAppState
|
||||||
|
extends AppSectionItemState<General>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NamingAppState
|
||||||
|
extends AppSectionItemState<NamingConfig>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NamingExamplesAppState
|
||||||
|
extends AppSectionItemState<NamingExample> {}
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -26,7 +47,9 @@ export interface ImportListAppState
|
|||||||
export interface IndexerAppState
|
export interface IndexerAppState
|
||||||
extends AppSectionState<Indexer>,
|
extends AppSectionState<Indexer>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {
|
||||||
|
isTestingAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotificationAppState
|
export interface NotificationAppState
|
||||||
extends AppSectionState<Notification>,
|
extends AppSectionState<Notification>,
|
||||||
@@ -36,18 +59,49 @@ export interface QualityProfilesAppState
|
|||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
|
export interface ReleaseProfilesAppState
|
||||||
|
extends AppSectionState<ReleaseProfile>,
|
||||||
|
AppSectionSaveState {
|
||||||
|
pendingChanges: Partial<ReleaseProfile>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomFormatAppState
|
||||||
|
extends AppSectionState<CustomFormat>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface ImportListOptionsSettingsAppState
|
||||||
|
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface ImportListExclusionsSettingsAppState
|
||||||
|
extends AppSectionState<ImportListExclusion>,
|
||||||
|
AppSectionSaveState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
AppSectionDeleteState {
|
||||||
|
pendingChanges: Partial<ImportListExclusion>;
|
||||||
|
}
|
||||||
|
|
||||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
|
advancedSettings: boolean;
|
||||||
|
customFormats: CustomFormatAppState;
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
|
general: GeneralAppState;
|
||||||
|
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||||
|
importListOptions: ImportListOptionsSettingsAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
|
naming: NamingAppState;
|
||||||
|
namingExamples: NamingExamplesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
qualityProfiles: QualityProfilesAppState;
|
qualityProfiles: QualityProfilesAppState;
|
||||||
|
releaseProfiles: ReleaseProfilesAppState;
|
||||||
ui: UiSettingsAppState;
|
ui: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
|
import DiskSpace from 'typings/DiskSpace';
|
||||||
|
import Health from 'typings/Health';
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import { AppSectionItemState } from './AppSectionState';
|
import Task from 'typings/Task';
|
||||||
|
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||||
|
|
||||||
|
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
||||||
|
export type HealthAppState = AppSectionState<Health>;
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
|
export type TaskAppState = AppSectionState<Task>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
|
diskSpace: DiskSpaceAppState;
|
||||||
|
health: HealthAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
|
tasks: TaskAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemAppState;
|
export default SystemAppState;
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ function Agenda(props) {
|
|||||||
<div className={styles.agenda}>
|
<div className={styles.agenda}>
|
||||||
{
|
{
|
||||||
items.map((item, index) => {
|
items.map((item, index) => {
|
||||||
const momentDate = moment(item.inCinemas);
|
const momentDate = moment(item.sortDate);
|
||||||
const showDate = index === 0 ||
|
const showDate = index === 0 ||
|
||||||
!moment(items[index - 1].inCinemas).isSame(momentDate, 'day');
|
!moment(items[index - 1].sortDate).isSame(momentDate, 'day');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgendaEventConnector
|
<AgendaEventConnector
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.event {
|
.overlay {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,5 +111,4 @@
|
|||||||
.releaseIcon {
|
.releaseIcon {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
width: 25px;
|
width: 25px;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import moment from 'moment';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
||||||
|
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AgendaEvent.css';
|
import styles from './AgendaEvent.css';
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ class AgendaEvent extends Component {
|
|||||||
startTime = moment(startTime);
|
startTime = moment(startTime);
|
||||||
const downloading = !!(queueItem || grabbed);
|
const downloading = !!(queueItem || grabbed);
|
||||||
const isMonitored = monitored;
|
const isMonitored = monitored;
|
||||||
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', downloading);
|
const statusStyle = getStatusStyle(hasFile, downloading, isMonitored, isAvailable);
|
||||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||||
const link = `/movie/${titleSlug}`;
|
const link = `/movie/${titleSlug}`;
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ class AgendaEvent extends Component {
|
|||||||
|
|
||||||
<div className={styles.overlay}>
|
<div className={styles.overlay}>
|
||||||
<div className={styles.date}>
|
<div className={styles.date}>
|
||||||
{(showDate) ? startTime.format(longDateFormat) : null}
|
{showDate ? startTime.format(longDateFormat) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.releaseIcon}>
|
<div className={styles.releaseIcon}>
|
||||||
@@ -147,7 +147,7 @@ class AgendaEvent extends Component {
|
|||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.MOVIE_FILE}
|
name={icons.MOVIE_FILE}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
title={translate('QualityCutoffHasNotBeenMet')}
|
title={translate('QualityCutoffNotMet')}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user