mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-17 21:44:48 -04:00
Compare commits
683 Commits
v1.13.3.42
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c4efa0226 | ||
|
|
50d31d0c5e | ||
|
|
f48c9f9f88 | ||
|
|
1ba2f26649 | ||
|
|
c880b6c09c | ||
|
|
6fca0d0b6c | ||
|
|
9907342055 | ||
|
|
71d1a59008 | ||
|
|
33fa39dc84 | ||
|
|
d133c82537 | ||
|
|
6b446e1404 | ||
|
|
b0e879da5c | ||
|
|
5edde8d9bd | ||
|
|
ef5d670c39 | ||
|
|
f568906876 | ||
|
|
331e92ac62 | ||
|
|
ec46b25be2 | ||
|
|
8b3837cb6e | ||
|
|
ade5aee4a9 | ||
|
|
c486013113 | ||
|
|
c512cafb4a | ||
|
|
454641e8b5 | ||
|
|
7cac3fc174 | ||
|
|
43aca69840 | ||
|
|
e8d4415a5c | ||
|
|
5858c2dda6 | ||
|
|
ce315afb2a | ||
|
|
407acb6844 | ||
|
|
c3a7fbdd86 | ||
|
|
472c6f4273 | ||
|
|
baa4baf3ca | ||
|
|
852d62dcf0 | ||
|
|
13493ddbce | ||
|
|
a4a8e890c1 | ||
|
|
688434ced9 | ||
|
|
2ed910459f | ||
|
|
878818e950 | ||
|
|
0884ac92ff | ||
|
|
9508329b99 | ||
|
|
15a03007d9 | ||
|
|
b188746f1a | ||
|
|
ed3b25b3d6 | ||
|
|
c006079ce6 | ||
|
|
9437ff9498 | ||
|
|
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 |
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
|
||||||
26
.github/workflows/close_invalid_issues.yml
vendored
Normal file
26
.github/workflows/close_invalid_issues.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Close issues without labels
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
close-issue:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
sparse-checkout: |
|
||||||
|
.github
|
||||||
|
- name: Close issue if no labels found
|
||||||
|
if: join(github.event.issue.labels) == ''
|
||||||
|
run: |
|
||||||
|
gh issue comment ${{ github.event.issue.number }} --body ":wave: @${{ github.event.issue.user.login }}, this issue was closed automatically because it was created without following an issue template. Please update the issue following the correct template for this issue. Once updated please reply to this issue so we can review and re-open. In the future, use the [issue templates](https://github.com/${{ github.repository }}/issues/new/choose) instead of creating your own."
|
||||||
|
gh issue close ${{ github.event.issue.number }} --reason "not planned"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
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)
|
||||||
|
|||||||
8
SECURITY.md
Normal file
8
SECURITY.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Please report (suspected) security vulnerabilities on Discord (preferred) to
|
||||||
|
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
|
||||||
|
us within 72 hours. If the issue is confirmed, we will release a patch as soon
|
||||||
|
as possible depending on complexity/severity.
|
||||||
@@ -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.13.3'
|
majorVersion: '2.3.5'
|
||||||
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.7.1'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2025'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-24.04'
|
||||||
macImage: 'macOS-11'
|
macImage: 'macOS-15'
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
56
build.sh
56
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
|
||||||
@@ -253,8 +253,10 @@ InstallInno()
|
|||||||
{
|
{
|
||||||
ProgressStart "Installing portable Inno Setup"
|
ProgressStart "Installing portable Inno Setup"
|
||||||
|
|
||||||
|
INNOVERSION=${INNOVERSION:-6.7.1}
|
||||||
|
|
||||||
rm -rf _inno
|
rm -rf _inno
|
||||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
|
curl -s -L --output innosetup.exe "https://github.com/jrsoftware/issrc/releases/download/is-${INNOVERSION//./_}/innosetup-${INNOVERSION}.exe"
|
||||||
mkdir _inno
|
mkdir _inno
|
||||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||||
rm innosetup.exe
|
rm innosetup.exe
|
||||||
@@ -377,15 +379,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 +414,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 +436,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'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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({
|
||||||
@@ -132,6 +133,12 @@ module.exports = (env) => {
|
|||||||
{
|
{
|
||||||
source: 'frontend/src/Content/robots.txt',
|
source: 'frontend/src/Content/robots.txt',
|
||||||
destination: path.join(distFolder, 'Content/robots.txt')
|
destination: path.join(distFolder, 'Content/robots.txt')
|
||||||
|
},
|
||||||
|
|
||||||
|
// manifest.json and browserconfig.xml
|
||||||
|
{
|
||||||
|
source: 'frontend/src/Content/*.(json|xml)',
|
||||||
|
destination: path.join(distFolder, 'Content')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -169,7 +176,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.42'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -190,7 +197,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>;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ 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>,
|
||||||
@@ -24,6 +25,12 @@ export interface ApplicationAppState
|
|||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {
|
||||||
|
isTestingAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneralAppState
|
||||||
|
extends AppSectionItemState<General>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface IndexerCategoryAppState
|
export interface IndexerCategoryAppState
|
||||||
@@ -41,6 +48,7 @@ interface SettingsAppState {
|
|||||||
appProfiles: AppProfileAppState;
|
appProfiles: AppProfileAppState;
|
||||||
applications: ApplicationAppState;
|
applications: ApplicationAppState;
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
|
general: GeneralAppState;
|
||||||
indexerCategories: IndexerCategoryAppState;
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,6 +3,7 @@ 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 CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
|
||||||
@@ -212,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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<browserconfig>
|
|
||||||
<msapplication>
|
|
||||||
<tile>
|
|
||||||
<square150x150logo src="/Content/Images/Icons/mstile-150x150.png"/>
|
|
||||||
<TileColor>#00ccff</TileColor>
|
|
||||||
</tile>
|
|
||||||
</msapplication>
|
|
||||||
</browserconfig>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Prowlarr",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": "../../../../",
|
|
||||||
"theme_color": "#3a3f51",
|
|
||||||
"background_color": "#3a3f51",
|
|
||||||
"display": "standalone"
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user