mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
682 Commits
v1.12.2.42
...
sql-fallba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab4d80faa6 | ||
|
|
a2afcff6df | ||
|
|
e4fb36e08f | ||
|
|
ff22fdf7d3 | ||
|
|
b3d46465ae | ||
|
|
eb57d20545 | ||
|
|
775b716c0f | ||
|
|
f7f3648dac | ||
|
|
c669048767 | ||
|
|
c282e4bef8 | ||
|
|
574721bfb5 | ||
|
|
3c7575b58e | ||
|
|
93d8f81750 | ||
|
|
364c7c9c7e | ||
|
|
54af7fd3d0 | ||
|
|
e6bc7fa062 | ||
|
|
98608e75a6 | ||
|
|
160320f3a2 | ||
|
|
c9baaf634e | ||
|
|
8bf2f68abe | ||
|
|
9434091912 | ||
|
|
2f7d821d45 | ||
|
|
471c9910a0 | ||
|
|
98ff2f5cb6 | ||
|
|
4d9982872a | ||
|
|
ae9326480e | ||
|
|
624cbd548f | ||
|
|
f5f98e4f53 | ||
|
|
8585dd447e | ||
|
|
dfffb3aa4e | ||
|
|
7eb2d956cf | ||
|
|
8da493dbaf | ||
|
|
f17cf6144f | ||
|
|
1b3adc4529 | ||
|
|
389f049a8b | ||
|
|
99b0fcd750 | ||
|
|
516b09ca91 | ||
|
|
770fd64013 | ||
|
|
f67c672ec7 | ||
|
|
80425f5ea4 | ||
|
|
758cae3f40 | ||
|
|
fbf4ff6777 | ||
|
|
98ee9c1703 | ||
|
|
c537e94f0f | ||
|
|
9e5dd2a2e6 | ||
|
|
f601ff98a2 | ||
|
|
540fafdebf | ||
|
|
532bffe772 | ||
|
|
bf80f7916c | ||
|
|
2f6a9dfffb | ||
|
|
94477e9cf9 | ||
|
|
52e21b3dfc | ||
|
|
cb4cc81ad0 | ||
|
|
7ada036480 | ||
|
|
f1c9ba40c4 | ||
|
|
8664fc095d | ||
|
|
23b9973ef7 | ||
|
|
d9f1d96e00 | ||
|
|
d9d045a548 | ||
|
|
c417c41133 | ||
|
|
d5853735ac | ||
|
|
dbc159f536 | ||
|
|
231cc91f97 | ||
|
|
1a075f201c | ||
|
|
de7f42cf30 | ||
|
|
fab74b58fa | ||
|
|
2b332a00d7 | ||
|
|
a0b0c1555c | ||
|
|
86b81948af | ||
|
|
54918e0c30 | ||
|
|
01dd793c0a | ||
|
|
950949e4bc | ||
|
|
fe198352a3 | ||
|
|
88502cd020 | ||
|
|
4924b45b56 | ||
|
|
aea8b7cd7e | ||
|
|
aafadb6111 | ||
|
|
c82f904d49 | ||
|
|
60740fa259 | ||
|
|
d36b32f414 | ||
|
|
14ccd6d2a5 | ||
|
|
bdc3b63df2 | ||
|
|
8eec321a0e | ||
|
|
06de2313ab | ||
|
|
a3f713bad8 | ||
|
|
7a1fca5e23 | ||
|
|
21c408a7da | ||
|
|
0e92108970 | ||
|
|
7d813ef97a | ||
|
|
c87995250a | ||
|
|
a9f7a376c7 | ||
|
|
c3ee3f2320 | ||
|
|
e8c26d0fea | ||
|
|
9c936121e8 | ||
|
|
40d2e40d94 | ||
|
|
837f50c91c | ||
|
|
f0a0202e5c | ||
|
|
708c94bc56 | ||
|
|
5ed82eaf09 | ||
|
|
7d77ad68fd | ||
|
|
6725358db5 | ||
|
|
c410e23460 | ||
|
|
903b86b9a2 | ||
|
|
52a49e6a34 | ||
|
|
a7d99f351c | ||
|
|
b0212dd780 | ||
|
|
c8f5099423 | ||
|
|
5cc4c3f302 | ||
|
|
c0d2cb42e9 | ||
|
|
8081f13052 | ||
|
|
84b672e617 | ||
|
|
ed586c2d72 | ||
|
|
233176e321 | ||
|
|
d1e3390bae | ||
|
|
1cd60c7a40 | ||
|
|
c61cfcd312 | ||
|
|
5eb4d112ca | ||
|
|
70f2361d69 | ||
|
|
1d6babaa15 | ||
|
|
0427add8d0 | ||
|
|
010c2b836d | ||
|
|
22c4c1fc9a | ||
|
|
d5f6cc94b8 | ||
|
|
411e96ef2a | ||
|
|
2b0e52ebca | ||
|
|
c6fa26ca7b | ||
|
|
c85f170d41 | ||
|
|
48a658571b | ||
|
|
0b3a5c9bc4 | ||
|
|
356d07ef34 | ||
|
|
0322d70d63 | ||
|
|
362f3fe223 | ||
|
|
075fd24f96 | ||
|
|
4ba72ea7f3 | ||
|
|
46f73c51bb | ||
|
|
3287d45661 | ||
|
|
71937fa44c | ||
|
|
6aefd46cd4 | ||
|
|
c8370c9e00 | ||
|
|
6be4203b41 | ||
|
|
1339373e43 | ||
|
|
fc9dfb0cf7 | ||
|
|
48301055ea | ||
|
|
8a9518c9c1 | ||
|
|
de099c6770 | ||
|
|
07711da4e0 | ||
|
|
7cb70716d0 | ||
|
|
548dedad5c | ||
|
|
7008626358 | ||
|
|
f6f2a3b00d | ||
|
|
2b16d93095 | ||
|
|
e63ee13d23 | ||
|
|
5c5a163151 | ||
|
|
023eec0ec0 | ||
|
|
5bc5f0e6b8 | ||
|
|
5cbacc01eb | ||
|
|
f4f1b38324 | ||
|
|
758dddd4ad | ||
|
|
73ee695633 | ||
|
|
27fbd7ef7e | ||
|
|
5125f256fb | ||
|
|
b99e8d0d65 | ||
|
|
d20b2cc9c0 | ||
|
|
8a1787bdb6 | ||
|
|
a19b8ea997 | ||
|
|
10ea6cd753 | ||
|
|
2c1b464715 | ||
|
|
3263454041 | ||
|
|
015db4a916 | ||
|
|
49268f3b8d | ||
|
|
f02a6f3e2c | ||
|
|
46b6124b97 | ||
|
|
53bc97b3be | ||
|
|
b09d4927cc | ||
|
|
328f3c0423 | ||
|
|
635e76526a | ||
|
|
790feed5ab | ||
|
|
59b5d2fc78 | ||
|
|
d5b12cf51a | ||
|
|
2d584f7eb6 | ||
|
|
0f1d647cd7 | ||
|
|
d6e8d89be4 | ||
|
|
8672129d5a | ||
|
|
44bdff8b8f | ||
|
|
4df8fc02f1 | ||
|
|
e101129cff | ||
|
|
147e732c9c | ||
|
|
a12381fb1d | ||
|
|
3a4de9cca1 | ||
|
|
43c988d951 | ||
|
|
a036e0fc37 | ||
|
|
56b9da16cf | ||
|
|
887c262589 | ||
|
|
12ff612775 | ||
|
|
0d3d27e46f | ||
|
|
d1846fde61 | ||
|
|
e6901506a0 | ||
|
|
08b4eddbc5 | ||
|
|
979db70e68 | ||
|
|
22834a852a | ||
|
|
f0540a5f8b | ||
|
|
1f7ac7d7d6 | ||
|
|
8ac68240ad | ||
|
|
b463a3f54b | ||
|
|
e15e57329e | ||
|
|
d8354408a4 | ||
|
|
6d2d49f7bd | ||
|
|
37610eec40 | ||
|
|
ed51208116 | ||
|
|
26e4dcad65 | ||
|
|
6eb21a02a1 | ||
|
|
8c2d5a404d | ||
|
|
3b83a00eaf | ||
|
|
a5a86a6f86 | ||
|
|
e7ed09a43d | ||
|
|
547bc2e58c | ||
|
|
8eb674c8d7 | ||
|
|
2c3621d25e | ||
|
|
2648f2c639 | ||
|
|
f4d621063b | ||
|
|
73494c462c | ||
|
|
36f6896f30 | ||
|
|
e01741a69e | ||
|
|
1dbff1235e | ||
|
|
1a9ad6b363 | ||
|
|
c88249300c | ||
|
|
7b8e352d87 | ||
|
|
81f7a6cbab | ||
|
|
523e46af2a | ||
|
|
2b4a6def2a | ||
|
|
9097c0ef6d | ||
|
|
4321c1d40c | ||
|
|
bb2548a08d | ||
|
|
3a9b841fad | ||
|
|
31203d1370 | ||
|
|
c8a910eaf4 | ||
|
|
9ab3c3e6c7 | ||
|
|
4659cb706a | ||
|
|
500759bf1f | ||
|
|
43c7c43257 | ||
|
|
9c2fced391 | ||
|
|
52ec5b6ff6 | ||
|
|
b46e657976 | ||
|
|
51fd30ba10 | ||
|
|
5fbb347108 | ||
|
|
54d3d44620 | ||
|
|
5ca18683ca | ||
|
|
6bdf5f5d69 | ||
|
|
7cba7152f1 | ||
|
|
cf012eb001 | ||
|
|
6b8a7993ff | ||
|
|
c6440bb21b | ||
|
|
b95eac98b9 | ||
|
|
0eb19ce834 | ||
|
|
4b8016d95d | ||
|
|
31d8d2419a | ||
|
|
d29ccd7749 | ||
|
|
e789f4ec54 | ||
|
|
58d495d618 | ||
|
|
f3328863e1 | ||
|
|
a23d792781 | ||
|
|
f066cf399d | ||
|
|
61e863cb31 | ||
|
|
b2afbc6872 | ||
|
|
aace65f88e | ||
|
|
9ab2d8b444 | ||
|
|
bc314061ef | ||
|
|
87b3dcd780 | ||
|
|
f3b99f68f6 | ||
|
|
c4a90e8ba4 | ||
|
|
41320ca2dc | ||
|
|
b8b32f8708 | ||
|
|
30c4bb24e8 | ||
|
|
b447db5d08 | ||
|
|
299001a513 | ||
|
|
2871f1f2a2 | ||
|
|
a9b93df0c9 | ||
|
|
2726787ee9 | ||
|
|
b917932f19 | ||
|
|
06ae85e6d1 | ||
|
|
b1c7e98664 | ||
|
|
62479737a7 | ||
|
|
8e69415d64 | ||
|
|
222dfb1821 | ||
|
|
94f439e238 | ||
|
|
903a88c121 | ||
|
|
9690ab6883 | ||
|
|
1e1a2b3b4a | ||
|
|
9dc2d3669c | ||
|
|
511c76e219 | ||
|
|
78329b7b92 | ||
|
|
4240048853 | ||
|
|
432af42ffd | ||
|
|
0d6c03f8d4 | ||
|
|
96830f975e | ||
|
|
13c538ff58 | ||
|
|
14250e9634 | ||
|
|
e2f7890d76 | ||
|
|
257d38de66 | ||
|
|
fd2a14e01b | ||
|
|
b4d76c7138 | ||
|
|
9655f37fa8 | ||
|
|
246fb9b855 | ||
|
|
25afadc9b2 | ||
|
|
3f547f0856 | ||
|
|
11e322b6d7 | ||
|
|
02ff133a62 | ||
|
|
47268aac87 | ||
|
|
8aad1ac554 | ||
|
|
9037cde439 | ||
|
|
2afafd79e4 | ||
|
|
f4fa2517d2 | ||
|
|
37bc46c1cd | ||
|
|
3e3a7ed4f0 | ||
|
|
04fa7d366d | ||
|
|
ed9a3214a2 | ||
|
|
66a9e1a653 | ||
|
|
8cb59c35fb | ||
|
|
94e9c05d60 | ||
|
|
8d2c4e1246 | ||
|
|
c05be39346 | ||
|
|
951d42a591 | ||
|
|
dd046d8a68 | ||
|
|
efa54a4d51 | ||
|
|
3f07c50cc5 | ||
|
|
94cf07ddb4 | ||
|
|
24063e06ab | ||
|
|
e8ebb87189 | ||
|
|
896e196767 | ||
|
|
9f5be75e6d | ||
|
|
9cc9e720bb | ||
|
|
a9c2cca66d | ||
|
|
9cc3646be5 | ||
|
|
d6bca449da | ||
|
|
cb5764c654 | ||
|
|
19a9b56fa4 | ||
|
|
a2b0f199f1 | ||
|
|
59bfad7614 | ||
|
|
aee3f2d12b | ||
|
|
11d58b4460 | ||
|
|
ee4de6c6ca | ||
|
|
8d16b88185 | ||
|
|
121ef8e80d | ||
|
|
d53fec7e75 | ||
|
|
c017a3cd7e | ||
|
|
27ea93090f | ||
|
|
d79845144e | ||
|
|
3f77900dd0 | ||
|
|
4e8b9e81cf | ||
|
|
a32ab3acfd | ||
|
|
942da3a5c0 | ||
|
|
17e1a72baf | ||
|
|
b454ded00a | ||
|
|
d4512393e2 | ||
|
|
97d1384726 | ||
|
|
ba002a7a4a | ||
|
|
349efab7a8 | ||
|
|
af9a6f42db | ||
|
|
6b20fa8abd | ||
|
|
029ad3903f | ||
|
|
a23d66930b | ||
|
|
710ab7ae09 | ||
|
|
434b07ae64 | ||
|
|
eee8c95ca6 | ||
|
|
1f5c514011 | ||
|
|
66d722e097 | ||
|
|
39befe5aa4 | ||
|
|
ab043e87dc | ||
|
|
58ae9c0a13 | ||
|
|
44c446943c | ||
|
|
8301b669fe | ||
|
|
6fa0b79c67 | ||
|
|
32d23d6636 | ||
|
|
b31b695887 | ||
|
|
33de32b138 | ||
|
|
753b53a529 | ||
|
|
123535b9a5 | ||
|
|
7a5fa452f0 | ||
|
|
281e712542 | ||
|
|
c2c34ecf53 | ||
|
|
615193617c | ||
|
|
1b58d50b6d | ||
|
|
99f9a0b4e6 | ||
|
|
696001a8bb | ||
|
|
31f057c097 | ||
|
|
0391537a60 | ||
|
|
521c1f760c | ||
|
|
3bf9b4f90f | ||
|
|
af86a6d34e | ||
|
|
3ecf5c6166 | ||
|
|
4da3e7b2b3 | ||
|
|
66f38f1566 | ||
|
|
04b513ad14 | ||
|
|
1ce7fda8bb | ||
|
|
6d09fad675 | ||
|
|
ad061e7ece | ||
|
|
3155343bcc | ||
|
|
f5e91f7bfd | ||
|
|
1d69f2ed3f | ||
|
|
1d233dbcab | ||
|
|
1aafb0b201 | ||
|
|
d7d5a2dd42 | ||
|
|
8060a65ef6 | ||
|
|
379071f838 | ||
|
|
5cbbd060a4 | ||
|
|
ef19673a76 | ||
|
|
c3cf8a6ebb | ||
|
|
c22b27525a | ||
|
|
eec3b01f5b | ||
|
|
e67a127a02 | ||
|
|
a074ebc951 | ||
|
|
d1cd814663 | ||
|
|
ac76646a20 | ||
|
|
6549f799f6 | ||
|
|
cca55fd66c | ||
|
|
2f67d2813a | ||
|
|
9a7a5fdc38 | ||
|
|
f1fdec6822 | ||
|
|
5464b23329 | ||
|
|
4c99971882 | ||
|
|
cc7769b601 | ||
|
|
cb2ed7daf9 | ||
|
|
78508094c8 | ||
|
|
b0f755a30c | ||
|
|
9d1384792a | ||
|
|
ea17116998 | ||
|
|
2c23681fc5 | ||
|
|
17aa2832ea | ||
|
|
5f3a329ef2 | ||
|
|
96f49da79e | ||
|
|
c7dfde0ce9 | ||
|
|
8cf32020f7 | ||
|
|
a5ed5a0e60 | ||
|
|
3279936fc9 | ||
|
|
8abccc709e | ||
|
|
76f30e7682 | ||
|
|
ab289b3e42 | ||
|
|
ef7e04065c | ||
|
|
d1084039b3 | ||
|
|
7bada440d2 | ||
|
|
803c4752db | ||
|
|
c0777474c0 | ||
|
|
66dcea5604 | ||
|
|
a2a12d2450 | ||
|
|
39593bd5a8 | ||
|
|
45d8a8a4e6 | ||
|
|
a4546c77ce | ||
|
|
d69bf6360a | ||
|
|
da9ce5b5c3 | ||
|
|
e092098101 | ||
|
|
1a89a79b74 | ||
|
|
cb6bf49922 | ||
|
|
4bcaba0be0 | ||
|
|
220ef723c7 | ||
|
|
9c599a6be4 | ||
|
|
715ce1fc6c | ||
|
|
8c3a192dd0 | ||
|
|
d22bf93dfd | ||
|
|
886054fdf8 | ||
|
|
4188510586 | ||
|
|
fedebca5e1 | ||
|
|
e2ce6437e9 | ||
|
|
bdae60bac9 | ||
|
|
2d6c818aec | ||
|
|
a1d19852dc | ||
|
|
104c95f28f | ||
|
|
55fa1ec637 | ||
|
|
b27a3d8272 | ||
|
|
089d450b46 | ||
|
|
358ac7434d | ||
|
|
9cd505fd8a | ||
|
|
20ac2687df | ||
|
|
9f075c09a2 | ||
|
|
3793538ba4 | ||
|
|
4c4b16d234 | ||
|
|
f5790bec2e | ||
|
|
6c0d08de56 | ||
|
|
ba344756b1 | ||
|
|
dfda86aca3 | ||
|
|
df6f83ed69 | ||
|
|
218d92a1ac | ||
|
|
df2b529d01 | ||
|
|
0ef42dbb4d | ||
|
|
1a428197b2 | ||
|
|
09d7983845 | ||
|
|
6e01f3187a | ||
|
|
468436b9f7 | ||
|
|
76c288a6e4 | ||
|
|
f95f67a7ca | ||
|
|
11864247eb | ||
|
|
74509ea7c9 | ||
|
|
948fe0a6dc | ||
|
|
a4257cbcde | ||
|
|
2929c3c898 | ||
|
|
2c5f2187c8 | ||
|
|
401ef88971 | ||
|
|
4fb3754048 | ||
|
|
596efe8fb0 | ||
|
|
076a4f2574 | ||
|
|
9561371a47 | ||
|
|
16254cf5f9 | ||
|
|
649a03e5a0 | ||
|
|
dd21d9b521 | ||
|
|
68b895d2ad | ||
|
|
634016ae1b | ||
|
|
83c6751847 | ||
|
|
04bb0c51b1 | ||
|
|
d2e9621de9 | ||
|
|
cb673ddc42 | ||
|
|
440618f2b6 | ||
|
|
ae79d45664 | ||
|
|
1877ccb513 | ||
|
|
b3098f2e4c | ||
|
|
3e0af062c1 | ||
|
|
858f85c50d | ||
|
|
938848be65 | ||
|
|
615f5899cc | ||
|
|
5a6b1313e8 | ||
|
|
ab7debb34b | ||
|
|
eee21de795 | ||
|
|
15fabbe7d0 | ||
|
|
6aef48c6e7 | ||
|
|
b29bc923fc | ||
|
|
b223e9b0cc | ||
|
|
77a982a7da | ||
|
|
ab3dc765b4 | ||
|
|
0261201360 | ||
|
|
1da3954879 | ||
|
|
742dd5ff54 | ||
|
|
a85406e3b7 | ||
|
|
73cdaf3d44 | ||
|
|
e26fa2dbf4 | ||
|
|
64be68a22d | ||
|
|
478a185968 | ||
|
|
4ff5d11a03 | ||
|
|
6000952b76 | ||
|
|
5fee2c4cd9 | ||
|
|
21d553cf1b | ||
|
|
782b2d3761 | ||
|
|
e1da3eee80 | ||
|
|
09af2da6b9 | ||
|
|
e3e9094d42 | ||
|
|
94634234ff | ||
|
|
a48d6029d9 | ||
|
|
9cc150b105 | ||
|
|
6a97d99876 | ||
|
|
c957168040 | ||
|
|
61bc35b3fa | ||
|
|
a84210c452 | ||
|
|
8af6ea1d8f | ||
|
|
1a894ac583 | ||
|
|
4f6e05414c | ||
|
|
5096a088d4 | ||
|
|
6581bddba3 | ||
|
|
292af28d42 | ||
|
|
37a6d03d52 | ||
|
|
fe35d450f0 | ||
|
|
6a9e27bc06 | ||
|
|
a989bf82ea | ||
|
|
ccc8d8002f | ||
|
|
eaaf8db486 | ||
|
|
c32fa7a84b | ||
|
|
57e21a78ee | ||
|
|
9cdf5d18d8 | ||
|
|
41b0a1211b | ||
|
|
1b8f09f2ce | ||
|
|
2f85de6b69 | ||
|
|
b2ef9d5b0a | ||
|
|
c80262d75b | ||
|
|
2a312d93ec | ||
|
|
e09df2fff3 | ||
|
|
f0c7d13b20 | ||
|
|
4dac60bef9 | ||
|
|
5aefb46790 | ||
|
|
41b043e551 | ||
|
|
5447fad1fc | ||
|
|
6a1e01abbd | ||
|
|
2803ad5ba0 | ||
|
|
8fa8a13036 | ||
|
|
41ce79ccce | ||
|
|
14ae062db2 | ||
|
|
d55a38da4a | ||
|
|
ab289cfc86 | ||
|
|
12671e9905 | ||
|
|
a33023b8c6 | ||
|
|
a3e134ce0b | ||
|
|
ee7c821cab | ||
|
|
ee4cf93aee | ||
|
|
2cacfba81f | ||
|
|
02e420580e | ||
|
|
d99398d3f8 | ||
|
|
9ea8335aa0 | ||
|
|
52a91a50b2 | ||
|
|
680bf46e25 | ||
|
|
d279c97f15 | ||
|
|
7d5d338c8e | ||
|
|
721ae1cac0 | ||
|
|
3881c9d753 | ||
|
|
131b344119 | ||
|
|
d226e52881 | ||
|
|
583815b4f7 | ||
|
|
f0a8d22e84 | ||
|
|
50c6f15e12 | ||
|
|
2e3a95f389 | ||
|
|
3d52096eb4 | ||
|
|
e981cacbda | ||
|
|
218371a318 | ||
|
|
30fd7c8c2a | ||
|
|
96d2d61fa0 | ||
|
|
17ff86aaea | ||
|
|
7f8c1ace14 | ||
|
|
dc0edb7bc1 | ||
|
|
2ac996c9f9 | ||
|
|
2ebabd69b5 | ||
|
|
b3738f1602 | ||
|
|
882152b911 | ||
|
|
a25e79031f | ||
|
|
cc85060b1b | ||
|
|
00bd9c241a | ||
|
|
1283e06f95 | ||
|
|
ab0108778a | ||
|
|
099b04f718 | ||
|
|
ecdc0a51a9 | ||
|
|
6c7c37affe | ||
|
|
45d378a2d9 | ||
|
|
007601cb19 | ||
|
|
5f0d6e2fdd | ||
|
|
ede9879c99 | ||
|
|
7287abc77c | ||
|
|
8c653b5c09 | ||
|
|
15c6b3c308 | ||
|
|
9676447c74 | ||
|
|
5d35f1dcc7 | ||
|
|
858f16195e | ||
|
|
a1a5dd574e | ||
|
|
a5ecc2dc9f | ||
|
|
7d46660583 | ||
|
|
22cbf40e3c | ||
|
|
25821c758f | ||
|
|
6153737a78 | ||
|
|
07adb45d63 | ||
|
|
02bc40b9b6 | ||
|
|
83e7e30e4f | ||
|
|
ae870fd46a | ||
|
|
33b7ba8725 | ||
|
|
dd2567a85f | ||
|
|
264ff8f885 | ||
|
|
629c6a8891 | ||
|
|
0ce2f96789 | ||
|
|
cd7d1571db | ||
|
|
4558f55282 | ||
|
|
21589fda57 | ||
|
|
3496263cd2 | ||
|
|
1bb1ec6106 | ||
|
|
2bfb838933 | ||
|
|
9eb291f578 | ||
|
|
8cf892124c | ||
|
|
47fb886930 | ||
|
|
5034a211cb | ||
|
|
ed1364b6ff | ||
|
|
71e18b616d | ||
|
|
f7bf21df68 | ||
|
|
d764e3405d | ||
|
|
16baceb784 | ||
|
|
5d2b80d15a | ||
|
|
a20a81f424 | ||
|
|
ebb66e9086 | ||
|
|
cb8797693e | ||
|
|
255c6335ae | ||
|
|
155cd53dcd | ||
|
|
ae70a96c10 | ||
|
|
16c0daf090 | ||
|
|
34c78c5a9d | ||
|
|
dd5b108ffd | ||
|
|
0b83986255 | ||
|
|
2bd25fb6f3 | ||
|
|
0f5eb5d3a3 | ||
|
|
c9434c61e3 | ||
|
|
ee969b7a06 | ||
|
|
cfdf88a1e2 | ||
|
|
70a5dae293 | ||
|
|
b53f8d4552 |
13
.devcontainer/Prowlarr.code-workspace
Normal file
13
.devcontainer/Prowlarr.code-workspace
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||||
|
// the frontend has vscode settings that are distinct from the backend
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../frontend"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||||
|
{
|
||||||
|
"name": "Prowlarr",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"nodeGypDependencies": true,
|
||||||
|
"version": "20",
|
||||||
|
"nvmVersion": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [9696],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
13
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -4,11 +4,18 @@ labels: ['Type: Bug', 'Status: Needs Triage']
|
|||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: I attest that there is not 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.
|
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:
|
options:
|
||||||
- label: I have searched the existing open and closed issues
|
- label: I have searched the existing open and closed issues
|
||||||
required: true
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: I attest this is not related to a Cardigann YML Indexer.
|
||||||
|
description: Please search to see if this is for a tracker [that is yml-based (Cardigann)](https://github.com/Prowlarr/indexers) these are synced to Prowlarr/Indexers from Jackett/Jackett.
|
||||||
|
options:
|
||||||
|
- label: I confirm this is not related to a Cardigann YML Indexer
|
||||||
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Current Behavior
|
label: Current Behavior
|
||||||
@@ -73,8 +80,8 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
|
label: I attest that Trace Logs have been provided as applicable. Reports will be closed if the required logs are not provided.
|
||||||
description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
|
description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
|
||||||
options:
|
options:
|
||||||
- label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
|
- label: I attest that I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
28
.github/labeler.yml
vendored
28
.github/labeler.yml
vendored
@@ -1,19 +1,31 @@
|
|||||||
'Area: API':
|
'Area: API':
|
||||||
- src/Prowlarr.Api.V1/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/Prowlarr.Api.V1/**/*
|
||||||
|
|
||||||
'Area: Db-migration':
|
'Area: Db-migration':
|
||||||
- src/NzbDrone.Core/Datastore/Migration/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Datastore/Migration/*
|
||||||
|
|
||||||
'Area: Download Clients':
|
'Area: Download Clients':
|
||||||
- src/NzbDrone.Core/Download/Clients/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Download/Clients/**/*
|
||||||
|
|
||||||
'Area: Indexer':
|
'Area: Indexer':
|
||||||
- src/NzbDrone.Core/Indexers/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Indexers/**/*
|
||||||
|
|
||||||
'Area: Notifications':
|
'Area: Notifications':
|
||||||
- src/NzbDrone.Core/Notifications/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file:
|
||||||
|
- src/NzbDrone.Core/Notifications/**/*
|
||||||
|
|
||||||
'Area: UI':
|
'Area: UI':
|
||||||
- frontend/**/*
|
- changed-files:
|
||||||
- package.json
|
- any-glob-to-any-file:
|
||||||
- yarn.lock
|
- frontend/**/*
|
||||||
|
- package.json
|
||||||
|
- yarn.lock
|
||||||
2
.github/workflows/label-actions.yml
vendored
2
.github/workflows/label-actions.yml
vendored
@@ -18,6 +18,6 @@ jobs:
|
|||||||
action:
|
action:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/label-actions@v3
|
- uses: dessant/label-actions@v4
|
||||||
with:
|
with:
|
||||||
process-only: 'issues, prs'
|
process-only: 'issues, prs'
|
||||||
|
|||||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -9,4 +9,4 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v5
|
||||||
|
|||||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v4
|
- uses: dessant/lock-threads@v5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: '90'
|
issue-inactive-days: '90'
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -127,6 +127,7 @@ coverage*.xml
|
|||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
|
.mono
|
||||||
|
|
||||||
# VS outout folders
|
# VS outout folders
|
||||||
bin
|
bin
|
||||||
|
|||||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||||
|
"name": "Run Prowlarr",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build dotnet",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/_output/net8.0/Prowlarr",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
44
.vscode/tasks.json
vendored
Normal file
44
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build dotnet",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"-restore",
|
||||||
|
"${workspaceFolder}/src/Prowlarr.sln",
|
||||||
|
"-p:GenerateFullPaths=true",
|
||||||
|
"-p:Configuration=Debug",
|
||||||
|
"-p:Platform=Posix",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/src/Prowlarr.sln",
|
||||||
|
"-property:GenerateFullPaths=true",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/src/Prowlarr.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1.3318" y1="43.7371" x2="67.0419" y2="26.0967">
|
|
||||||
<stop offset="0.1237" style="stop-color:#7866FF"/>
|
|
||||||
<stop offset="0.5376" style="stop-color:#FE2EB6"/>
|
|
||||||
<stop offset="0.8548" style="stop-color:#FD0486"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_1_);" points="67.3,16 43.7,0 0,31.1 11.1,70 58.9,60.3 "/>
|
|
||||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="45.9148" y1="38.9098" x2="67.6577" y2="9.0989">
|
|
||||||
<stop offset="0.1237" style="stop-color:#FF0080"/>
|
|
||||||
<stop offset="0.2587" style="stop-color:#FE0385"/>
|
|
||||||
<stop offset="0.4109" style="stop-color:#FA0C92"/>
|
|
||||||
<stop offset="0.5713" style="stop-color:#F41BA9"/>
|
|
||||||
<stop offset="0.7363" style="stop-color:#EB2FC8"/>
|
|
||||||
<stop offset="0.8656" style="stop-color:#E343E6"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_2_);" points="67.3,16 43.7,0 38,15.7 38,47.8 70,47.8 "/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
|
|
||||||
<rect x="17.4" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
|
||||||
<g>
|
|
||||||
<path style="fill:#FFFFFF;" d="M17.4,19.1h6.9c5.6,0,9.5,3.8,9.5,8.9V28c0,5-3.9,8.9-9.5,8.9h-6.9V19.1z M21.4,22.7v10.7h3
|
|
||||||
c3.2,0,5.4-2.2,5.4-5.3V28c0-3.2-2.2-5.4-5.4-5.4H21.4z"/>
|
|
||||||
<polygon style="fill:#FFFFFF;" points="40.3,22.7 34.9,22.7 34.9,19.1 49.6,19.1 49.6,22.7 44.2,22.7 44.2,37 40.3,37 "/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,66 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve"
|
|
||||||
>
|
|
||||||
<g>
|
|
||||||
<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24">
|
|
||||||
<stop offset="0" style="stop-color:#FCEE39"/>
|
|
||||||
<stop offset="1" style="stop-color:#F37B3D"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9
|
|
||||||
c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0
|
|
||||||
c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/>
|
|
||||||
<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546">
|
|
||||||
<stop offset="0" style="stop-color:#EF5A6B"/>
|
|
||||||
<stop offset="0.57" style="stop-color:#F26F4E"/>
|
|
||||||
<stop offset="1" style="stop-color:#F37B3D"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0
|
|
||||||
c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3
|
|
||||||
c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/>
|
|
||||||
<linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562">
|
|
||||||
<stop offset="0" style="stop-color:#7C59A4"/>
|
|
||||||
<stop offset="0.3852" style="stop-color:#AF4C92"/>
|
|
||||||
<stop offset="0.7654" style="stop-color:#DC4183"/>
|
|
||||||
<stop offset="0.957" style="stop-color:#ED3D7D"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9
|
|
||||||
c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0
|
|
||||||
c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/>
|
|
||||||
<linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971">
|
|
||||||
<stop offset="0" style="stop-color:#EF5A6B"/>
|
|
||||||
<stop offset="0.364" style="stop-color:#EE4E72"/>
|
|
||||||
<stop offset="1" style="stop-color:#ED3D7D"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0
|
|
||||||
l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0
|
|
||||||
c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/>
|
|
||||||
<g id="XMLID_3008_">
|
|
||||||
<rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/>
|
|
||||||
<rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/>
|
|
||||||
<g id="XMLID_3009_">
|
|
||||||
<path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0
|
|
||||||
l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/>
|
|
||||||
<path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0
|
|
||||||
L45.3,43.8z"/>
|
|
||||||
<path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/>
|
|
||||||
<path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0
|
|
||||||
c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5
|
|
||||||
l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/>
|
|
||||||
<path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0
|
|
||||||
c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1
|
|
||||||
l-1.5,0v2H50.6z"/>
|
|
||||||
<path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z
|
|
||||||
M58.8,59l-0.9-2.3L57,59L58.8,59z"/>
|
|
||||||
<path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/>
|
|
||||||
<path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z"
|
|
||||||
/>
|
|
||||||
<path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0
|
|
||||||
c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6
|
|
||||||
c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7
|
|
||||||
C76.1,62.5,74.7,62,73.7,61.1z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,50 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="22.9451" y1="75.7869" x2="74.7868" y2="20.6415">
|
|
||||||
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
|
||||||
<stop offset="0.4044" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.4677" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.6505" style="stop-color:#EB8523"/>
|
|
||||||
<stop offset="0.9516" style="stop-color:#FEBD11"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_1_);" points="49.8,15.2 36,36.7 58.4,70 70,23.1 "/>
|
|
||||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.7187" y1="73.2922" x2="69.5556" y2="18.1519">
|
|
||||||
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
|
||||||
<stop offset="0.4044" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.4677" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.7043" style="stop-color:#EB8523"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_2_);" points="51.1,15.7 49,0 18.8,33.6 27.6,42.3 20.8,70 58.4,70 "/>
|
|
||||||
</g>
|
|
||||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1.8281" y1="53.4275" x2="48.8245" y2="9.2255">
|
|
||||||
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
|
||||||
<stop offset="0.6613" style="stop-color:#C41E57"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_3_);" points="49,0 11.6,0 0,47.1 55.6,47.1 "/>
|
|
||||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="49.8935" y1="-11.5569" x2="48.8588" y2="24.0352">
|
|
||||||
<stop offset="0.5" style="stop-color:#C41E57"/>
|
|
||||||
<stop offset="0.6668" style="stop-color:#D13F48"/>
|
|
||||||
<stop offset="0.7952" style="stop-color:#D94F39"/>
|
|
||||||
<stop offset="0.8656" style="stop-color:#DD5433"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_4_);" points="55.3,47.1 51.1,15.7 49,0 41.7,23 "/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
|
|
||||||
<rect x="13.4" y="13.5" transform="matrix(-1 2.577289e-003 -2.577289e-003 -1 70.0288 70.081)" style="fill:#000000;" width="43.2" height="43.2"/>
|
|
||||||
|
|
||||||
<rect x="17.6" y="48.6" transform="matrix(1 -2.577289e-003 2.577289e-003 1 -0.1287 6.634109e-002)" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
|
||||||
<path style="fill:#FFFFFF;" d="M17.4,19.1l8.2,0c2.3,0,4,0.6,5.2,1.8c1,1,1.5,2.4,1.5,4.1l0,0.1c0,1.5-0.3,2.6-1.1,3.5
|
|
||||||
c-0.7,0.9-1.6,1.6-2.8,2l4.4,6.4l-4.6,0l-3.7-5.5l-3.3,0l0,5.5l-3.9,0L17.4,19.1z M25.3,27.8c1,0,1.7-0.2,2.2-0.7
|
|
||||||
c0.5-0.5,0.8-1.1,0.8-1.8l0-0.1c0-0.9-0.3-1.5-0.8-1.9c-0.5-0.4-1.3-0.6-2.3-0.6l-3.9,0l0,5.1L25.3,27.8z"/>
|
|
||||||
<path style="fill:#FFFFFF;" d="M36,33.2l-1.9,0l0-3.3l2.5,0l0.6-3.8l-2.3,0l0-3.3l2.8,0l0.6-3.7l3.4,0l-0.6,3.7l3.7,0l0.6-3.7
|
|
||||||
l3.4,0l-0.6,3.7l1.9,0l0,3.3l-2.5,0L47,29.9l2.3,0l0,3.3l-2.8,0L45.8,37l-3.4,0l0.7-3.8l-3.7,0L38.7,37l-3.4,0L36,33.2z
|
|
||||||
M43.7,29.9l0.6-3.8l-3.7,0L40,29.9L43.7,29.9z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="linear-gradient" x1="70.22612" y1="27.79912" x2="-5.13024" y2="63.12242" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.22111" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.2356" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.35559" stop-color="#ca135c"/>
|
|
||||||
<stop offset="0.46633" stop-color="#ce1e57"/>
|
|
||||||
<stop offset="0.5735" stop-color="#d4314e"/>
|
|
||||||
<stop offset="0.67844" stop-color="#dc4b41"/>
|
|
||||||
<stop offset="0.78179" stop-color="#e66d31"/>
|
|
||||||
<stop offset="0.88253" stop-color="#f3961d"/>
|
|
||||||
<stop offset="0.94241" stop-color="#fcb20f"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-2" x1="24.65904" y1="61.99608" x2="46.04762" y2="2.93445" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0.04188" stop-color="#077cfb"/>
|
|
||||||
<stop offset="0.44503" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.95812" stop-color="#077cfb"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-3" x1="17.39552" y1="63.34592" x2="33.19389" y2="7.20092" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0.27749" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.97382" stop-color="#fcb20f"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<title>rider</title>
|
|
||||||
<g>
|
|
||||||
<polygon points="70 27.237 63.391 23.75 20.926 0 3.827 17.921 21.619 41.068 60.537 44.397 70 27.237" fill="url(#linear-gradient)"/>
|
|
||||||
<polygon points="50.423 16.132 44.271 1.107 27.643 17.471 11.768 50.194 49.411 70 70 57.98 50.423 16.132" fill="url(#linear-gradient-2)"/>
|
|
||||||
<polygon points="20.926 0 0 14.095 7.779 62.172 27.848 69.889 53.78 48.823 20.926 0" fill="url(#linear-gradient-3)"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<rect x="13.30219" y="13.19311" width="43.61371" height="43.61371"/>
|
|
||||||
<g>
|
|
||||||
<path d="M17.22741,18.86293h8.39564a7.38416,7.38416,0,0,1,5.34268,1.85358,5.86989,5.86989,0,0,1,1.52648,4.1433h0A5.74339,5.74339,0,0,1,28.567,30.5296l4.47041,6.54206H28.34891L24.42368,31.1838h-3.162v5.88785H17.22741V18.86293h0ZM25.296,27.69471c1.96262,0,3.053-1.09034,3.053-2.61682h0c0-1.74455-1.19938-2.61682-3.162-2.61682H21.15265v5.23365H25.296Z" fill="#fff"/>
|
|
||||||
<path d="M36.09034,18.86293H43.2866c5.77882,0,9.70405,3.92523,9.70405,9.15888h0c0,5.12461-3.92523,9.15888-9.70405,9.15888H36.09034V18.86293Zm4.03427,3.59813V33.47352h3.162a5.23727,5.23727,0,0,0,5.56075-5.45171h0a5.26493,5.26493,0,0,0-5.56075-5.56075h-3.162Z" fill="#fff"/>
|
|
||||||
</g>
|
|
||||||
<rect x="17.22741" y="48.62925" width="16.35514" height="2.72586" fill="#fff"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="25.0676" y1="1.4599" x2="43.1829" y2="66.675">
|
|
||||||
<stop offset="0.2849" style="stop-color:#00CDD7"/>
|
|
||||||
<stop offset="0.9409" style="stop-color:#2086D7"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_1_);" points="9.4,63.3 0,7.3 17.5,0.1 28.6,6.7 38.8,1.2 60.1,9.4 48.1,70 "/>
|
|
||||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="30.7199" y1="9.7343" x2="61.365" y2="54.6713">
|
|
||||||
<stop offset="0.1398" style="stop-color:#FFF045"/>
|
|
||||||
<stop offset="0.3656" style="stop-color:#00CDD7"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_2_);" points="70,23.7 61,1.4 44.6,0 19.3,24.3 26.1,55.6 38.8,64.6 70,46 62.3,31.7 "/>
|
|
||||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="61.0819" y1="15.2899" x2="65.1065" y2="29.5436">
|
|
||||||
<stop offset="0.2849" style="stop-color:#00CDD7"/>
|
|
||||||
<stop offset="0.9409" style="stop-color:#2086D7"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_3_);" points="56,20.4 62.3,31.7 70,23.7 64.4,9.8 "/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
|
|
||||||
<rect x="17.5" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
|
||||||
<path style="fill:#FFFFFF;" d="M38.7,34.3l2.3-2.8c1.6,1.3,3.3,2.2,5.3,2.2c1.6,0,2.5-0.6,2.5-1.7v-0.1c0-1-0.6-1.5-3.6-2.3
|
|
||||||
c-3.6-0.9-5.8-1.9-5.8-5.5v-0.1c0-3.3,2.6-5.4,6.2-5.4c2.6,0,4.8,0.8,6.6,2.3l-2,3c-1.6-1.1-3.1-1.8-4.6-1.8
|
|
||||||
c-1.5,0-2.3,0.7-2.3,1.6v0.1c0,1.2,0.8,1.6,3.8,2.4c3.6,1,5.6,2.3,5.6,5.4v0.1c0,3.6-2.7,5.6-6.5,5.6
|
|
||||||
C43.5,37.2,40.8,36.2,38.7,34.3"/>
|
|
||||||
</g>
|
|
||||||
<polygon style="fill:#FFFFFF;" points="35.2,19 32.5,29.4 29.5,19 26.5,19 23.4,29.4 20.7,19 16.6,19 21.7,36.9 25,36.9 28,26.5
|
|
||||||
30.9,36.9 34.3,36.9 39.4,19 "/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
14
README.md
14
README.md
@@ -1,7 +1,7 @@
|
|||||||
# Prowlarr
|
# Prowlarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
|
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
||||||
[](https://wiki.servarr.com/prowlarr/installation/docker)
|
[](https://wiki.servarr.com/prowlarr/installation/docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
@@ -68,16 +68,16 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
|
|||||||
|
|
||||||
## JetBrains
|
## JetBrains
|
||||||
|
|
||||||
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
Thank you to [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains" width="96">](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
||||||
|
|
||||||
- [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/ReSharper_icon.png" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||||
- [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/WebStorm_icon.png" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||||
- [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/Rider_icon.png" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||||
- [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/dotTrace_icon.png" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
- Copyright 2010-2022
|
- Copyright 2010-2025
|
||||||
|
|
||||||
Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box)
|
Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box)
|
||||||
|
|||||||
@@ -9,18 +9,18 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '1.12.2'
|
majorVersion: '2.3.0'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.417'
|
dotnetVersion: '8.0.405'
|
||||||
nodeVersion: '16.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-22.04'
|
||||||
macImage: 'macOS-11'
|
macImage: 'macOS-13'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
@@ -106,7 +106,7 @@ stages:
|
|||||||
echo "Extra platforms already enabled"
|
echo "Extra platforms already enabled"
|
||||||
else
|
else
|
||||||
echo "Enabling extra platform support"
|
echo "Enabling extra platform support"
|
||||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
|
||||||
fi
|
fi
|
||||||
displayName: Enable Extra Platform Support
|
displayName: Enable Extra Platform Support
|
||||||
- bash: ./build.sh --backend --enable-extra-platforms
|
- bash: ./build.sh --backend --enable-extra-platforms
|
||||||
@@ -122,27 +122,23 @@ stages:
|
|||||||
artifact: '$(osName)Backend'
|
artifact: '$(osName)Backend'
|
||||||
displayName: Publish Backend
|
displayName: Publish Backend
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
- publish: '$(testsFolder)/net8.0/win-x64/publish'
|
||||||
artifact: win-x64-tests
|
artifact: win-x64-tests
|
||||||
displayName: Publish win-x64 Test Package
|
displayName: Publish win-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
- publish: '$(testsFolder)/net8.0/linux-x64/publish'
|
||||||
artifact: linux-x64-tests
|
artifact: linux-x64-tests
|
||||||
displayName: Publish linux-x64 Test Package
|
displayName: Publish linux-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
- publish: '$(testsFolder)/net8.0/linux-musl-x64/publish'
|
||||||
artifact: linux-x86-tests
|
|
||||||
displayName: Publish linux-x86 Test Package
|
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
|
||||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
|
||||||
artifact: linux-musl-x64-tests
|
artifact: linux-musl-x64-tests
|
||||||
displayName: Publish linux-musl-x64 Test Package
|
displayName: Publish linux-musl-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
- publish: '$(testsFolder)/net8.0/freebsd-x64/publish'
|
||||||
artifact: freebsd-x64-tests
|
artifact: freebsd-x64-tests
|
||||||
displayName: Publish freebsd-x64 Test Package
|
displayName: Publish freebsd-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
- publish: '$(testsFolder)/net8.0/osx-x64/publish'
|
||||||
artifact: osx-x64-tests
|
artifact: osx-x64-tests
|
||||||
displayName: Publish osx-x64 Test Package
|
displayName: Publish osx-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
@@ -166,10 +162,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -189,7 +185,7 @@ stages:
|
|||||||
artifact: '$(osName)Frontend'
|
artifact: '$(osName)Frontend'
|
||||||
displayName: Publish Frontend
|
displayName: Publish Frontend
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
|
|
||||||
- stage: Installer
|
- stage: Installer
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- Build_Backend
|
- Build_Backend
|
||||||
@@ -259,21 +255,21 @@ stages:
|
|||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/win-x64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create win-x86 zip
|
displayName: Create win-x86 zip
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).windows-core-x86.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
rootFolderOrFile: $(artifactsFolder)/win-x86/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-x64 app
|
displayName: Create osx-x64 app
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-x64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-x64 tar
|
displayName: Create osx-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -281,14 +277,14 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-x64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-arm64 app
|
displayName: Create osx-arm64 app
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).osx-app-core-arm64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-arm64 tar
|
displayName: Create osx-arm64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -296,7 +292,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-x64 tar
|
displayName: Create linux-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -304,7 +300,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-x64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-musl-x64 tar
|
displayName: Create linux-musl-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -312,15 +308,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net8.0
|
||||||
- task: ArchiveFiles@2
|
|
||||||
displayName: Create linux-x86 tar
|
|
||||||
inputs:
|
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).linux-core-x86.tar.gz'
|
|
||||||
archiveType: 'tar'
|
|
||||||
tarCompression: 'gz'
|
|
||||||
includeRootFolder: false
|
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
|
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-arm tar
|
displayName: Create linux-arm tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -328,7 +316,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-arm/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-musl-arm tar
|
displayName: Create linux-musl-arm tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -336,7 +324,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-arm64 tar
|
displayName: Create linux-arm64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -344,7 +332,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-musl-arm64 tar
|
displayName: Create linux-musl-arm64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -352,7 +340,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create freebsd-x64 tar
|
displayName: Create freebsd-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -360,7 +348,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net8.0
|
||||||
- publish: $(Build.ArtifactStagingDirectory)
|
- publish: $(Build.ArtifactStagingDirectory)
|
||||||
artifact: 'Packages'
|
artifact: 'Packages'
|
||||||
displayName: Publish Packages
|
displayName: Publish Packages
|
||||||
@@ -391,7 +379,7 @@ stages:
|
|||||||
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
|
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
|
||||||
SENTRY_ORG: $(sentryOrg)
|
SENTRY_ORG: $(sentryOrg)
|
||||||
SENTRY_URL: $(sentryUrl)
|
SENTRY_URL: $(sentryUrl)
|
||||||
|
|
||||||
- stage: Unit_Test
|
- stage: Unit_Test
|
||||||
displayName: Unit Tests
|
displayName: Unit Tests
|
||||||
dependsOn: Build_Backend
|
dependsOn: Build_Backend
|
||||||
@@ -476,6 +464,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: ne(variables['testName'], 'freebsd-x64')
|
||||||
|
|
||||||
- job: Unit_Docker
|
- job: Unit_Docker
|
||||||
displayName: Unit Docker
|
displayName: Unit Docker
|
||||||
@@ -487,29 +476,19 @@ stages:
|
|||||||
testName: 'Musl Net Core'
|
testName: 'Musl Net Core'
|
||||||
artifactName: linux-musl-x64-tests
|
artifactName: linux-musl-x64-tests
|
||||||
containerImage: ghcr.io/servarr/testimages:alpine
|
containerImage: ghcr.io/servarr/testimages:alpine
|
||||||
linux-x86:
|
|
||||||
testName: 'linux-x86'
|
|
||||||
artifactName: linux-x86-tests
|
|
||||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
container: $[ variables['containerImage'] ]
|
container: $[ variables['containerImage'] ]
|
||||||
|
|
||||||
timeoutInMinutes: 10
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .NET'
|
displayName: 'Install .NET'
|
||||||
inputs:
|
inputs:
|
||||||
version: $(dotnetVersion)
|
version: $(dotnetVersion)
|
||||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
|
||||||
- bash: |
|
|
||||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
|
||||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
|
||||||
displayName: 'Install .NET'
|
|
||||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
|
||||||
- checkout: none
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
@@ -532,7 +511,8 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
|
|
||||||
- job: Unit_LinuxCore_Postgres14
|
- job: Unit_LinuxCore_Postgres14
|
||||||
displayName: Unit Native LinuxCore with Postgres14 Database
|
displayName: Unit Native LinuxCore with Postgres14 Database
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
@@ -549,7 +529,7 @@ stages:
|
|||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
timeoutInMinutes: 10
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .net core'
|
||||||
@@ -585,6 +565,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
|
|
||||||
- job: Unit_LinuxCore_Postgres15
|
- job: Unit_LinuxCore_Postgres15
|
||||||
displayName: Unit Native LinuxCore with Postgres15 Database
|
displayName: Unit Native LinuxCore with Postgres15 Database
|
||||||
@@ -597,12 +578,12 @@ stages:
|
|||||||
Prowlarr__Postgres__Port: '5432'
|
Prowlarr__Postgres__Port: '5432'
|
||||||
Prowlarr__Postgres__User: 'prowlarr'
|
Prowlarr__Postgres__User: 'prowlarr'
|
||||||
Prowlarr__Postgres__Password: 'prowlarr'
|
Prowlarr__Postgres__Password: 'prowlarr'
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
timeoutInMinutes: 10
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .net core'
|
||||||
@@ -638,6 +619,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
|
|
||||||
- stage: Integration
|
- stage: Integration
|
||||||
displayName: Integration
|
displayName: Integration
|
||||||
@@ -681,7 +663,7 @@ stages:
|
|||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .net core'
|
||||||
@@ -703,7 +685,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -720,6 +702,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Integration Tests'
|
testRunTitle: '$(testName) Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_LinuxCore_Postgres14
|
- job: Integration_LinuxCore_Postgres14
|
||||||
@@ -757,7 +740,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -782,6 +765,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
|
|
||||||
@@ -820,7 +804,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -845,6 +829,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_FreeBSD
|
- job: Integration_FreeBSD
|
||||||
@@ -891,6 +876,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'FreeBSD Integration Tests'
|
testRunTitle: 'FreeBSD Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: false
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_Docker
|
- job: Integration_Docker
|
||||||
@@ -904,29 +890,18 @@ stages:
|
|||||||
artifactName: linux-musl-x64-tests
|
artifactName: linux-musl-x64-tests
|
||||||
containerImage: ghcr.io/servarr/testimages:alpine
|
containerImage: ghcr.io/servarr/testimages:alpine
|
||||||
pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz'
|
pattern: 'Prowlarr.*.linux-musl-core-x64.tar.gz'
|
||||||
linux-x86:
|
|
||||||
testName: 'linux-x86'
|
|
||||||
artifactName: linux-x86-tests
|
|
||||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
|
||||||
pattern: 'Prowlarr.*.linux-core-x86.tar.gz'
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
container: $[ variables['containerImage'] ]
|
container: $[ variables['containerImage'] ]
|
||||||
|
|
||||||
timeoutInMinutes: 15
|
timeoutInMinutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .NET'
|
displayName: 'Install .NET'
|
||||||
inputs:
|
inputs:
|
||||||
version: $(dotnetVersion)
|
version: $(dotnetVersion)
|
||||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
|
||||||
- bash: |
|
|
||||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
|
||||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
|
||||||
displayName: 'Install .NET'
|
|
||||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
|
||||||
- checkout: none
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
@@ -943,7 +918,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -960,12 +935,13 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Integration Tests'
|
testRunTitle: '$(testName) Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- stage: Automation
|
- stage: Automation
|
||||||
displayName: Automation
|
displayName: Automation
|
||||||
dependsOn: Packages
|
dependsOn: Packages
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Automation
|
- job: Automation
|
||||||
strategy:
|
strategy:
|
||||||
@@ -991,7 +967,7 @@ stages:
|
|||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .net core'
|
||||||
@@ -1013,7 +989,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -1041,6 +1017,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(osName) Automation Tests'
|
testRunTitle: '$(osName) Automation Tests'
|
||||||
failTaskOnFailedTests: $(failBuild)
|
failTaskOnFailedTests: $(failBuild)
|
||||||
|
failTaskOnMissingResultsFile: $(failBuild)
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- stage: Analyze
|
- stage: Analyze
|
||||||
@@ -1075,10 +1052,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1116,7 +1093,7 @@ stages:
|
|||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
persistCredentials: true
|
persistCredentials: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
- bash: ./docs.sh Windows
|
- bash: ./docs.sh Windows
|
||||||
displayName: Create openapi.json
|
displayName: Create openapi.json
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -1169,39 +1146,35 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@1
|
- task: SonarCloudPrepare@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'prowlarr'
|
organization: 'prowlarr'
|
||||||
scannerMode: 'MSBuild'
|
scannerMode: 'dotnet'
|
||||||
projectKey: 'Prowlarr_Prowlarr'
|
projectKey: 'Prowlarr_Prowlarr'
|
||||||
projectName: 'Prowlarr'
|
projectName: 'Prowlarr'
|
||||||
projectVersion: '$(prowlarrVersion)'
|
projectVersion: '$(prowlarrVersion)'
|
||||||
extraProperties: |
|
extraProperties: |
|
||||||
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
|
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
|
||||||
sonar.coverage.exclusions=**/Prowlarr.Api.V1/**/*
|
sonar.coverage.exclusions=**/Prowlarr.Api.V1/**/*
|
||||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
sonar.cs.cobertura.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.cobertura.xml
|
||||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||||
- bash: |
|
- bash: |
|
||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net8.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net8.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@1
|
- task: SonarCloudAnalyze@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@4
|
- task: reportgenerator@5
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.cobertura.xml'
|
||||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||||
- task: PublishCodeCoverageResults@1
|
publishCodeCoverageResults: true
|
||||||
displayName: Publish Coverage Report
|
sourcedirs: src
|
||||||
inputs:
|
|
||||||
codeCoverageTool: 'cobertura'
|
|
||||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
|
||||||
reportDirectory: './CoverageResults/combined/'
|
|
||||||
|
|
||||||
- stage: Report_Out
|
- stage: Report_Out
|
||||||
dependsOn:
|
dependsOn:
|
||||||
@@ -1233,4 +1206,3 @@ stages:
|
|||||||
DISCORDCHANNELID: $(discordChannelId)
|
DISCORDCHANNELID: $(discordChannelId)
|
||||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||||
DISCORDTHREADID: $(discordThreadId)
|
DISCORDTHREADID: $(discordThreadId)
|
||||||
|
|
||||||
|
|||||||
52
build.sh
52
build.sh
@@ -33,14 +33,14 @@ EnableExtraPlatformsInSDK()
|
|||||||
echo "Extra platforms already enabled"
|
echo "Extra platforms already enabled"
|
||||||
else
|
else
|
||||||
echo "Enabling extra platform support"
|
echo "Enabling extra platform support"
|
||||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
EnableExtraPlatforms()
|
EnableExtraPlatforms()
|
||||||
{
|
{
|
||||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
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
|
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +79,9 @@ Build()
|
|||||||
|
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:SelfContained=True -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
|
||||||
else
|
else
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:SelfContained=True -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ProgressEnd 'Build'
|
ProgressEnd 'Build'
|
||||||
@@ -137,7 +137,7 @@ PackageLinux()
|
|||||||
|
|
||||||
echo "Adding Prowlarr.Mono to UpdatePackage"
|
echo "Adding Prowlarr.Mono to UpdatePackage"
|
||||||
cp $folder/Prowlarr.Mono.* $folder/Prowlarr.Update
|
cp $folder/Prowlarr.Mono.* $folder/Prowlarr.Update
|
||||||
if [ "$framework" = "net6.0" ]; then
|
if [ "$framework" = "net8.0" ]; then
|
||||||
cp $folder/Mono.Posix.NETStandard.* $folder/Prowlarr.Update
|
cp $folder/Mono.Posix.NETStandard.* $folder/Prowlarr.Update
|
||||||
cp $folder/libMonoPosixHelper.* $folder/Prowlarr.Update
|
cp $folder/libMonoPosixHelper.* $folder/Prowlarr.Update
|
||||||
fi
|
fi
|
||||||
@@ -165,7 +165,7 @@ PackageMacOS()
|
|||||||
|
|
||||||
echo "Adding Prowlarr.Mono to UpdatePackage"
|
echo "Adding Prowlarr.Mono to UpdatePackage"
|
||||||
cp $folder/Prowlarr.Mono.* $folder/Prowlarr.Update
|
cp $folder/Prowlarr.Mono.* $folder/Prowlarr.Update
|
||||||
if [ "$framework" = "net6.0" ]; then
|
if [ "$framework" = "net8.0" ]; then
|
||||||
cp $folder/Mono.Posix.NETStandard.* $folder/Prowlarr.Update
|
cp $folder/Mono.Posix.NETStandard.* $folder/Prowlarr.Update
|
||||||
cp $folder/libMonoPosixHelper.* $folder/Prowlarr.Update
|
cp $folder/libMonoPosixHelper.* $folder/Prowlarr.Update
|
||||||
fi
|
fi
|
||||||
@@ -377,15 +377,14 @@ then
|
|||||||
Build
|
Build
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "win-x64"
|
PackageTests "net8.0" "win-x64"
|
||||||
PackageTests "net6.0" "win-x86"
|
PackageTests "net8.0" "win-x86"
|
||||||
PackageTests "net6.0" "linux-x64"
|
PackageTests "net8.0" "linux-x64"
|
||||||
PackageTests "net6.0" "linux-musl-x64"
|
PackageTests "net8.0" "linux-musl-x64"
|
||||||
PackageTests "net6.0" "osx-x64"
|
PackageTests "net8.0" "osx-x64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "freebsd-x64"
|
PackageTests "net8.0" "freebsd-x64"
|
||||||
PackageTests "net6.0" "linux-x86"
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
PackageTests "$FRAMEWORK" "$RID"
|
PackageTests "$FRAMEWORK" "$RID"
|
||||||
@@ -413,20 +412,19 @@ then
|
|||||||
|
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
Package "net6.0" "win-x64"
|
Package "net8.0" "win-x64"
|
||||||
Package "net6.0" "win-x86"
|
Package "net8.0" "win-x86"
|
||||||
Package "net6.0" "linux-x64"
|
Package "net8.0" "linux-x64"
|
||||||
Package "net6.0" "linux-musl-x64"
|
Package "net8.0" "linux-musl-x64"
|
||||||
Package "net6.0" "linux-arm64"
|
Package "net8.0" "linux-arm64"
|
||||||
Package "net6.0" "linux-musl-arm64"
|
Package "net8.0" "linux-musl-arm64"
|
||||||
Package "net6.0" "linux-arm"
|
Package "net8.0" "linux-arm"
|
||||||
Package "net6.0" "linux-musl-arm"
|
Package "net8.0" "linux-musl-arm"
|
||||||
Package "net6.0" "osx-x64"
|
Package "net8.0" "osx-x64"
|
||||||
Package "net6.0" "osx-arm64"
|
Package "net8.0" "osx-arm64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
Package "net6.0" "freebsd-x64"
|
Package "net8.0" "freebsd-x64"
|
||||||
Package "net6.0" "linux-x86"
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
Package "$FRAMEWORK" "$RID"
|
Package "$FRAMEWORK" "$RID"
|
||||||
@@ -436,7 +434,7 @@ fi
|
|||||||
if [ "$INSTALLER" = "YES" ];
|
if [ "$INSTALLER" = "YES" ];
|
||||||
then
|
then
|
||||||
InstallInno
|
InstallInno
|
||||||
BuildInstaller "net6.0" "win-x64"
|
BuildInstaller "net8.0" "win-x64"
|
||||||
BuildInstaller "net6.0" "win-x86"
|
BuildInstaller "net8.0" "win-x86"
|
||||||
RemoveInno
|
RemoveInno
|
||||||
fi
|
fi
|
||||||
|
|||||||
25
docs.sh
Normal file → Executable file
25
docs.sh
Normal file → Executable file
@@ -1,13 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net8.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
ARCHITECTURE="${2:-x64}"
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
RUNTIME="win-x64"
|
RUNTIME="win-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Linux" ]; then
|
elif [ "$PLATFORM" = "Linux" ]; then
|
||||||
RUNTIME="linux-x64"
|
RUNTIME="linux-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Mac" ]; then
|
elif [ "$PLATFORM" = "Mac" ]; then
|
||||||
RUNTIME="osx-x64"
|
RUNTIME="osx-$ARCHITECTURE"
|
||||||
else
|
else
|
||||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -21,17 +26,23 @@ slnFile=src/Prowlarr.sln
|
|||||||
|
|
||||||
platform=Posix
|
platform=Posix
|
||||||
|
|
||||||
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
|
application=Prowlarr.Console.dll
|
||||||
|
else
|
||||||
|
application=Prowlarr.dll
|
||||||
|
fi
|
||||||
|
|
||||||
dotnet clean $slnFile -c Debug
|
dotnet clean $slnFile -c Debug
|
||||||
dotnet clean $slnFile -c Release
|
dotnet clean $slnFile -c Release
|
||||||
|
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||||
|
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 8.1.4 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/prowlarr.console.dll" v1 &
|
dotnet tool run swagger tofile --output ./src/Prowlarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||||
|
|
||||||
sleep 30
|
sleep 45
|
||||||
|
|
||||||
kill %1
|
kill %1
|
||||||
|
|
||||||
|
|||||||
@@ -357,11 +357,16 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||||
'no-shadow': 'off',
|
'@typescript-eslint/no-unused-vars': [
|
||||||
// These should be enabled after cleaning things up
|
'error',
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
{
|
||||||
|
args: 'after-used',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true
|
||||||
|
}
|
||||||
|
],
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'react/prop-types': 'off',
|
'no-shadow': 'off',
|
||||||
'prettier/prettier': 'error',
|
'prettier/prettier': 'error',
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
'error',
|
'error',
|
||||||
@@ -374,7 +379,41 @@ module.exports = {
|
|||||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
|
||||||
|
// React Hooks
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
|
|
||||||
|
// React
|
||||||
|
'react/function-component-definition': 'error',
|
||||||
|
'react/hook-use-state': 'error',
|
||||||
|
'react/jsx-boolean-value': ['error', 'always'],
|
||||||
|
'react/jsx-curly-brace-presence': [
|
||||||
|
'error',
|
||||||
|
{ props: 'never', children: 'never' }
|
||||||
|
],
|
||||||
|
'react/jsx-fragments': 'error',
|
||||||
|
'react/jsx-handler-names': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
eventHandlerPrefix: 'on',
|
||||||
|
eventHandlerPropPrefix: 'on'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||||
|
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||||
|
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||||
|
'react/jsx-sort-props': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
callbacksLast: true,
|
||||||
|
noSortAlphabetically: true,
|
||||||
|
reservedFirst: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/self-closing-comp': 'error'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const loose = true;
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'@babel/plugin-transform-logical-assignment-operators',
|
||||||
|
|
||||||
// Stage 1
|
// Stage 1
|
||||||
'@babel/plugin-proposal-export-default-from',
|
'@babel/plugin-proposal-export-default-from',
|
||||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ module.exports = (env) => {
|
|||||||
const config = {
|
const config = {
|
||||||
mode: isProduction ? 'production' : 'development',
|
mode: isProduction ? 'production' : 'development',
|
||||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||||
|
target: 'web',
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
@@ -65,7 +66,7 @@ module.exports = (env) => {
|
|||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: '[name]-[contenthash].js',
|
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ module.exports = (env) => {
|
|||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: 'Content/styles.css',
|
filename: 'Content/styles.css',
|
||||||
chunkFilename: 'Content/[id]-[chunkhash].css'
|
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
@@ -169,7 +170,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.42'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -190,7 +191,7 @@ module.exports = (env) => {
|
|||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
modules: {
|
modules: {
|
||||||
localIdentName: '[name]/[local]/[hash:base64:5]'
|
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const mixinsFiles = [
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'autoprefixer',
|
||||||
['postcss-mixins', {
|
['postcss-mixins', {
|
||||||
mixinsFiles
|
mixinsFiles
|
||||||
}],
|
}],
|
||||||
|
|||||||
@@ -1,31 +1,30 @@
|
|||||||
import { ConnectedRouter } from 'connected-react-router';
|
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DocumentTitle from 'react-document-title';
|
import DocumentTitle from 'react-document-title';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { Store } from 'redux';
|
||||||
import PageConnector from 'Components/Page/PageConnector';
|
import PageConnector from 'Components/Page/PageConnector';
|
||||||
import ApplyTheme from './ApplyTheme';
|
import ApplyTheme from './ApplyTheme';
|
||||||
import AppRoutes from './AppRoutes';
|
import AppRoutes from './AppRoutes';
|
||||||
|
|
||||||
function App({ store, history }) {
|
interface AppProps {
|
||||||
|
store: Store;
|
||||||
|
history: ConnectedRouterProps['history'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function App({ store, history }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title={window.Prowlarr.instanceName}>
|
<DocumentTitle title={window.Prowlarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<ApplyTheme>
|
<ApplyTheme />
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
<AppRoutes app={App} />
|
<AppRoutes />
|
||||||
</PageConnector>
|
</PageConnector>
|
||||||
</ApplyTheme>
|
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
App.propTypes = {
|
|
||||||
store: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
|
||||||
import NotFound from 'Components/NotFound';
|
|
||||||
import Switch from 'Components/Router/Switch';
|
|
||||||
import HistoryConnector from 'History/HistoryConnector';
|
|
||||||
import IndexerIndex from 'Indexer/Index/IndexerIndex';
|
|
||||||
import IndexerStats from 'Indexer/Stats/IndexerStats';
|
|
||||||
import SearchIndexConnector from 'Search/SearchIndexConnector';
|
|
||||||
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
|
|
||||||
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
|
||||||
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
|
||||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
|
||||||
import Settings from 'Settings/Settings';
|
|
||||||
import TagSettings from 'Settings/Tags/TagSettings';
|
|
||||||
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
|
||||||
import BackupsConnector from 'System/Backup/BackupsConnector';
|
|
||||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|
||||||
import Logs from 'System/Logs/Logs';
|
|
||||||
import Status from 'System/Status/Status';
|
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
|
||||||
|
|
||||||
function AppRoutes(props) {
|
|
||||||
const {
|
|
||||||
app
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{/*
|
|
||||||
Indexers
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
component={IndexerIndex}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
window.Prowlarr.urlBase &&
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
addUrlBase={false}
|
|
||||||
render={() => {
|
|
||||||
return (
|
|
||||||
<Redirect
|
|
||||||
to={getPathWithUrlBase('/')}
|
|
||||||
component={app}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/indexers/stats"
|
|
||||||
component={IndexerStats}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Search
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/search"
|
|
||||||
component={SearchIndexConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Activity
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/history"
|
|
||||||
component={HistoryConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Settings
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/settings"
|
|
||||||
component={Settings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/indexers"
|
|
||||||
component={IndexerSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/applications"
|
|
||||||
component={ApplicationSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/downloadclients"
|
|
||||||
component={DownloadClientSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/connect"
|
|
||||||
component={NotificationSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/tags"
|
|
||||||
component={TagSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/general"
|
|
||||||
component={GeneralSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/ui"
|
|
||||||
component={UISettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/development"
|
|
||||||
component={DevelopmentSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
System
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/status"
|
|
||||||
component={Status}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/tasks"
|
|
||||||
component={Tasks}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/backup"
|
|
||||||
component={BackupsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/updates"
|
|
||||||
component={UpdatesConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/events"
|
|
||||||
component={LogsTableConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/logs/files"
|
|
||||||
component={Logs}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Not Found
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="*"
|
|
||||||
component={NotFound}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppRoutes.propTypes = {
|
|
||||||
app: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppRoutes;
|
|
||||||
117
frontend/src/App/AppRoutes.tsx
Normal file
117
frontend/src/App/AppRoutes.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
|
import NotFound from 'Components/NotFound';
|
||||||
|
import Switch from 'Components/Router/Switch';
|
||||||
|
import HistoryConnector from 'History/HistoryConnector';
|
||||||
|
import IndexerIndex from 'Indexer/Index/IndexerIndex';
|
||||||
|
import IndexerStats from 'Indexer/Stats/IndexerStats';
|
||||||
|
import SearchIndexConnector from 'Search/SearchIndexConnector';
|
||||||
|
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
|
||||||
|
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
||||||
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
|
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
||||||
|
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||||
|
import Settings from 'Settings/Settings';
|
||||||
|
import TagSettings from 'Settings/Tags/TagSettings';
|
||||||
|
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
||||||
|
import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||||
|
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
|
import Logs from 'System/Logs/Logs';
|
||||||
|
import Status from 'System/Status/Status';
|
||||||
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
|
import Updates from 'System/Updates/Updates';
|
||||||
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
|
|
||||||
|
function RedirectWithUrlBase() {
|
||||||
|
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
{/*
|
||||||
|
Indexers
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/" component={IndexerIndex} />
|
||||||
|
|
||||||
|
{window.Prowlarr.urlBase && (
|
||||||
|
<Route
|
||||||
|
exact={true}
|
||||||
|
path="/"
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
addUrlBase={false}
|
||||||
|
render={RedirectWithUrlBase}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Route path="/indexers/stats" component={IndexerStats} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Search
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/search" component={SearchIndexConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Activity
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/history" component={HistoryConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Settings
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/settings" component={Settings} />
|
||||||
|
|
||||||
|
<Route path="/settings/indexers" component={IndexerSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/applications" component={ApplicationSettings} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/downloadclients"
|
||||||
|
component={DownloadClientSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/connect" component={NotificationSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/tags" component={TagSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/general" component={GeneralSettingsConnector} />
|
||||||
|
|
||||||
|
<Route path="/settings/ui" component={UISettingsConnector} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/development"
|
||||||
|
component={DevelopmentSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
System
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/system/status" component={Status} />
|
||||||
|
|
||||||
|
<Route path="/system/tasks" component={Tasks} />
|
||||||
|
|
||||||
|
<Route path="/system/backup" component={BackupsConnector} />
|
||||||
|
|
||||||
|
<Route path="/system/updates" component={Updates} />
|
||||||
|
|
||||||
|
<Route path="/system/events" component={LogsTableConnector} />
|
||||||
|
|
||||||
|
<Route path="/system/logs/files" component={Logs} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Not Found
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="*" component={NotFound} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppRoutes;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector';
|
|
||||||
|
|
||||||
function AppUpdatedModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
closeOnBackgroundClick={false}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AppUpdatedModalContentConnector
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppUpdatedModal;
|
|
||||||
28
frontend/src/App/AppUpdatedModal.tsx
Normal file
28
frontend/src/App/AppUpdatedModal.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
||||||
|
|
||||||
|
interface AppUpdatedModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: (...args: unknown[]) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppUpdatedModal(props: AppUpdatedModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AppUpdatedModalContent onModalClose={handleModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppUpdatedModal;
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import AppUpdatedModal from './AppUpdatedModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onModalClose() {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(AppUpdatedModal);
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
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 { kinds } from 'Helpers/Props';
|
|
||||||
import UpdateChanges from 'System/Updates/UpdateChanges';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AppUpdatedModalContent.css';
|
|
||||||
|
|
||||||
function mergeUpdates(items, version, prevVersion) {
|
|
||||||
let installedIndex = items.findIndex((u) => u.version === version);
|
|
||||||
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
|
|
||||||
|
|
||||||
if (installedIndex === -1) {
|
|
||||||
installedIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (installedPreviouslyIndex === -1) {
|
|
||||||
installedPreviouslyIndex = items.length;
|
|
||||||
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
|
||||||
installedPreviouslyIndex += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
|
||||||
|
|
||||||
if (!appliedUpdates.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appliedChanges = { new: [], fixed: [] };
|
|
||||||
appliedUpdates.forEach((u) => {
|
|
||||||
if (u.changes) {
|
|
||||||
appliedChanges.new.push(... u.changes.new);
|
|
||||||
appliedChanges.fixed.push(... u.changes.fixed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
|
|
||||||
|
|
||||||
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
|
||||||
mergedUpdate.changes = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergedUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AppUpdatedModalContent(props) {
|
|
||||||
const {
|
|
||||||
version,
|
|
||||||
prevVersion,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
onSeeChangesPress,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const update = mergeUpdates(items, version, prevVersion);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('AppUpdated')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error && !!update &&
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
!update.changes &&
|
|
||||||
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!update.changes &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.changes}>
|
|
||||||
{translate('WhatsNew')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('New')}
|
|
||||||
changes={update.changes.new}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('Fixed')}
|
|
||||||
changes={update.changes.fixed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isPopulated && !error &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onSeeChangesPress}
|
|
||||||
>
|
|
||||||
{translate('RecentChanges')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Reload')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModalContent.propTypes = {
|
|
||||||
version: PropTypes.string.isRequired,
|
|
||||||
prevVersion: PropTypes.string,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onSeeChangesPress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppUpdatedModalContent;
|
|
||||||
145
frontend/src/App/AppUpdatedModalContent.tsx
Normal file
145
frontend/src/App/AppUpdatedModalContent.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
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 usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||||
|
import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||||
|
import Update from 'typings/Update';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
import styles from './AppUpdatedModalContent.css';
|
||||||
|
|
||||||
|
function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
|
||||||
|
let installedIndex = items.findIndex((u) => u.version === version);
|
||||||
|
let installedPreviouslyIndex = items.findIndex(
|
||||||
|
(u) => u.version === prevVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (installedIndex === -1) {
|
||||||
|
installedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installedPreviouslyIndex === -1) {
|
||||||
|
installedPreviouslyIndex = items.length;
|
||||||
|
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
||||||
|
installedPreviouslyIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
||||||
|
|
||||||
|
if (!appliedUpdates.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedChanges: Update['changes'] = { new: [], fixed: [] };
|
||||||
|
|
||||||
|
appliedUpdates.forEach((u: Update) => {
|
||||||
|
if (u.changes) {
|
||||||
|
appliedChanges.new.push(...u.changes.new);
|
||||||
|
appliedChanges.fixed.push(...u.changes.fixed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], {
|
||||||
|
changes: appliedChanges,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
||||||
|
mergedUpdate.changes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppUpdatedModalContentProps {
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { version, prevVersion } = useSelector((state: AppState) => state.app);
|
||||||
|
const { isPopulated, error, items } = useSelector(
|
||||||
|
(state: AppState) => state.system.updates
|
||||||
|
);
|
||||||
|
const previousVersion = usePrevious(version);
|
||||||
|
|
||||||
|
const { onModalClose } = props;
|
||||||
|
|
||||||
|
const update = mergeUpdates(items, version, prevVersion);
|
||||||
|
|
||||||
|
const handleSeeChangesPress = useCallback(() => {
|
||||||
|
window.location.href = `${window.Prowlarr.urlBase}/system/updates`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchUpdates());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (version !== previousVersion) {
|
||||||
|
dispatch(fetchUpdates());
|
||||||
|
}
|
||||||
|
}, [version, previousVersion, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('AppUpdated')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
<InlineMarkdown
|
||||||
|
data={translate('AppUpdatedVersion', { version })}
|
||||||
|
blockClassName={styles.version}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPopulated && !error && !!update ? (
|
||||||
|
<div>
|
||||||
|
{update.changes ? (
|
||||||
|
<div className={styles.maintenance}>
|
||||||
|
{translate('MaintenanceRelease')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{update.changes ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.changes}>{translate('WhatsNew')}</div>
|
||||||
|
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('New')}
|
||||||
|
changes={update.changes.new}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('Fixed')}
|
||||||
|
changes={update.changes.fixed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isPopulated && !error ? <LoadingIndicator /> : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={handleSeeChangesPress}>
|
||||||
|
{translate('RecentChanges')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.PRIMARY} onPress={onModalClose}>
|
||||||
|
{translate('Reload')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppUpdatedModalContent;
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
|
||||||
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.app.version,
|
|
||||||
(state) => state.app.prevVersion,
|
|
||||||
(state) => state.system.updates,
|
|
||||||
(version, prevVersion, updates) => {
|
|
||||||
const {
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = updates;
|
|
||||||
|
|
||||||
return {
|
|
||||||
version,
|
|
||||||
prevVersion,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
dispatchFetchUpdates() {
|
|
||||||
dispatch(fetchUpdates());
|
|
||||||
},
|
|
||||||
|
|
||||||
onSeeChangesPress() {
|
|
||||||
window.location = `${window.Prowlarr.urlBase}/system/updates`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppUpdatedModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.version !== this.props.version) {
|
|
||||||
this.props.dispatchFetchUpdates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dispatchFetchUpdates,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppUpdatedModalContent {...otherProps} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModalContentConnector.propTypes = {
|
|
||||||
version: PropTypes.string.isRequired,
|
|
||||||
dispatchFetchUpdates: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector);
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import themes from 'Styles/Themes';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
|
||||||
(
|
|
||||||
theme
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
theme
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApplyTheme({ theme, children }) {
|
|
||||||
// Update the CSS Variables
|
|
||||||
|
|
||||||
const updateCSSVariables = useCallback(() => {
|
|
||||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
|
||||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
|
||||||
|
|
||||||
// Loop through each array key and set the CSS Variables
|
|
||||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
|
||||||
// Based on our snippet from MDN
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
`--${cssVariableKey}`,
|
|
||||||
arrayOfVariableValues[index]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
// On Component Mount and Component Update
|
|
||||||
useEffect(() => {
|
|
||||||
updateCSSVariables(theme);
|
|
||||||
}, [updateCSSVariables, theme]);
|
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyTheme.propTypes = {
|
|
||||||
theme: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(ApplyTheme);
|
|
||||||
33
frontend/src/App/ApplyTheme.tsx
Normal file
33
frontend/src/App/ApplyTheme.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import themes from 'Styles/Themes';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
|
||||||
|
function createThemeSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
||||||
|
(theme) => {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplyTheme() {
|
||||||
|
const theme = useSelector(createThemeSelector());
|
||||||
|
|
||||||
|
const updateCSSVariables = useCallback(() => {
|
||||||
|
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||||
|
document.documentElement.style.setProperty(`--${key}`, value);
|
||||||
|
});
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
// On Component Mount and Component Update
|
||||||
|
useEffect(() => {
|
||||||
|
updateCSSVariables();
|
||||||
|
}, [updateCSSVariables, theme]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApplyTheme;
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import PropTypes from 'prop-types';
|
import React, { useCallback } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
@@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ConnectionLostModal.css';
|
import styles from './ConnectionLostModal.css';
|
||||||
|
|
||||||
function ConnectionLostModal(props) {
|
interface ConnectionLostModalProps {
|
||||||
const {
|
isOpen: boolean;
|
||||||
isOpen,
|
}
|
||||||
onModalClose
|
|
||||||
} = props;
|
function ConnectionLostModal(props: ConnectionLostModalProps) {
|
||||||
|
const { isOpen } = props;
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
isOpen={isOpen}
|
<ModalContent onModalClose={handleModalClose}>
|
||||||
onModalClose={onModalClose}
|
<ModalHeader>{translate('ConnectionLost')}</ModalHeader>
|
||||||
>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('ConnectionLost')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>{translate('ConnectionLostToBackend')}</div>
|
||||||
{translate('ConnectionLostToBackend')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.automatic}>
|
<div className={styles.automatic}>
|
||||||
{translate('ConnectionLostReconnect')}
|
{translate('ConnectionLostReconnect')}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button kind={kinds.PRIMARY} onPress={handleModalClose}>
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Reload')}
|
{translate('Reload')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
@@ -48,9 +42,4 @@ function ConnectionLostModal(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionLostModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectionLostModal;
|
export default ConnectionLostModal;
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ConnectionLostModal from './ConnectionLostModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onModalClose() {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal);
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import Column from 'Components/Table/Column';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import { FilterBuilderProp } from './AppState';
|
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
responseJSON: {
|
||||||
@@ -18,10 +19,18 @@ export interface AppSectionSaveState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PagedAppSectionState {
|
export interface PagedAppSectionState {
|
||||||
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalRecords?: number;
|
||||||
|
}
|
||||||
|
export interface TableAppSectionState {
|
||||||
|
columns: Column[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionFilterState<T> {
|
export interface AppSectionFilterState<T> {
|
||||||
|
selectedFilterKey: string;
|
||||||
|
filters: PropertyFilter[];
|
||||||
filterBuilderProps: FilterBuilderProp<T>[];
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +47,7 @@ export interface AppSectionItemState<T> {
|
|||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
error: Error;
|
error: Error;
|
||||||
|
pendingChanges: Partial<T>;
|
||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,20 @@ export interface CustomFilter {
|
|||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionState {
|
||||||
|
isConnected: boolean;
|
||||||
|
isReconnecting: boolean;
|
||||||
|
version: string;
|
||||||
|
prevVersion?: string;
|
||||||
|
dimensions: {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
app: AppSectionState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
indexerHistory: IndexerHistoryAppState;
|
indexerHistory: IndexerHistoryAppState;
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ interface IndexerAppState
|
|||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {
|
AppSectionSaveState {
|
||||||
itemMap: Record<number, number>;
|
itemMap: Record<number, number>;
|
||||||
|
|
||||||
|
isTestingAll: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
|
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import AppSectionState, {
|
|||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
|
import { IndexerCategory } from 'Indexer/Indexer';
|
||||||
import Application from 'typings/Application';
|
import Application from 'typings/Application';
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import General from 'typings/Settings/General';
|
||||||
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
|
||||||
export interface AppProfileAppState
|
export interface AppProfileAppState
|
||||||
extends AppSectionState<Application>,
|
extends AppSectionState<Application>,
|
||||||
@@ -22,6 +24,17 @@ export interface ApplicationAppState
|
|||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {
|
||||||
|
isTestingAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneralAppState
|
||||||
|
extends AppSectionItemState<General>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface IndexerCategoryAppState
|
||||||
|
extends AppSectionState<IndexerCategory>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
@@ -35,6 +48,8 @@ interface SettingsAppState {
|
|||||||
appProfiles: AppProfileAppState;
|
appProfiles: AppProfileAppState;
|
||||||
applications: ApplicationAppState;
|
applications: ApplicationAppState;
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
|
general: GeneralAppState;
|
||||||
|
indexerCategories: IndexerCategoryAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
ui: UiSettingsAppState;
|
ui: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
|
import Health from 'typings/Health';
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import { AppSectionItemState } from './AppSectionState';
|
import Task from 'typings/Task';
|
||||||
|
import Update from 'typings/Update';
|
||||||
|
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||||
|
|
||||||
|
export type HealthAppState = AppSectionState<Health>;
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
|
export type TaskAppState = AppSectionState<Task>;
|
||||||
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
|
health: HealthAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
|
tasks: TaskAppState;
|
||||||
|
updates: UpdateAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemAppState;
|
export default SystemAppState;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export interface CommandBody {
|
|||||||
lastStartTime: string;
|
lastStartTime: string;
|
||||||
trigger: string;
|
trigger: string;
|
||||||
suppressMessages: boolean;
|
suppressMessages: boolean;
|
||||||
seriesId?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Command extends ModelBase {
|
interface Command extends ModelBase {
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ class StackedBarChart extends Component {
|
|||||||
size: 14,
|
size: 14,
|
||||||
family: defaultFontFamily
|
family: defaultFontFamily
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
position: 'average'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
className,
|
||||||
titleClassName,
|
titleClassName,
|
||||||
descriptionClassName,
|
descriptionClassName,
|
||||||
title,
|
title,
|
||||||
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={className}>
|
||||||
<DescriptionListItemTitle
|
<DescriptionListItemTitle
|
||||||
className={titleClassName}
|
className={titleClassName}
|
||||||
>
|
>
|
||||||
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DescriptionListItem.propTypes = {
|
DescriptionListItem.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
titleClassName: PropTypes.string,
|
titleClassName: PropTypes.string,
|
||||||
descriptionClassName: PropTypes.string,
|
descriptionClassName: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
|||||||
@@ -63,11 +63,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
|||||||
<div>{info.componentStack}</div>
|
<div>{info.componentStack}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{
|
<div className={styles.version}>Version: {window.Prowlarr.version}</div>
|
||||||
<div className={styles.version}>
|
|
||||||
Version: {window.Prowlarr.version}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { IndexerCategory } from 'Indexer/Indexer';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
const indexerCategoriesSelector = createSelector(
|
||||||
|
(state: AppState) => state.settings.indexerCategories,
|
||||||
|
(categories) => categories.items
|
||||||
|
);
|
||||||
|
|
||||||
|
function CategoryFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
const categories: IndexerCategory[] = useSelector(indexerCategoriesSelector);
|
||||||
|
|
||||||
|
const tagList = categories.reduce(
|
||||||
|
(acc: { id: number; name: string }[], element) => {
|
||||||
|
acc.push({
|
||||||
|
id: element.id,
|
||||||
|
name: `${element.name} (${element.id})`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (element.subCategories && element.subCategories.length > 0) {
|
||||||
|
element.subCategories.forEach((subCat) => {
|
||||||
|
acc.push({
|
||||||
|
id: subCat.id,
|
||||||
|
name: `${subCat.name} (${subCat.id})`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategoryFilterBuilderRowValue;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { maxBy } from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
@@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
if (id) {
|
if (id) {
|
||||||
dispatchSetFilter({ selectedFilterKey: id });
|
dispatchSetFilter({ selectedFilterKey: id });
|
||||||
} else {
|
} else {
|
||||||
const last = customFilters[customFilters.length -1];
|
const last = maxBy(customFilters, 'id');
|
||||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
labelErrors: [
|
labelErrors: [
|
||||||
{
|
{
|
||||||
message: 'Label is required'
|
message: translate('LabelIsRequired')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Custom Filter
|
{translate('CustomFilter')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className={styles.labelContainer}>
|
<div className={styles.labelContainer}>
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>
|
||||||
Label
|
{translate('Label')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.labelInputContainer}>
|
<div className={styles.labelInputContainer}>
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import React, { Component } from 'react';
|
|||||||
import SelectInput from 'Components/Form/SelectInput';
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
|
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
|
||||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||||
@@ -56,6 +58,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||||||
case filterBuilderValueTypes.BOOL:
|
case filterBuilderValueTypes.BOOL:
|
||||||
return BoolFilterBuilderRowValue;
|
return BoolFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.CATEGORY:
|
||||||
|
return CategoryFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.DATE:
|
case filterBuilderValueTypes.DATE:
|
||||||
return DateFilterBuilderRowValue;
|
return DateFilterBuilderRowValue;
|
||||||
|
|
||||||
@@ -208,7 +213,7 @@ class FilterBuilderRow extends Component {
|
|||||||
key: name,
|
key: name,
|
||||||
value: typeof label === 'function' ? label() : label
|
value: typeof label === 'function' ? label() : label
|
||||||
};
|
};
|
||||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
}).sort(sortByProp('value'));
|
||||||
|
|
||||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { filterBuilderTypes } from 'Helpers/Props';
|
import { filterBuilderTypes } from 'Helpers/Props';
|
||||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
function createTagListSelector() {
|
function createTagListSelector() {
|
||||||
@@ -38,7 +38,7 @@ function createTagListSelector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []).sort(sortByName);
|
}, []).sort(sortByProp('name'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.uniqBy(items, 'id');
|
return _.uniqBy(items, 'id');
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
|||||||
dispatchSetFilter
|
dispatchSetFilter
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// Assume that delete and then unmounting means the delete was successful.
|
// Assume that delete and then unmounting means the deletion was successful.
|
||||||
// Moving this check to a ancestor would be more accurate, but would have
|
// Moving this check to an ancestor would be more accurate, but would have
|
||||||
// more boilerplate.
|
// more boilerplate.
|
||||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CustomFilter from './CustomFilter';
|
import CustomFilter from './CustomFilter';
|
||||||
import styles from './CustomFiltersModalContent.css';
|
import styles from './CustomFiltersModalContent.css';
|
||||||
@@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) {
|
|||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters
|
customFilters
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort((a, b) => sortByProp(a, b, 'label'))
|
||||||
.map((customFilter) => {
|
.map((customFilter) => {
|
||||||
return (
|
return (
|
||||||
<CustomFilter
|
<CustomFilter
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
(appProfiles, includeNoChange, includeMixed) => {
|
(appProfiles, includeNoChange, includeMixed) => {
|
||||||
@@ -24,16 +24,20 @@ function createMapStateToProps() {
|
|||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
get value() {
|
||||||
disabled: true
|
return translate('NoChange');
|
||||||
|
},
|
||||||
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeMixed) {
|
if (includeMixed) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
get value() {
|
||||||
disabled: true
|
return `(${translate('Mixed')})`;
|
||||||
|
},
|
||||||
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import SelectInput from './SelectInput';
|
|
||||||
|
|
||||||
const availabilityOptions = [
|
|
||||||
{ key: 'announced', value: 'Announced' },
|
|
||||||
{ key: 'inCinemas', value: 'In Cinemas' },
|
|
||||||
{ key: 'released', value: 'Released' },
|
|
||||||
{ key: 'preDB', value: 'PreDB' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function AvailabilitySelectInput(props) {
|
|
||||||
const values = [...availabilityOptions];
|
|
||||||
|
|
||||||
const {
|
|
||||||
includeNoChange,
|
|
||||||
includeMixed
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
value: 'No Change',
|
|
||||||
disabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMixed) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'mixed',
|
|
||||||
value: '(Mixed)',
|
|
||||||
disabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SelectInput
|
|
||||||
{...props}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AvailabilitySelectInput.propTypes = {
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
includeMixed: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AvailabilitySelectInput.defaultProps = {
|
|
||||||
includeNoChange: false,
|
|
||||||
includeMixed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AvailabilitySelectInput;
|
|
||||||
@@ -3,7 +3,8 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
@@ -21,16 +22,17 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
const values = items
|
const values = items
|
||||||
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
|
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
|
||||||
.sort(sortByName)
|
.sort(sortByProp('name'))
|
||||||
.map((downloadClient) => ({
|
.map((downloadClient) => ({
|
||||||
key: downloadClient.id,
|
key: downloadClient.id,
|
||||||
value: downloadClient.name
|
value: downloadClient.name,
|
||||||
|
hint: `(${downloadClient.id})`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (includeAny) {
|
if (includeAny) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 0,
|
key: 0,
|
||||||
value: '(Any)'
|
value: `(${translate('Any')})`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
|||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
|
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
function isArrowKey(keyCode) {
|
||||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||||
}
|
}
|
||||||
@@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
|
|||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
onComputeMaxHeight = (data) => {
|
||||||
const {
|
|
||||||
top,
|
|
||||||
bottom
|
|
||||||
} = data.offsets.reference;
|
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
if ((/^botton/).test(data.placement)) {
|
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
||||||
data.styles.maxHeight = windowHeight - bottom;
|
|
||||||
} else {
|
|
||||||
data.styles.maxHeight = top;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -271,26 +264,29 @@ class EnhancedSelectInput extends Component {
|
|||||||
this.setState({ isOpen: !this.state.isOpen });
|
this.setState({ isOpen: !this.state.isOpen });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSelect = (value) => {
|
onSelect = (newValue) => {
|
||||||
if (Array.isArray(this.props.value)) {
|
const { name, value, values, onChange } = this.props;
|
||||||
let newValue = null;
|
|
||||||
const index = this.props.value.indexOf(value);
|
if (Array.isArray(value)) {
|
||||||
|
let arrayValue = null;
|
||||||
|
const index = value.indexOf(newValue);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
|
||||||
} else {
|
} else {
|
||||||
newValue = [...this.props.value];
|
arrayValue = [...value];
|
||||||
newValue.splice(index, 1);
|
arrayValue.splice(index, 1);
|
||||||
}
|
}
|
||||||
this.props.onChange({
|
onChange({
|
||||||
name: this.props.name,
|
name,
|
||||||
value: newValue
|
value: arrayValue
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({ isOpen: false });
|
this.setState({ isOpen: false });
|
||||||
|
|
||||||
this.props.onChange({
|
onChange({
|
||||||
name: this.props.name,
|
name,
|
||||||
value
|
value: newValue
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -457,6 +453,10 @@ class EnhancedSelectInput extends Component {
|
|||||||
order: 851,
|
order: 851,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fn: this.onComputeMaxHeight
|
fn: this.onComputeMaxHeight
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: true,
|
||||||
|
boundariesElement: 'viewport'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -485,7 +485,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
values.map((v, index) => {
|
values.map((v, index) => {
|
||||||
const hasParent = v.parentKey !== undefined;
|
const hasParent = v.parentKey !== undefined;
|
||||||
const depth = hasParent ? 1 : 0;
|
const depth = hasParent ? 1 : 0;
|
||||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
|
||||||
return (
|
return (
|
||||||
<OptionComponent
|
<OptionComponent
|
||||||
key={v.key}
|
key={v.key}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ function getSelectOptions(items) {
|
|||||||
key: option.value,
|
key: option.value,
|
||||||
value: option.name,
|
value: option.name,
|
||||||
hint: option.hint,
|
hint: option.hint,
|
||||||
parentKey: option.parentValue
|
parentKey: option.parentValue,
|
||||||
|
isDisabled: option.isDisabled
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -147,7 +148,7 @@ EnhancedSelectInputConnector.propTypes = {
|
|||||||
provider: PropTypes.string.isRequired,
|
provider: PropTypes.string.isRequired,
|
||||||
providerData: PropTypes.object.isRequired,
|
providerData: PropTypes.object.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectOptionsProviderAction: PropTypes.string,
|
selectOptionsProviderAction: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
.validationFailures {
|
.validationFailures {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|||||||
1
frontend/src/Components/Form/Form.css.d.ts
vendored
1
frontend/src/Components/Form/Form.css.d.ts
vendored
@@ -1,6 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'details': string;
|
||||||
'validationFailures': string;
|
'validationFailures': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import { kinds } from 'Helpers/Props';
|
import Icon from 'Components/Icon';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import styles from './Form.css';
|
import styles from './Form.css';
|
||||||
|
|
||||||
function Form(props) {
|
function Form(props) {
|
||||||
@@ -26,6 +28,18 @@ function Form(props) {
|
|||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
>
|
>
|
||||||
{error.errorMessage}
|
{error.errorMessage}
|
||||||
|
|
||||||
|
{
|
||||||
|
error.detailedDescription ?
|
||||||
|
<Tooltip
|
||||||
|
className={styles.details}
|
||||||
|
anchor={<Icon name={icons.INFO} />}
|
||||||
|
tooltip={error.detailedDescription}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -39,6 +53,18 @@ function Form(props) {
|
|||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
{warning.errorMessage}
|
{warning.errorMessage}
|
||||||
|
|
||||||
|
{
|
||||||
|
warning.detailedDescription ?
|
||||||
|
<Tooltip
|
||||||
|
className={styles.details}
|
||||||
|
anchor={<Icon name={icons.INFO} />}
|
||||||
|
tooltip={warning.detailedDescription}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './FormInputButton.css';
|
|
||||||
|
|
||||||
function FormInputButton(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
canSpin,
|
|
||||||
isLastButton,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (canSpin) {
|
|
||||||
return (
|
|
||||||
<SpinnerButton
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
!isLastButton && styles.middleButton
|
|
||||||
)}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
!isLastButton && styles.middleButton
|
|
||||||
)}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormInputButton.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
isLastButton: PropTypes.bool.isRequired,
|
|
||||||
canSpin: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FormInputButton.defaultProps = {
|
|
||||||
className: styles.button,
|
|
||||||
isLastButton: true,
|
|
||||||
canSpin: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormInputButton;
|
|
||||||
38
frontend/src/Components/Form/FormInputButton.tsx
Normal file
38
frontend/src/Components/Form/FormInputButton.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import Button, { ButtonProps } from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import styles from './FormInputButton.css';
|
||||||
|
|
||||||
|
export interface FormInputButtonProps extends ButtonProps {
|
||||||
|
canSpin?: boolean;
|
||||||
|
isLastButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormInputButton({
|
||||||
|
className = styles.button,
|
||||||
|
canSpin = false,
|
||||||
|
isLastButton = true,
|
||||||
|
...otherProps
|
||||||
|
}: FormInputButtonProps) {
|
||||||
|
if (canSpin) {
|
||||||
|
return (
|
||||||
|
<SpinnerButton
|
||||||
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormInputButton;
|
||||||
@@ -5,7 +5,6 @@ import { inputTypes, kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AppProfileSelectInputConnector from './AppProfileSelectInputConnector';
|
import AppProfileSelectInputConnector from './AppProfileSelectInputConnector';
|
||||||
import AutoCompleteInput from './AutoCompleteInput';
|
import AutoCompleteInput from './AutoCompleteInput';
|
||||||
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
|
||||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||||
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
|
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
|
||||||
import CheckInput from './CheckInput';
|
import CheckInput from './CheckInput';
|
||||||
@@ -37,9 +36,6 @@ function getComponent(type) {
|
|||||||
case inputTypes.AUTO_COMPLETE:
|
case inputTypes.AUTO_COMPLETE:
|
||||||
return AutoCompleteInput;
|
return AutoCompleteInput;
|
||||||
|
|
||||||
case inputTypes.AVAILABILITY_SELECT:
|
|
||||||
return AvailabilitySelectInput;
|
|
||||||
|
|
||||||
case inputTypes.CAPTCHA:
|
case inputTypes.CAPTCHA:
|
||||||
return CaptchaInputConnector;
|
return CaptchaInputConnector;
|
||||||
|
|
||||||
@@ -260,6 +256,7 @@ FormInputGroup.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
values: PropTypes.arrayOf(PropTypes.any),
|
||||||
|
isFloat: PropTypes.bool,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
min: PropTypes.number,
|
min: PropTypes.number,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function FormInputHelpText(props) {
|
|||||||
isCheckInput && styles.isCheckInput
|
isCheckInput && styles.isCheckInput
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div dangerouslySetInnerHTML={{ __html: text }} />
|
{text}
|
||||||
|
|
||||||
{
|
{
|
||||||
link ?
|
link ?
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
createSortedSectionSelector('indexers', sortByName),
|
createSortedSectionSelector('indexers', sortByProp('name')),
|
||||||
(value, indexers) => {
|
(value, indexers) => {
|
||||||
const values = [];
|
const values = [];
|
||||||
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
.input {
|
|
||||||
composes: input from '~Components/Form/TextInput.css';
|
|
||||||
|
|
||||||
font-family: $passwordFamily;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './PasswordInput.css';
|
|
||||||
|
|
||||||
// Prevent a user from copying (or cutting) the password from the input
|
// Prevent a user from copying (or cutting) the password from the input
|
||||||
function onCopy(e) {
|
function onCopy(e) {
|
||||||
@@ -13,17 +11,14 @@ function PasswordInput(props) {
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
{...props}
|
{...props}
|
||||||
|
type="password"
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
className: PropTypes.string.isRequired
|
...TextInput.props
|
||||||
};
|
|
||||||
|
|
||||||
PasswordInput.defaultProps = {
|
|
||||||
className: styles.input
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordInput;
|
export default PasswordInput;
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
.select {
|
.select {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
composes: input from '~Components/Form/Input.css';
|
composes: input from '~Components/Form/Input.css';
|
||||||
|
|
||||||
padding: 0 11px;
|
padding: 0 30px 0 11px;
|
||||||
|
background-image: none, linear-gradient(-135deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(-225deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(var(--inputBackgroundColor) 42%, var(--textColor) 42%);
|
||||||
|
background-position: right 30px center, right bottom, right bottom, right bottom;
|
||||||
|
background-size: 1px 100%, 35px 27px, 30px 35px, 30px 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { kinds, sizes } from 'Helpers/Props';
|
|
||||||
import styles from './Label.css';
|
|
||||||
|
|
||||||
function Label(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
kind,
|
|
||||||
size,
|
|
||||||
outline,
|
|
||||||
children,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[kind],
|
|
||||||
styles[size],
|
|
||||||
outline && styles.outline
|
|
||||||
)}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Label.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string,
|
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
|
||||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
|
||||||
outline: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.node.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Label.defaultProps = {
|
|
||||||
className: styles.label,
|
|
||||||
kind: kinds.DEFAULT,
|
|
||||||
size: sizes.SMALL,
|
|
||||||
outline: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Label;
|
|
||||||
33
frontend/src/Components/Label.tsx
Normal file
33
frontend/src/Components/Label.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ComponentProps, ReactNode } from 'react';
|
||||||
|
import { kinds, sizes } from 'Helpers/Props';
|
||||||
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
|
import styles from './Label.css';
|
||||||
|
|
||||||
|
export interface LabelProps extends ComponentProps<'span'> {
|
||||||
|
kind?: Extract<Kind, keyof typeof styles>;
|
||||||
|
size?: Extract<Size, keyof typeof styles>;
|
||||||
|
outline?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Label({
|
||||||
|
className = styles.label,
|
||||||
|
kind = kinds.DEFAULT,
|
||||||
|
size = sizes.SMALL,
|
||||||
|
outline = false,
|
||||||
|
...otherProps
|
||||||
|
}: LabelProps) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
styles[kind],
|
||||||
|
styles[size],
|
||||||
|
outline && styles.outline
|
||||||
|
)}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { align, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import Link from './Link';
|
|
||||||
import styles from './Button.css';
|
|
||||||
|
|
||||||
class Button extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
buttonGroupPosition,
|
|
||||||
kind,
|
|
||||||
size,
|
|
||||||
children,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[kind],
|
|
||||||
styles[size],
|
|
||||||
buttonGroupPosition && styles[buttonGroupPosition]
|
|
||||||
)}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Button.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
buttonGroupPosition: PropTypes.oneOf(align.all),
|
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
|
||||||
size: PropTypes.oneOf(sizes.all),
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
Button.defaultProps = {
|
|
||||||
className: styles.button,
|
|
||||||
kind: kinds.DEFAULT,
|
|
||||||
size: sizes.MEDIUM
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Button;
|
|
||||||
37
frontend/src/Components/Link/Button.tsx
Normal file
37
frontend/src/Components/Link/Button.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { align, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
|
import Link, { LinkProps } from './Link';
|
||||||
|
import styles from './Button.css';
|
||||||
|
|
||||||
|
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
|
||||||
|
buttonGroupPosition?: Extract<
|
||||||
|
(typeof align.all)[number],
|
||||||
|
keyof typeof styles
|
||||||
|
>;
|
||||||
|
kind?: Extract<Kind, keyof typeof styles>;
|
||||||
|
size?: Extract<Size, keyof typeof styles>;
|
||||||
|
children: Required<LinkProps['children']>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Button({
|
||||||
|
className = styles.button,
|
||||||
|
buttonGroupPosition,
|
||||||
|
kind = kinds.DEFAULT,
|
||||||
|
size = sizes.MEDIUM,
|
||||||
|
...otherProps
|
||||||
|
}: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
styles[kind],
|
||||||
|
styles[size],
|
||||||
|
buttonGroupPosition && styles[buttonGroupPosition]
|
||||||
|
)}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import Clipboard from 'clipboard';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormInputButton from 'Components/Form/FormInputButton';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
|
||||||
import styles from './ClipboardButton.css';
|
|
||||||
|
|
||||||
class ClipboardButton extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._id = getUniqueElememtId();
|
|
||||||
this._successTimeout = null;
|
|
||||||
this._testResultTimeout = null;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
showSuccess: false,
|
|
||||||
showError: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._clipboard = new Clipboard(`#${this._id}`, {
|
|
||||||
text: () => this.props.value,
|
|
||||||
container: document.getElementById(this._id)
|
|
||||||
});
|
|
||||||
|
|
||||||
this._clipboard.on('success', this.onSuccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
const {
|
|
||||||
showSuccess,
|
|
||||||
showError
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (showSuccess || showError) {
|
|
||||||
this._testResultTimeout = setTimeout(this.resetState, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._clipboard) {
|
|
||||||
this._clipboard.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._testResultTimeout) {
|
|
||||||
clearTimeout(this._testResultTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = () => {
|
|
||||||
this.setState({
|
|
||||||
showSuccess: false,
|
|
||||||
showError: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSuccess = () => {
|
|
||||||
this.setState({
|
|
||||||
showSuccess: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onError = () => {
|
|
||||||
this.setState({
|
|
||||||
showError: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
className,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
showSuccess,
|
|
||||||
showError
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const showStateIcon = showSuccess || showError;
|
|
||||||
const iconName = showError ? icons.DANGER : icons.CHECK;
|
|
||||||
const iconKind = showError ? kinds.DANGER : kinds.SUCCESS;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormInputButton
|
|
||||||
id={this._id}
|
|
||||||
className={className}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<span className={showStateIcon ? styles.showStateIcon : undefined}>
|
|
||||||
{
|
|
||||||
showSuccess &&
|
|
||||||
<span className={styles.stateIconContainer}>
|
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
kind={iconKind}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
<span className={styles.clipboardIconContainer}>
|
|
||||||
<Icon name={icons.CLIPBOARD} />
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</FormInputButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClipboardButton.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ClipboardButton.defaultProps = {
|
|
||||||
className: styles.button
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClipboardButton;
|
|
||||||
76
frontend/src/Components/Link/ClipboardButton.tsx
Normal file
76
frontend/src/Components/Link/ClipboardButton.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import copy from 'copy-to-clipboard';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import FormInputButton from 'Components/Form/FormInputButton';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import { ButtonProps } from './Button';
|
||||||
|
import styles from './ClipboardButton.css';
|
||||||
|
|
||||||
|
export interface ClipboardButtonProps extends Omit<ButtonProps, 'children'> {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClipboardState = 'success' | 'error' | null;
|
||||||
|
|
||||||
|
export default function ClipboardButton({
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
className = styles.button,
|
||||||
|
...otherProps
|
||||||
|
}: ClipboardButtonProps) {
|
||||||
|
const [state, setState] = useState<ClipboardState>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setState(null);
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
if ('clipboard' in navigator) {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
} else {
|
||||||
|
copy(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState('success');
|
||||||
|
} catch (e) {
|
||||||
|
setState('error');
|
||||||
|
console.error(`Failed to copy to clipboard`, e);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormInputButton
|
||||||
|
className={className}
|
||||||
|
onClick={handleClick}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<span className={state ? styles.showStateIcon : undefined}>
|
||||||
|
{state ? (
|
||||||
|
<span className={styles.stateIconContainer}>
|
||||||
|
<Icon
|
||||||
|
name={state === 'error' ? icons.DANGER : icons.CHECK}
|
||||||
|
kind={state === 'error' ? kinds.DANGER : kinds.SUCCESS}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<span className={styles.clipboardIconContainer}>
|
||||||
|
<Icon name={icons.CLIPBOARD} />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</FormInputButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,96 +1,93 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, {
|
import React, {
|
||||||
ComponentClass,
|
ComponentPropsWithoutRef,
|
||||||
FunctionComponent,
|
ElementType,
|
||||||
SyntheticEvent,
|
SyntheticEvent,
|
||||||
useCallback,
|
useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import styles from './Link.css';
|
import styles from './Link.css';
|
||||||
|
|
||||||
interface ReactRouterLinkProps {
|
export type LinkProps<C extends ElementType = 'button'> =
|
||||||
to?: string;
|
ComponentPropsWithoutRef<C> & {
|
||||||
}
|
component?: C;
|
||||||
|
to?: string;
|
||||||
|
target?: string;
|
||||||
|
isDisabled?: LinkProps<C>['disabled'];
|
||||||
|
noRouter?: boolean;
|
||||||
|
onPress?(event: SyntheticEvent): void;
|
||||||
|
};
|
||||||
|
|
||||||
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
|
export default function Link<C extends ElementType = 'button'>({
|
||||||
className?: string;
|
className,
|
||||||
component?:
|
component,
|
||||||
| string
|
to,
|
||||||
| FunctionComponent<LinkProps>
|
target,
|
||||||
| ComponentClass<LinkProps, unknown>;
|
type,
|
||||||
to?: string;
|
isDisabled,
|
||||||
target?: string;
|
noRouter,
|
||||||
isDisabled?: boolean;
|
onPress,
|
||||||
noRouter?: boolean;
|
...otherProps
|
||||||
onPress?(event: SyntheticEvent): void;
|
}: LinkProps<C>) {
|
||||||
}
|
const Component = component || 'button';
|
||||||
function Link(props: LinkProps) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
component = 'button',
|
|
||||||
to,
|
|
||||||
target,
|
|
||||||
type,
|
|
||||||
isDisabled,
|
|
||||||
noRouter = false,
|
|
||||||
onPress,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
(event: SyntheticEvent) => {
|
(event: SyntheticEvent) => {
|
||||||
if (!isDisabled && onPress) {
|
if (isDisabled) {
|
||||||
onPress(event);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPress?.(event);
|
||||||
},
|
},
|
||||||
[isDisabled, onPress]
|
[isDisabled, onPress]
|
||||||
);
|
);
|
||||||
|
|
||||||
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
|
const linkClass = classNames(
|
||||||
target,
|
|
||||||
};
|
|
||||||
let el = component;
|
|
||||||
|
|
||||||
if (to) {
|
|
||||||
if (/\w+?:\/\//.test(to)) {
|
|
||||||
el = 'a';
|
|
||||||
linkProps.href = to;
|
|
||||||
linkProps.target = target || '_blank';
|
|
||||||
linkProps.rel = 'noreferrer';
|
|
||||||
} else if (noRouter) {
|
|
||||||
el = 'a';
|
|
||||||
linkProps.href = to;
|
|
||||||
linkProps.target = target || '_self';
|
|
||||||
} else {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
el = RouterLink;
|
|
||||||
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
|
|
||||||
linkProps.target = target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (el === 'button' || el === 'input') {
|
|
||||||
linkProps.type = type || 'button';
|
|
||||||
linkProps.disabled = isDisabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
linkProps.className = classNames(
|
|
||||||
className,
|
className,
|
||||||
styles.link,
|
styles.link,
|
||||||
to && styles.to,
|
to && styles.to,
|
||||||
isDisabled && 'isDisabled'
|
isDisabled && 'isDisabled'
|
||||||
);
|
);
|
||||||
|
|
||||||
const elementProps = {
|
if (to) {
|
||||||
...otherProps,
|
const toLink = /\w+?:\/\//.test(to);
|
||||||
type,
|
|
||||||
...linkProps,
|
|
||||||
};
|
|
||||||
|
|
||||||
elementProps.onClick = onClick;
|
if (toLink || noRouter) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={to}
|
||||||
|
target={target || (toLink ? '_blank' : '_self')}
|
||||||
|
rel={toLink ? 'noreferrer' : undefined}
|
||||||
|
className={linkClass}
|
||||||
|
onClick={onClick}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return React.createElement(el, elementProps);
|
return (
|
||||||
|
<RouterLink
|
||||||
|
to={`${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`}
|
||||||
|
target={target}
|
||||||
|
className={linkClass}
|
||||||
|
onClick={onClick}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
type={
|
||||||
|
component === 'button' || component === 'input'
|
||||||
|
? type || 'button'
|
||||||
|
: type
|
||||||
|
}
|
||||||
|
target={target}
|
||||||
|
className={linkClass}
|
||||||
|
disabled={isDisabled}
|
||||||
|
onClick={onClick}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Link;
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterMenuItem from './FilterMenuItem';
|
import FilterMenuItem from './FilterMenuItem';
|
||||||
import MenuContent from './MenuContent';
|
import MenuContent from './MenuContent';
|
||||||
@@ -47,7 +48,7 @@ class FilterMenuContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
customFilters
|
customFilters
|
||||||
.sort((a, b) => a.label.localeCompare(b.label))
|
.sort(sortByProp('label'))
|
||||||
.map((filter) => {
|
.map((filter) => {
|
||||||
return (
|
return (
|
||||||
<FilterMenuItem
|
<FilterMenuItem
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
.modal {
|
.modal {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
max-width: 90%;
|
||||||
max-height: 90%;
|
max-height: 90%;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import IndexerSearchInputConnector from './IndexerSearchInputConnector';
|
import IndexerSearchInputConnector from './IndexerSearchInputConnector';
|
||||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||||
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
|
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||||
import styles from './PageHeader.css';
|
import styles from './PageHeader.css';
|
||||||
|
|
||||||
class PageHeader extends Component {
|
class PageHeader extends Component {
|
||||||
@@ -87,7 +87,8 @@ class PageHeader extends Component {
|
|||||||
to="https://translate.servarr.com/projects/servarr/prowlarr/"
|
to="https://translate.servarr.com/projects/servarr/prowlarr/"
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
<PageHeaderActionsMenuConnector
|
|
||||||
|
<PageHeaderActionsMenu
|
||||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Menu from 'Components/Menu/Menu';
|
|
||||||
import MenuButton from 'Components/Menu/MenuButton';
|
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import MenuItem from 'Components/Menu/MenuItem';
|
|
||||||
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './PageHeaderActionsMenu.css';
|
|
||||||
|
|
||||||
function PageHeaderActionsMenu(props) {
|
|
||||||
const {
|
|
||||||
formsAuth,
|
|
||||||
onKeyboardShortcutsPress,
|
|
||||||
onRestartPress,
|
|
||||||
onShutdownPress
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Menu alignMenu={align.RIGHT}>
|
|
||||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
|
||||||
<Icon
|
|
||||||
name={icons.INTERACTIVE}
|
|
||||||
title={translate('Menu')}
|
|
||||||
/>
|
|
||||||
</MenuButton>
|
|
||||||
|
|
||||||
<MenuContent>
|
|
||||||
<MenuItem onPress={onKeyboardShortcutsPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.KEYBOARD}
|
|
||||||
/>
|
|
||||||
{translate('KeyboardShortcuts')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItemSeparator />
|
|
||||||
|
|
||||||
<MenuItem onPress={onRestartPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.RESTART}
|
|
||||||
/>
|
|
||||||
{translate('Restart')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem onPress={onShutdownPress}>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.SHUTDOWN}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
/>
|
|
||||||
{translate('Shutdown')}
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
{
|
|
||||||
formsAuth &&
|
|
||||||
<div className={styles.separator} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
formsAuth &&
|
|
||||||
<MenuItem
|
|
||||||
to={`${window.Prowlarr.urlBase}/logout`}
|
|
||||||
noRouter={true}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.itemIcon}
|
|
||||||
name={icons.LOGOUT}
|
|
||||||
/>
|
|
||||||
Logout
|
|
||||||
</MenuItem>
|
|
||||||
}
|
|
||||||
</MenuContent>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PageHeaderActionsMenu.propTypes = {
|
|
||||||
formsAuth: PropTypes.bool.isRequired,
|
|
||||||
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
|
||||||
onRestartPress: PropTypes.func.isRequired,
|
|
||||||
onShutdownPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageHeaderActionsMenu;
|
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Menu from 'Components/Menu/Menu';
|
||||||
|
import MenuButton from 'Components/Menu/MenuButton';
|
||||||
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
|
import MenuItem from 'Components/Menu/MenuItem';
|
||||||
|
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import { restart, shutdown } from 'Store/Actions/systemActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './PageHeaderActionsMenu.css';
|
||||||
|
|
||||||
|
interface PageHeaderActionsMenuProps {
|
||||||
|
onKeyboardShortcutsPress(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
|
||||||
|
const { onKeyboardShortcutsPress } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { authentication, isDocker } = useSelector(
|
||||||
|
(state: AppState) => state.system.status.item
|
||||||
|
);
|
||||||
|
|
||||||
|
const formsAuth = authentication === 'forms';
|
||||||
|
|
||||||
|
const handleRestartPress = useCallback(() => {
|
||||||
|
dispatch(restart());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleShutdownPress = useCallback(() => {
|
||||||
|
dispatch(shutdown());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Menu alignMenu={align.RIGHT}>
|
||||||
|
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||||
|
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
|
||||||
|
</MenuButton>
|
||||||
|
|
||||||
|
<MenuContent>
|
||||||
|
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||||
|
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
|
||||||
|
{translate('KeyboardShortcuts')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
{isDocker ? null : (
|
||||||
|
<>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
|
||||||
|
<MenuItem onPress={handleRestartPress}>
|
||||||
|
<Icon className={styles.itemIcon} name={icons.RESTART} />
|
||||||
|
{translate('Restart')}
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<MenuItem onPress={handleShutdownPress}>
|
||||||
|
<Icon
|
||||||
|
className={styles.itemIcon}
|
||||||
|
name={icons.SHUTDOWN}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
/>
|
||||||
|
{translate('Shutdown')}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formsAuth ? (
|
||||||
|
<>
|
||||||
|
<MenuItemSeparator />
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
to={`${window.Prowlarr.urlBase}/logout`}
|
||||||
|
noRouter={true}
|
||||||
|
>
|
||||||
|
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
|
||||||
|
{translate('Logout')}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageHeaderActionsMenu;
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { restart, shutdown } from 'Store/Actions/systemActions';
|
|
||||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.system.status,
|
|
||||||
(status) => {
|
|
||||||
return {
|
|
||||||
formsAuth: status.item.authentication === 'forms'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
restart,
|
|
||||||
shutdown
|
|
||||||
};
|
|
||||||
|
|
||||||
class PageHeaderActionsMenuConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRestartPress = () => {
|
|
||||||
this.props.restart();
|
|
||||||
};
|
|
||||||
|
|
||||||
onShutdownPress = () => {
|
|
||||||
this.props.shutdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PageHeaderActionsMenu
|
|
||||||
{...this.props}
|
|
||||||
onRestartPress={this.onRestartPress}
|
|
||||||
onShutdownPress={this.onShutdownPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PageHeaderActionsMenuConnector.propTypes = {
|
|
||||||
restart: PropTypes.func.isRequired,
|
|
||||||
shutdown: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
import AppUpdatedModal from 'App/AppUpdatedModal';
|
||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
import ConnectionLostModal from 'App/ConnectionLostModal';
|
||||||
import SignalRConnector from 'Components/SignalRConnector';
|
import SignalRConnector from 'Components/SignalRConnector';
|
||||||
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
@@ -102,12 +102,12 @@ class Page extends Component {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AppUpdatedModalConnector
|
<AppUpdatedModal
|
||||||
isOpen={this.state.isUpdatedModalOpen}
|
isOpen={this.state.isUpdatedModalOpen}
|
||||||
onModalClose={this.onUpdatedModalClose}
|
onModalClose={this.onUpdatedModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConnectionLostModalConnector
|
<ConnectionLostModal
|
||||||
isOpen={this.state.isConnectionLostModalOpen}
|
isOpen={this.state.isConnectionLostModalOpen}
|
||||||
onModalClose={this.onConnectionLostModalClose}
|
onModalClose={this.onConnectionLostModalClose}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Scroller from 'Components/Scroller/Scroller';
|
|||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector';
|
import HealthStatus from 'System/Status/Health/HealthStatus';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MessagesConnector from './Messages/MessagesConnector';
|
import MessagesConnector from './Messages/MessagesConnector';
|
||||||
import PageSidebarItem from './PageSidebarItem';
|
import PageSidebarItem from './PageSidebarItem';
|
||||||
@@ -87,7 +87,7 @@ const links = [
|
|||||||
{
|
{
|
||||||
title: () => translate('Status'),
|
title: () => translate('Status'),
|
||||||
to: '/system/status',
|
to: '/system/status',
|
||||||
statusComponent: HealthStatusConnector
|
statusComponent: HealthStatus
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: () => translate('Tasks'),
|
title: () => translate('Tasks'),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
composes: link;
|
composes: link;
|
||||||
|
|
||||||
padding: 10px 24px;
|
padding: 10px 24px;
|
||||||
|
padding-left: 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.isActiveLink {
|
.isActiveLink {
|
||||||
@@ -41,10 +42,6 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noIcon {
|
|
||||||
margin-left: 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ interface CssExports {
|
|||||||
'isActiveParentLink': string;
|
'isActiveParentLink': string;
|
||||||
'item': string;
|
'item': string;
|
||||||
'link': string;
|
'link': string;
|
||||||
'noIcon': string;
|
|
||||||
'status': string;
|
'status': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -63,9 +63,7 @@ class PageSidebarItem extends Component {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
<span className={isChildItem ? styles.noIcon : null}>
|
{typeof title === 'function' ? title() : title}
|
||||||
{typeof title === 'function' ? title() : title}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{
|
{
|
||||||
!!StatusComponent &&
|
!!StatusComponent &&
|
||||||
|
|||||||
@@ -22,11 +22,14 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
color: var(--toolbarLabelColor);
|
color: var(--toolbarLabelColor);
|
||||||
font-size: $extraSmallFontSize;
|
font-size: $extraSmallFontSize;
|
||||||
line-height: calc($extraSmallFontSize + 1px);
|
line-height: calc($extraSmallFontSize + 1px);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function PageToolbarButton(props) {
|
|||||||
isDisabled && styles.isDisabled
|
isDisabled && styles.isDisabled
|
||||||
)}
|
)}
|
||||||
isDisabled={isDisabled || isSpinning}
|
isDisabled={isDisabled || isSpinning}
|
||||||
|
title={label}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -141,6 +141,16 @@ class SignalRConnector extends Component {
|
|||||||
console.error(`signalR: Unable to find handler for ${name}`);
|
console.error(`signalR: Unable to find handler for ${name}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleApplications = ({ action, resource }) => {
|
||||||
|
const section = 'settings.applications';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleCommand = (body) => {
|
handleCommand = (body) => {
|
||||||
if (body.action === 'sync') {
|
if (body.action === 'sync') {
|
||||||
this.props.dispatchFetchCommands();
|
this.props.dispatchFetchCommands();
|
||||||
@@ -150,8 +160,8 @@ class SignalRConnector extends Component {
|
|||||||
const resource = body.resource;
|
const resource = body.resource;
|
||||||
const status = resource.status;
|
const status = resource.status;
|
||||||
|
|
||||||
// Both sucessful and failed commands need to be
|
// Both successful and failed commands need to be
|
||||||
// completed, otherwise they spin until they timeout.
|
// completed, otherwise they spin until they time out.
|
||||||
|
|
||||||
if (status === 'completed' || status === 'failed') {
|
if (status === 'completed' || status === 'failed') {
|
||||||
this.props.dispatchFinishCommand(resource);
|
this.props.dispatchFinishCommand(resource);
|
||||||
@@ -160,6 +170,16 @@ class SignalRConnector extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleDownloadclient = ({ action, resource }) => {
|
||||||
|
const section = 'settings.downloadClients';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleHealth = () => {
|
handleHealth = () => {
|
||||||
this.props.dispatchFetchHealth();
|
this.props.dispatchFetchHealth();
|
||||||
};
|
};
|
||||||
@@ -168,14 +188,33 @@ class SignalRConnector extends Component {
|
|||||||
this.props.dispatchFetchIndexerStatus();
|
this.props.dispatchFetchIndexerStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleIndexer = (body) => {
|
handleIndexer = ({ action, resource }) => {
|
||||||
const action = body.action;
|
|
||||||
const section = 'indexers';
|
const section = 'indexers';
|
||||||
|
|
||||||
if (action === 'updated') {
|
if (action === 'created' || action === 'updated') {
|
||||||
this.props.dispatchUpdateItem({ section, ...body.resource });
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
} else if (action === 'deleted') {
|
} else if (action === 'deleted') {
|
||||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleIndexerproxy = ({ action, resource }) => {
|
||||||
|
const section = 'settings.indexerProxies';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleNotification = ({ action, resource }) => {
|
||||||
|
const section = 'settings.notifications';
|
||||||
|
|
||||||
|
if (action === 'created' || action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: resource.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import React from 'react';
|
|||||||
|
|
||||||
type PropertyFunction<T> = () => T;
|
type PropertyFunction<T> = () => T;
|
||||||
|
|
||||||
|
// TODO: Convert to generic so `name` can be a type
|
||||||
interface Column {
|
interface Column {
|
||||||
name: string;
|
name: string;
|
||||||
label: string | PropertyFunction<string> | React.ReactNode;
|
label: string | PropertyFunction<string> | React.ReactNode;
|
||||||
|
className?: string;
|
||||||
columnLabel?: string;
|
columnLabel?: string;
|
||||||
isSortable?: boolean;
|
isSortable?: boolean;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
|||||||
@@ -65,17 +65,30 @@ class VirtualTable extends Component {
|
|||||||
|
|
||||||
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
|
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
|
||||||
this.setState({ scrollRestored: true });
|
this.setState({ scrollRestored: true });
|
||||||
this._grid.scrollToPosition({ scrollTop });
|
this._gridScrollToPosition({ scrollTop });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
||||||
this._grid.scrollToCell({
|
this._gridScrollToCell({
|
||||||
rowIndex: scrollIndex,
|
rowIndex: scrollIndex,
|
||||||
columnIndex: 0
|
columnIndex: 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gridScrollToCell = ({ rowIndex = 0, columnIndex = 0 }) => {
|
||||||
|
const scrollOffset = this._grid.getOffsetForCell({
|
||||||
|
rowIndex,
|
||||||
|
columnIndex
|
||||||
|
});
|
||||||
|
|
||||||
|
this._gridScrollToPosition(scrollOffset);
|
||||||
|
};
|
||||||
|
|
||||||
|
_gridScrollToPosition = ({ scrollTop = 0, scrollLeft = 0 }) => {
|
||||||
|
this.props.scroller?.scrollTo({ top: scrollTop, left: scrollLeft });
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
|
|||||||
54
frontend/src/Components/Table/usePaging.ts
Normal file
54
frontend/src/Components/Table/usePaging.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
interface PagingOptions {
|
||||||
|
page: number;
|
||||||
|
totalPages: number;
|
||||||
|
gotoPage: ({ page }: { page: number }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePaging(options: PagingOptions) {
|
||||||
|
const { page, totalPages, gotoPage } = options;
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleFirstPagePress = useCallback(() => {
|
||||||
|
dispatch(gotoPage({ page: 1 }));
|
||||||
|
}, [dispatch, gotoPage]);
|
||||||
|
|
||||||
|
const handlePreviousPagePress = useCallback(() => {
|
||||||
|
dispatch(gotoPage({ page: Math.max(page - 1, 1) }));
|
||||||
|
}, [page, dispatch, gotoPage]);
|
||||||
|
|
||||||
|
const handleNextPagePress = useCallback(() => {
|
||||||
|
dispatch(gotoPage({ page: Math.min(page + 1, totalPages) }));
|
||||||
|
}, [page, totalPages, dispatch, gotoPage]);
|
||||||
|
|
||||||
|
const handleLastPagePress = useCallback(() => {
|
||||||
|
dispatch(gotoPage({ page: totalPages }));
|
||||||
|
}, [totalPages, dispatch, gotoPage]);
|
||||||
|
|
||||||
|
const handlePageSelect = useCallback(
|
||||||
|
(page: number) => {
|
||||||
|
dispatch(gotoPage({ page }));
|
||||||
|
},
|
||||||
|
[dispatch, gotoPage]
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
return {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default usePaging;
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import Label from './Label';
|
import Label from './Label';
|
||||||
import styles from './TagList.css';
|
import styles from './TagList.css';
|
||||||
|
|
||||||
function TagList({ tags, tagList }) {
|
function TagList({ tags, tagList }) {
|
||||||
const sortedTags = tags
|
const sortedTags = tags
|
||||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||||
.filter((t) => t !== undefined)
|
.filter((tag) => !!tag)
|
||||||
.sort((a, b) => a.label.localeCompare(b.label));
|
.sort(sortByProp('label'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
|
|||||||
@@ -25,14 +25,3 @@
|
|||||||
font-family: 'Ubuntu Mono';
|
font-family: 'Ubuntu Mono';
|
||||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* text-security-disc
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-family: 'text-security-disc';
|
|
||||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -15,5 +15,5 @@
|
|||||||
"start_url": "../../../../",
|
"start_url": "../../../../",
|
||||||
"theme_color": "#3a3f51",
|
"theme_color": "#3a3f51",
|
||||||
"background_color": "#3a3f51",
|
"background_color": "#3a3f51",
|
||||||
"display": "minimal-ui"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|||||||
3
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
3
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
type DownloadProtocol = 'usenet' | 'torrent' | 'unknown';
|
||||||
|
|
||||||
|
export default DownloadProtocol;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user