mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-06 13:30:39 -05:00
Compare commits
847 Commits
test-rebas
...
notifiarr-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09d423848d | ||
|
|
e69371deca | ||
|
|
5ae8deb9d6 | ||
|
|
291674fc18 | ||
|
|
774cd04d32 | ||
|
|
3eb0533b17 | ||
|
|
2fe11ca1a9 | ||
|
|
c8ae6b0299 | ||
|
|
2086ae1e2c | ||
|
|
2aeec97d5e | ||
|
|
45e9d14916 | ||
|
|
ded45a53f3 | ||
|
|
d5cbb8f84f | ||
|
|
cac0fca0d2 | ||
|
|
9b5a050c40 | ||
|
|
9fa67dbe5f | ||
|
|
505ef5ee63 | ||
|
|
87a2d7382e | ||
|
|
7c74a8df01 | ||
|
|
f8324b3c92 | ||
|
|
cb03f3bf6b | ||
|
|
d6f85ec4f9 | ||
|
|
dd0dd921b0 | ||
|
|
1f02423148 | ||
|
|
7a4fba851f | ||
|
|
2d5efc268f | ||
|
|
cad5762f31 | ||
|
|
bcc8370d05 | ||
|
|
3481168df5 | ||
|
|
337a30ac0f | ||
|
|
eaad4de4dc | ||
|
|
b98ae37abf | ||
|
|
5dac84f9d8 | ||
|
|
ef21c73619 | ||
|
|
14d74f2eca | ||
|
|
c3cbbb7627 | ||
|
|
ad3a58c422 | ||
|
|
347b154882 | ||
|
|
63ccc155d6 | ||
|
|
76613316fa | ||
|
|
f33e9f2bbc | ||
|
|
e4a3d7b273 | ||
|
|
46c2e0ba82 | ||
|
|
8616373f96 | ||
|
|
b3f99d8c20 | ||
|
|
c0c0847963 | ||
|
|
c92f9d03c0 | ||
|
|
f7bf1e243d | ||
|
|
20b1f41ac1 | ||
|
|
f703db1e00 | ||
|
|
20f67c8035 | ||
|
|
56ae497bfa | ||
|
|
e760dc56c6 | ||
|
|
7ad26b386c | ||
|
|
199085249f | ||
|
|
b84041d95f | ||
|
|
615facb3c4 | ||
|
|
b4fd1340c2 | ||
|
|
1da27b0978 | ||
|
|
b0a3ddef9c | ||
|
|
a59706ceb4 | ||
|
|
ce58e6ecdb | ||
|
|
d01ce8b908 | ||
|
|
575f995916 | ||
|
|
c17c8815ef | ||
|
|
8445941510 | ||
|
|
32ae18ae6f | ||
|
|
b91ec241a6 | ||
|
|
17ce8187fd | ||
|
|
41615fe026 | ||
|
|
9cff8f31e9 | ||
|
|
21538b972d | ||
|
|
5a7b4d41d8 | ||
|
|
16f67265c9 | ||
|
|
03d2a85821 | ||
|
|
fa68a9559f | ||
|
|
c4a2a432d2 | ||
|
|
0b3eda4de3 | ||
|
|
a1d4b3a0cf | ||
|
|
28f055e838 | ||
|
|
8d91fb81bd | ||
|
|
085913a852 | ||
|
|
8599fc4ee6 | ||
|
|
68f017cd84 | ||
|
|
f34171444e | ||
|
|
10760471e0 | ||
|
|
4ecabcd3d6 | ||
|
|
1af2e14474 | ||
|
|
fe836c56ee | ||
|
|
ae611cca74 | ||
|
|
c446f9835e | ||
|
|
2ff8bf204f | ||
|
|
5aa17e7a67 | ||
|
|
312cf2d364 | ||
|
|
a9852ded2f | ||
|
|
4286f546e9 | ||
|
|
482fe04161 | ||
|
|
cbdc2c51c4 | ||
|
|
f55fda9bac | ||
|
|
3841f7708e | ||
|
|
f2bfed0252 | ||
|
|
bc7e4ea622 | ||
|
|
38f16e6b85 | ||
|
|
af57024c63 | ||
|
|
54faa58a4d | ||
|
|
8ca35a709c | ||
|
|
72a0e38b36 | ||
|
|
b25f8449a2 | ||
|
|
cf0a15a308 | ||
|
|
a677098f0f | ||
|
|
a40cce9c71 | ||
|
|
6d94136273 | ||
|
|
f6d620f969 | ||
|
|
8d9302da77 | ||
|
|
f5cbab6ac9 | ||
|
|
cb0ae36033 | ||
|
|
93a42e2fe9 | ||
|
|
0f9ab46dd6 | ||
|
|
3a360d7a1e | ||
|
|
8e37aa2e78 | ||
|
|
000022a927 | ||
|
|
2bfc50b59b | ||
|
|
4f46ab9a9a | ||
|
|
e7410959fe | ||
|
|
ee1112026a | ||
|
|
65681cad10 | ||
|
|
9653f9bbca | ||
|
|
6f97ca9a55 | ||
|
|
c0e193dd1f | ||
|
|
ecf1e1a130 | ||
|
|
967dae5132 | ||
|
|
f4b96dc45e | ||
|
|
8181749629 | ||
|
|
9bca258541 | ||
|
|
77d2db3c47 | ||
|
|
1dd61e826b | ||
|
|
857a5f5005 | ||
|
|
a321288a71 | ||
|
|
62a3355546 | ||
|
|
6db135877a | ||
|
|
fa1985509d | ||
|
|
01a5c456c4 | ||
|
|
3ec451e85e | ||
|
|
bf7fc6fa19 | ||
|
|
cbf2f0f4fb | ||
|
|
565fb77713 | ||
|
|
3ea0c8e5fa | ||
|
|
61454955be | ||
|
|
34506025ac | ||
|
|
ef35efb127 | ||
|
|
2a4f681b17 | ||
|
|
24e2ff56dd | ||
|
|
c5787b0c87 | ||
|
|
d6ba6bbfd1 | ||
|
|
de72cfcaaa | ||
|
|
90d671d68d | ||
|
|
b4b14a5359 | ||
|
|
759c132797 | ||
|
|
dc1fbb3a7e | ||
|
|
eb431f09fd | ||
|
|
0031214fb3 | ||
|
|
94adb4d582 | ||
|
|
bc3764dcda | ||
|
|
64fafc599c | ||
|
|
53f66606bc | ||
|
|
706f8310bb | ||
|
|
ae4994571a | ||
|
|
258847a83b | ||
|
|
43f1d77b9f | ||
|
|
f451796cf5 | ||
|
|
620fb57b30 | ||
|
|
bca70e931f | ||
|
|
c7f0a8578a | ||
|
|
c92563ea10 | ||
|
|
66e7ce6f27 | ||
|
|
b1da9a1934 | ||
|
|
1491788081 | ||
|
|
33e1c4a537 | ||
|
|
8c3dd3730a | ||
|
|
02927dc37d | ||
|
|
be46d5ae8f | ||
|
|
e4fdf71eee | ||
|
|
9a2260a00f | ||
|
|
399a370e7a | ||
|
|
ff27ad2096 | ||
|
|
76bcf94f1d | ||
|
|
472da10149 | ||
|
|
8011112919 | ||
|
|
6d8adec7dc | ||
|
|
29c7404185 | ||
|
|
f6ff53ca31 | ||
|
|
2dff18490e | ||
|
|
6d7657e8b3 | ||
|
|
0db6cf272f | ||
|
|
5827644300 | ||
|
|
d1f1052d7d | ||
|
|
341d64ebf7 | ||
|
|
c817d6c0d8 | ||
|
|
735fceb074 | ||
|
|
0e43f67a9f | ||
|
|
eb95fe265a | ||
|
|
3e32f7d49f | ||
|
|
4b8c739b5c | ||
|
|
eeb172d3bf | ||
|
|
e2745c5956 | ||
|
|
ede981f737 | ||
|
|
e2c1d76516 | ||
|
|
05a8a3d764 | ||
|
|
da262f3d95 | ||
|
|
a5b1711827 | ||
|
|
7b0802cfd6 | ||
|
|
16fcba02ba | ||
|
|
b3c217d713 | ||
|
|
975ff0baf3 | ||
|
|
0bb8672119 | ||
|
|
9ebeee8b4f | ||
|
|
894adbe91e | ||
|
|
e7628476f7 | ||
|
|
3e5045e496 | ||
|
|
95d93dfa09 | ||
|
|
842f80d567 | ||
|
|
fc8b6014ec | ||
|
|
6fb24128f8 | ||
|
|
8dbd006978 | ||
|
|
fed67044f0 | ||
|
|
f7c05d98a9 | ||
|
|
25b37ace34 | ||
|
|
e1eb9a0ba7 | ||
|
|
b6055c3339 | ||
|
|
0e78d8d535 | ||
|
|
0cf7953cab | ||
|
|
0d3cd24fb2 | ||
|
|
41d7a1f355 | ||
|
|
608c0f268f | ||
|
|
8c38e4ede8 | ||
|
|
4f791f965a | ||
|
|
0b0bebf3aa | ||
|
|
05a0508d78 | ||
|
|
6cecf23b88 | ||
|
|
fb0128e3be | ||
|
|
6947e6e34c | ||
|
|
c51ae664aa | ||
|
|
1d694af98e | ||
|
|
bc678e1976 | ||
|
|
c0db20a860 | ||
|
|
621bb20756 | ||
|
|
326d395da4 | ||
|
|
7be2effa05 | ||
|
|
8f965a7cd5 | ||
|
|
15dee29057 | ||
|
|
61617ff5bc | ||
|
|
27e3b5e630 | ||
|
|
1b6694739e | ||
|
|
a7c0eabb56 | ||
|
|
7c5188638f | ||
|
|
4ce405728a | ||
|
|
615acdaebe | ||
|
|
d460cbf319 | ||
|
|
4887ed0d2f | ||
|
|
bf852cadbe | ||
|
|
05d24821f7 | ||
|
|
04e575903f | ||
|
|
dfb9558868 | ||
|
|
808fb5f2aa | ||
|
|
9f62ee34f1 | ||
|
|
847a9eae93 | ||
|
|
a9e181425b | ||
|
|
357245a9eb | ||
|
|
8280908c39 | ||
|
|
b05bd685bc | ||
|
|
33e5351add | ||
|
|
a1c2986af8 | ||
|
|
2983b60026 | ||
|
|
0d9e98cf51 | ||
|
|
5d63ef01b6 | ||
|
|
0bb391235f | ||
|
|
c4d02c1666 | ||
|
|
7c6adb0da7 | ||
|
|
8f2ae2beff | ||
|
|
f53a1d7942 | ||
|
|
cef15887a4 | ||
|
|
5f946c0aa3 | ||
|
|
7528ed4084 | ||
|
|
e8ddaf6ccf | ||
|
|
c5281d04f1 | ||
|
|
ec0e930a54 | ||
|
|
fb7ec5c61e | ||
|
|
cb2bd0273f | ||
|
|
d6e55e2913 | ||
|
|
a3a914adfe | ||
|
|
1835f15460 | ||
|
|
d1caed5c7d | ||
|
|
6bdfe01fbc | ||
|
|
225f0b310a | ||
|
|
6a0c99d32b | ||
|
|
04cf0594f6 | ||
|
|
2001035257 | ||
|
|
355dcaed6c | ||
|
|
3d3292fd3d | ||
|
|
78d073a0a8 | ||
|
|
ca8f768f84 | ||
|
|
b57ba3d6e2 | ||
|
|
d48f2a1cb6 | ||
|
|
d8e4d388dc | ||
|
|
47ae2032e5 | ||
|
|
89319f9833 | ||
|
|
07829a19b8 | ||
|
|
27259c1ebd | ||
|
|
d14a64caf9 | ||
|
|
af2e743994 | ||
|
|
398ab902dc | ||
|
|
007edcba9e | ||
|
|
26b0034bbb | ||
|
|
e842137385 | ||
|
|
294346db35 | ||
|
|
859dd091f3 | ||
|
|
72ae466892 | ||
|
|
12c67891fb | ||
|
|
ffc97d8489 | ||
|
|
bdcee8c7c1 | ||
|
|
6325432a34 | ||
|
|
1e82c7a67a | ||
|
|
8b19f02ade | ||
|
|
3c4c6b855e | ||
|
|
9d78e5bfd8 | ||
|
|
0e5f45a457 | ||
|
|
2317665f33 | ||
|
|
73950ca431 | ||
|
|
a746a9a4b1 | ||
|
|
7a969a63e4 | ||
|
|
7c10f3a74d | ||
|
|
8edadbbfa2 | ||
|
|
dc2de62b03 | ||
|
|
cdf8b0bc8f | ||
|
|
17535bd8d6 | ||
|
|
5f1be9e447 | ||
|
|
c9cb0a9774 | ||
|
|
ba9f618405 | ||
|
|
468ebc3307 | ||
|
|
2558660b7b | ||
|
|
a774cf0682 | ||
|
|
924e393d1a | ||
|
|
98cc8f0a4b | ||
|
|
665d87b8d7 | ||
|
|
7c662d4628 | ||
|
|
d5199ebcd7 | ||
|
|
466876da62 | ||
|
|
109fd22d1f | ||
|
|
001e24aaae | ||
|
|
bbff60a2b0 | ||
|
|
4b88cc5ea5 | ||
|
|
cf49404e13 | ||
|
|
8a074c61f0 | ||
|
|
8dd6049603 | ||
|
|
9249f0ca5d | ||
|
|
4712fedb0e | ||
|
|
d8d09c2517 | ||
|
|
59c1bf0b1f | ||
|
|
1571ef4172 | ||
|
|
60866f4af6 | ||
|
|
4541d3d3b0 | ||
|
|
9150f6889f | ||
|
|
474699b67d | ||
|
|
2a05112d7d | ||
|
|
c1a846dd2b | ||
|
|
1f22cae4f8 | ||
|
|
b1e92e7f73 | ||
|
|
85e945430b | ||
|
|
2e5a9b8dd4 | ||
|
|
fcd5005502 | ||
|
|
331ef56e9a | ||
|
|
19549098a7 | ||
|
|
848b183ab8 | ||
|
|
6adadc0ade | ||
|
|
9ee1b5482a | ||
|
|
ae88bc30e1 | ||
|
|
af9f8a9a18 | ||
|
|
71a34c7650 | ||
|
|
7f8dc3d2b4 | ||
|
|
332997aefe | ||
|
|
0a20188508 | ||
|
|
493ce1b20c | ||
|
|
b3dd116d27 | ||
|
|
ca7ba125d2 | ||
|
|
1a3bb381b1 | ||
|
|
8d6e0c6bc3 | ||
|
|
6cb2bf6b5f | ||
|
|
e791f22333 | ||
|
|
eaa1578c65 | ||
|
|
e0236e781a | ||
|
|
92a9b5b451 | ||
|
|
226ad22748 | ||
|
|
fe942524a2 | ||
|
|
8917b61ea9 | ||
|
|
60a49e3a03 | ||
|
|
b8a3f09891 | ||
|
|
6e7f0deef5 | ||
|
|
4f986de366 | ||
|
|
4bf658e239 | ||
|
|
45d0e20608 | ||
|
|
438b75e8cf | ||
|
|
d590769607 | ||
|
|
104d65b4ea | ||
|
|
25cb29e325 | ||
|
|
d3f9474eab | ||
|
|
d5a74e7064 | ||
|
|
80ba57d4b5 | ||
|
|
9fe1663267 | ||
|
|
0d860de88a | ||
|
|
38a6553654 | ||
|
|
916391edc3 | ||
|
|
ac36675c6e | ||
|
|
d76f8e715d | ||
|
|
27e799b624 | ||
|
|
d34e588588 | ||
|
|
1be94da583 | ||
|
|
4066aa1472 | ||
|
|
050dd67972 | ||
|
|
ed1935c85d | ||
|
|
b625bd00ea | ||
|
|
a7c71c2093 | ||
|
|
a6774eed6e | ||
|
|
a1f740646f | ||
|
|
759b78e816 | ||
|
|
740e0edc04 | ||
|
|
46fa7e80a0 | ||
|
|
45aa744b0f | ||
|
|
3e72770ec5 | ||
|
|
529ffa8864 | ||
|
|
99a2e1f0fa | ||
|
|
9ddd08b0de | ||
|
|
79dfd8f14d | ||
|
|
db885a5111 | ||
|
|
b23e225271 | ||
|
|
68dd6bc98e | ||
|
|
966e9f43f1 | ||
|
|
45671c89f9 | ||
|
|
8ad917e94b | ||
|
|
3de0df2162 | ||
|
|
e4225aa2c2 | ||
|
|
a877c5b25d | ||
|
|
1f06c4f4e2 | ||
|
|
c31bbb63fb | ||
|
|
92715ac850 | ||
|
|
cbf382625f | ||
|
|
e6250bfe0f | ||
|
|
ca00b8a3c7 | ||
|
|
39b2326bc5 | ||
|
|
907b7dc429 | ||
|
|
9a78c8b512 | ||
|
|
5dc328346c | ||
|
|
79bd4728bf | ||
|
|
eb26b38f2c | ||
|
|
811c84a845 | ||
|
|
214fa4c06e | ||
|
|
fc714182ad | ||
|
|
7c96ab88d1 | ||
|
|
d6de4fc41f | ||
|
|
33b4ed44de | ||
|
|
81b12a7441 | ||
|
|
8406dbe49d | ||
|
|
cbaf973eeb | ||
|
|
44711d7585 | ||
|
|
c00d760cb0 | ||
|
|
9ad580effa | ||
|
|
2acc3f4584 | ||
|
|
3fac1c23b2 | ||
|
|
a596a1fe9a | ||
|
|
1835edcb67 | ||
|
|
f928e2c3a8 | ||
|
|
e48ed1806c | ||
|
|
2f13fe0760 | ||
|
|
d01645052d | ||
|
|
d822523394 | ||
|
|
0ffc44f3da | ||
|
|
5fff24bf69 | ||
|
|
a34853c754 | ||
|
|
cc13f7126c | ||
|
|
218a82998a | ||
|
|
ee81ad2add | ||
|
|
648a41ed7b | ||
|
|
eb9b9d57ed | ||
|
|
3ab29eee60 | ||
|
|
fe13823b43 | ||
|
|
13d8554e7e | ||
|
|
4d840d6f43 | ||
|
|
a0e2747004 | ||
|
|
c484a29099 | ||
|
|
23772ce312 | ||
|
|
63cfec517d | ||
|
|
e1465f5336 | ||
|
|
7fda41c18b | ||
|
|
aa45bc3938 | ||
|
|
2ca55ae729 | ||
|
|
977c4e653b | ||
|
|
c052363368 | ||
|
|
9fb7a1051e | ||
|
|
3abda061ba | ||
|
|
065f03a01a | ||
|
|
f6a04f7890 | ||
|
|
62928b227b | ||
|
|
c93870ff60 | ||
|
|
0bcad2e57b | ||
|
|
6acd42e82b | ||
|
|
8711c5d824 | ||
|
|
ba6b52c4c9 | ||
|
|
96db74494a | ||
|
|
62221c2a7f | ||
|
|
80b0c594b5 | ||
|
|
6612dab547 | ||
|
|
292aacc766 | ||
|
|
a4755631c3 | ||
|
|
c5dcb22c01 | ||
|
|
1db7ee5111 | ||
|
|
90d610b33a | ||
|
|
2984e2e1ca | ||
|
|
36080d4665 | ||
|
|
93f0f33e84 | ||
|
|
129591de61 | ||
|
|
3767c830d5 | ||
|
|
774180262b | ||
|
|
dc843ec63e | ||
|
|
bfb42929a2 | ||
|
|
be09385c9d | ||
|
|
3c3f3a4d90 | ||
|
|
222236bbde | ||
|
|
85f93eba98 | ||
|
|
4181334d06 | ||
|
|
d1ed249039 | ||
|
|
0416b47b69 | ||
|
|
d7f92daf08 | ||
|
|
a146f6d223 | ||
|
|
8864b6b2c8 | ||
|
|
1dff396b9e | ||
|
|
a28595887a | ||
|
|
bdbbcd7fac | ||
|
|
d668bd5277 | ||
|
|
651808497f | ||
|
|
ebdc51b821 | ||
|
|
755068a44e | ||
|
|
3f5668705c | ||
|
|
9571c992df | ||
|
|
7b3f7bb582 | ||
|
|
ab1928ee95 | ||
|
|
7d4c92cc96 | ||
|
|
00e4555736 | ||
|
|
23142a59d7 | ||
|
|
5dfd8d1a50 | ||
|
|
f45cc29816 | ||
|
|
fd291aeb96 | ||
|
|
11577b6db9 | ||
|
|
146fe04cce | ||
|
|
4a7b14fa39 | ||
|
|
8748b18ae9 | ||
|
|
3b84b5da1d | ||
|
|
d57ef9805d | ||
|
|
54f336f3fd | ||
|
|
7a129a4f89 | ||
|
|
877f40116a | ||
|
|
aa043a53b1 | ||
|
|
51e25b2a3c | ||
|
|
99cedbf96e | ||
|
|
97e6a353fa | ||
|
|
5996f4d000 | ||
|
|
2067f655bd | ||
|
|
3798ff19e5 | ||
|
|
273680aed6 | ||
|
|
2421c57bac | ||
|
|
d6d41ac20d | ||
|
|
5de68ad8d4 | ||
|
|
089e327596 | ||
|
|
bddf4d9801 | ||
|
|
88a8f33bdb | ||
|
|
a2668edabf | ||
|
|
c435b3853b | ||
|
|
c226abf184 | ||
|
|
ca5fbf7cc3 | ||
|
|
395bb7cacf | ||
|
|
13e06ffcfd | ||
|
|
4b3bb051a2 | ||
|
|
68a7417760 | ||
|
|
1deb9d576f | ||
|
|
3fa656433c | ||
|
|
a1e90bef2e | ||
|
|
63b8dcc2e3 | ||
|
|
12588e8e08 | ||
|
|
aab35b1a48 | ||
|
|
0792e7c016 | ||
|
|
ed12e2e534 | ||
|
|
5cd8574792 | ||
|
|
2c5c7789dc | ||
|
|
bbcc26f489 | ||
|
|
d61106aee8 | ||
|
|
c7b11a83b9 | ||
|
|
1a1d99f206 | ||
|
|
ed44f83e27 | ||
|
|
e2f380df25 | ||
|
|
d2e2f68cf9 | ||
|
|
694cd1b178 | ||
|
|
5b1277daff | ||
|
|
4b31a143a9 | ||
|
|
36f718dde5 | ||
|
|
82f477c239 | ||
|
|
c5e51248d2 | ||
|
|
d4fb6824b6 | ||
|
|
cb3cdd6980 | ||
|
|
d441385a74 | ||
|
|
1b680d9faa | ||
|
|
0d6119a804 | ||
|
|
c26b787b6f | ||
|
|
482815629b | ||
|
|
19ad139cf8 | ||
|
|
c18f41e81c | ||
|
|
6826ff1d4b | ||
|
|
22a17201ff | ||
|
|
ddf5ec5f66 | ||
|
|
26cd77f1c5 | ||
|
|
20e1874787 | ||
|
|
a85d25cb82 | ||
|
|
afb9f3809f | ||
|
|
d87bf5ae63 | ||
|
|
144134446d | ||
|
|
23f2bbc700 | ||
|
|
a8388e12c1 | ||
|
|
4f40ad0480 | ||
|
|
27bc97a1a6 | ||
|
|
73f81465e9 | ||
|
|
a8c91f2bc8 | ||
|
|
4b0586bd3d | ||
|
|
8f3f90d407 | ||
|
|
47b23417e0 | ||
|
|
5a9d75857d | ||
|
|
2691cb3fce | ||
|
|
9d77337726 | ||
|
|
5192c76717 | ||
|
|
f884a2689a | ||
|
|
5714f1c913 | ||
|
|
a84725f867 | ||
|
|
106a1c339b | ||
|
|
72de94308b | ||
|
|
400f77584d | ||
|
|
427f76fbe0 | ||
|
|
8a7765c855 | ||
|
|
fea34add4b | ||
|
|
399ee8d2e7 | ||
|
|
3940d4aa28 | ||
|
|
f0742e3750 | ||
|
|
41f5f0f2d4 | ||
|
|
c1f2ea6c8a | ||
|
|
2d7aa20448 | ||
|
|
9be948b7cc | ||
|
|
110e867bd3 | ||
|
|
9f37b1c484 | ||
|
|
d078dacaab | ||
|
|
996841db45 | ||
|
|
404da4ae22 | ||
|
|
fa25324463 | ||
|
|
652fdae7d9 | ||
|
|
6a61702a91 | ||
|
|
8bc44f2a29 | ||
|
|
8ec13f5ead | ||
|
|
f584d2d8d2 | ||
|
|
7072b913a6 | ||
|
|
e29b0c318e | ||
|
|
dd341ef1e1 | ||
|
|
2bea1965c6 | ||
|
|
0252617730 | ||
|
|
f6e87858a8 | ||
|
|
6fa56b8873 | ||
|
|
c749a660e7 | ||
|
|
e6992da18c | ||
|
|
80e8d5e5e7 | ||
|
|
61fdb6eba2 | ||
|
|
98611c7d02 | ||
|
|
ad2b3e5cc5 | ||
|
|
34bd1a5876 | ||
|
|
b652cf9563 | ||
|
|
fa459ea7ac | ||
|
|
acb6fc01b3 | ||
|
|
1e0e8adc77 | ||
|
|
35ab21ab04 | ||
|
|
d61daeac8e | ||
|
|
c8c37435be | ||
|
|
a6f8391f40 | ||
|
|
025cb04035 | ||
|
|
3fc60dc56d | ||
|
|
f5b2acdbdf | ||
|
|
9a0b247c8f | ||
|
|
8c50b866ff | ||
|
|
d6170dbfed | ||
|
|
58ddbcd77e | ||
|
|
d348232e0d | ||
|
|
940da91ca4 | ||
|
|
543fe6729a | ||
|
|
6bcede5064 | ||
|
|
fbfbe4a931 | ||
|
|
5a0d52aef9 | ||
|
|
2bf86248af | ||
|
|
96cb26050b | ||
|
|
32833b5fc4 | ||
|
|
1cdcfe25c0 | ||
|
|
c61315b90e | ||
|
|
d22ca1fe4f | ||
|
|
791bba471f | ||
|
|
446a0591db | ||
|
|
69773db77a | ||
|
|
d51af026fb | ||
|
|
fb130fd0e9 | ||
|
|
760de88e7c | ||
|
|
4a1b2af535 | ||
|
|
5cc0331c75 | ||
|
|
d3e8c7e0c9 | ||
|
|
16b3817202 | ||
|
|
eaf46b0550 | ||
|
|
da1686b53c | ||
|
|
52337350d9 | ||
|
|
faf8fbbc2a | ||
|
|
e6ceafe0b7 | ||
|
|
cf42e02586 | ||
|
|
93eca6a749 | ||
|
|
d7813c2255 | ||
|
|
239240768b | ||
|
|
36b8df87d2 | ||
|
|
7c14bd1c80 | ||
|
|
fd81ca86db | ||
|
|
42262877b0 | ||
|
|
843e46f890 | ||
|
|
5ae4899ff2 | ||
|
|
bc4aed17a2 | ||
|
|
8adf67ed5a | ||
|
|
7cc9a67b74 | ||
|
|
c8263fd856 | ||
|
|
d7cdbbecc8 | ||
|
|
64e2f6457d | ||
|
|
93ba5ade9e | ||
|
|
ffc12656ee | ||
|
|
1e735da9f1 | ||
|
|
96072e61e0 | ||
|
|
b05b8c9e7a | ||
|
|
f73b9491ad | ||
|
|
7842acc76e | ||
|
|
ad11ef9d2a | ||
|
|
70c572534a | ||
|
|
4236afe850 | ||
|
|
1ff5d814f4 | ||
|
|
7921a228ad | ||
|
|
3af8051e3c | ||
|
|
cf0439d4c5 | ||
|
|
5176bdc786 | ||
|
|
0c8ad37a8f | ||
|
|
4fe6029be7 | ||
|
|
05bc9f11ee | ||
|
|
267bdb4cdf | ||
|
|
6d9126bca4 | ||
|
|
8853f1cfb3 | ||
|
|
5afe37e929 | ||
|
|
755fec154b | ||
|
|
2d1573251b | ||
|
|
da5cdd6661 | ||
|
|
7f64162a7a | ||
|
|
b4289664dc | ||
|
|
4fb13a64b1 | ||
|
|
a06ceeb31e | ||
|
|
18a9f69f60 | ||
|
|
36685edd49 | ||
|
|
e4ca35d2d2 | ||
|
|
c534ab570f | ||
|
|
1f88450045 | ||
|
|
b15ab3ae27 | ||
|
|
360e68a793 | ||
|
|
4aebf02d14 | ||
|
|
7002628514 | ||
|
|
4af4d45873 | ||
|
|
fe85e18a62 | ||
|
|
4d2781d128 | ||
|
|
5a49fb9a14 | ||
|
|
5ca32a7e84 | ||
|
|
d6634e7da1 | ||
|
|
a6ec2f5367 | ||
|
|
459dcc2ed6 | ||
|
|
acd5796d87 | ||
|
|
e18f4bb71c | ||
|
|
051af8a9a6 | ||
|
|
47f9572f83 | ||
|
|
4ca774182a | ||
|
|
53527c518b | ||
|
|
84ff9abf44 | ||
|
|
ceef604201 | ||
|
|
6b8d2a60a7 | ||
|
|
3b656e05a2 | ||
|
|
a9e03ed1cc | ||
|
|
bb504ec275 | ||
|
|
19a496c31c | ||
|
|
db51254827 | ||
|
|
24ab9768e9 | ||
|
|
79cff81ffd | ||
|
|
8d43d5d7b0 | ||
|
|
97e6240177 | ||
|
|
371ef929f3 | ||
|
|
ebcde8f602 | ||
|
|
ea4044f237 | ||
|
|
739ebf25c0 | ||
|
|
0c6a51c7f7 | ||
|
|
4fcc463d6a | ||
|
|
0d8c259237 | ||
|
|
6943cc0011 | ||
|
|
3a5752f3cc | ||
|
|
7e00dd731c | ||
|
|
bb66af7185 | ||
|
|
16b2458903 | ||
|
|
d33d27a55f | ||
|
|
1e7efbe3d6 | ||
|
|
230198c1e7 | ||
|
|
d803847342 | ||
|
|
7693593230 | ||
|
|
f36eee0dc2 | ||
|
|
0db2f38dfe | ||
|
|
c58be51a03 | ||
|
|
7ea1bf71dd | ||
|
|
1d6749ef52 | ||
|
|
9216fe28d0 | ||
|
|
649ecd94ea | ||
|
|
634153b658 | ||
|
|
3101544484 | ||
|
|
ff40d82ef1 | ||
|
|
d4383d4180 | ||
|
|
72314c4388 | ||
|
|
17b5187729 | ||
|
|
e30c078962 | ||
|
|
d808934cf4 | ||
|
|
b3b4955fb2 | ||
|
|
4765236940 | ||
|
|
32a49083e7 | ||
|
|
9c096aae10 | ||
|
|
90b59a56e1 | ||
|
|
134523247c | ||
|
|
e5ba8af3ce | ||
|
|
10fd15b50a | ||
|
|
dd93531432 | ||
|
|
762a085f9a | ||
|
|
8d63da1cfb | ||
|
|
7a95cedbad | ||
|
|
44d73f3e7e | ||
|
|
786247e9bc | ||
|
|
c152cc2517 | ||
|
|
3d86e29972 | ||
|
|
e3c239e848 |
@@ -19,10 +19,10 @@ indent_size = 4
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:refactoring
|
||||
dotnet_style_qualification_for_property = false:refactoring
|
||||
dotnet_style_qualification_for_method = false:refactoring
|
||||
dotnet_style_qualification_for_event = false:refactoring
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
@@ -32,10 +32,6 @@ csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||
|
||||
@@ -110,13 +106,13 @@ dotnet_diagnostic.SA1643.severity = none
|
||||
dotnet_diagnostic.SA1648.severity = none
|
||||
dotnet_diagnostic.SA1649.severity = none
|
||||
dotnet_diagnostic.SA1651.severity = none
|
||||
dotnet_diagnostic.SX1101.severity = warning
|
||||
dotnet_diagnostic.SX1309.severity = warning
|
||||
|
||||
# Microsoft Analyzers that fail and need to be sorted thru
|
||||
dotnet_diagnostic.ASP0000.severity = suggestion
|
||||
dotnet_diagnostic.CA1000.severity = suggestion
|
||||
dotnet_diagnostic.CA1001.severity = suggestion
|
||||
dotnet_diagnostic.CA1002.severity = suggestion
|
||||
dotnet_diagnostic.CA1003.severity = suggestion
|
||||
dotnet_diagnostic.CA1008.severity = suggestion
|
||||
dotnet_diagnostic.CA1010.severity = suggestion
|
||||
@@ -163,10 +159,16 @@ dotnet_diagnostic.CA1304.severity = suggestion
|
||||
dotnet_diagnostic.CA1305.severity = suggestion
|
||||
dotnet_diagnostic.CA1307.severity = suggestion
|
||||
dotnet_diagnostic.CA1308.severity = suggestion
|
||||
dotnet_diagnostic.CA1309.severity = suggestion
|
||||
dotnet_diagnostic.CA1310.severity = suggestion
|
||||
dotnet_diagnostic.CA1401.severity = suggestion
|
||||
dotnet_diagnostic.CA1416.severity = suggestion
|
||||
dotnet_diagnostic.CA1507.severity = suggestion
|
||||
dotnet_diagnostic.CA1508.severity = suggestion
|
||||
dotnet_diagnostic.CA1707.severity = suggestion
|
||||
dotnet_diagnostic.CA1708.severity = suggestion
|
||||
dotnet_diagnostic.CA1710.severity = suggestion
|
||||
dotnet_diagnostic.CA1711.severity = suggestion
|
||||
dotnet_diagnostic.CA1712.severity = suggestion
|
||||
dotnet_diagnostic.CA1714.severity = suggestion
|
||||
dotnet_diagnostic.CA1715.severity = suggestion
|
||||
@@ -175,12 +177,14 @@ dotnet_diagnostic.CA1717.severity = suggestion
|
||||
dotnet_diagnostic.CA1720.severity = suggestion
|
||||
dotnet_diagnostic.CA1721.severity = suggestion
|
||||
dotnet_diagnostic.CA1724.severity = suggestion
|
||||
dotnet_diagnostic.CA1725.severity = suggestion
|
||||
dotnet_diagnostic.CA1801.severity = suggestion
|
||||
dotnet_diagnostic.CA1802.severity = suggestion
|
||||
dotnet_diagnostic.CA1805.severity = suggestion
|
||||
dotnet_diagnostic.CA1806.severity = suggestion
|
||||
dotnet_diagnostic.CA1810.severity = suggestion
|
||||
dotnet_diagnostic.CA1812.severity = suggestion
|
||||
dotnet_diagnostic.CA1813.severity = suggestion
|
||||
dotnet_diagnostic.CA1814.severity = suggestion
|
||||
dotnet_diagnostic.CA1815.severity = suggestion
|
||||
dotnet_diagnostic.CA1816.severity = suggestion
|
||||
@@ -210,6 +214,7 @@ dotnet_diagnostic.CA2101.severity = suggestion
|
||||
dotnet_diagnostic.CA2119.severity = suggestion
|
||||
dotnet_diagnostic.CA2153.severity = suggestion
|
||||
dotnet_diagnostic.CA2200.severity = suggestion
|
||||
dotnet_diagnostic.CA2201.severity = suggestion
|
||||
dotnet_diagnostic.CA2207.severity = suggestion
|
||||
dotnet_diagnostic.CA2208.severity = suggestion
|
||||
dotnet_diagnostic.CA2211.severity = suggestion
|
||||
@@ -255,6 +260,8 @@ dotnet_diagnostic.CA5374.severity = suggestion
|
||||
dotnet_diagnostic.CA5379.severity = suggestion
|
||||
dotnet_diagnostic.CA5384.severity = suggestion
|
||||
dotnet_diagnostic.CA5385.severity = suggestion
|
||||
dotnet_diagnostic.CA5392.severity = suggestion
|
||||
dotnet_diagnostic.CA5394.severity = suggestion
|
||||
dotnet_diagnostic.CA5397.severity = suggestion
|
||||
|
||||
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Support Requests will be closed immediately, if you are unsure go to our Reddit or Discord first. Exceptions do not mean you found a bug!
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Platform Information (please complete the following information):**
|
||||
- OS: [e.g. Windows 10 2004 / Ubuntu 20.10]
|
||||
- Docker: [Yes/No]
|
||||
- Mono or.NET Core Version: [e.g. Mono 5.8 or .Net Core 3.1.10] (found under System -> Status)
|
||||
- Browser and Version [e.g. chrome 86.0.4240.198] (Only needed for UI issues)
|
||||
- Readarr Version [e.g. 0.3.0.430]
|
||||
- Readarr Branch [e.g. master]
|
||||
|
||||
**Trace Logs**
|
||||
Turn on Trace logs under Settings -> General and wait for the bug to occur again. **Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
|
||||
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
75
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Bug Report
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
examples:
|
||||
- **OS**: Ubuntu 20.04
|
||||
- **Readarr**: Readarr 0.1.0.432
|
||||
- **Docker Install**: Yes
|
||||
- **Using Reverse Proxy**: No
|
||||
- **Browser**: Firefox 90 (If UI related)
|
||||
- **Database**: Sqlite 3.36.0
|
||||
value: |
|
||||
- OS:
|
||||
- Readarr:
|
||||
- Docker Install:
|
||||
- Using Reverse Proxy:
|
||||
- Browser:
|
||||
- Database:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What branch are you running?
|
||||
options:
|
||||
- Master
|
||||
- Develop
|
||||
- Nightly
|
||||
- Other (This issue will be closed)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Trace Logs?
|
||||
description: |
|
||||
Trace Logs (https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files)
|
||||
***Generally speaking, all bug reports must have trace logs provided.***
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
validations:
|
||||
required: true
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support via Discord
|
||||
url: https://discord.gg/rkEXY2Rbgn
|
||||
url: https://readarr.com/discord
|
||||
about: Chat with users and devs on support and setup related topics.
|
||||
- name: Support via Reddit
|
||||
url: https://reddit.com/r/Readarr
|
||||
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Feature Request
|
||||
description: 'Suggest an idea for Readarr'
|
||||
labels: ['Type: Feature Request', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe
|
||||
description: A clear and concise description of what the problem is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: true
|
||||
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,14 +1,16 @@
|
||||
#### Database Migration
|
||||
YES | NO
|
||||
YES - XXXX | NO
|
||||
|
||||
#### Description
|
||||
A few sentences describing the overall goals of the pull request's commits.
|
||||
|
||||
#### Screenshot (if UI related)
|
||||
|
||||
#### Todos
|
||||
- [ ] Tests
|
||||
- [ ] Wiki Updates
|
||||
|
||||
- [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json)
|
||||
- [ ] [Wiki Updates](https://wiki.servarr.com)
|
||||
|
||||
#### Issues Fixed or Closed by this PR
|
||||
|
||||
*
|
||||
* Fixes #XXXX
|
||||
41
.github/workflows/azuresync.yml
vendored
Normal file
41
.github/workflows/azuresync.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Sync issue to Azure DevOps work item
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
|
||||
|
||||
concurrency: azuresync-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
alert:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Readarr"
|
||||
ado_wit: "Bug"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Readarr"
|
||||
ado_wit: "User Story"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
21
.github/workflows/lock.yml
vendored
Normal file
21
.github/workflows/lock.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: 'Lock threads'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: '90'
|
||||
issue-exclude-created-before: ''
|
||||
issue-exclude-labels: ''
|
||||
issue-lock-labels: ''
|
||||
issue-lock-comment: ''
|
||||
issue-lock-reason: 'resolved'
|
||||
process-only: ''
|
||||
21
.github/workflows/support.yml
vendored
Normal file
21
.github/workflows/support.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: 'Support requests'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
jobs:
|
||||
support:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Type: Support'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord)
|
||||
or [Subreddit](https://reddit.com/r/readarr)
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
}
|
||||
}
|
||||
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
<development@readarr.com>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -1,49 +1,13 @@
|
||||
# How to Contribute #
|
||||
# How to Contribute
|
||||
|
||||
We're always looking for people to help make Readarr even better, there are a number of ways to contribute.
|
||||
|
||||
## Documentation ##
|
||||
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/Readarr) the better.
|
||||
This file has been moved to the wiki for the latest details please see the [contributing wiki page](https://wiki.servarr.com/readarr/contributing).
|
||||
|
||||
## Development ##
|
||||
## Documentation
|
||||
|
||||
### Tools required ###
|
||||
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
|
||||
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 10.X.X or higher)
|
||||
- [Yarn](https://yarnpkg.com/)
|
||||
- .NET Core 3.1.
|
||||
Setup guides, [FAQ](https://wiki.servarr.com/readarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/readarr) the better.
|
||||
|
||||
### Getting started ###
|
||||
## Development
|
||||
|
||||
1. Fork Readarr
|
||||
2. Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
|
||||
3. Install the required Node Packages `yarn install`
|
||||
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
|
||||
5. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31`
|
||||
6. Debug the project in Visual Studio
|
||||
7. Open http://localhost:8787
|
||||
|
||||
### Contributing Code ###
|
||||
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Readarr/Readarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
|
||||
- Rebase from Readarr's develop branch, don't merge
|
||||
- Make meaningful commits, or squash them
|
||||
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
|
||||
- Reach out to us on the discord if you have any questions
|
||||
- Add tests (unit/integration)
|
||||
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
|
||||
- One feature/bug fix per pull request to keep things clean and easy to understand
|
||||
- Use 4 spaces instead of tabs, this is the default for VS 2019 and WebStorm (to my knowledge)
|
||||
|
||||
### Pull Requesting ###
|
||||
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
|
||||
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
|
||||
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
|
||||
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
|
||||
- new-feature (Good)
|
||||
- fix-bug (Good)
|
||||
- patch (Bad)
|
||||
- develop (Bad)
|
||||
|
||||
If you have any questions about any of this, please let us know.
|
||||
See the [Wiki Page](https://wiki.servarr.com/readarr/contributing)
|
||||
|
||||
56
README.md
56
README.md
@@ -1,52 +1,68 @@
|
||||
# Readarr
|
||||
|
||||
[](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://hub.docker.com/r/hotio/readarr)
|
||||
[](#backers) [](#sponsors)
|
||||
[](https://translate.servarr.com/engage/readarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/readarr/installation#docker)
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
[](#mega-sponsors)
|
||||
|
||||
### Readarr is in early stages of development, alpha/beta binary builds are not yet available. Use of any test builds isn't recommend, and may have detrimental effects on your library.
|
||||
### Readarr is currently in beta testing and is generally still in a work in progress. Features may be broken, incomplete, or cause spontaneous combustion
|
||||
|
||||
Readarr is a ebook (and maybe eventually magazine/audiobook) collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort and rename them.
|
||||
Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort, and rename them.
|
||||
Note that only one type of a given book is supported. If you want both an audiobook and ebook of a given book you will need multiple instances.
|
||||
|
||||
## Major Features Include:
|
||||
## Major Features Include
|
||||
|
||||
* Can watch for better quality of the ebooks and audiobooks you have and do an automatic upgrade. *e.g. from PDF to AZW3*
|
||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
* Automatically detects new books.
|
||||
* Can scan your existing library and download any missing books.
|
||||
* Automatically detects new books
|
||||
* Can scan your existing library and download any missing books
|
||||
* Automatic failed download handling will try another release if one fails
|
||||
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||
* Advanced customization for profiles, such that Readarr will always download the copy you want
|
||||
* Fully configurable book renaming
|
||||
* Full integration with SABnzbd and NZBGet
|
||||
* Full integration with Calibre (add to library, conversion)
|
||||
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
|
||||
* Full integration with Calibre (add to library, conversion) (Requires Calibre Content Server)
|
||||
* And a beautiful UI
|
||||
|
||||
## Support
|
||||
|
||||
[](https://discord.gg/rkEXY2Rbgn)
|
||||
[](https://github.com/Readarr/Readarr/issues)
|
||||
[](https://wiki.servarr.com/Readarr)
|
||||
[](https://wiki.servarr.com/readarr)
|
||||
[](https://readarr.com/discord)
|
||||
[](https://www.reddit.com/r/readarr)
|
||||
|
||||
## Contributors
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||
<a href="https://github.com/Readarr/Readarr/graphs/contributors"><img src="https://opencollective.com/Readarr/contributors.svg?width=890&button=false" /></a>
|
||||
[](https://github.com/Readarr/Readarr/issues)
|
||||
|
||||
## Contributors & Developers
|
||||
|
||||
[API Documentation](https://readarr.com/docs/api/)
|
||||
|
||||
This project exists thanks to all the people who contribute.
|
||||
- [Contribute (GitHub)](CONTRIBUTING.md)
|
||||
- [Contribution (Wiki Article)](https://wiki.servarr.com/readarr/contributing)
|
||||
|
||||
[](https://github.com/Readarr/Readarr/graphs/contributors)
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/readarr#backer)
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Readarr#backer)
|
||||
|
||||
<img src="https://opencollective.com/readarr/backers.svg?width=890"></a>
|
||||
[](https://opencollective.com/Readarr#backer)
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/readarr#sponsor)
|
||||
|
||||
<img src="https://opencollective.com/readarr/sponsors.svg?width=890"></a>
|
||||
[](https://opencollective.com/readarr#sponsor)
|
||||
|
||||
## Mega Sponsors
|
||||
<img src="https://opencollective.com/readarr/tiers/mega-sponsor.svg?width=890"></a>
|
||||
|
||||
[](https://opencollective.com/readarr#mega-sponsor)
|
||||
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2021
|
||||
* Copyright 2010-2022
|
||||
|
||||
8
SECURITY.md
Normal file
8
SECURITY.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities on Discord (preferred) to
|
||||
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
|
||||
us within 72 hours. If the issue is confirmed, we will release a patch as soon
|
||||
as possible depending on complexity/severity.
|
||||
File diff suppressed because it is too large
Load Diff
168
build.sh
168
build.sh
@@ -1,4 +1,4 @@
|
||||
#! /bin/bash
|
||||
#! /usr/bin/env bash
|
||||
set -e
|
||||
|
||||
outputFolder='_output'
|
||||
@@ -27,6 +27,25 @@ UpdateVersionNumber()
|
||||
fi
|
||||
}
|
||||
|
||||
EnableExtraPlatformsInSDK()
|
||||
{
|
||||
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
}
|
||||
|
||||
EnableExtraPlatforms()
|
||||
{
|
||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
fi
|
||||
}
|
||||
|
||||
LintUI()
|
||||
{
|
||||
ProgressStart 'ESLint'
|
||||
@@ -57,9 +76,6 @@ Build()
|
||||
platform=Posix
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
|
||||
@@ -77,11 +93,11 @@ YarnInstall()
|
||||
ProgressEnd 'yarn install'
|
||||
}
|
||||
|
||||
RunGulp()
|
||||
RunWebpack()
|
||||
{
|
||||
ProgressStart 'Running gulp'
|
||||
yarn run build --production
|
||||
ProgressEnd 'Running gulp'
|
||||
ProgressStart 'Running webpack'
|
||||
yarn run build --env production
|
||||
ProgressEnd 'Running webpack'
|
||||
}
|
||||
|
||||
PackageFiles()
|
||||
@@ -120,7 +136,7 @@ PackageLinux()
|
||||
|
||||
echo "Adding Readarr.Mono to UpdatePackage"
|
||||
cp $folder/Readarr.Mono.* $folder/Readarr.Update
|
||||
if [ "$framework" = "netcoreapp3.1" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
|
||||
fi
|
||||
@@ -131,17 +147,13 @@ PackageLinux()
|
||||
PackageMacOS()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating MacOS Package for $framework"
|
||||
ProgressStart "Creating MacOS Package for $framework $runtime"
|
||||
|
||||
local folder=$artifactsFolder/macos/$framework/Readarr
|
||||
local folder=$artifactsFolder/$runtime/$framework/Readarr
|
||||
|
||||
PackageFiles "$folder" "$framework" "osx-x64"
|
||||
|
||||
if [ "$framework" = "net462" ]; then
|
||||
echo "Adding Startup script"
|
||||
cp macOS/Readarr $folder
|
||||
fi
|
||||
PackageFiles "$folder" "$framework" "$runtime"
|
||||
|
||||
echo "Removing Service helpers"
|
||||
rm -f $folder/ServiceUninstall.*
|
||||
@@ -152,7 +164,7 @@ PackageMacOS()
|
||||
|
||||
echo "Adding Readarr.Mono to UpdatePackage"
|
||||
cp $folder/Readarr.Mono.* $folder/Readarr.Update
|
||||
if [ "$framework" = "netcoreapp3.1" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
|
||||
fi
|
||||
@@ -163,10 +175,11 @@ PackageMacOS()
|
||||
PackageMacOSApp()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating macOS App Package for $framework"
|
||||
ProgressStart "Creating macOS App Package for $framework $runtime"
|
||||
|
||||
local folder=$artifactsFolder/macos-app/$framework
|
||||
local folder="$artifactsFolder/$runtime-app/$framework"
|
||||
|
||||
rm -rf $folder
|
||||
mkdir -p $folder
|
||||
@@ -174,7 +187,7 @@ PackageMacOSApp()
|
||||
mkdir -p $folder/Readarr.app/Contents/MacOS
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $artifactsFolder/macos/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
|
||||
cp -r $artifactsFolder/$runtime/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $folder/Readarr.app/Contents/MacOS/Readarr.Update
|
||||
@@ -186,12 +199,13 @@ PackageWindows()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating Windows Package for $framework"
|
||||
|
||||
ProgressStart "Creating $runtime Package for $framework"
|
||||
|
||||
local folder=$artifactsFolder/$runtime/$framework/Readarr
|
||||
|
||||
PackageFiles "$folder" "$framework" "$runtime"
|
||||
cp -r $outputFolder/$framework-windows/$runtime/publish/* $folder
|
||||
|
||||
echo "Removing Readarr.Mono"
|
||||
rm -f $folder/Readarr.Mono.*
|
||||
@@ -201,7 +215,7 @@ PackageWindows()
|
||||
echo "Adding Readarr.Windows to UpdatePackage"
|
||||
cp $folder/Readarr.Windows.* $folder/Readarr.Update
|
||||
|
||||
ProgressEnd 'Creating Windows Package'
|
||||
ProgressEnd "Creating $runtime Package for $framework"
|
||||
}
|
||||
|
||||
Package()
|
||||
@@ -213,19 +227,45 @@ Package()
|
||||
IFS='-' read -ra SPLIT <<< "$runtime"
|
||||
|
||||
case "${SPLIT[0]}" in
|
||||
linux)
|
||||
linux|freebsd*)
|
||||
PackageLinux "$framework" "$runtime"
|
||||
;;
|
||||
win)
|
||||
PackageWindows "$framework" "$runtime"
|
||||
;;
|
||||
osx)
|
||||
PackageMacOS "$framework"
|
||||
PackageMacOSApp "$framework"
|
||||
PackageMacOS "$framework" "$runtime"
|
||||
PackageMacOSApp "$framework" "$runtime"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
BuildInstaller()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
./_inno/ISCC.exe setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
|
||||
}
|
||||
|
||||
InstallInno()
|
||||
{
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
ProgressEnd "Installed portable Inno Setup"
|
||||
}
|
||||
|
||||
RemoveInno()
|
||||
{
|
||||
rm -rf _inno
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
{
|
||||
local framework="$1"
|
||||
@@ -257,7 +297,10 @@ if [ $# -eq 0 ]; then
|
||||
BACKEND=YES
|
||||
FRONTEND=YES
|
||||
PACKAGES=YES
|
||||
INSTALLER=NO
|
||||
LINT=YES
|
||||
ENABLE_EXTRA_PLATFORMS=NO
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
@@ -269,6 +312,14 @@ case $key in
|
||||
BACKEND=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-bsd|--enable-extra-platforms)
|
||||
ENABLE_EXTRA_PLATFORMS=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-extra-platforms-in-sdk)
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
|
||||
shift # past argument
|
||||
;;
|
||||
-r|--runtime)
|
||||
RID="$2"
|
||||
shift # past argument
|
||||
@@ -287,6 +338,10 @@ case $key in
|
||||
PACKAGES=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--installer)
|
||||
INSTALLER=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--lint)
|
||||
LINT=YES
|
||||
shift # past argument
|
||||
@@ -306,18 +361,31 @@ esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
|
||||
then
|
||||
EnableExtraPlatformsInSDK
|
||||
fi
|
||||
|
||||
if [ "$BACKEND" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
EnableExtraPlatforms
|
||||
fi
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
PackageTests "netcoreapp3.1" "win-x64"
|
||||
PackageTests "netcoreapp3.1" "win-x86"
|
||||
PackageTests "netcoreapp3.1" "linux-x64"
|
||||
PackageTests "netcoreapp3.1" "linux-musl-x64"
|
||||
PackageTests "netcoreapp3.1" "osx-x64"
|
||||
PackageTests "net462" "linux-x64"
|
||||
PackageTests "net6.0" "win-x64"
|
||||
PackageTests "net6.0" "win-x86"
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
@@ -326,7 +394,7 @@ fi
|
||||
if [ "$FRONTEND" = "YES" ];
|
||||
then
|
||||
YarnInstall
|
||||
RunGulp
|
||||
RunWebpack
|
||||
fi
|
||||
|
||||
if [ "$LINT" = "YES" ];
|
||||
@@ -345,16 +413,30 @@ then
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
Package "netcoreapp3.1" "win-x64"
|
||||
Package "netcoreapp3.1" "win-x86"
|
||||
Package "netcoreapp3.1" "linux-x64"
|
||||
Package "netcoreapp3.1" "linux-musl-x64"
|
||||
Package "netcoreapp3.1" "linux-arm64"
|
||||
Package "netcoreapp3.1" "linux-musl-arm64"
|
||||
Package "netcoreapp3.1" "linux-arm"
|
||||
Package "netcoreapp3.1" "osx-x64"
|
||||
Package "net462" "linux-x64"
|
||||
Package "net6.0" "win-x64"
|
||||
Package "net6.0" "win-x86"
|
||||
Package "net6.0" "linux-x64"
|
||||
Package "net6.0" "linux-musl-x64"
|
||||
Package "net6.0" "linux-arm64"
|
||||
Package "net6.0" "linux-musl-arm64"
|
||||
Package "net6.0" "linux-arm"
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
Package "net6.0" "freebsd-x64"
|
||||
Package "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$INSTALLER" = "YES" ];
|
||||
then
|
||||
InstallInno
|
||||
BuildInstaller "net6.0" "win-x64"
|
||||
BuildInstaller "net6.0" "win-x86"
|
||||
RemoveInno
|
||||
fi
|
||||
|
||||
5
debian/changelog
vendored
5
debian/changelog
vendored
@@ -1,5 +0,0 @@
|
||||
nzbdrone {version} {branch}; urgency=low
|
||||
|
||||
* Automatic Release.
|
||||
|
||||
-- NzbDrone <contact@nzbdrone.com> Mon, 26 Aug 2013 00:00:00 -0700
|
||||
1
debian/compat
vendored
1
debian/compat
vendored
@@ -1 +0,0 @@
|
||||
8
|
||||
12
debian/control
vendored
12
debian/control
vendored
@@ -1,12 +0,0 @@
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Sonarr <contact@nzbdrone.com>
|
||||
Source: nzbdrone
|
||||
Homepage: https://readarr.com
|
||||
Vcs-Git: git@github.com:readarr/Readarr.git
|
||||
Vcs-Browser: https://github.com/readarr/Readarr
|
||||
|
||||
Package: nzbdrone
|
||||
Architecture: all
|
||||
Depends: libmono-cil-dev (>= 3.2), sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
|
||||
Description: Readarr is a music collection manager
|
||||
24
debian/copyright
vendored
24
debian/copyright
vendored
@@ -1,24 +0,0 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: nzbdrone
|
||||
Source: https://github.com/readarr/Readarr
|
||||
|
||||
Files: *
|
||||
Copyright: 2010-2016 Readarr <hello@readarr.com>
|
||||
|
||||
License: GPL-3.0+
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
|
||||
1
debian/install
vendored
1
debian/install
vendored
@@ -1 +0,0 @@
|
||||
nzbdrone_bin/* opt/NzbDrone
|
||||
13
debian/rules
vendored
13
debian/rules
vendored
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
# Sample debian/rules that uses debhelper.
|
||||
# This file was originally written by Joey Hess and Craig Small.
|
||||
# As a special exception, when this file is copied by dh-make into a
|
||||
# dh-make output file, you may use that output file without restriction.
|
||||
# This special exception was added by Craig Small in version 0.37 of dh-make.
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
%:
|
||||
dh $@
|
||||
@@ -6,8 +6,10 @@ const dirs = fs
|
||||
.map((dirent) => dirent.name)
|
||||
.join('|');
|
||||
|
||||
const frontendFolder = __dirname;
|
||||
|
||||
module.exports = {
|
||||
parser: 'babel-eslint',
|
||||
parser: '@babel/eslint-parser',
|
||||
|
||||
env: {
|
||||
browser: true,
|
||||
@@ -25,6 +27,9 @@ module.exports = {
|
||||
parserOptions: {
|
||||
ecmaVersion: 6,
|
||||
sourceType: 'module',
|
||||
babelOptions: {
|
||||
configFile: `${frontendFolder}/babel.config.js`,
|
||||
},
|
||||
ecmaFeatures: {
|
||||
modules: true,
|
||||
impliedStrict: true
|
||||
@@ -34,6 +39,7 @@ module.exports = {
|
||||
plugins: [
|
||||
'filenames',
|
||||
'react',
|
||||
'react-hooks',
|
||||
'simple-import-sort',
|
||||
'import'
|
||||
],
|
||||
@@ -271,7 +277,7 @@ module.exports = {
|
||||
|
||||
// ImportSort
|
||||
|
||||
'simple-import-sort/sort': 'error',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'import/newline-after-import': 'error',
|
||||
|
||||
// React
|
||||
@@ -303,13 +309,15 @@ module.exports = {
|
||||
'react/react-in-jsx-scope': 2,
|
||||
'react/self-closing-comp': 2,
|
||||
'react/sort-comp': 2,
|
||||
'react/jsx-wrap-multilines': 2
|
||||
'react/jsx-wrap-multilines': 2,
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.js'],
|
||||
rules: {
|
||||
'simple-import-sort/sort': [
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
|
||||
270
frontend/build/webpack.config.js
Normal file
270
frontend/build/webpack.config.js
Normal file
@@ -0,0 +1,270 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const FileManagerPlugin = require('filemanager-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const LiveReloadPlugin = require('webpack-livereload-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
module.exports = (env) => {
|
||||
const uiFolder = 'UI';
|
||||
const frontendFolder = path.join(__dirname, '..');
|
||||
const srcFolder = path.join(frontendFolder, 'src');
|
||||
const isProduction = !!env.production;
|
||||
const isProfiling = isProduction && !!env.profile;
|
||||
const inlineWebWorkers = 'no-fallback';
|
||||
|
||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||
|
||||
console.log('Source Folder:', srcFolder);
|
||||
console.log('Output Folder:', distFolder);
|
||||
console.log('isProduction:', isProduction);
|
||||
console.log('isProfiling:', isProfiling);
|
||||
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
ignored: /node_modules/
|
||||
},
|
||||
|
||||
entry: {
|
||||
index: 'index.js'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
modules: [
|
||||
srcFolder,
|
||||
path.join(srcFolder, 'Shims'),
|
||||
'node_modules'
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery'
|
||||
},
|
||||
fallback: {
|
||||
buffer: false,
|
||||
http: false,
|
||||
https: false,
|
||||
url: false,
|
||||
util: false,
|
||||
net: false
|
||||
}
|
||||
},
|
||||
|
||||
output: {
|
||||
path: distFolder,
|
||||
publicPath: '/',
|
||||
filename: '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
moduleIds: 'deterministic',
|
||||
chunkIds: 'named',
|
||||
splitChunks: {
|
||||
chunks: 'initial',
|
||||
name: 'vendors'
|
||||
}
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProduction,
|
||||
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: 'Content/styles.css'
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'frontend/src/index.ejs',
|
||||
filename: 'index.html',
|
||||
publicPath: '/'
|
||||
}),
|
||||
|
||||
new FileManagerPlugin({
|
||||
events: {
|
||||
onEnd: {
|
||||
copy: [
|
||||
// HTML
|
||||
{
|
||||
source: 'frontend/src/*.html',
|
||||
destination: distFolder
|
||||
},
|
||||
|
||||
// Fonts
|
||||
{
|
||||
source: 'frontend/src/Content/Fonts/*.*',
|
||||
destination: path.join(distFolder, 'Content/Fonts')
|
||||
},
|
||||
|
||||
// Icon Images
|
||||
{
|
||||
source: 'frontend/src/Content/Images/Icons/*.*',
|
||||
destination: path.join(distFolder, 'Content/Images/Icons')
|
||||
},
|
||||
|
||||
// Images
|
||||
{
|
||||
source: 'frontend/src/Content/Images/*.*',
|
||||
destination: path.join(distFolder, 'Content/Images')
|
||||
},
|
||||
|
||||
// Robots
|
||||
{
|
||||
source: 'frontend/src/Content/robots.txt',
|
||||
destination: path.join(distFolder, 'Content/robots.txt')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
new LiveReloadPlugin()
|
||||
],
|
||||
|
||||
resolveLoader: {
|
||||
modules: [
|
||||
'node_modules',
|
||||
'frontend/build/webpack/'
|
||||
]
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
filename: '[name].js',
|
||||
inline: inlineWebWorkers
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js?$/,
|
||||
exclude: /(node_modules|JsLibraries)/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
configFile: `${frontendFolder}/babel.config.js`,
|
||||
envName: isProduction ? 'production' : 'development',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// CSS Modules
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /(node_modules|globals.css)/,
|
||||
use: [
|
||||
{ loader: MiniCssExtractPlugin.loader },
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
config: 'frontend/postcss.config.js'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Global styles
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: /(node_modules|globals.css)/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Fonts
|
||||
{
|
||||
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10240,
|
||||
mimetype: 'application/font-woff',
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
if (isProfiling) {
|
||||
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
|
||||
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
|
||||
|
||||
config.optimization.minimizer = [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true, // Must be set to true if using source-maps in production
|
||||
terserOptions: {
|
||||
mangle: false,
|
||||
keep_classnames: true,
|
||||
keep_fnames: true
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
|
||||
require('./clean');
|
||||
require('./copy');
|
||||
require('./webpack');
|
||||
|
||||
gulp.task('build',
|
||||
gulp.series('clean',
|
||||
gulp.parallel(
|
||||
'webpack',
|
||||
'copyHtml',
|
||||
'copyFonts',
|
||||
'copyImages',
|
||||
'copyRobots'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const del = require('del');
|
||||
|
||||
const paths = require('./helpers/paths');
|
||||
|
||||
gulp.task('clean', () => {
|
||||
return del([paths.dest.root]);
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
const path = require('path');
|
||||
const gulp = require('gulp');
|
||||
const print = require('gulp-print').default;
|
||||
const cache = require('gulp-cached');
|
||||
const livereload = require('gulp-livereload');
|
||||
const paths = require('./helpers/paths.js');
|
||||
|
||||
gulp.task('copyHtml', () => {
|
||||
return gulp.src(paths.src.html, { base: paths.src.root })
|
||||
.pipe(cache('copyHtml'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('copyFonts', () => {
|
||||
return gulp.src(
|
||||
path.join(paths.src.fonts, '**', '*.*'), { base: paths.src.root }
|
||||
)
|
||||
.pipe(cache('copyFonts'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('copyImages', () => {
|
||||
return gulp.src(
|
||||
path.join(paths.src.images, '**', '*.*'), { base: paths.src.root }
|
||||
)
|
||||
.pipe(cache('copyImages'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('copyRobots', () => {
|
||||
return gulp.src(paths.src.robots, { base: paths.src.root })
|
||||
.pipe(cache('copyRobots'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
require('./build.js');
|
||||
require('./clean.js');
|
||||
require('./copy.js');
|
||||
require('./watch.js');
|
||||
require('./webpack.js');
|
||||
@@ -1,6 +0,0 @@
|
||||
const colors = require('ansi-colors');
|
||||
|
||||
module.exports = function errorHandler(error) {
|
||||
console.log(colors.red(`Error (${error.plugin}): ${error.message}`));
|
||||
this.emit('end');
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
const root = './frontend/src';
|
||||
|
||||
const paths = {
|
||||
src: {
|
||||
root,
|
||||
html: `${root}/*.html`,
|
||||
scripts: `${root}/**/*.js`,
|
||||
content: `${root}/Content/`,
|
||||
fonts: `${root}/Content/Fonts/`,
|
||||
images: `${root}/Content/Images/`,
|
||||
robots: `${root}/Content/robots.txt`,
|
||||
exclude: {
|
||||
libs: `!${root}/JsLibraries/**`
|
||||
}
|
||||
},
|
||||
dest: {
|
||||
root: './_output/UI/',
|
||||
content: './_output/UI/Content/',
|
||||
fonts: './_output/UI/Content/Fonts/',
|
||||
images: './_output/UI/Content/Images/'
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = paths;
|
||||
@@ -1,19 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const livereload = require('gulp-livereload');
|
||||
const gulpWatch = require('gulp-watch');
|
||||
const paths = require('./helpers/paths.js');
|
||||
|
||||
require('./copy.js');
|
||||
require('./webpack.js');
|
||||
|
||||
function watch() {
|
||||
livereload.listen({ start: true });
|
||||
|
||||
gulp.task('webpackWatch')();
|
||||
gulpWatch(paths.src.html, gulp.series('copyHtml'));
|
||||
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
|
||||
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
|
||||
gulpWatch(paths.src.robots, gulp.series('copyRobots'));
|
||||
}
|
||||
|
||||
gulp.task('watch', gulp.series('build', watch));
|
||||
@@ -1,271 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const webpackStream = require('webpack-stream');
|
||||
const livereload = require('gulp-livereload');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const errorHandler = require('./helpers/errorHandler');
|
||||
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackPluginHtmlTags = require('html-webpack-plugin/lib/html-tags');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
const uiFolder = 'UI';
|
||||
const frontendFolder = path.join(__dirname, '..');
|
||||
const srcFolder = path.join(frontendFolder, 'src');
|
||||
const isProduction = process.argv.indexOf('--production') > -1;
|
||||
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
|
||||
const inlineWebWorkers = 'no-fallback';
|
||||
|
||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||
|
||||
console.log('Source Folder:', srcFolder);
|
||||
console.log('Output Folder:', distFolder);
|
||||
console.log('isProduction:', isProduction);
|
||||
console.log('isProfiling:', isProfiling);
|
||||
|
||||
const cssVarsFiles = [
|
||||
'../src/Styles/Variables/colors',
|
||||
'../src/Styles/Variables/dimensions',
|
||||
'../src/Styles/Variables/fonts',
|
||||
'../src/Styles/Variables/animations',
|
||||
'../src/Styles/Variables/zIndexes'
|
||||
].map(require.resolve);
|
||||
|
||||
// Override the way HtmlWebpackPlugin injects the scripts
|
||||
// TODO: Find a better way to get these paths without
|
||||
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
|
||||
const head = assetTags.headTags.map((v) => {
|
||||
const href = v.attributes.href
|
||||
.replace('\\', '/')
|
||||
.replace('%5C', '/');
|
||||
|
||||
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${href}` };
|
||||
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||
});
|
||||
const body = assetTags.bodyTags.map((v) => {
|
||||
v.attributes = { src: `/${v.attributes.src}` };
|
||||
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||
});
|
||||
|
||||
return html
|
||||
.replace('<!-- webpack bundles head -->', head.join('\r\n '))
|
||||
.replace('<!-- webpack bundles body -->', body.join('\r\n '));
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
new OptimizeCssAssetsPlugin({}),
|
||||
|
||||
new webpack.DefinePlugin({
|
||||
__DEV__: !isProduction,
|
||||
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
|
||||
}),
|
||||
|
||||
new MiniCssExtractPlugin({
|
||||
filename: path.join('Content', 'styles.css')
|
||||
}),
|
||||
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'frontend/src/index.html',
|
||||
filename: 'index.html'
|
||||
})
|
||||
];
|
||||
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: '#source-map',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
},
|
||||
|
||||
watchOptions: {
|
||||
ignored: /node_modules/
|
||||
},
|
||||
|
||||
entry: {
|
||||
index: 'index.js'
|
||||
},
|
||||
|
||||
resolve: {
|
||||
modules: [
|
||||
srcFolder,
|
||||
path.join(srcFolder, 'Shims'),
|
||||
'node_modules'
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery'
|
||||
}
|
||||
},
|
||||
|
||||
output: {
|
||||
path: distFolder,
|
||||
filename: '[name].js',
|
||||
sourceMapFilename: '[file].map'
|
||||
},
|
||||
|
||||
optimization: {
|
||||
chunkIds: 'named',
|
||||
splitChunks: {
|
||||
chunks: 'initial'
|
||||
}
|
||||
},
|
||||
|
||||
performance: {
|
||||
hints: false
|
||||
},
|
||||
|
||||
plugins,
|
||||
|
||||
resolveLoader: {
|
||||
modules: [
|
||||
'node_modules',
|
||||
'frontend/gulp/webpack/'
|
||||
]
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
filename: '[name].js',
|
||||
inline: inlineWebWorkers
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js?$/,
|
||||
exclude: /(node_modules|JsLibraries)/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
configFile: `${frontendFolder}/babel.config.js`,
|
||||
envName: isProduction ? 'production' : 'development',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// CSS Modules
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /(node_modules|globals.css)/,
|
||||
use: [
|
||||
{ loader: MiniCssExtractPlugin.loader },
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
modules: {
|
||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
ident: 'postcss',
|
||||
config: {
|
||||
ctx: {
|
||||
cssVarsFiles
|
||||
},
|
||||
path: 'frontend/postcss.config.js'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Global styles
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: /(node_modules|globals.css)/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Fonts
|
||||
{
|
||||
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10240,
|
||||
mimetype: 'application/font-woff',
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
if (isProfiling) {
|
||||
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
|
||||
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
|
||||
|
||||
config.optimization.minimizer = [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true, // Must be set to true if using source-maps in production
|
||||
terserOptions: {
|
||||
mangle: false,
|
||||
keep_classnames: true,
|
||||
keep_fnames: true
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
gulp.task('webpack', () => {
|
||||
return webpackStream(config)
|
||||
.pipe(gulp.dest('_output/UI'));
|
||||
});
|
||||
|
||||
gulp.task('webpackWatch', () => {
|
||||
config.watch = true;
|
||||
|
||||
return webpackStream(config, webpack)
|
||||
.on('error', errorHandler)
|
||||
.pipe(gulp.dest('_output/UI'))
|
||||
.on('error', errorHandler)
|
||||
.pipe(livereload())
|
||||
.on('error', errorHandler);
|
||||
});
|
||||
@@ -1,23 +1,32 @@
|
||||
const reload = require('require-nocache')(module);
|
||||
|
||||
module.exports = (ctx, configPath, options) => {
|
||||
const config = {
|
||||
plugins: {
|
||||
'postcss-mixins': {
|
||||
mixinsDir: [
|
||||
'frontend/src/Styles/Mixins'
|
||||
]
|
||||
},
|
||||
'postcss-simple-vars': {
|
||||
variables: () =>
|
||||
ctx.options.cssVarsFiles.reduce((acc, vars) => {
|
||||
return Object.assign(acc, reload(vars));
|
||||
}, {})
|
||||
},
|
||||
'postcss-color-function': {},
|
||||
'postcss-nested': {}
|
||||
}
|
||||
};
|
||||
const cssVarsFiles = [
|
||||
'./src/Styles/Variables/colors',
|
||||
'./src/Styles/Variables/dimensions',
|
||||
'./src/Styles/Variables/fonts',
|
||||
'./src/Styles/Variables/animations',
|
||||
'./src/Styles/Variables/zIndexes'
|
||||
].map(require.resolve);
|
||||
|
||||
return config;
|
||||
const mixinsFiles = [
|
||||
'frontend/src/Styles/Mixins/cover.css',
|
||||
'frontend/src/Styles/Mixins/linkOverlay.css',
|
||||
'frontend/src/Styles/Mixins/scroller.css',
|
||||
'frontend/src/Styles/Mixins/truncate.css'
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
['postcss-mixins', {
|
||||
mixinsFiles
|
||||
}],
|
||||
['postcss-simple-vars', {
|
||||
variables: () =>
|
||||
cssVarsFiles.reduce((acc, vars) => {
|
||||
return Object.assign(acc, reload(vars));
|
||||
}, {})
|
||||
}],
|
||||
'postcss-color-function',
|
||||
'postcss-nested'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
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 } from 'Helpers/Props';
|
||||
import BlacklistRowConnector from './BlacklistRowConnector';
|
||||
|
||||
class Blacklist extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
isAuthorFetching,
|
||||
isAuthorPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
totalRecords,
|
||||
isClearingBlacklistExecuting,
|
||||
onClearBlacklistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const isAllPopulated = isPopulated && isAuthorPopulated;
|
||||
const isAnyFetching = isFetching || isAuthorFetching;
|
||||
|
||||
return (
|
||||
<PageContent title="Blacklist">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Clear"
|
||||
iconName={icons.CLEAR}
|
||||
isSpinning={isClearingBlacklistExecuting}
|
||||
onPress={onClearBlacklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isAnyFetching && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isAnyFetching && !!error &&
|
||||
<div>Unable to load blacklist</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !error && !items.length &&
|
||||
<div>
|
||||
No history blacklist
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlacklistRowConnector
|
||||
key={item.id}
|
||||
columns={columns}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Blacklist.propTypes = {
|
||||
isAuthorFetching: PropTypes.bool.isRequired,
|
||||
isAuthorPopulated: PropTypes.bool.isRequired,
|
||||
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,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
onClearBlacklistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Blacklist;
|
||||
242
frontend/src/Activity/Blocklist/Blocklist.js
Normal file
242
frontend/src/Activity/Blocklist/Blocklist.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
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,
|
||||
isAuthorFetching,
|
||||
isAuthorPopulated,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlocklistExecuting,
|
||||
onClearBlocklistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const isAllPopulated = isPopulated && isAuthorPopulated;
|
||||
const isAnyFetching = isFetching || isAuthorFetching;
|
||||
|
||||
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>
|
||||
{
|
||||
isAnyFetching && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isAnyFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToLoadBlocklist')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !error && !items.length &&
|
||||
<div>
|
||||
{translate('NoHistoryBlocklist')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !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('RemoveSelectedMessageText')}
|
||||
confirmLabel={translate('RemoveSelected')}
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Blocklist.propTypes = {
|
||||
isAuthorFetching: PropTypes.bool.isRequired,
|
||||
isAuthorPopulated: PropTypes.bool.isRequired,
|
||||
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;
|
||||
@@ -4,34 +4,34 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
||||
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 Blacklist from './Blacklist';
|
||||
import Blocklist from './Blocklist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blacklist,
|
||||
(state) => state.blocklist,
|
||||
(state) => state.authors,
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
|
||||
(blacklist, authors, isClearingBlacklistExecuting) => {
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||
(blocklist, authors, isClearingBlocklistExecuting) => {
|
||||
return {
|
||||
isAuthorFetching: authors.isFetching,
|
||||
isAuthorPopulated: authors.isPopulated,
|
||||
isClearingBlacklistExecuting,
|
||||
...blacklist
|
||||
isClearingBlocklistExecuting,
|
||||
...blocklist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...blacklistActions,
|
||||
...blocklistActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class BlacklistConnector extends Component {
|
||||
class BlocklistConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -39,27 +39,27 @@ class BlacklistConnector extends Component {
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchBlacklist,
|
||||
gotoBlacklistFirstPage
|
||||
fetchBlocklist,
|
||||
gotoBlocklistFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchBlacklist();
|
||||
fetchBlocklist();
|
||||
} else {
|
||||
gotoBlacklistFirstPage();
|
||||
gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearBlacklist();
|
||||
this.props.clearBlocklist();
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
}
|
||||
|
||||
@@ -67,45 +67,49 @@ class BlacklistConnector extends Component {
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchBlacklist();
|
||||
this.props.fetchBlocklist();
|
||||
}
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoBlacklistPreviousPage();
|
||||
this.props.gotoBlocklistPreviousPage();
|
||||
}
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoBlacklistNextPage();
|
||||
this.props.gotoBlocklistNextPage();
|
||||
}
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoBlacklistLastPage();
|
||||
this.props.gotoBlocklistLastPage();
|
||||
}
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoBlacklistPage({ page });
|
||||
this.props.gotoBlocklistPage({ page });
|
||||
}
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlocklistItems({ ids });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlacklistSort({ sortKey });
|
||||
this.props.setBlocklistSort({ sortKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlacklistTableOption(payload);
|
||||
this.props.setBlocklistTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
onClearBlacklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -113,37 +117,39 @@ class BlacklistConnector extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Blacklist
|
||||
<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}
|
||||
onClearBlacklistPress={this.onClearBlacklistPress}
|
||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlacklistConnector.propTypes = {
|
||||
BlocklistConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchBlacklist: PropTypes.func.isRequired,
|
||||
gotoBlacklistFirstPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||
setBlacklistSort: PropTypes.func.isRequired,
|
||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlacklist: PropTypes.func.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)(BlacklistConnector)
|
||||
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
|
||||
);
|
||||
@@ -8,8 +8,9 @@ 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 BlacklistDetailsModal extends Component {
|
||||
class BlocklistDetailsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -39,19 +40,19 @@ class BlacklistDetailsModal extends Component {
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Name"
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Protocol"
|
||||
title={translate('Protocol')}
|
||||
data={protocol}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Indexer"
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
}
|
||||
@@ -59,7 +60,7 @@ class BlacklistDetailsModal extends Component {
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Message"
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
@@ -77,7 +78,7 @@ class BlacklistDetailsModal extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
BlacklistDetailsModal.propTypes = {
|
||||
BlocklistDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
@@ -86,4 +87,4 @@ BlacklistDetailsModal.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlacklistDetailsModal;
|
||||
export default BlocklistDetailsModal;
|
||||
@@ -5,12 +5,14 @@ import BookQuality from 'Book/BookQuality';
|
||||
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 BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||
import styles from './BlacklistRow.css';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||
import styles from './BlocklistRow.css';
|
||||
|
||||
class BlacklistRow extends Component {
|
||||
class BlocklistRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -39,6 +41,7 @@ class BlacklistRow extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
author,
|
||||
sourceTitle,
|
||||
quality,
|
||||
@@ -46,7 +49,9 @@ class BlacklistRow extends Component {
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onRemovePress
|
||||
} = this.props;
|
||||
|
||||
@@ -56,6 +61,12 @@ class BlacklistRow extends Component {
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
@@ -67,7 +78,7 @@ class BlacklistRow extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'authors.sortName') {
|
||||
if (name === 'authorMetadata.sortName') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<AuthorNameLink
|
||||
@@ -131,7 +142,7 @@ class BlacklistRow extends Component {
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title="Remove from blacklist"
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRemovePress}
|
||||
@@ -144,7 +155,7 @@ class BlacklistRow extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
<BlacklistDetailsModal
|
||||
<BlocklistDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
@@ -158,7 +169,7 @@ class BlacklistRow extends Component {
|
||||
|
||||
}
|
||||
|
||||
BlacklistRow.propTypes = {
|
||||
BlocklistRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
author: PropTypes.object.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
@@ -167,8 +178,10 @@ BlacklistRow.propTypes = {
|
||||
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 BlacklistRow;
|
||||
export default BlocklistRow;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
|
||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import BlacklistRow from './BlacklistRow';
|
||||
import BlocklistRow from './BlocklistRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
@@ -18,9 +18,9 @@ function createMapStateToProps() {
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemovePress() {
|
||||
dispatch(removeFromBlacklist({ id: props.id }));
|
||||
dispatch(removeBlocklistItem({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
|
||||
@@ -9,6 +9,7 @@ import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryDetails.css';
|
||||
|
||||
function getDetailedList(statusMessages) {
|
||||
@@ -77,14 +78,14 @@ function HistoryDetails(props) {
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title="Indexer"
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
}
|
||||
@@ -93,7 +94,7 @@ function HistoryDetails(props) {
|
||||
!!releaseGroup &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Release Group"
|
||||
title={translate('ReleaseGroup')}
|
||||
data={releaseGroup}
|
||||
/>
|
||||
}
|
||||
@@ -114,7 +115,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!downloadClient &&
|
||||
<DescriptionListItem
|
||||
title="Download Client"
|
||||
title={translate('DownloadClient')}
|
||||
data={downloadClient}
|
||||
/>
|
||||
}
|
||||
@@ -122,7 +123,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!downloadId &&
|
||||
<DescriptionListItem
|
||||
title="Grab ID"
|
||||
title={translate('GrabID')}
|
||||
data={downloadId}
|
||||
/>
|
||||
}
|
||||
@@ -130,7 +131,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title="Age (when grabbed)"
|
||||
title={translate('AgeWhenGrabbed')}
|
||||
data={formatAge(age, ageHours, ageMinutes)}
|
||||
/>
|
||||
}
|
||||
@@ -138,7 +139,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!publishedDate &&
|
||||
<DescriptionListItem
|
||||
title="Published Date"
|
||||
title={translate('PublishedDate')}
|
||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/>
|
||||
}
|
||||
@@ -155,14 +156,14 @@ function HistoryDetails(props) {
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Message"
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
@@ -180,7 +181,7 @@ function HistoryDetails(props) {
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
@@ -188,7 +189,7 @@ function HistoryDetails(props) {
|
||||
!!droppedPath &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Source"
|
||||
title={translate('Source')}
|
||||
data={droppedPath}
|
||||
/>
|
||||
}
|
||||
@@ -197,7 +198,7 @@ function HistoryDetails(props) {
|
||||
!!importedPath &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Imported To"
|
||||
title={translate('ImportedTo')}
|
||||
data={importedPath}
|
||||
/>
|
||||
}
|
||||
@@ -214,13 +215,13 @@ function HistoryDetails(props) {
|
||||
|
||||
switch (reason) {
|
||||
case 'Manual':
|
||||
reasonMessage = 'File was deleted by via UI';
|
||||
reasonMessage = translate('FileWasDeletedByViaUI');
|
||||
break;
|
||||
case 'MissingFromDisk':
|
||||
reasonMessage = 'Readarr was unable to find the file on disk so it was removed';
|
||||
reasonMessage = translate('MissingFromDisk');
|
||||
break;
|
||||
case 'Upgrade':
|
||||
reasonMessage = 'File was deleted to import an upgrade';
|
||||
reasonMessage = translate('FileWasDeletedByUpgrade');
|
||||
break;
|
||||
default:
|
||||
reasonMessage = '';
|
||||
@@ -229,12 +230,12 @@ function HistoryDetails(props) {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Name"
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Reason"
|
||||
title={translate('Reason')}
|
||||
data={reasonMessage}
|
||||
/>
|
||||
</DescriptionList>
|
||||
@@ -250,12 +251,12 @@ function HistoryDetails(props) {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Source Path"
|
||||
title={translate('SourcePath')}
|
||||
data={sourcePath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Destination Path"
|
||||
title={translate('DestinationPath')}
|
||||
data={path}
|
||||
/>
|
||||
</DescriptionList>
|
||||
@@ -271,7 +272,7 @@ function HistoryDetails(props) {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Path"
|
||||
title={translate('Path')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
{
|
||||
@@ -286,7 +287,7 @@ function HistoryDetails(props) {
|
||||
})
|
||||
}
|
||||
<DescriptionListItem
|
||||
title="Existing tags scrubbed"
|
||||
title={translate('ExistingTagsScrubbed')}
|
||||
data={tagsScrubbed === 'True' ? <Icon name={icons.CHECK} /> : <Icon name={icons.REMOVE} />}
|
||||
/>
|
||||
</DescriptionList>
|
||||
@@ -301,14 +302,14 @@ function HistoryDetails(props) {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Name"
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!statusMessages &&
|
||||
<DescriptionListItem
|
||||
title="Import failures"
|
||||
title={translate('ImportFailures')}
|
||||
data={getDetailedList(JSON.parse(statusMessages))}
|
||||
/>
|
||||
}
|
||||
@@ -332,14 +333,14 @@ function HistoryDetails(props) {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Name"
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title="Indexer"
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
}
|
||||
@@ -347,7 +348,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!releaseGroup &&
|
||||
<DescriptionListItem
|
||||
title="Release Group"
|
||||
title={translate('ReleaseGroup')}
|
||||
data={releaseGroup}
|
||||
/>
|
||||
}
|
||||
@@ -368,7 +369,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!downloadClient &&
|
||||
<DescriptionListItem
|
||||
title="Download Client"
|
||||
title={translate('DownloadClient')}
|
||||
data={downloadClient}
|
||||
/>
|
||||
}
|
||||
@@ -376,7 +377,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!downloadId &&
|
||||
<DescriptionListItem
|
||||
title="Grab ID"
|
||||
title={translate('GrabID')}
|
||||
data={downloadId}
|
||||
/>
|
||||
}
|
||||
@@ -384,7 +385,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title="Age (when grabbed)"
|
||||
title={translate('AgeWhenGrabbed')}
|
||||
data={formatAge(age, ageHours, ageMinutes)}
|
||||
/>
|
||||
}
|
||||
@@ -392,7 +393,7 @@ function HistoryDetails(props) {
|
||||
{
|
||||
!!publishedDate &&
|
||||
<DescriptionListItem
|
||||
title="Published Date"
|
||||
title={translate('PublishedDate')}
|
||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/>
|
||||
}
|
||||
@@ -409,14 +410,14 @@ function HistoryDetails(props) {
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Message"
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
@@ -428,7 +429,7 @@ function HistoryDetails(props) {
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
@@ -12,32 +12,11 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
class History extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// Don't update when fetching has completed if items have changed,
|
||||
// before books start fetching or when books start fetching.
|
||||
|
||||
if (
|
||||
(
|
||||
this.props.isFetching &&
|
||||
nextProps.isPopulated &&
|
||||
hasDifferentItems(this.props.items, nextProps.items)
|
||||
) ||
|
||||
(!this.props.isBooksFetching && nextProps.isBooksFetching)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -66,11 +45,11 @@ class History extends Component {
|
||||
const hasError = error || booksError;
|
||||
|
||||
return (
|
||||
<PageContent title="History">
|
||||
<PageContent title={translate('History')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Refresh"
|
||||
label={translate('Refresh')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={onFirstPagePress}
|
||||
@@ -83,7 +62,7 @@ class History extends Component {
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
@@ -98,7 +77,7 @@ class History extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetchingAny && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -106,7 +85,9 @@ class History extends Component {
|
||||
|
||||
{
|
||||
!isFetchingAny && hasError &&
|
||||
<div>Unable to load history</div>
|
||||
<div>
|
||||
{translate('UnableToLoadHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -149,7 +130,7 @@ class History extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import History from './History';
|
||||
|
||||
@@ -29,9 +26,7 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...historyActions,
|
||||
fetchBooks,
|
||||
clearBooks
|
||||
...historyActions
|
||||
};
|
||||
|
||||
class HistoryConnector extends Component {
|
||||
@@ -55,21 +50,9 @@ class HistoryConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const bookIds = selectUniqueIds(this.props.items, 'bookId');
|
||||
if (bookIds.length) {
|
||||
this.props.fetchBooks({ bookIds });
|
||||
} else {
|
||||
this.props.clearBooks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearHistory();
|
||||
this.props.clearBooks();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -150,9 +133,7 @@ HistoryConnector.propTypes = {
|
||||
setHistorySort: PropTypes.func.isRequired,
|
||||
setHistoryFilter: PropTypes.func.isRequired,
|
||||
setHistoryTableOption: PropTypes.func.isRequired,
|
||||
clearHistory: PropTypes.func.isRequired,
|
||||
fetchBooks: PropTypes.func.isRequired,
|
||||
clearBooks: PropTypes.func.isRequired
|
||||
clearHistory: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
|
||||
@@ -23,8 +23,6 @@ function getIconName(eventType) {
|
||||
return icons.RETAG;
|
||||
case 'bookImportIncomplete':
|
||||
return icons.DOWNLOADED;
|
||||
case 'downloadImported':
|
||||
return icons.DOWNLOADED;
|
||||
case 'downloadIgnored':
|
||||
return icons.IGNORE;
|
||||
default:
|
||||
@@ -61,8 +59,6 @@ function getTooltip(eventType, data) {
|
||||
return 'Book file tags updated';
|
||||
case 'bookImportIncomplete':
|
||||
return 'Files downloaded but not all could be imported';
|
||||
case 'downloadImported':
|
||||
return 'Download completed and successfully imported';
|
||||
case 'downloadIgnored':
|
||||
return 'Book Download Ignored';
|
||||
default:
|
||||
|
||||
@@ -93,7 +93,7 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'authors.sortName') {
|
||||
if (name === 'authorMetadata.sortName') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<AuthorNameLink
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
@@ -13,7 +13,9 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } 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';
|
||||
@@ -30,30 +32,21 @@ class Queue extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._shouldBlockRefresh = false;
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isPendingSelected: false,
|
||||
isConfirmRemoveModalOpen: false
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// Don't update when fetching has completed if items have changed,
|
||||
// before books start fetching or when books start fetching.
|
||||
|
||||
if (
|
||||
this.props.isFetching &&
|
||||
nextProps.isPopulated &&
|
||||
hasDifferentItems(this.props.items, nextProps.items) &&
|
||||
nextProps.items.some((e) => e.bookId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.props.isBooksFetching && nextProps.isBooksFetching) {
|
||||
shouldComponentUpdate() {
|
||||
if (this._shouldBlockRefresh) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -61,9 +54,22 @@ class Queue extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
isBooksFetching
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(!isBooksFetching && prevProps.isBooksFetching) ||
|
||||
(!isFetching && prevProps.isFetching) ||
|
||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.bookId))
|
||||
) {
|
||||
this.setState((state) => {
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -89,6 +95,10 @@ class Queue extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
||||
this._shouldBlockRefresh = isOpen;
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
@@ -104,15 +114,19 @@ class Queue extends Component {
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -124,7 +138,6 @@ class Queue extends Component {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
isAuthorFetching,
|
||||
isAuthorPopulated,
|
||||
isBooksFetching,
|
||||
@@ -144,7 +157,8 @@ class Queue extends Component {
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isPendingSelected
|
||||
isPendingSelected,
|
||||
items
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isAuthorFetching || isBooksFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
@@ -155,11 +169,11 @@ class Queue extends Component {
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
return (
|
||||
<PageContent title="Queue">
|
||||
<PageContent title={translate('Queue')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Refresh"
|
||||
label={translate('Refresh')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={onRefreshPress}
|
||||
@@ -168,7 +182,7 @@ class Queue extends Component {
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Grab Selected"
|
||||
label={translate('GrabSelected')}
|
||||
iconName={icons.DOWNLOAD}
|
||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||
isSpinning={isGrabbing}
|
||||
@@ -176,7 +190,7 @@ class Queue extends Component {
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Remove Selected"
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={disableSelectedActions}
|
||||
isSpinning={isRemoving}
|
||||
@@ -193,14 +207,14 @@ class Queue extends Component {
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isRefreshing && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -209,14 +223,14 @@ class Queue extends Component {
|
||||
{
|
||||
!isRefreshing && hasError &&
|
||||
<div>
|
||||
Failed to load Queue
|
||||
{translate('FailedToLoadQueue')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !hasError && !items.length &&
|
||||
isAllPopulated && !hasError && !items.length &&
|
||||
<div>
|
||||
Queue is empty
|
||||
{translate('QueueIsEmpty')}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -243,6 +257,7 @@ class Queue extends Component {
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -257,7 +272,7 @@ class Queue extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
@@ -269,6 +284,17 @@ class Queue extends Component {
|
||||
return !!(item && item.authorId && item.bookId);
|
||||
})
|
||||
)}
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -4,12 +4,9 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Queue from './Queue';
|
||||
|
||||
@@ -37,8 +34,6 @@ function createMapStateToProps() {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...queueActions,
|
||||
fetchBooks,
|
||||
clearBooks,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
@@ -51,6 +46,7 @@ class QueueConnector extends Component {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchQueue,
|
||||
fetchQueueStatus,
|
||||
gotoQueueFirstPage
|
||||
} = this.props;
|
||||
|
||||
@@ -61,19 +57,11 @@ class QueueConnector extends Component {
|
||||
} else {
|
||||
gotoQueueFirstPage();
|
||||
}
|
||||
|
||||
fetchQueueStatus();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const bookIds = selectUniqueIds(this.props.items, 'bookId');
|
||||
|
||||
if (bookIds.length) {
|
||||
this.props.fetchBooks({ bookIds });
|
||||
} else {
|
||||
this.props.clearBooks();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.includeUnknownAuthorItems !==
|
||||
prevProps.includeUnknownAuthorItems
|
||||
@@ -84,8 +72,6 @@ class QueueConnector extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearQueue();
|
||||
this.props.clearBooks();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -171,6 +157,7 @@ QueueConnector.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeUnknownAuthorItems: PropTypes.bool.isRequired,
|
||||
fetchQueue: PropTypes.func.isRequired,
|
||||
fetchQueueStatus: PropTypes.func.isRequired,
|
||||
gotoQueueFirstPage: PropTypes.func.isRequired,
|
||||
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
||||
gotoQueueNextPage: PropTypes.func.isRequired,
|
||||
@@ -181,8 +168,6 @@ QueueConnector.propTypes = {
|
||||
clearQueue: PropTypes.func.isRequired,
|
||||
grabQueueItems: PropTypes.func.isRequired,
|
||||
removeQueueItems: PropTypes.func.isRequired,
|
||||
fetchBooks: PropTypes.func.isRequired,
|
||||
clearBooks: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function QueueDetails(props) {
|
||||
const {
|
||||
@@ -23,7 +24,7 @@ function QueueDetails(props) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.PENDING}
|
||||
title={`Release will be processed ${moment(estimatedCompletionTime).fromNow()}`}
|
||||
title={translate('ReleaseWillBeProcessedInterp', [moment(estimatedCompletionTime).fromNow()])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +35,7 @@ function QueueDetails(props) {
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.DANGER}
|
||||
title={`Import failed: ${errorMessage}`}
|
||||
title={translate('ImportFailedInterp', [errorMessage])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -47,7 +48,7 @@ function QueueDetails(props) {
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={`Download failed: ${errorMessage}`}
|
||||
title={translate('DownloadFailedInterp', [errorMessage])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -57,7 +58,7 @@ function QueueDetails(props) {
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title="Download failed: check download client for more details"
|
||||
title={translate('DownloadFailedCheckDownloadClientForMoreDetails')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -67,7 +68,7 @@ function QueueDetails(props) {
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.WARNING}
|
||||
title="Download warning: check download client for more details"
|
||||
title={translate('DownloadWarningCheckDownloadClientForMoreDetails')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -76,7 +77,7 @@ function QueueDetails(props) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
title={`Book is downloading - ${progress.toFixed(1)}% ${title}`}
|
||||
title={translate('BookIsDownloadingInterp', [progress.toFixed(1), title])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 {
|
||||
|
||||
@@ -54,13 +55,15 @@ class QueueOptions extends Component {
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>Show Unknown Author Items</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('ShowUnknownAuthorItems')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownAuthorItems"
|
||||
value={includeUnknownAuthorItems}
|
||||
helpText="Show items without a author in the queue, this could include removed authors, movies or anything else in Readarr's category"
|
||||
helpText={translate('IncludeUnknownAuthorItemsHelpText')}
|
||||
onChange={this.onOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -15,6 +15,8 @@ import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import TimeleftCell from './TimeleftCell';
|
||||
@@ -41,20 +43,33 @@ class QueueRow extends Component {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blacklist, skipredownload) => {
|
||||
this.props.onRemoveQueueItemPress(blacklist, skipredownload);
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -137,7 +152,7 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'authors.sortName') {
|
||||
if (name === 'authorMetadata.sortName') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
@@ -229,6 +244,12 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell key={name}>{formatBytes(size)}</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'outputPath') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
@@ -285,7 +306,7 @@ class QueueRow extends Component {
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
}
|
||||
title="Manual Download"
|
||||
title={translate('ManualDownload')}
|
||||
body="This release failed parsing checks and was manually downloaded from an interactive search. Import is likely to fail."
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
@@ -310,7 +331,7 @@ class QueueRow extends Component {
|
||||
}
|
||||
|
||||
<SpinnerIconButton
|
||||
title="Remove from queue"
|
||||
title={translate('RemoveFromQueue')}
|
||||
name={icons.REMOVE}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveQueueItemPress}
|
||||
@@ -335,7 +356,8 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!(author && book)}
|
||||
canIgnore={!!author}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
@@ -376,7 +398,8 @@ QueueRow.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
onRemoveQueueItemPress: PropTypes.func.isRequired
|
||||
onRemoveQueueItemPress: PropTypes.func.isRequired,
|
||||
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
QueueRow.defaultProps = {
|
||||
|
||||
@@ -4,6 +4,7 @@ import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueueStatusCell.css';
|
||||
|
||||
function getDetailedPopoverBody(statusMessages) {
|
||||
@@ -49,11 +50,7 @@ function QueueStatusCell(props) {
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let title = 'Downloading';
|
||||
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
let title = translate('Title');
|
||||
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
@@ -71,17 +68,24 @@ function QueueStatusCell(props) {
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ' - Waiting to Import';
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ' - Importing';
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ' - Waiting to Process';
|
||||
iconKind = kinds.DANGER;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = 'Pending';
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 {
|
||||
|
||||
@@ -21,7 +22,7 @@ class RemoveQueueItemModal extends Component {
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false,
|
||||
blocklist: false,
|
||||
skipredownload: false
|
||||
};
|
||||
}
|
||||
@@ -32,7 +33,7 @@ class RemoveQueueItemModal extends Component {
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false,
|
||||
blocklist: false,
|
||||
skipredownload: false
|
||||
});
|
||||
}
|
||||
@@ -44,8 +45,8 @@ class RemoveQueueItemModal extends Component {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
}
|
||||
|
||||
onSkipReDownloadChange = ({ value }) => {
|
||||
@@ -71,10 +72,11 @@ class RemoveQueueItemModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist, skipredownload } = this.state;
|
||||
const { remove, blocklist, skipredownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -94,40 +96,50 @@ class RemoveQueueItemModal extends Component {
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
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>Remove From Download Client</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText="Prevents Readarr from automatically grabbing this release again"
|
||||
onChange={this.onBlacklistChange}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blacklist &&
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>Skip Redownload</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipredownload"
|
||||
value={skipredownload}
|
||||
helpText="Prevents Readarr from trying download an alternative release for this item"
|
||||
helpText={translate('SkipredownloadHelpText')}
|
||||
onChange={this.onSkipReDownloadChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -157,6 +169,7 @@ 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
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 {
|
||||
@@ -22,7 +23,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false,
|
||||
blocklist: false,
|
||||
skipredownload: false
|
||||
};
|
||||
}
|
||||
@@ -33,7 +34,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false,
|
||||
blocklist: false,
|
||||
skipredownload: false
|
||||
});
|
||||
}
|
||||
@@ -45,8 +46,8 @@ class RemoveQueueItemsModal extends Component {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
}
|
||||
|
||||
onSkipReDownloadChange = ({ value }) => {
|
||||
@@ -72,10 +73,11 @@ class RemoveQueueItemsModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist, skipredownload } = this.state;
|
||||
const { remove, blocklist, skipredownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -95,42 +97,50 @@ class RemoveQueueItemsModal extends Component {
|
||||
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Blacklist Release{selectedCount > 1 ? 's' : ''}
|
||||
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText="Prevents Readarr from automatically grabbing these files again"
|
||||
onChange={this.onBlacklistChange}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blacklist &&
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>Skip Redownload</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipredownload"
|
||||
value={skipredownload}
|
||||
helpText="Prevents Readarr from trying download alternative releases for the removed items"
|
||||
helpText={translate('SkipredownloadHelpText')}
|
||||
onChange={this.onSkipReDownloadChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -160,6 +170,7 @@ 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
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ 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) {
|
||||
@@ -19,35 +20,35 @@ function TimeleftCell(props) {
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (status === 'Delay') {
|
||||
if (status === 'delay') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={`Delaying download until ${date} at ${time}`}
|
||||
title={translate('DelayingDownloadUntilInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={`Retrying download ${date} at ${time}`}
|
||||
title={translate('RetryingDownloadInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (!timeleft) {
|
||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
-
|
||||
|
||||
@@ -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 AuthorMonitorNewItemsOptionsPopoverContent() {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllBooks')}
|
||||
data={translate('DataNewAllBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('NewBooks')}
|
||||
data={translate('DataNewBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data={translate('DataNewNone')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthorMonitorNewItemsOptionsPopoverContent;
|
||||
@@ -1,45 +1,52 @@
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function AuthorMonitoringOptionsPopoverContent() {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="All Books"
|
||||
data="Monitor all books"
|
||||
/>
|
||||
<>
|
||||
<Alert>
|
||||
{translate('MonitoringOptionsHelpText')}
|
||||
</Alert>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllBooks')}
|
||||
data={translate('DataAllBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Future Books"
|
||||
data="Monitor books that have not released yet"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('FutureBooks')}
|
||||
data={translate('DataFutureBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Missing Books"
|
||||
data="Monitor books that do not have files or have not released yet"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('MissingBooks')}
|
||||
data={translate('DataMissingBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Existing Books"
|
||||
data="Monitor books that have files or have not released yet"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('ExistingBooks')}
|
||||
data={translate('DataExistingBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="First Book"
|
||||
data="Monitor the first book. All other books will be ignored"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('FirstBook')}
|
||||
data={translate('DataFirstBook')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Latest Book"
|
||||
data="Monitor the latest book and future books"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('LatestBook')}
|
||||
data={translate('DataLatestBook')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="None"
|
||||
data="No books will be monitored"
|
||||
/>
|
||||
</DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data={translate('DataNone')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title="Readarr">
|
||||
<DocumentTitle title={window.Readarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
|
||||
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||
import AuthorDetailsPageConnector from 'Author/Details/AuthorDetailsPageConnector';
|
||||
import AuthorEditorConnector from 'Author/Editor/AuthorEditorConnector';
|
||||
import AuthorIndexConnector from 'Author/Index/AuthorIndexConnector';
|
||||
import BookDetailsPageConnector from 'Book/Details/BookDetailsPageConnector';
|
||||
import BookIndexConnector from 'Book/Index/BookIndexConnector';
|
||||
import BookshelfConnector from 'Bookshelf/BookshelfConnector';
|
||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import AddNewItemConnector from 'Search/AddNewItemConnector';
|
||||
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||
@@ -70,19 +71,26 @@ function AppRoutes(props) {
|
||||
/>
|
||||
}
|
||||
|
||||
<Route
|
||||
path="/authors"
|
||||
component={AuthorIndexConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/add/search"
|
||||
component={AddNewItemConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/authoreditor"
|
||||
component={AuthorEditorConnector}
|
||||
exact={true}
|
||||
path="/shelf"
|
||||
component={BookshelfConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/bookshelf"
|
||||
component={BookshelfConnector}
|
||||
exact={true}
|
||||
path="/books"
|
||||
component={BookIndexConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
@@ -124,8 +132,8 @@ function AppRoutes(props) {
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/activity/blacklist"
|
||||
component={BlacklistConnector}
|
||||
path="/activity/blocklist"
|
||||
component={BlocklistConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
@@ -207,6 +215,11 @@ function AppRoutes(props) {
|
||||
component={UISettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/development"
|
||||
component={DevelopmentSettingsConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
System
|
||||
*/}
|
||||
|
||||
@@ -8,6 +8,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AppUpdatedModalContent.css';
|
||||
|
||||
function AppUpdatedModalContent(props) {
|
||||
@@ -38,7 +39,9 @@ function AppUpdatedModalContent(props) {
|
||||
<div>
|
||||
{
|
||||
!update.changes &&
|
||||
<div className={styles.maintenance}>Maintenance release</div>
|
||||
<div className={styles.maintenance}>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -49,12 +52,12 @@ function AppUpdatedModalContent(props) {
|
||||
</div>
|
||||
|
||||
<UpdateChanges
|
||||
title="New"
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title="Fixed"
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -122,9 +122,17 @@ class AuthorImage extends Component {
|
||||
placeholder,
|
||||
size,
|
||||
lazy,
|
||||
overflow
|
||||
overflow,
|
||||
blurBackground
|
||||
} = this.props;
|
||||
|
||||
const blurStyle = {
|
||||
...style,
|
||||
objectFit: 'fill',
|
||||
filter: 'blur(8px)',
|
||||
WebkitFilter: 'blur(8px)'
|
||||
};
|
||||
|
||||
const {
|
||||
url,
|
||||
hasError,
|
||||
@@ -168,13 +176,26 @@ class AuthorImage extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={isLoaded ? url : placeholder}
|
||||
onError={this.onError}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
<>
|
||||
{
|
||||
blurBackground ?
|
||||
<img
|
||||
style={blurStyle}
|
||||
src={isLoaded ? url : placeholder}
|
||||
onError={this.onError}
|
||||
onLoad={this.onLoad}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={isLoaded ? url : placeholder}
|
||||
onError={this.onError}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -188,6 +209,7 @@ AuthorImage.propTypes = {
|
||||
size: PropTypes.number.isRequired,
|
||||
lazy: PropTypes.bool.isRequired,
|
||||
overflow: PropTypes.bool.isRequired,
|
||||
blurBackground: PropTypes.bool.isRequired,
|
||||
onError: PropTypes.func,
|
||||
onLoad: PropTypes.func
|
||||
};
|
||||
@@ -195,7 +217,8 @@ AuthorImage.propTypes = {
|
||||
AuthorImage.defaultProps = {
|
||||
size: 250,
|
||||
lazy: true,
|
||||
overflow: false
|
||||
overflow: false,
|
||||
blurBackground: false
|
||||
};
|
||||
|
||||
export default AuthorImage;
|
||||
|
||||
@@ -8,7 +8,6 @@ function AuthorPoster(props) {
|
||||
return (
|
||||
<AuthorImage
|
||||
{...props}
|
||||
coverType="poster"
|
||||
placeholder={posterPlaceholder}
|
||||
/>
|
||||
);
|
||||
@@ -19,7 +18,8 @@ AuthorPoster.propTypes = {
|
||||
};
|
||||
|
||||
AuthorPoster.defaultProps = {
|
||||
size: 250
|
||||
size: 250,
|
||||
coverType: 'poster'
|
||||
};
|
||||
|
||||
export default AuthorPoster;
|
||||
|
||||
@@ -11,6 +11,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteAuthorModalContent.css';
|
||||
|
||||
class DeleteAuthorModalContent extends Component {
|
||||
@@ -67,7 +68,7 @@ class DeleteAuthorModalContent extends Component {
|
||||
const addImportListExclusion = this.state.addImportListExclusion;
|
||||
|
||||
let deleteFilesLabel = `Delete ${bookFileCount} Book Files`;
|
||||
let deleteFilesHelpText = 'Delete the book files and author folder';
|
||||
let deleteFilesHelpText = translate('DeleteFilesHelpText');
|
||||
|
||||
if (bookFileCount === 0) {
|
||||
deleteFilesLabel = 'Delete Author Folder';
|
||||
@@ -106,13 +107,15 @@ class DeleteAuthorModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Add List Exclusion</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('AddListExclusion')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="addImportListExclusion"
|
||||
value={addImportListExclusion}
|
||||
helpText="Prevent author from being added to Readarr by Import lists"
|
||||
helpText={translate('AddImportListExclusionHelpText')}
|
||||
kind={kinds.DANGER}
|
||||
onChange={this.onAddImportListExclusionChange}
|
||||
/>
|
||||
@@ -121,7 +124,9 @@ class DeleteAuthorModalContent extends Component {
|
||||
{
|
||||
deleteFiles &&
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
<div>The author folder <strong>{path}</strong> and all of its content will be deleted.</div>
|
||||
<div>
|
||||
{translate('TheAuthorFolderAndAllOfItsContentWillBeDeleted', [path])}
|
||||
</div>
|
||||
|
||||
{
|
||||
!!bookFileCount &&
|
||||
|
||||
@@ -2,55 +2,12 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.backdropOverlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $black;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.headerContent {
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex-shrink: 0;
|
||||
margin-right: 35px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.metadataMessage {
|
||||
color: $helpTextColor;
|
||||
text-align: center;
|
||||
@@ -58,97 +15,6 @@
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.toggleMonitoredContainer {
|
||||
align-self: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.monitorToggleButton {
|
||||
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||
|
||||
width: 40px;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
|
||||
.alternateTitlesIconContainer {
|
||||
align-self: flex-end;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.authorNavigationButtons {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.authorNavigationButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
margin-left: 5px;
|
||||
width: 30px;
|
||||
color: #e1e2e3;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.runtime {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.detailsLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.path,
|
||||
.sizeOnDisk,
|
||||
.qualityProfileName,
|
||||
.links,
|
||||
.tags {
|
||||
margin-left: 8px;
|
||||
font-weight: 300;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.overview {
|
||||
flex: 1 0 auto;
|
||||
margin-top: 8px;
|
||||
min-height: 0;
|
||||
font-size: $intermediateFontSize;
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -178,18 +44,43 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.authorNavigationButtons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
margin-top: 10px;
|
||||
padding: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.authorUpButton,
|
||||
.authorNavigationButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
margin-left: 5px;
|
||||
width: 30px;
|
||||
color: #e1e2e3;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.contentContainer {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.headerContent {
|
||||
.authorNavigationButtons {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
.poster {
|
||||
.authorNavigationButtons {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.authorNavigationButton {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,37 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import AuthorPoster from 'Author/AuthorPoster';
|
||||
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
|
||||
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||
import AuthorAlternateTitles from './AuthorAlternateTitles';
|
||||
import AuthorDetailsLinks from './AuthorDetailsLinks';
|
||||
import AuthorDetailsHeaderConnector from './AuthorDetailsHeaderConnector';
|
||||
import AuthorDetailsSeasonConnector from './AuthorDetailsSeasonConnector';
|
||||
import AuthorDetailsSeriesConnector from './AuthorDetailsSeriesConnector';
|
||||
import AuthorTagsConnector from './AuthorTagsConnector';
|
||||
import styles from './AuthorDetails.css';
|
||||
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
function getFanartUrl(images) {
|
||||
const fanartImage = _.find(images, { coverType: 'fanart' });
|
||||
if (fanartImage) {
|
||||
// Remove protocol
|
||||
return fanartImage.url.replace(/^https?:/, '');
|
||||
}
|
||||
}
|
||||
|
||||
function getExpandedState(newState) {
|
||||
return {
|
||||
allExpanded: newState.allSelected,
|
||||
@@ -75,13 +54,57 @@ class AuthorDetails extends Component {
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isMonitorOptionsModalOpen: false,
|
||||
isEditorActive: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {},
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
selectedTabIndex: 0
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setSelectedState = (items) => {
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((item) => {
|
||||
const isItemSelected = selectedState[item.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[item.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[item.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
}
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -128,6 +151,18 @@ class AuthorDetails extends Component {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
|
||||
onMonitorOptionsPress = () => {
|
||||
this.setState({ isMonitorOptionsModalOpen: true });
|
||||
}
|
||||
|
||||
onMonitorOptionsClose = () => {
|
||||
this.setState({ isMonitorOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
onBookEditorTogglePress = () => {
|
||||
this.setState({ isEditorActive: !this.state.isEditorActive });
|
||||
}
|
||||
|
||||
onExpandAllPress = () => {
|
||||
const {
|
||||
allExpanded,
|
||||
@@ -151,6 +186,27 @@ class AuthorDetails extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectAllPress = () => {
|
||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||
}
|
||||
|
||||
onSelectedChange = (items, id, value, shiftKey = false) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSelected = (changes) => {
|
||||
this.props.onSaveSelected({
|
||||
bookIds: this.getSelectedIds(),
|
||||
...changes
|
||||
});
|
||||
}
|
||||
|
||||
onTabSelect = (index, lastIndex) => {
|
||||
this.setState({ selectedTabIndex: index });
|
||||
}
|
||||
@@ -162,18 +218,8 @@ class AuthorDetails extends Component {
|
||||
const {
|
||||
id,
|
||||
authorName,
|
||||
ratings,
|
||||
path,
|
||||
statistics,
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
status,
|
||||
overview,
|
||||
links,
|
||||
images,
|
||||
alternateTitles,
|
||||
tags,
|
||||
isSaving,
|
||||
isRefreshing,
|
||||
isSearching,
|
||||
isFetching,
|
||||
@@ -187,38 +233,31 @@ class AuthorDetails extends Component {
|
||||
hasBookFiles,
|
||||
previousAuthor,
|
||||
nextAuthor,
|
||||
onMonitorTogglePress,
|
||||
onRefreshPress,
|
||||
onSearchPress
|
||||
onSearchPress,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
statistics
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookFileCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isRetagModalOpen,
|
||||
isEditAuthorModalOpen,
|
||||
isDeleteAuthorModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isMonitorOptionsModalOpen,
|
||||
isEditorActive,
|
||||
allSelected,
|
||||
selectedState,
|
||||
allExpanded,
|
||||
allCollapsed,
|
||||
expandedState,
|
||||
selectedTabIndex
|
||||
} = this.state;
|
||||
|
||||
const continuing = status === 'continuing';
|
||||
|
||||
let bookFilesCountMessage = 'No book files';
|
||||
|
||||
if (bookFileCount === 1) {
|
||||
bookFilesCountMessage = '1 book file';
|
||||
} else if (bookFileCount > 1) {
|
||||
bookFilesCountMessage = `${bookFileCount} book files`;
|
||||
}
|
||||
|
||||
let expandIcon = icons.EXPAND_INDETERMINATE;
|
||||
|
||||
if (allExpanded) {
|
||||
@@ -227,46 +266,48 @@ class AuthorDetails extends Component {
|
||||
expandIcon = icons.EXPAND;
|
||||
}
|
||||
|
||||
const selectedBookIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={authorName}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Refresh & Scan"
|
||||
label={translate('RefreshAndScan')}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
title="Refresh information and scan disk"
|
||||
title={translate('RefreshInformationAndScanDisk')}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Search Monitored"
|
||||
label={translate('SearchMonitored')}
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!monitored || !hasMonitoredBooks || !hasBooks}
|
||||
isSpinning={isSearching}
|
||||
title={hasMonitoredBooks ? undefined : 'No monitored books for this author'}
|
||||
title={hasMonitoredBooks ? undefined : translate('HasMonitoredBooksNoMonitoredBooksForThisAuthor')}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Preview Rename"
|
||||
label={translate('PreviewRename')}
|
||||
iconName={icons.ORGANIZE}
|
||||
isDisabled={!hasBookFiles}
|
||||
onPress={this.onOrganizePress}
|
||||
/>
|
||||
|
||||
{/* <PageToolbarButton */}
|
||||
{/* label="Preview Retag" */}
|
||||
{/* iconName={icons.RETAG} */}
|
||||
{/* isDisabled={!hasBookFiles} */}
|
||||
{/* onPress={this.onRetagPress} */}
|
||||
{/* /> */}
|
||||
<PageToolbarButton
|
||||
label={translate('PreviewRetag')}
|
||||
iconName={icons.RETAG}
|
||||
isDisabled={!hasBookFiles}
|
||||
onPress={this.onRetagPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Manual Import"
|
||||
label={translate('ManualImport')}
|
||||
iconName={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
@@ -274,256 +315,98 @@ class AuthorDetails extends Component {
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Edit"
|
||||
label={translate('BookMonitoring')}
|
||||
iconName={icons.MONITORED}
|
||||
onPress={this.onMonitorOptionsPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Edit')}
|
||||
iconName={icons.EDIT}
|
||||
onPress={this.onEditAuthorPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Delete"
|
||||
label={translate('Delete')}
|
||||
iconName={icons.DELETE}
|
||||
onPress={this.onDeleteAuthorPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={translate('BookList')}
|
||||
iconName={icons.AUTHOR_CONTINUING}
|
||||
onPress={this.onBookEditorTogglePress}
|
||||
/> :
|
||||
<PageToolbarButton
|
||||
label={translate('BookEditor')}
|
||||
iconName={icons.EDIT}
|
||||
onPress={this.onBookEditorTogglePress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={icons.CHECK_SQUARE}
|
||||
onPress={this.onSelectAllPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<PageToolbarButton
|
||||
label={allExpanded ? 'Collapse All' : 'Expand All'}
|
||||
label={allExpanded ? translate('AllExpandedCollapseAll') : translate('AllExpandedExpandAll')}
|
||||
iconName={expandIcon}
|
||||
onPress={this.onExpandAllPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector innerClassName={styles.innerContentBody}>
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={styles.backdrop}
|
||||
style={{
|
||||
backgroundImage: `url(${getFanartUrl(images)})`
|
||||
}}
|
||||
>
|
||||
<div className={styles.backdropOverlay} />
|
||||
</div>
|
||||
|
||||
<div className={styles.headerContent}>
|
||||
<AuthorPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
<PageContentBody innerClassName={styles.innerContentBody}>
|
||||
<SwipeHeaderConnector
|
||||
className={styles.header}
|
||||
nextLink={`/author/${nextAuthor.titleSlug}`}
|
||||
nextComponent={(width) => <AuthorDetailsHeaderConnector authorId={nextAuthor.id} width={width} />}
|
||||
prevLink={`/author/${previousAuthor.titleSlug}`}
|
||||
prevComponent={(width) => <AuthorDetailsHeaderConnector authorId={previousAuthor.id} width={width} />}
|
||||
currentComponent={(width) => <AuthorDetailsHeaderConnector authorId={id} width={width} />}
|
||||
>
|
||||
<div className={styles.authorNavigationButtons}>
|
||||
<IconButton
|
||||
className={styles.authorNavigationButton}
|
||||
name={icons.ARROW_LEFT}
|
||||
size={30}
|
||||
title={translate('GoToInterp', [previousAuthor.authorName])}
|
||||
to={`/author/${previousAuthor.titleSlug}`}
|
||||
/>
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
size={40}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
className={styles.authorUpButton}
|
||||
name={icons.ARROW_UP}
|
||||
size={30}
|
||||
title={translate('GoToAuthorListing')}
|
||||
to={{
|
||||
pathname: '/',
|
||||
state: { restoreScrollPosition: true }
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={styles.title}>
|
||||
{authorName}
|
||||
</div>
|
||||
|
||||
{
|
||||
!!alternateTitles.length &&
|
||||
<div className={styles.alternateTitlesIconContainer}>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.ALTERNATE_TITLES}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
title="Alternate Titles"
|
||||
body={<AuthorAlternateTitles alternateTitles={alternateTitles} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.authorNavigationButtons}>
|
||||
<IconButton
|
||||
className={styles.authorNavigationButton}
|
||||
name={icons.ARROW_LEFT}
|
||||
size={30}
|
||||
title={`Go to ${previousAuthor.authorName}`}
|
||||
to={`/author/${previousAuthor.titleSlug}`}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.authorNavigationButton}
|
||||
name={icons.ARROW_UP}
|
||||
size={30}
|
||||
title={'Go to author listing'}
|
||||
to={'/'}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.authorNavigationButton}
|
||||
name={icons.ARROW_RIGHT}
|
||||
size={30}
|
||||
title={`Go to ${nextAuthor.authorName}`}
|
||||
to={`/author/${nextAuthor.titleSlug}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
iconSize={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.detailsLabels}>
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.FOLDER}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.path}>
|
||||
{path}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={bookFilesCountMessage}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title="Quality Profile"
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.PROFILE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? 'Monitored' : 'Unmonitored'}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={continuing ? 'More books are expected' : 'No additional books are expected'}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{continuing ? 'Continuing' : 'Deceased'}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
Links
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
<AuthorDetailsLinks
|
||||
links={links}
|
||||
/>
|
||||
}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
|
||||
{
|
||||
!!tags.length &&
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.TAGS}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.tags}>
|
||||
Tags
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={<AuthorTagsConnector authorId={id} />}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
|
||||
}
|
||||
</div>
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
line={Math.floor(125 / (defaultFontSize * lineHeight))}
|
||||
text={stripHtml(overview)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<IconButton
|
||||
className={styles.authorNavigationButton}
|
||||
name={icons.ARROW_RIGHT}
|
||||
size={30}
|
||||
title={translate('GoToInterp', [nextAuthor.authorName])}
|
||||
to={`/author/${nextAuthor.titleSlug}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SwipeHeaderConnector>
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
@@ -533,12 +416,16 @@ class AuthorDetails extends Component {
|
||||
|
||||
{
|
||||
!isFetching && booksError &&
|
||||
<div>Loading books failed</div>
|
||||
<div>
|
||||
{translate('LoadingBooksFailed')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && bookFilesError &&
|
||||
<div>Loading book files failed</div>
|
||||
<div>
|
||||
{translate('LoadingBookFilesFailed')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -551,35 +438,35 @@ class AuthorDetails extends Component {
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
Books
|
||||
{translate('BooksTotal', [statistics.totalBookCount])}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
Series
|
||||
{translate('SeriesTotal', [series.length])}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
History
|
||||
{translate('History')}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
Search
|
||||
{translate('Search')}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
Files
|
||||
{translate('FilesTotal', [statistics.bookFileCount])}
|
||||
</Tab>
|
||||
|
||||
{
|
||||
@@ -596,7 +483,11 @@ class AuthorDetails extends Component {
|
||||
<AuthorDetailsSeasonConnector
|
||||
authorId={id}
|
||||
isExpanded={true}
|
||||
selectedState={selectedState}
|
||||
onExpandPress={this.onExpandPress}
|
||||
setSelectedState={this.setSelectedState}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
isEditorActive={isEditorActive}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -641,14 +532,13 @@ class AuthorDetails extends Component {
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.metadataMessage}>
|
||||
Missing or too many books? Modify or create a new
|
||||
<Link to='/settings/profiles'> Metadata Profile </Link>
|
||||
{translate('TooManyBooks')}
|
||||
<Link to='/settings/profiles'> {translate('MetadataProfile')} </Link>
|
||||
or manually
|
||||
<Link to={`/add/search?term=${encodeURIComponent(authorName)}`}> Search </Link>
|
||||
<Link to={`/add/search?term=${encodeURIComponent(authorName)}`}> {translate('Search')} </Link>
|
||||
for new items!
|
||||
</div>
|
||||
|
||||
@@ -686,7 +576,26 @@ class AuthorDetails extends Component {
|
||||
showImportMode={false}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
|
||||
<MonitoringOptionsModal
|
||||
isOpen={isMonitorOptionsModalOpen}
|
||||
authorId={id}
|
||||
onModalClose={this.onMonitorOptionsClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isEditorActive &&
|
||||
<BookEditorFooter
|
||||
bookIds={selectedBookIds}
|
||||
selectedCount={selectedBookIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
}
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -706,7 +615,6 @@ AuthorDetails.propTypes = {
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
@@ -720,15 +628,20 @@ AuthorDetails.propTypes = {
|
||||
hasBookFiles: PropTypes.bool.isRequired,
|
||||
previousAuthor: PropTypes.object.isRequired,
|
||||
nextAuthor: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AuthorDetails.defaultProps = {
|
||||
statistics: {},
|
||||
tags: [],
|
||||
isSaving: false
|
||||
tags: []
|
||||
};
|
||||
|
||||
export default AuthorDetails;
|
||||
|
||||
@@ -6,14 +6,15 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { toggleAuthorMonitored } from 'Store/Actions/authorActions';
|
||||
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
|
||||
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
|
||||
import { saveBookEditor } from 'Store/Actions/bookIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
import { clearSeries, fetchSeries } from 'Store/Actions/seriesActions';
|
||||
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
@@ -21,7 +22,8 @@ import AuthorDetails from './AuthorDetails';
|
||||
|
||||
const selectBooks = createSelector(
|
||||
(state) => state.books,
|
||||
(books) => {
|
||||
(state) => state.bookIndex,
|
||||
(books, index) => {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
@@ -29,6 +31,13 @@ const selectBooks = createSelector(
|
||||
error
|
||||
} = books;
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = index;
|
||||
|
||||
const hasBooks = !!items.length;
|
||||
const hasMonitoredBooks = items.some((e) => e.monitored);
|
||||
|
||||
@@ -37,7 +46,11 @@ const selectBooks = createSelector(
|
||||
isBooksPopulated: isPopulated,
|
||||
booksError: error,
|
||||
hasBooks,
|
||||
hasMonitoredBooks
|
||||
hasMonitoredBooks,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -94,8 +107,9 @@ function createMapStateToProps() {
|
||||
selectBookFiles,
|
||||
createAllAuthorSelector(),
|
||||
createCommandsSelector(),
|
||||
(titleSlug, books, series, bookFiles, allAuthors, commands) => {
|
||||
const sortedAuthor = _.orderBy(allAuthors, 'sortName');
|
||||
createDimensionsSelector(),
|
||||
(titleSlug, books, series, bookFiles, allAuthors, commands, dimensions) => {
|
||||
const sortedAuthor = _.orderBy(allAuthors, 'sortNameLastFirst');
|
||||
const authorIndex = _.findIndex(sortedAuthor, { titleSlug });
|
||||
const author = sortedAuthor[authorIndex];
|
||||
|
||||
@@ -108,7 +122,11 @@ function createMapStateToProps() {
|
||||
isBooksPopulated,
|
||||
booksError,
|
||||
hasBooks,
|
||||
hasMonitoredBooks
|
||||
hasMonitoredBooks,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = books;
|
||||
|
||||
const {
|
||||
@@ -168,6 +186,10 @@ function createMapStateToProps() {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
booksError,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
seriesError,
|
||||
bookFilesError,
|
||||
hasBooks,
|
||||
@@ -176,17 +198,17 @@ function createMapStateToProps() {
|
||||
series: seriesItems,
|
||||
hasBookFiles,
|
||||
previousAuthor,
|
||||
nextAuthor
|
||||
nextAuthor,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchBooks,
|
||||
clearBooks,
|
||||
fetchSeries,
|
||||
clearSeries,
|
||||
saveBookEditor,
|
||||
fetchBookFiles,
|
||||
clearBookFiles,
|
||||
toggleAuthorMonitored,
|
||||
@@ -245,7 +267,6 @@ class AuthorDetailsConnector extends Component {
|
||||
populate = () => {
|
||||
const authorId = this.props.id;
|
||||
|
||||
this.props.fetchBooks({ authorId });
|
||||
this.props.fetchSeries({ authorId });
|
||||
this.props.fetchBookFiles({ authorId });
|
||||
this.props.fetchQueueDetails({ authorId });
|
||||
@@ -253,7 +274,6 @@ class AuthorDetailsConnector extends Component {
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.cancelFetchReleases();
|
||||
this.props.clearBooks();
|
||||
this.props.clearSeries();
|
||||
this.props.clearBookFiles();
|
||||
this.props.clearQueueDetails();
|
||||
@@ -284,6 +304,10 @@ class AuthorDetailsConnector extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.saveBookEditor(payload);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -294,6 +318,7 @@ class AuthorDetailsConnector extends Component {
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onSearchPress={this.onSearchPress}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -307,10 +332,9 @@ AuthorDetailsConnector.propTypes = {
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
isRenamingFiles: PropTypes.bool.isRequired,
|
||||
isRenamingAuthor: PropTypes.bool.isRequired,
|
||||
fetchBooks: PropTypes.func.isRequired,
|
||||
clearBooks: PropTypes.func.isRequired,
|
||||
fetchSeries: PropTypes.func.isRequired,
|
||||
clearSeries: PropTypes.func.isRequired,
|
||||
saveBookEditor: PropTypes.func.isRequired,
|
||||
fetchBookFiles: PropTypes.func.isRequired,
|
||||
clearBookFiles: PropTypes.func.isRequired,
|
||||
toggleAuthorMonitored: PropTypes.func.isRequired,
|
||||
|
||||
148
frontend/src/Author/Details/AuthorDetailsHeader.css
Normal file
148
frontend/src/Author/Details/AuthorDetailsHeader.css
Normal file
@@ -0,0 +1,148 @@
|
||||
.header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.backdropOverlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $black;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.headerContent {
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex-shrink: 0;
|
||||
margin-right: 35px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 50px;
|
||||
line-height: 60px;
|
||||
}
|
||||
|
||||
.toggleMonitoredContainer {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.monitorToggleButton {
|
||||
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||
|
||||
width: 40px;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
|
||||
.alternateTitlesIconContainer {
|
||||
align-self: flex-end;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.authorNavigationButtons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
margin-top: 10px;
|
||||
padding: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.authorUpButton,
|
||||
.authorNavigationButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
margin-left: 5px;
|
||||
width: 30px;
|
||||
color: #e1e2e3;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.detailsLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.path,
|
||||
.sizeOnDisk,
|
||||
.qualityProfileName,
|
||||
.links,
|
||||
.tags {
|
||||
margin-left: 8px;
|
||||
font-weight: 300;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.overview {
|
||||
flex: 1 1 auto;
|
||||
margin-top: 8px;
|
||||
min-height: 0;
|
||||
font-size: $intermediateFontSize;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.headerContent {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 30px;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
.poster {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
340
frontend/src/Author/Details/AuthorDetailsHeader.js
Normal file
340
frontend/src/Author/Details/AuthorDetailsHeader.js
Normal file
@@ -0,0 +1,340 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import AuthorPoster from 'Author/AuthorPoster';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Marquee from 'Components/Marquee';
|
||||
import Measure from 'Components/Measure';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorAlternateTitles from './AuthorAlternateTitles';
|
||||
import AuthorDetailsLinks from './AuthorDetailsLinks';
|
||||
import AuthorTagsConnector from './AuthorTagsConnector';
|
||||
import styles from './AuthorDetailsHeader.css';
|
||||
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
function getFanartUrl(images) {
|
||||
const fanartImage = images.find((x) => x.coverType === 'fanart');
|
||||
|
||||
if (fanartImage) {
|
||||
// Remove protocol
|
||||
return fanartImage.url.replace(/^https?:/, '');
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorDetailsHeader extends Component {
|
||||
|
||||
//
|
||||
// Lifecyle
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
overviewHeight: 0,
|
||||
titleWidth: 0
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOverviewMeasure = ({ height }) => {
|
||||
this.setState({ overviewHeight: height });
|
||||
}
|
||||
|
||||
onTitleMeasure = ({ width }) => {
|
||||
this.setState({ titleWidth: width });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
width,
|
||||
authorName,
|
||||
ratings,
|
||||
path,
|
||||
statistics,
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
status,
|
||||
overview,
|
||||
links,
|
||||
images,
|
||||
alternateTitles,
|
||||
tags,
|
||||
isSaving,
|
||||
isSmallScreen,
|
||||
onMonitorTogglePress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookFileCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
overviewHeight,
|
||||
titleWidth
|
||||
} = this.state;
|
||||
|
||||
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
||||
|
||||
const continuing = status === 'continuing';
|
||||
|
||||
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
||||
|
||||
if (bookFileCount === 1) {
|
||||
bookFilesCountMessage = '1 book file';
|
||||
} else if (bookFileCount > 1) {
|
||||
bookFilesCountMessage = `${bookFileCount} book files`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.header} style={{ width }} >
|
||||
<div
|
||||
className={styles.backdrop}
|
||||
style={{
|
||||
backgroundImage: `url(${getFanartUrl(images)})`
|
||||
}}
|
||||
>
|
||||
<div className={styles.backdropOverlay} />
|
||||
</div>
|
||||
|
||||
<div className={styles.headerContent}>
|
||||
<AuthorPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
/>
|
||||
|
||||
<div className={styles.info}>
|
||||
<Measure
|
||||
className={styles.titleRow}
|
||||
onMeasure={this.onTitleMeasure}
|
||||
>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
size={isSmallScreen ? 30: 40}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.title} style={{ width: marqueeWidth }}>
|
||||
<Marquee text={authorName} />
|
||||
</div>
|
||||
|
||||
{
|
||||
!!alternateTitles.length &&
|
||||
<div className={styles.alternateTitlesIconContainer}>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.ALTERNATE_TITLES}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
title={translate('AlternateTitles')}
|
||||
body={<AuthorAlternateTitles alternateTitles={alternateTitles} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Measure>
|
||||
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
iconSize={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.detailsLabels}>
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.FOLDER}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.path}>
|
||||
{path}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={bookFilesCountMessage}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={translate('QualityProfile')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.PROFILE}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{monitored ? 'Monitored' : 'Unmonitored'}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.qualityProfileName}>
|
||||
{continuing ? 'Continuing' : 'Deceased'}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
Links
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
<AuthorDetailsLinks
|
||||
links={links}
|
||||
/>
|
||||
}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
|
||||
{
|
||||
!!tags.length &&
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.TAGS}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.tags}>
|
||||
Tags
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={<AuthorTagsConnector authorId={id} />}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
|
||||
}
|
||||
</div>
|
||||
<Measure
|
||||
onMeasure={this.onOverviewMeasure}
|
||||
className={styles.overview}
|
||||
>
|
||||
<TextTruncate
|
||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||
text={stripHtml(overview)}
|
||||
/>
|
||||
</Measure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthorDetailsHeader.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
width: PropTypes.number.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthorDetailsHeader;
|
||||
71
frontend/src/Author/Details/AuthorDetailsHeaderConnector.js
Normal file
71
frontend/src/Author/Details/AuthorDetailsHeaderConnector.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/* eslint max-params: 0 */
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { toggleAuthorMonitored } from 'Store/Actions/authorActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import AuthorDetailsHeader from './AuthorDetailsHeader';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.authors,
|
||||
createAuthorSelector(),
|
||||
createDimensionsSelector(),
|
||||
(authors, author, dimensions) => {
|
||||
const alternateTitles = _.reduce(author.alternateTitles, (acc, alternateTitle) => {
|
||||
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
|
||||
(alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined)) {
|
||||
acc.push(alternateTitle.title);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...author,
|
||||
isSaving: authors.isSaving,
|
||||
alternateTitles,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleAuthorMonitored
|
||||
};
|
||||
|
||||
class AuthorDetailsHeaderConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMonitorTogglePress = (monitored) => {
|
||||
this.props.toggleAuthorMonitored({
|
||||
authorId: this.props.authorId,
|
||||
monitored
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AuthorDetailsHeader
|
||||
{...this.props}
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthorDetailsHeaderConnector.propTypes = {
|
||||
authorId: PropTypes.number.isRequired,
|
||||
toggleAuthorMonitored: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthorDetailsHeaderConnector);
|
||||
@@ -7,8 +7,9 @@ import { createSelector } from 'reselect';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorDetailsConnector from './AuthorDetailsConnector';
|
||||
import styles from './AuthorDetails.css';
|
||||
|
||||
@@ -74,9 +75,9 @@ class AuthorDetailsPageConnector extends Component {
|
||||
if (isFetching && !isPopulated) {
|
||||
return (
|
||||
<PageContent title='loading'>
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<LoadingIndicator />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -92,7 +93,7 @@ class AuthorDetailsPageConnector extends Component {
|
||||
if (!titleSlug) {
|
||||
return (
|
||||
<NotFound
|
||||
message="Sorry, that author cannot be found."
|
||||
message={translate('SorryThatAuthorCannotBeFound')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import getToggledRange from 'Utilities/Table/getToggledRange';
|
||||
import BookRowConnector from './BookRowConnector';
|
||||
import styles from './AuthorDetailsSeason.css';
|
||||
@@ -21,6 +22,26 @@ class AuthorDetailsSeason extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.setSelectedState(this.props.items);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
setSelectedState
|
||||
} = this.props;
|
||||
|
||||
if (sortKey !== prevProps.sortKey ||
|
||||
sortDirection !== prevProps.sortDirection ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
setSelectedState(items);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -42,26 +63,42 @@ class AuthorDetailsSeason extends Component {
|
||||
this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
const {
|
||||
onSelectedChange,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
return onSelectedChange(items, id, value, shiftKey);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isEditorActive,
|
||||
columns,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
onSortPress,
|
||||
onTableOptionChange
|
||||
onTableOptionChange,
|
||||
selectedState
|
||||
} = this.props;
|
||||
|
||||
let titleColumns = columns;
|
||||
if (!isEditorActive) {
|
||||
titleColumns = columns.filter((x) => x.name !== 'select');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.bookType}
|
||||
>
|
||||
<div className={styles.books}>
|
||||
<Table
|
||||
columns={columns}
|
||||
columns={titleColumns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
@@ -76,6 +113,9 @@ class AuthorDetailsSeason extends Component {
|
||||
columns={columns}
|
||||
{...item}
|
||||
onMonitorBookPress={this.onMonitorBookPress}
|
||||
isEditorActive={isEditorActive}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -92,9 +132,13 @@ AuthorDetailsSeason.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onExpandPress: PropTypes.func.isRequired,
|
||||
setSelectedState: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onMonitorBookPress: PropTypes.func.isRequired,
|
||||
uiSettings: PropTypes.object.isRequired
|
||||
|
||||
@@ -4,24 +4,22 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setBooksSort, setBooksTableOption, toggleBooksMonitored } from 'Store/Actions/bookActions';
|
||||
import { setAuthorDetailsId, setAuthorDetailsSort } from 'Store/Actions/authorDetailsActions';
|
||||
import { setBooksTableOption, toggleBooksMonitored } from 'Store/Actions/bookActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import AuthorDetailsSeason from './AuthorDetailsSeason';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { label }) => label,
|
||||
createClientSideCollectionSelector('books'),
|
||||
createClientSideCollectionSelector('books', 'authorDetails'),
|
||||
createAuthorSelector(),
|
||||
createCommandsSelector(),
|
||||
createDimensionsSelector(),
|
||||
createUISettingsSelector(),
|
||||
(label, books, author, commands, dimensions, uiSettings) => {
|
||||
(books, author, dimensions, uiSettings) => {
|
||||
|
||||
const booksInGroup = books.items;
|
||||
|
||||
@@ -47,14 +45,22 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setAuthorDetailsId,
|
||||
setAuthorDetailsSort,
|
||||
toggleBooksMonitored,
|
||||
setBooksTableOption,
|
||||
dispatchSetBookSort: setBooksSort,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class AuthorDetailsSeasonConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.setAuthorDetailsId({ authorId: this.props.authorId });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -63,7 +69,7 @@ class AuthorDetailsSeasonConnector extends Component {
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.dispatchSetBookSort({ sortKey });
|
||||
this.props.setAuthorDetailsSort({ sortKey });
|
||||
}
|
||||
|
||||
onMonitorBookPress = (bookIds, monitored) => {
|
||||
@@ -92,7 +98,8 @@ AuthorDetailsSeasonConnector.propTypes = {
|
||||
authorId: PropTypes.number.isRequired,
|
||||
toggleBooksMonitored: PropTypes.func.isRequired,
|
||||
setBooksTableOption: PropTypes.func.isRequired,
|
||||
dispatchSetBookSort: PropTypes.func.isRequired,
|
||||
setAuthorDetailsId: PropTypes.func.isRequired,
|
||||
setAuthorDetailsSort: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -100,12 +100,6 @@
|
||||
composes: actionButton;
|
||||
|
||||
margin-right: 15px;
|
||||
|
||||
/* position: absolute; */
|
||||
/* top: 50%; */
|
||||
/* left: 90%; */
|
||||
/* margin-top: -12px; */
|
||||
/* margin-left: -15px; */
|
||||
}
|
||||
|
||||
.noBooks {
|
||||
|
||||
@@ -8,6 +8,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, sortDirections } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getToggledRange from 'Utilities/Table/getToggledRange';
|
||||
import BookRowConnector from './BookRowConnector';
|
||||
import styles from './AuthorDetailsSeries.css';
|
||||
@@ -152,7 +153,7 @@ class AuthorDetailsSeries extends Component {
|
||||
<Icon
|
||||
className={styles.expandButtonIcon}
|
||||
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
||||
title={isExpanded ? 'Hide books' : 'Show books'}
|
||||
title={isExpanded ? translate('IsExpandedHideBooks') : translate('IsExpandedShowBooks')}
|
||||
size={24}
|
||||
/>
|
||||
|
||||
@@ -198,7 +199,7 @@ class AuthorDetailsSeries extends Component {
|
||||
iconClassName={styles.collapseButtonIcon}
|
||||
name={icons.COLLAPSE}
|
||||
size={20}
|
||||
title="Hide books"
|
||||
title={translate('HideBooks')}
|
||||
onPress={this.onExpandPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,12 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setBooksTableOption, toggleBooksMonitored } from 'Store/Actions/bookActions';
|
||||
import { toggleBooksMonitored } from 'Store/Actions/bookActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { setSeriesSort } from 'Store/Actions/seriesActions';
|
||||
import { setSeriesSort, setSeriesTableOption } from 'Store/Actions/seriesActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
// import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import AuthorDetailsSeries from './AuthorDetailsSeries';
|
||||
|
||||
@@ -70,7 +69,7 @@ function createMapStateToProps() {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleBooksMonitored,
|
||||
setBooksTableOption,
|
||||
setSeriesTableOption,
|
||||
dispatchSetSeriesSort: setSeriesSort,
|
||||
executeCommand
|
||||
};
|
||||
@@ -81,7 +80,7 @@ class AuthorDetailsSeasonConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBooksTableOption(payload);
|
||||
this.props.setSeriesTableOption(payload);
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
@@ -113,7 +112,7 @@ class AuthorDetailsSeasonConnector extends Component {
|
||||
AuthorDetailsSeasonConnector.propTypes = {
|
||||
authorId: PropTypes.number.isRequired,
|
||||
toggleBooksMonitored: PropTypes.func.isRequired,
|
||||
setBooksTableOption: PropTypes.func.isRequired,
|
||||
setSeriesTableOption: PropTypes.func.isRequired,
|
||||
dispatchSetSeriesSort: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
.title {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monitored {
|
||||
@@ -10,8 +8,22 @@
|
||||
width: 42px;
|
||||
}
|
||||
|
||||
.position,
|
||||
.rating,
|
||||
.status {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.releaseDate {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.pageCount {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
@@ -2,27 +2,15 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import BookSearchCellConnector from 'Book/BookSearchCellConnector';
|
||||
import BookTitleLink from 'Book/BookTitleLink';
|
||||
import Label from 'Components/Label';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import StarRating from 'Components/StarRating';
|
||||
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 { kinds, sizes } from 'Helpers/Props';
|
||||
import BookStatus from './BookStatus';
|
||||
import styles from './BookRow.css';
|
||||
|
||||
function getBookCountKind(monitored, bookFileCount, bookCount) {
|
||||
if (bookFileCount === bookCount && bookCount > 0) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (!monitored) {
|
||||
return kinds.WARNING;
|
||||
}
|
||||
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
class BookRow extends Component {
|
||||
|
||||
//
|
||||
@@ -68,7 +56,6 @@ class BookRow extends Component {
|
||||
id,
|
||||
authorId,
|
||||
monitored,
|
||||
statistics,
|
||||
releaseDate,
|
||||
title,
|
||||
seriesTitle,
|
||||
@@ -78,14 +65,15 @@ class BookRow extends Component {
|
||||
isSaving,
|
||||
authorMonitored,
|
||||
titleSlug,
|
||||
bookFiles,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
} = statistics;
|
||||
const bookFile = bookFiles[0];
|
||||
const isAvailable = Date.parse(releaseDate) < new Date();
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
@@ -100,6 +88,18 @@ class BookRow extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEditorActive && name === 'select') {
|
||||
return (
|
||||
<TableSelectCell
|
||||
key={name}
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
isDisabled={false}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'monitored') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -145,7 +145,7 @@ class BookRow extends Component {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.title}
|
||||
className={styles.position}
|
||||
>
|
||||
{position || ''}
|
||||
</TableRowCell>
|
||||
@@ -154,7 +154,10 @@ class BookRow extends Component {
|
||||
|
||||
if (name === 'rating') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.rating}
|
||||
>
|
||||
{
|
||||
<StarRating
|
||||
rating={ratings.value}
|
||||
@@ -168,6 +171,7 @@ class BookRow extends Component {
|
||||
if (name === 'releaseDate') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
className={styles.releaseDate}
|
||||
key={name}
|
||||
date={releaseDate}
|
||||
/>
|
||||
@@ -178,6 +182,7 @@ class BookRow extends Component {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.pageCount}
|
||||
>
|
||||
{pageCount || ''}
|
||||
</TableRowCell>
|
||||
@@ -190,15 +195,11 @@ class BookRow extends Component {
|
||||
key={name}
|
||||
className={styles.status}
|
||||
>
|
||||
<Label
|
||||
title={`${totalBookCount} books total. ${bookFileCount} books with files.`}
|
||||
kind={getBookCountKind(monitored, bookFileCount, bookCount)}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
{
|
||||
<span>{bookFileCount} / {bookCount}</span>
|
||||
}
|
||||
</Label>
|
||||
<BookStatus
|
||||
isAvailable={isAvailable}
|
||||
monitored={monitored}
|
||||
bookFile={bookFile}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -228,22 +229,18 @@ BookRow.propTypes = {
|
||||
releaseDate: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
seriesTitle: PropTypes.string.isRequired,
|
||||
position: PropTypes.number,
|
||||
position: PropTypes.string,
|
||||
pageCount: PropTypes.number,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
authorMonitored: PropTypes.bool.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
bookFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMonitorBookPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
BookRow.defaultProps = {
|
||||
statistics: {
|
||||
bookCount: 0,
|
||||
bookFileCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default BookRow;
|
||||
|
||||
@@ -2,17 +2,38 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import createBookFileSelector from 'Store/Selectors/createBookFileSelector';
|
||||
import BookRow from './BookRow';
|
||||
|
||||
const selectBookFiles = createSelector(
|
||||
(state) => state.bookFiles,
|
||||
(bookFiles) => {
|
||||
const {
|
||||
items
|
||||
} = bookFiles;
|
||||
|
||||
const bookFileDict = items.reduce((acc, file) => {
|
||||
const bookId = file.bookId;
|
||||
if (!acc.hasOwnProperty(bookId)) {
|
||||
acc[bookId] = [];
|
||||
}
|
||||
|
||||
acc[bookId].push(file);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return bookFileDict;
|
||||
}
|
||||
);
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createAuthorSelector(),
|
||||
createBookFileSelector(),
|
||||
(author = {}, bookFile) => {
|
||||
selectBookFiles,
|
||||
(state, { id }) => id,
|
||||
(author = {}, bookFiles, bookId) => {
|
||||
return {
|
||||
authorMonitored: author.monitored,
|
||||
bookFilePath: bookFile ? bookFile.path : null
|
||||
bookFiles: bookFiles[bookId] ?? []
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
78
frontend/src/Author/Details/BookStatus.js
Normal file
78
frontend/src/Author/Details/BookStatus.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import BookQuality from 'Book/BookQuality';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './BookStatus.css';
|
||||
|
||||
function BookStatus(props) {
|
||||
const {
|
||||
isAvailable,
|
||||
monitored,
|
||||
bookFile
|
||||
} = props;
|
||||
|
||||
const hasBookFile = !!bookFile;
|
||||
|
||||
if (hasBookFile) {
|
||||
const quality = bookFile.quality;
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<BookQuality
|
||||
title={quality.quality.name}
|
||||
size={bookFile.size}
|
||||
quality={quality}
|
||||
isMonitored={monitored}
|
||||
isCutoffNotMet={bookFile.qualityCutoffNotMet}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!monitored) {
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Label
|
||||
title={translate('NotMonitored')}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('NotMonitored')}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAvailable) {
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Label
|
||||
title={translate('BookAvailableButMissing')}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{translate('Missing')}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Label
|
||||
title={translate('NotAvailable')}
|
||||
kind={kinds.INFO}
|
||||
>
|
||||
{translate('NotAvailable')}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BookStatus.propTypes = {
|
||||
isAvailable: PropTypes.bool,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
bookFile: PropTypes.object
|
||||
};
|
||||
|
||||
export default BookStatus;
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent';
|
||||
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
|
||||
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
@@ -15,6 +16,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditAuthorModalContent.css';
|
||||
|
||||
class EditAuthorModalContent extends Component {
|
||||
@@ -72,6 +74,7 @@ class EditAuthorModalContent extends Component {
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
path,
|
||||
@@ -87,19 +90,48 @@ class EditAuthorModalContent extends Component {
|
||||
<ModalBody>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Monitored</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('Monitored')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText="Download monitored books from this author"
|
||||
helpText={translate('MonitoredHelpText')}
|
||||
{...monitored}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Quality Profile</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('MonitorNewItems')}
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MonitorNewItems')}
|
||||
body={<AuthorMonitorNewItemsOptionsPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
|
||||
name="monitorNewItems"
|
||||
helpText={translate('MonitorNewItemsHelpText')}
|
||||
{...monitorNewItems}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('QualityProfile')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
@@ -122,7 +154,7 @@ class EditAuthorModalContent extends Component {
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title="Metadata Profile"
|
||||
title={translate('MetadataProfile')}
|
||||
body={<AuthorMetadataProfilePopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
@@ -132,7 +164,7 @@ class EditAuthorModalContent extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.METADATA_PROFILE_SELECT}
|
||||
name="metadataProfileId"
|
||||
helpText="Changes will take place on next author refresh"
|
||||
helpText={translate('MetadataProfileIdHelpText')}
|
||||
includeNone={true}
|
||||
{...metadataProfileId}
|
||||
onChange={onInputChange}
|
||||
@@ -141,7 +173,9 @@ class EditAuthorModalContent extends Component {
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('Path')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
@@ -152,7 +186,9 @@ class EditAuthorModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('Tags')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
|
||||
@@ -39,6 +39,7 @@ function createMapStateToProps() {
|
||||
|
||||
const authorSettings = _.pick(author, [
|
||||
'monitored',
|
||||
'monitorNewItems',
|
||||
'qualityProfileId',
|
||||
'metadataProfileId',
|
||||
'path',
|
||||
|
||||
@@ -6,3 +6,25 @@
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.searchForNewBookLabelContainer {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.searchForNewBookLabel {
|
||||
margin-right: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.searchForNewBookContainer {
|
||||
composes: container from '~Components/Form/CheckInput.css';
|
||||
|
||||
flex: 0 1 0;
|
||||
}
|
||||
|
||||
.searchForNewBookInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
@@ -10,58 +11,114 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import styles from './RetagAuthorModalContent.css';
|
||||
|
||||
function RetagAuthorModalContent(props) {
|
||||
const {
|
||||
authorNames,
|
||||
onModalClose,
|
||||
onRetagAuthorPress
|
||||
} = props;
|
||||
class RetagAuthorModalContent extends Component {
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Retag Selected Author
|
||||
</ModalHeader>
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
<ModalBody>
|
||||
<Alert>
|
||||
Tip: To preview the tags that will be written... select "Cancel" then click any author name and use the
|
||||
<Icon
|
||||
className={styles.retagIcon}
|
||||
name={icons.RETAG}
|
||||
/>
|
||||
</Alert>
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
<div className={styles.message}>
|
||||
Are you sure you want to re-tag all files in the {authorNames.length} selected author?
|
||||
</div>
|
||||
<ul>
|
||||
{
|
||||
authorNames.map((authorName) => {
|
||||
return (
|
||||
<li key={authorName}>
|
||||
{authorName}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</ModalBody>
|
||||
this.state = {
|
||||
updateCovers: false,
|
||||
embedMetadata: false
|
||||
};
|
||||
}
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
//
|
||||
// Listeners
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRetagAuthorPress}
|
||||
>
|
||||
Retag
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
onCheckInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
}
|
||||
|
||||
onRetagAuthorPress = () => {
|
||||
this.props.onRetagAuthorPress(this.state.updateCovers, this.state.embedMetadata);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
authorNames,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Retag Selected Author
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert>
|
||||
Tip: To preview the tags that will be written... select "Cancel" then click any author name and use the
|
||||
<Icon
|
||||
className={styles.retagIcon}
|
||||
name={icons.RETAG}
|
||||
/>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.message}>
|
||||
Are you sure you want to re-tag all files in the {authorNames.length} selected author?
|
||||
</div>
|
||||
<ul>
|
||||
{
|
||||
authorNames.map((authorName) => {
|
||||
return (
|
||||
<li key={authorName}>
|
||||
{authorName}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<label className={styles.searchForNewBookLabelContainer}>
|
||||
<span className={styles.searchForNewBookLabel}>
|
||||
Update Covers
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
containerClassName={styles.searchForNewBookContainer}
|
||||
className={styles.searchForNewBookInput}
|
||||
name="updateCovers"
|
||||
value={this.state.updateCovers}
|
||||
onChange={this.onCheckInputChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className={styles.searchForNewBookLabelContainer}>
|
||||
<span className={styles.searchForNewBookLabel}>
|
||||
Embed Metadata
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
containerClassName={styles.searchForNewBookContainer}
|
||||
className={styles.searchForNewBookInput}
|
||||
name="embedMetadata"
|
||||
value={this.state.embedMetadata}
|
||||
onChange={this.onCheckInputChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRetagAuthorPress}
|
||||
>
|
||||
Retag
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RetagAuthorModalContent.propTypes = {
|
||||
|
||||
@@ -36,10 +36,12 @@ class RetagAuthorModalContentConnector extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRetagAuthorPress = () => {
|
||||
onRetagAuthorPress = (updateCovers, embedMetadata) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.RETAG_AUTHOR,
|
||||
authorIds: this.props.authorIds
|
||||
authorIds: this.props.authorIds,
|
||||
updateCovers,
|
||||
embedMetadata
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import NoAuthor from 'Author/NoAuthor';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import RetagAuthorModal from './AudioTags/RetagAuthorModal';
|
||||
import AuthorEditorFilterModalConnector from './AuthorEditorFilterModalConnector';
|
||||
import AuthorEditorFooter from './AuthorEditorFooter';
|
||||
import AuthorEditorRowConnector from './AuthorEditorRowConnector';
|
||||
import OrganizeAuthorModal from './Organize/OrganizeAuthorModal';
|
||||
|
||||
function getColumns(showMetadataProfile) {
|
||||
return [
|
||||
{
|
||||
name: 'status',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sortName',
|
||||
label: 'Name',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
label: 'Quality Profile',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'metadataProfileId',
|
||||
label: 'Metadata Profile',
|
||||
isSortable: true,
|
||||
isVisible: showMetadataProfile
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
class AuthorEditor extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isOrganizingAuthorModalOpen: false,
|
||||
isRetaggingAuthorModalOpen: false,
|
||||
columns: getColumns(props.showMetadataProfile)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = this.props;
|
||||
|
||||
const hasFinishedDeleting = prevProps.isDeleting &&
|
||||
!isDeleting &&
|
||||
!deleteError;
|
||||
|
||||
if (hasFinishedDeleting) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSelected = (changes) => {
|
||||
this.props.onSaveSelected({
|
||||
authorIds: this.getSelectedIds(),
|
||||
...changes
|
||||
});
|
||||
}
|
||||
|
||||
onOrganizeAuthorPress = () => {
|
||||
this.setState({ isOrganizingAuthorModalOpen: true });
|
||||
}
|
||||
|
||||
onOrganizeAuthorModalClose = (organized) => {
|
||||
this.setState({ isOrganizingAuthorModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
onRetagAuthorPress = () => {
|
||||
this.setState({ isRetaggingAuthorModalOpen: true });
|
||||
}
|
||||
|
||||
onRetagAuthorModalClose = (organized) => {
|
||||
this.setState({ isRetaggingAuthorModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalItems,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
showMetadataProfile,
|
||||
onSortPress,
|
||||
onFilterSelect
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
columns
|
||||
} = this.state;
|
||||
|
||||
const selectedAuthorIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title="Author Editor">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection />
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={AuthorEditorFilterModalConnector}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>{getErrorMessage(error, 'Failed to load author from API')}</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSortPress={onSortPress}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<AuthorEditorRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
columns={columns}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoAuthor totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
|
||||
<AuthorEditorFooter
|
||||
authorIds={selectedAuthorIds}
|
||||
selectedCount={selectedAuthorIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
isOrganizingAuthor={isOrganizingAuthor}
|
||||
isRetaggingAuthor={isRetaggingAuthor}
|
||||
showMetadataProfile={showMetadataProfile}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
|
||||
onRetagAuthorPress={this.onRetagAuthorPress}
|
||||
/>
|
||||
|
||||
<OrganizeAuthorModal
|
||||
isOpen={this.state.isOrganizingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onOrganizeAuthorModalClose}
|
||||
/>
|
||||
|
||||
<RetagAuthorModal
|
||||
isOpen={this.state.isRetaggingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onRetagAuthorModalClose}
|
||||
/>
|
||||
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthorEditor.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingAuthor: PropTypes.bool.isRequired,
|
||||
isRetaggingAuthor: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthorEditor;
|
||||
@@ -1,92 +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 { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort } from 'Store/Actions/authorEditorActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import AuthorEditor from './AuthorEditor';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.metadataProfiles,
|
||||
createClientSideCollectionSelector('authors', 'authorEditor'),
|
||||
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
|
||||
(metadataProfiles, author, isOrganizingAuthor, isRetaggingAuthor) => {
|
||||
return {
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
showMetadataProfile: metadataProfiles.items.length > 1,
|
||||
...author
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetAuthorEditorSort: setAuthorEditorSort,
|
||||
dispatchSetAuthorEditorFilter: setAuthorEditorFilter,
|
||||
dispatchSaveAuthorEditor: saveAuthorEditor,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class AuthorEditorConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.dispatchSetAuthorEditorSort({ sortKey });
|
||||
}
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.dispatchSetAuthorEditorFilter({ selectedFilterKey });
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveAuthorEditor(payload);
|
||||
}
|
||||
|
||||
onMoveSelected = (payload) => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.MOVE_AUTHOR,
|
||||
...payload
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AuthorEditor
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthorEditorConnector.propTypes = {
|
||||
dispatchSetAuthorEditorSort: PropTypes.func.isRequired,
|
||||
dispatchSetAuthorEditorFilter: PropTypes.func.isRequired,
|
||||
dispatchSaveAuthorEditor: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthorEditorConnector);
|
||||
@@ -1,11 +1,23 @@
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dropdownContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -24,12 +36,14 @@
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.deleteSelectedButton {
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
margin-bottom: 10px;
|
||||
margin-left: 50px;
|
||||
height: 35px;
|
||||
}
|
||||
@@ -48,6 +62,10 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.dropdownContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -61,6 +79,7 @@
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: block;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
|
||||
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
|
||||
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
|
||||
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
|
||||
import DeleteAuthorModal from './Delete/DeleteAuthorModal';
|
||||
import TagsModal from './Tags/TagsModal';
|
||||
@@ -25,6 +27,7 @@ class AuthorEditorFooter extends Component {
|
||||
|
||||
this.state = {
|
||||
monitored: NO_CHANGE,
|
||||
monitorNewItems: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
metadataProfileId: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
@@ -45,6 +48,7 @@ class AuthorEditorFooter extends Component {
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
monitored: NO_CHANGE,
|
||||
monitorNewItems: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
metadataProfileId: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
@@ -138,13 +142,13 @@ class AuthorEditorFooter extends Component {
|
||||
isDeleting,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
showMetadataProfile,
|
||||
onOrganizeAuthorPress,
|
||||
onRetagAuthorPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
rootFolderPath,
|
||||
@@ -163,41 +167,58 @@ class AuthorEditorFooter extends Component {
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Monitor Author"
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Quality Profile"
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
showMetadataProfile &&
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.dropdownContainer}>
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Metadata Profile"
|
||||
label={translate('MonitorAuthor')}
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MonitorNewItems')}
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<MonitorNewItemsSelectInput
|
||||
name="monitorNewItems"
|
||||
value={monitorNewItems}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('QualityProfile')}
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MetadataProfile')}
|
||||
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
@@ -210,33 +231,35 @@ class AuthorEditorFooter extends Component {
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label="Root Folder"
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
<div
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('RootFolder')}
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={`${selectedCount} Author(s) Selected`}
|
||||
isSaving={false}
|
||||
/>
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
|
||||
isSaving={false}
|
||||
/>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<div>
|
||||
<SpinnerButton
|
||||
className={styles.organizeSelectedButton}
|
||||
kind={kinds.WARNING}
|
||||
@@ -244,7 +267,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={onOrganizeAuthorPress}
|
||||
>
|
||||
Rename Files
|
||||
{translate('RenameFiles')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -254,7 +277,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={onRetagAuthorPress}
|
||||
>
|
||||
Write Metadata Tags
|
||||
{translate('WriteMetadataTags')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -263,19 +286,20 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={this.onTagsPress}
|
||||
>
|
||||
Set Readarr Tags
|
||||
{translate('SetReadarrTags')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.deleteSelectedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedCount || isDeleting}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
Delete
|
||||
</SpinnerButton>
|
||||
<SpinnerButton
|
||||
className={styles.deleteSelectedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedCount || isDeleting}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
@@ -7,6 +6,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
|
||||
class AuthorEditorRow extends Component {
|
||||
|
||||
@@ -27,11 +27,11 @@ class AuthorEditorRow extends Component {
|
||||
status,
|
||||
titleSlug,
|
||||
authorName,
|
||||
authorType,
|
||||
monitored,
|
||||
metadataProfile,
|
||||
qualityProfile,
|
||||
path,
|
||||
statistics,
|
||||
tags,
|
||||
columns,
|
||||
isSelected,
|
||||
@@ -46,39 +46,85 @@ class AuthorEditorRow extends Component {
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<AuthorStatusCell
|
||||
authorType={authorType}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
/>
|
||||
|
||||
<TableRowCell>
|
||||
<AuthorNameLink
|
||||
titleSlug={titleSlug}
|
||||
authorName={authorName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{qualityProfile.name}
|
||||
</TableRowCell>
|
||||
|
||||
{
|
||||
_.find(columns, { name: 'metadataProfileId' }).isVisible &&
|
||||
<TableRowCell>
|
||||
{metadataProfile.name}
|
||||
</TableRowCell>
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<AuthorStatusCell
|
||||
key={name}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sortName') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
>
|
||||
<AuthorNameLink
|
||||
titleSlug={titleSlug}
|
||||
authorName={authorName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{qualityProfile.name}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'metadataProfileId') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{metadataProfile.name}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{path}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{formatBytes(statistics.sizeOnDisk)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'tags') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<TagListConnector
|
||||
tags={tags}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<TableRowCell>
|
||||
{path}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<TagListConnector
|
||||
tags={tags}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -89,11 +135,11 @@ AuthorEditorRow.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
authorName: PropTypes.string.isRequired,
|
||||
authorType: PropTypes.string,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
metadataProfile: PropTypes.object.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { bulkDeleteAuthor } from 'Store/Actions/authorEditorActions';
|
||||
import { bulkDeleteAuthor } from 'Store/Actions/authorIndexActions';
|
||||
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
|
||||
import DeleteAuthorModalContent from './DeleteAuthorModalContent';
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ function OrganizeAuthorModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
<Alert>
|
||||
Tip: To preview a rename... select "Cancel" then click any author name and use the
|
||||
Tip: To preview a rename, select "Cancel", then select any author name and use the
|
||||
<Icon
|
||||
className={styles.renameIcon}
|
||||
name={icons.ORGANIZE}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 './TagsModalContent.css';
|
||||
|
||||
class TagsModalContent extends Component {
|
||||
@@ -74,7 +75,9 @@ class TagsModalContent extends Component {
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('Tags')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
@@ -85,7 +88,9 @@ class TagsModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Apply Tags</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('ApplyTags')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
@@ -93,17 +98,19 @@ class TagsModalContent extends Component {
|
||||
value={applyTags}
|
||||
values={applyTagsOptions}
|
||||
helpTexts={[
|
||||
'How to apply tags to the selected author',
|
||||
'Add: Add the tags the existing list of tags',
|
||||
'Remove: Remove the entered tags',
|
||||
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)'
|
||||
translate('ApplyTagsHelpTexts1'),
|
||||
translate('ApplyTagsHelpTexts2'),
|
||||
translate('ApplyTagsHelpTexts3'),
|
||||
translate('ApplyTagsHelpTexts4')
|
||||
]}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Result</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('Result')}
|
||||
</FormLabel>
|
||||
|
||||
<div className={styles.result}>
|
||||
{
|
||||
@@ -120,7 +127,7 @@ class TagsModalContent extends Component {
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={removeTag ? 'Removing tag' : 'Existing tag'}
|
||||
title={removeTag ? translate('RemoveTagRemovingTag') : translate('RemoveTagExistingTag')}
|
||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
@@ -146,7 +153,7 @@ class TagsModalContent extends Component {
|
||||
return (
|
||||
<Label
|
||||
key={tag.id}
|
||||
title={'Adding tag'}
|
||||
title={translate('AddingTag')}
|
||||
kind={kinds.SUCCESS}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
.sourceTitle {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.details,
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
@@ -11,6 +11,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthorHistoryRow.css';
|
||||
|
||||
function getTitle(eventType) {
|
||||
@@ -94,7 +95,7 @@ class AuthorHistoryRow extends Component {
|
||||
{book.title}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<TableRowCell className={styles.sourceTitle}>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
|
||||
@@ -132,7 +133,7 @@ class AuthorHistoryRow extends Component {
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<IconButton
|
||||
title="Mark as failed"
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
onPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
@@ -142,9 +143,9 @@ class AuthorHistoryRow extends Component {
|
||||
<ConfirmModal
|
||||
isOpen={isMarkAsFailedModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Mark as Failed"
|
||||
message={`Are you sure you want to mark '${sourceTitle}' as failed?`}
|
||||
confirmLabel="Mark as Failed"
|
||||
title={translate('MarkAsFailed')}
|
||||
message={translate('MarkAsFailedMessageText', [sourceTitle])}
|
||||
confirmLabel={translate('MarkAsFailed')}
|
||||
onConfirm={this.onConfirmMarkAsFailed}
|
||||
onCancel={this.onMarkAsFailedModalClose}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
|
||||
|
||||
const columns = [
|
||||
@@ -69,12 +70,16 @@ class AuthorHistoryTableContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load history.</div>
|
||||
<div>
|
||||
{translate('UnableToLoadHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !hasItems && !error &&
|
||||
<div>No history.</div>
|
||||
<div>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import RetagAuthorModal from 'Author/Editor/AudioTags/RetagAuthorModal';
|
||||
import AuthorEditorFooter from 'Author/Editor/AuthorEditorFooter';
|
||||
import OrganizeAuthorModal from 'Author/Editor/Organize/OrganizeAuthorModal';
|
||||
import NoAuthor from 'Author/NoAuthor';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
@@ -14,9 +17,11 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import AuthorIndexFooterConnector from './AuthorIndexFooterConnector';
|
||||
import AuthorIndexBannersConnector from './Banners/AuthorIndexBannersConnector';
|
||||
import AuthorIndexBannerOptionsModal from './Banners/Options/AuthorIndexBannerOptionsModal';
|
||||
import AuthorIndexFilterMenu from './Menus/AuthorIndexFilterMenu';
|
||||
import AuthorIndexSortMenu from './Menus/AuthorIndexSortMenu';
|
||||
import AuthorIndexViewMenu from './Menus/AuthorIndexViewMenu';
|
||||
@@ -33,10 +38,6 @@ function getViewComponent(view) {
|
||||
return AuthorIndexPostersConnector;
|
||||
}
|
||||
|
||||
if (view === 'banners') {
|
||||
return AuthorIndexBannersConnector;
|
||||
}
|
||||
|
||||
if (view === 'overview') {
|
||||
return AuthorIndexOverviewsConnector;
|
||||
}
|
||||
@@ -57,13 +58,20 @@ class AuthorIndex extends Component {
|
||||
jumpBarItems: { order: [] },
|
||||
jumpToCharacter: null,
|
||||
isPosterOptionsModalOpen: false,
|
||||
isBannerOptionsModalOpen: false,
|
||||
isOverviewOptionsModalOpen: false
|
||||
isOverviewOptionsModalOpen: false,
|
||||
isEditorActive: false,
|
||||
isOrganizingAuthorModalOpen: false,
|
||||
isRetaggingAuthorModalOpen: false,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -78,6 +86,7 @@ class AuthorIndex extends Component {
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
if (this.state.jumpToCharacter != null) {
|
||||
@@ -92,6 +101,48 @@ class AuthorIndex extends Component {
|
||||
this.setState({ scroller: ref });
|
||||
}
|
||||
|
||||
getSelectedIds = () => {
|
||||
if (this.state.allUnselected) {
|
||||
return [];
|
||||
}
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
setSelectedState() {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((author) => {
|
||||
const isItemSelected = selectedState[author.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[author.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[author.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
}
|
||||
|
||||
setJumpBarItems() {
|
||||
const {
|
||||
items,
|
||||
@@ -100,13 +151,13 @@ class AuthorIndex extends Component {
|
||||
} = this.props;
|
||||
|
||||
// Reset if not sorting by sortName
|
||||
if (sortKey !== 'sortName') {
|
||||
if (sortKey !== 'sortName' && sortKey !== 'sortNameLastFirst') {
|
||||
this.setState({ jumpBarItems: { order: [] } });
|
||||
return;
|
||||
}
|
||||
|
||||
const characters = _.reduce(items, (acc, item) => {
|
||||
let char = item.sortName.charAt(0);
|
||||
let char = item[sortKey].charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
char = '#';
|
||||
@@ -147,14 +198,6 @@ class AuthorIndex extends Component {
|
||||
this.setState({ isPosterOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
onBannerOptionsPress = () => {
|
||||
this.setState({ isBannerOptionsModalOpen: true });
|
||||
}
|
||||
|
||||
onBannerOptionsModalClose = () => {
|
||||
this.setState({ isBannerOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
onOverviewOptionsPress = () => {
|
||||
this.setState({ isOverviewOptionsModalOpen: true });
|
||||
}
|
||||
@@ -163,10 +206,72 @@ class AuthorIndex extends Component {
|
||||
this.setState({ isOverviewOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
onEditorTogglePress = () => {
|
||||
if (this.state.isEditorActive) {
|
||||
this.setState({ isEditorActive: false });
|
||||
} else {
|
||||
const newState = selectAll(this.state.selectedState, false);
|
||||
newState.isEditorActive = true;
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
onJumpBarItemPress = (jumpToCharacter) => {
|
||||
this.setState({ jumpToCharacter });
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectAllPress = () => {
|
||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSelected = (changes) => {
|
||||
this.props.onSaveSelected({
|
||||
authorIds: this.getSelectedIds(),
|
||||
...changes
|
||||
});
|
||||
}
|
||||
|
||||
onOrganizeAuthorPress = () => {
|
||||
this.setState({ isOrganizingAuthorModalOpen: true });
|
||||
}
|
||||
|
||||
onOrganizeAuthorModalClose = (organized) => {
|
||||
this.setState({ isOrganizingAuthorModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
onRetagAuthorPress = () => {
|
||||
this.setState({ isRetaggingAuthorModalOpen: true });
|
||||
}
|
||||
|
||||
onRetagAuthorModalClose = (organized) => {
|
||||
this.setState({ isRetaggingAuthorModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
onRefreshAuthorPress = () => {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const refreshIds = this.state.isEditorActive && selectedIds.length > 0 ? selectedIds : [];
|
||||
|
||||
this.props.onRefreshAuthorPress(refreshIds);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -186,11 +291,16 @@ class AuthorIndex extends Component {
|
||||
view,
|
||||
isRefreshingAuthor,
|
||||
isRssSyncExecuting,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
onFilterSelect,
|
||||
onViewSelect,
|
||||
onRefreshAuthorPress,
|
||||
onRssSyncPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -200,34 +310,70 @@ class AuthorIndex extends Component {
|
||||
jumpBarItems,
|
||||
jumpToCharacter,
|
||||
isPosterOptionsModalOpen,
|
||||
isBannerOptionsModalOpen,
|
||||
isOverviewOptionsModalOpen
|
||||
isOverviewOptionsModalOpen,
|
||||
isEditorActive,
|
||||
selectedState,
|
||||
allSelected,
|
||||
allUnselected
|
||||
} = this.state;
|
||||
|
||||
const selectedAuthorIds = this.getSelectedIds();
|
||||
|
||||
const ViewComponent = getViewComponent(view);
|
||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||
const hasNoAuthor = !totalItems;
|
||||
|
||||
const refreshLabel = isEditorActive && selectedAuthorIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll');
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Update all"
|
||||
label={refreshLabel}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isRefreshingAuthor}
|
||||
onPress={onRefreshAuthorPress}
|
||||
onPress={this.onRefreshAuthorPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="RSS Sync"
|
||||
label={translate('RSSSync')}
|
||||
iconName={icons.RSS}
|
||||
isSpinning={isRssSyncExecuting}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={onRssSyncPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={translate('AuthorIndex')}
|
||||
iconName={icons.AUTHOR_CONTINUING}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onEditorTogglePress}
|
||||
/> :
|
||||
<PageToolbarButton
|
||||
label={translate('AuthorEditor')}
|
||||
iconName={icons.EDIT}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onEditorTogglePress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={icons.CHECK_SQUARE}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onSelectAllPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
@@ -242,7 +388,7 @@ class AuthorIndex extends Component {
|
||||
optionsComponent={AuthorIndexTableOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper> :
|
||||
@@ -252,7 +398,7 @@ class AuthorIndex extends Component {
|
||||
{
|
||||
view === 'posters' ?
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
label={translate('Options')}
|
||||
iconName={icons.POSTER}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onPosterOptionsPress}
|
||||
@@ -260,21 +406,10 @@ class AuthorIndex extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'banners' ?
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.POSTER}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onBannerOptionsPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'overview' ?
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
label={translate('Options')}
|
||||
iconName={icons.OVERVIEW}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onOverviewOptionsPress}
|
||||
@@ -282,11 +417,7 @@ class AuthorIndex extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
(view === 'posters' || view === 'banners' || view === 'overview') &&
|
||||
|
||||
<PageToolbarSeparator />
|
||||
}
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<AuthorIndexViewMenu
|
||||
view={view}
|
||||
@@ -312,7 +443,7 @@ class AuthorIndex extends Component {
|
||||
</PageToolbar>
|
||||
|
||||
<div className={styles.pageContentBodyWrapper}>
|
||||
<PageContentBodyConnector
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
className={styles.contentBody}
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
@@ -340,6 +471,12 @@ class AuthorIndex extends Component {
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
jumpToCharacter={jumpToCharacter}
|
||||
isEditorActive={isEditorActive}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
selectedState={selectedState}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
@@ -351,7 +488,7 @@ class AuthorIndex extends Component {
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoAuthor totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isLoaded && !!jumpBarItems.order.length &&
|
||||
@@ -362,22 +499,46 @@ class AuthorIndex extends Component {
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
isLoaded && isEditorActive &&
|
||||
<AuthorEditorFooter
|
||||
authorIds={selectedAuthorIds}
|
||||
selectedCount={selectedAuthorIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
isOrganizingAuthor={isOrganizingAuthor}
|
||||
isRetaggingAuthor={isRetaggingAuthor}
|
||||
showMetadataProfile={true}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
|
||||
onRetagAuthorPress={this.onRetagAuthorPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<AuthorIndexPosterOptionsModal
|
||||
isOpen={isPosterOptionsModalOpen}
|
||||
onModalClose={this.onPosterOptionsModalClose}
|
||||
/>
|
||||
|
||||
<AuthorIndexBannerOptionsModal
|
||||
isOpen={isBannerOptionsModalOpen}
|
||||
onModalClose={this.onBannerOptionsModalClose}
|
||||
|
||||
/>
|
||||
|
||||
<AuthorIndexOverviewOptionsModal
|
||||
isOpen={isOverviewOptionsModalOpen}
|
||||
onModalClose={this.onOverviewOptionsModalClose}
|
||||
|
||||
/>
|
||||
|
||||
<OrganizeAuthorModal
|
||||
isOpen={this.state.isOrganizingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onOrganizeAuthorModalClose}
|
||||
/>
|
||||
|
||||
<RetagAuthorModal
|
||||
isOpen={this.state.isRetaggingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onRetagAuthorModalClose}
|
||||
/>
|
||||
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -397,14 +558,21 @@ AuthorIndex.propTypes = {
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
view: PropTypes.string.isRequired,
|
||||
isRefreshingAuthor: PropTypes.bool.isRequired,
|
||||
isOrganizingAuthor: PropTypes.bool.isRequired,
|
||||
isRetaggingAuthor: PropTypes.bool.isRequired,
|
||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onViewSelect: PropTypes.func.isRequired,
|
||||
onRefreshAuthorPress: PropTypes.func.isRequired,
|
||||
onRssSyncPress: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthorIndex;
|
||||
|
||||
@@ -5,11 +5,10 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
|
||||
import { saveAuthorEditor, setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createAuthorClientSideCollectionItemsSelector
|
||||
from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
|
||||
import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import AuthorIndex from './AuthorIndex';
|
||||
@@ -19,16 +18,22 @@ function createMapStateToProps() {
|
||||
createAuthorClientSideCollectionItemsSelector('authorIndex'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
author,
|
||||
isRefreshingAuthor,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isRssSyncExecuting,
|
||||
dimensionsState
|
||||
) => {
|
||||
return {
|
||||
...author,
|
||||
isRefreshingAuthor,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isRssSyncExecuting,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
@@ -54,9 +59,14 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatch(setAuthorView({ view }));
|
||||
},
|
||||
|
||||
onRefreshAuthorPress() {
|
||||
dispatchSaveAuthorEditor(payload) {
|
||||
dispatch(saveAuthorEditor(payload));
|
||||
},
|
||||
|
||||
onRefreshAuthorPress(items) {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.REFRESH_AUTHOR
|
||||
name: commandNames.BULK_REFRESH_AUTHOR,
|
||||
authorIds: items
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -77,6 +87,10 @@ class AuthorIndexConnector extends Component {
|
||||
this.props.dispatchSetAuthorView(view);
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveAuthorEditor(payload);
|
||||
}
|
||||
|
||||
onScroll = ({ scrollTop }) => {
|
||||
scrollPositions.authorIndex = scrollTop;
|
||||
}
|
||||
@@ -90,6 +104,7 @@ class AuthorIndexConnector extends Component {
|
||||
{...this.props}
|
||||
onViewSelect={this.onViewSelect}
|
||||
onScroll={this.onScroll}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -98,7 +113,8 @@ class AuthorIndexConnector extends Component {
|
||||
AuthorIndexConnector.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
view: PropTypes.string.isRequired,
|
||||
dispatchSetAuthorView: PropTypes.func.isRequired
|
||||
dispatchSetAuthorView: PropTypes.func.isRequired,
|
||||
dispatchSaveAuthorEditor: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withScrollPosition(
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthorIndexFooter.css';
|
||||
|
||||
class AuthorIndexFooter extends PureComponent {
|
||||
@@ -60,7 +61,9 @@ class AuthorIndexFooter extends PureComponent {
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>Continuing (All books downloaded)</div>
|
||||
<div>
|
||||
{translate('ContinuingAllBooksDownloaded')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
@@ -70,7 +73,9 @@ class AuthorIndexFooter extends PureComponent {
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>Ended (All books downloaded)</div>
|
||||
<div>
|
||||
{translate('EndedAllBooksDownloaded')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
@@ -80,7 +85,9 @@ class AuthorIndexFooter extends PureComponent {
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>Missing Books (Author monitored)</div>
|
||||
<div>
|
||||
{translate('MissingBooksAuthorMonitored')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
@@ -90,55 +97,57 @@ class AuthorIndexFooter extends PureComponent {
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>Missing Books (Author not monitored)</div>
|
||||
<div>
|
||||
{translate('MissingBooksAuthorNotMonitored')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statistics}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Authors"
|
||||
title={translate('Authors')}
|
||||
data={count}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Ended"
|
||||
title={translate('Ended')}
|
||||
data={ended}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Continuing"
|
||||
title={translate('Continuing')}
|
||||
data={continuing}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Monitored"
|
||||
title={translate('Monitored')}
|
||||
data={monitored}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Unmonitored"
|
||||
title={translate('Unmonitored')}
|
||||
data={count - monitored}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Books"
|
||||
title={translate('Books')}
|
||||
data={books}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Files"
|
||||
title={translate('Files')}
|
||||
data={bookFiles}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Total File Size"
|
||||
title={translate('TotalFileSize')}
|
||||
data={formatBytes(totalFileSize)}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user