mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-04-16 21:35:04 -04:00
Compare commits
532 Commits
v1.18.0.45
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46ce8e2701 | ||
|
|
c687bdb1fb | ||
|
|
b2d49164bc | ||
|
|
28bd80d3aa | ||
|
|
0ffcfccf1d | ||
|
|
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 |
@@ -2,11 +2,11 @@
|
|||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||||
{
|
{
|
||||||
"name": "Prowlarr",
|
"name": "Prowlarr",
|
||||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
"image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"nodeGypDependencies": true,
|
"nodeGypDependencies": true,
|
||||||
"version": "16",
|
"version": "20",
|
||||||
"nvmVersion": "latest"
|
"nvmVersion": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
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
|
||||||
|
|||||||
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
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -10,7 +10,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build dotnet",
|
"preLaunchTask": "build dotnet",
|
||||||
// If you have changed target frameworks, make sure to update the program path.
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
"program": "${workspaceFolder}/_output/net6.0/Prowlarr",
|
"program": "${workspaceFolder}/_output/net8.0/Prowlarr",
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
|||||||
@@ -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.18.0'
|
majorVersion: '2.3.7'
|
||||||
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.421'
|
dotnetVersion: '8.0.405'
|
||||||
nodeVersion: '20.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'))
|
||||||
@@ -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
|
||||||
@@ -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,20 +1,25 @@
|
|||||||
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>
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
@@ -22,9 +27,4 @@ function App({ store, history }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,13 +1,9 @@
|
|||||||
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import themes from 'Styles/Themes';
|
import themes from 'Styles/Themes';
|
||||||
import AppState from './State/AppState';
|
import AppState from './State/AppState';
|
||||||
|
|
||||||
interface ApplyThemeProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createThemeSelector() {
|
function createThemeSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
||||||
@@ -17,7 +13,7 @@ function createThemeSelector() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ApplyTheme({ children }: ApplyThemeProps) {
|
function ApplyTheme() {
|
||||||
const theme = useSelector(createThemeSelector());
|
const theme = useSelector(createThemeSelector());
|
||||||
|
|
||||||
const updateCSSVariables = useCallback(() => {
|
const updateCSSVariables = useCallback(() => {
|
||||||
@@ -31,7 +27,7 @@ function ApplyTheme({ children }: ApplyThemeProps) {
|
|||||||
updateCSSVariables();
|
updateCSSVariables();
|
||||||
}, [updateCSSVariables, theme]);
|
}, [updateCSSVariables, theme]);
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApplyTheme;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ export interface CustomFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
|
isConnected: boolean;
|
||||||
|
isReconnecting: boolean;
|
||||||
|
version: string;
|
||||||
|
prevVersion?: string;
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ class StackedBarChart extends Component {
|
|||||||
size: 14,
|
size: 14,
|
||||||
family: defaultFontFamily
|
family: defaultFontFamily
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
position: 'average'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,7 +22,7 @@ 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,
|
||||||
@@ -31,7 +32,7 @@ function createMapStateToProps() {
|
|||||||
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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
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) {
|
||||||
@@ -30,10 +31,12 @@ function Form(props) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
error.detailedDescription ?
|
error.detailedDescription ?
|
||||||
<Icon
|
<Tooltip
|
||||||
containerClassName={styles.details}
|
className={styles.details}
|
||||||
name={icons.INFO}
|
anchor={<Icon name={icons.INFO} />}
|
||||||
title={error.detailedDescription}
|
tooltip={error.detailedDescription}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -53,10 +56,12 @@ function Form(props) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
warning.detailedDescription ?
|
warning.detailedDescription ?
|
||||||
<Icon
|
<Tooltip
|
||||||
containerClassName={styles.details}
|
className={styles.details}
|
||||||
name={icons.INFO}
|
anchor={<Icon name={icons.INFO} />}
|
||||||
title={warning.detailedDescription}
|
tooltip={warning.detailedDescription}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,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}>
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
11
frontend/src/Content/browserconfig.xml
Normal file
11
frontend/src/Content/browserconfig.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="__URL_BASE__/Content/Images/Icons/mstile-150x150.png" />
|
||||||
|
<TileColor>
|
||||||
|
#00ccff
|
||||||
|
</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
19
frontend/src/Content/manifest.json
Normal file
19
frontend/src/Content/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "__INSTANCE_NAME__",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "__URL_BASE__/",
|
||||||
|
"theme_color": "#3a3f51",
|
||||||
|
"background_color": "#3a3f51",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
enum DownloadProtocol {
|
type DownloadProtocol = 'usenet' | 'torrent' | 'unknown';
|
||||||
Unknown = 'unknown',
|
|
||||||
Usenet = 'usenet',
|
|
||||||
Torrent = 'torrent',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DownloadProtocol;
|
export default DownloadProtocol;
|
||||||
|
|||||||
9
frontend/src/Helpers/Hooks/useCurrentPage.ts
Normal file
9
frontend/src/Helpers/Hooks/useCurrentPage.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
function useCurrentPage() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
return history.action === 'POP';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useCurrentPage;
|
||||||
@@ -3,15 +3,15 @@ import { useCallback, useState } from 'react';
|
|||||||
export default function useModalOpenState(
|
export default function useModalOpenState(
|
||||||
initialState: boolean
|
initialState: boolean
|
||||||
): [boolean, () => void, () => void] {
|
): [boolean, () => void, () => void] {
|
||||||
const [isOpen, setOpen] = useState(initialState);
|
const [isOpen, setIsOpen] = useState(initialState);
|
||||||
|
|
||||||
const setModalOpen = useCallback(() => {
|
const setModalOpen = useCallback(() => {
|
||||||
setOpen(true);
|
setIsOpen(true);
|
||||||
}, [setOpen]);
|
}, [setIsOpen]);
|
||||||
|
|
||||||
const setModalClosed = useCallback(() => {
|
const setModalClosed = useCallback(() => {
|
||||||
setOpen(false);
|
setIsOpen(false);
|
||||||
}, [setOpen]);
|
}, [setIsOpen]);
|
||||||
|
|
||||||
return [isOpen, setModalOpen, setModalClosed];
|
return [isOpen, setModalOpen, setModalClosed];
|
||||||
}
|
}
|
||||||
|
|||||||
56
frontend/src/Helpers/Hooks/useTheme.ts
Normal file
56
frontend/src/Helpers/Hooks/useTheme.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import themes from 'Styles/Themes';
|
||||||
|
|
||||||
|
function createThemeSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
|
||||||
|
(theme) => theme
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useTheme = () => {
|
||||||
|
const selectedTheme = useSelector(createThemeSelector());
|
||||||
|
const [resolvedTheme, setResolvedTheme] = useState(selectedTheme);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTheme !== 'auto') {
|
||||||
|
setResolvedTheme(selectedTheme);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const applySystemTheme = () => {
|
||||||
|
setResolvedTheme(
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
applySystemTheme();
|
||||||
|
|
||||||
|
window
|
||||||
|
.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
.addEventListener('change', applySystemTheme);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window
|
||||||
|
.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
.removeEventListener('change', applySystemTheme);
|
||||||
|
};
|
||||||
|
}, [selectedTheme]);
|
||||||
|
|
||||||
|
return resolvedTheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTheme;
|
||||||
|
|
||||||
|
export const useThemeColor = (color: string) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const themeVariables = themes[theme];
|
||||||
|
|
||||||
|
// @ts-expect-error - themeVariables is a string indexable type
|
||||||
|
return themeVariables[color];
|
||||||
|
};
|
||||||
3
frontend/src/Helpers/Props/TooltipPosition.ts
Normal file
3
frontend/src/Helpers/Props/TooltipPosition.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';
|
||||||
|
|
||||||
|
export default TooltipPosition;
|
||||||
@@ -7,7 +7,6 @@ export const PRIMARY = 'primary';
|
|||||||
export const PURPLE = 'purple';
|
export const PURPLE = 'purple';
|
||||||
export const SUCCESS = 'success';
|
export const SUCCESS = 'success';
|
||||||
export const WARNING = 'warning';
|
export const WARNING = 'warning';
|
||||||
export const QUEUE = 'queue';
|
|
||||||
|
|
||||||
export const all = [
|
export const all = [
|
||||||
DANGER,
|
DANGER,
|
||||||
@@ -19,5 +18,15 @@ export const all = [
|
|||||||
PURPLE,
|
PURPLE,
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
WARNING,
|
WARNING,
|
||||||
QUEUE
|
] as const;
|
||||||
];
|
|
||||||
|
export type Kind =
|
||||||
|
| 'danger'
|
||||||
|
| 'default'
|
||||||
|
| 'disabled'
|
||||||
|
| 'info'
|
||||||
|
| 'inverse'
|
||||||
|
| 'primary'
|
||||||
|
| 'purple'
|
||||||
|
| 'success'
|
||||||
|
| 'warning';
|
||||||
@@ -4,4 +4,6 @@ export const MEDIUM = 'medium';
|
|||||||
export const LARGE = 'large';
|
export const LARGE = 'large';
|
||||||
export const EXTRA_LARGE = 'extraLarge';
|
export const EXTRA_LARGE = 'extraLarge';
|
||||||
|
|
||||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const;
|
||||||
|
|
||||||
|
export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge';
|
||||||
@@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryDetails.css';
|
import styles from './HistoryDetails.css';
|
||||||
|
|
||||||
@@ -10,7 +11,10 @@ function HistoryDetails(props) {
|
|||||||
const {
|
const {
|
||||||
indexer,
|
indexer,
|
||||||
eventType,
|
eventType,
|
||||||
data
|
date,
|
||||||
|
data,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (eventType === 'indexerQuery' || eventType === 'indexerRss') {
|
if (eventType === 'indexerQuery' || eventType === 'indexerRss') {
|
||||||
@@ -22,7 +26,9 @@ function HistoryDetails(props) {
|
|||||||
offset,
|
offset,
|
||||||
source,
|
source,
|
||||||
host,
|
host,
|
||||||
url
|
url,
|
||||||
|
elapsedTime,
|
||||||
|
cached
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -104,6 +110,24 @@ function HistoryDetails(props) {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
elapsedTime ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('ElapsedTime')}
|
||||||
|
data={`${elapsedTime}ms${cached === '1' ? ' (cached)' : ''}`}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
date ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Date')}
|
||||||
|
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -111,10 +135,19 @@ function HistoryDetails(props) {
|
|||||||
if (eventType === 'releaseGrabbed') {
|
if (eventType === 'releaseGrabbed') {
|
||||||
const {
|
const {
|
||||||
source,
|
source,
|
||||||
|
host,
|
||||||
grabTitle,
|
grabTitle,
|
||||||
url
|
url,
|
||||||
|
publishedDate,
|
||||||
|
infoUrl,
|
||||||
|
downloadClient,
|
||||||
|
downloadClientName,
|
||||||
|
elapsedTime,
|
||||||
|
grabMethod
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
{
|
{
|
||||||
@@ -135,6 +168,15 @@ function HistoryDetails(props) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
data ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Host')}
|
||||||
|
data={host}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
data ?
|
data ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
@@ -144,6 +186,33 @@ function HistoryDetails(props) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
infoUrl ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('InfoUrl')}
|
||||||
|
data={<Link to={infoUrl}>{infoUrl}</Link>}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
publishedDate ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('PublishedDate')}
|
||||||
|
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
downloadClientNameInfo ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DownloadClient')}
|
||||||
|
data={downloadClientNameInfo}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
data ?
|
data ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
@@ -152,11 +221,40 @@ function HistoryDetails(props) {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
elapsedTime ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('ElapsedTime')}
|
||||||
|
data={`${elapsedTime}ms`}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
grabMethod ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Redirected')}
|
||||||
|
data={grabMethod.toLowerCase() === 'redirect' ? translate('Yes') : translate('No')}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
date ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Date')}
|
||||||
|
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventType === 'indexerAuth') {
|
if (eventType === 'indexerAuth') {
|
||||||
|
const { elapsedTime } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DescriptionList
|
<DescriptionList
|
||||||
descriptionClassName={styles.description}
|
descriptionClassName={styles.description}
|
||||||
@@ -170,6 +268,24 @@ function HistoryDetails(props) {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
elapsedTime ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('ElapsedTime')}
|
||||||
|
data={`${elapsedTime}ms`}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
date ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Date')}
|
||||||
|
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -181,6 +297,15 @@ function HistoryDetails(props) {
|
|||||||
title={translate('Name')}
|
title={translate('Name')}
|
||||||
data={data.query}
|
data={data.query}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
date ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Date')}
|
||||||
|
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -188,6 +313,7 @@ function HistoryDetails(props) {
|
|||||||
HistoryDetails.propTypes = {
|
HistoryDetails.propTypes = {
|
||||||
indexer: PropTypes.object.isRequired,
|
indexer: PropTypes.object.isRequired,
|
||||||
eventType: PropTypes.string.isRequired,
|
eventType: PropTypes.string.isRequired,
|
||||||
|
date: PropTypes.string.isRequired,
|
||||||
data: PropTypes.object.isRequired,
|
data: PropTypes.object.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired
|
timeFormat: PropTypes.string.isRequired
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ function HistoryDetailsModal(props) {
|
|||||||
isOpen,
|
isOpen,
|
||||||
eventType,
|
eventType,
|
||||||
indexer,
|
indexer,
|
||||||
|
date,
|
||||||
data,
|
data,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
@@ -49,6 +50,7 @@ function HistoryDetailsModal(props) {
|
|||||||
<HistoryDetails
|
<HistoryDetails
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
indexer={indexer}
|
indexer={indexer}
|
||||||
|
date={date}
|
||||||
data={data}
|
data={data}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
@@ -71,6 +73,7 @@ HistoryDetailsModal.propTypes = {
|
|||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
eventType: PropTypes.string.isRequired,
|
eventType: PropTypes.string.isRequired,
|
||||||
indexer: PropTypes.object.isRequired,
|
indexer: PropTypes.object.isRequired,
|
||||||
|
date: PropTypes.string.isRequired,
|
||||||
data: PropTypes.object.isRequired,
|
data: PropTypes.object.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -20,21 +20,22 @@ function getIconName(eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconKind(successful) {
|
function getIconKind(successful, redirect) {
|
||||||
switch (successful) {
|
if (redirect) {
|
||||||
case false:
|
return kinds.INFO;
|
||||||
return kinds.DANGER;
|
} else if (!successful) {
|
||||||
default:
|
return kinds.DANGER;
|
||||||
return kinds.DEFAULT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return kinds.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTooltip(eventType, data, indexer) {
|
function getTooltip(eventType, data, indexer, redirect) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'indexerQuery':
|
case 'indexerQuery':
|
||||||
return `Query "${data.query}" sent to ${indexer.name}`;
|
return `Query "${data.query}" sent to ${indexer.name}`;
|
||||||
case 'releaseGrabbed':
|
case 'releaseGrabbed':
|
||||||
return `Release grabbed from ${indexer.name}`;
|
return redirect ? `Release grabbed via redirect from ${indexer.name}` : `Release grabbed from ${indexer.name}`;
|
||||||
case 'indexerAuth':
|
case 'indexerAuth':
|
||||||
return `Auth attempted for ${indexer.name}`;
|
return `Auth attempted for ${indexer.name}`;
|
||||||
case 'indexerRss':
|
case 'indexerRss':
|
||||||
@@ -45,9 +46,12 @@ function getTooltip(eventType, data, indexer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, successful, data, indexer }) {
|
function HistoryEventTypeCell({ eventType, successful, data, indexer }) {
|
||||||
|
const { grabMethod } = data;
|
||||||
|
const redirect = grabMethod && grabMethod.toLowerCase() === 'redirect';
|
||||||
|
|
||||||
const iconName = getIconName(eventType);
|
const iconName = getIconName(eventType);
|
||||||
const iconKind = getIconKind(successful);
|
const iconKind = getIconKind(successful, redirect);
|
||||||
const tooltip = getTooltip(eventType, data, indexer);
|
const tooltip = getTooltip(eventType, data, indexer, redirect);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ class HistoryRow extends Component {
|
|||||||
key={parameter.key}
|
key={parameter.key}
|
||||||
title={parameter.title}
|
title={parameter.title}
|
||||||
value={data[parameter.key]}
|
value={data[parameter.key]}
|
||||||
|
queryType={data.queryType}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -370,8 +371,9 @@ class HistoryRow extends Component {
|
|||||||
return (
|
return (
|
||||||
<RelativeDateCell
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={date}
|
|
||||||
className={styles.date}
|
className={styles.date}
|
||||||
|
date={date}
|
||||||
|
includeSeconds={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -408,6 +410,7 @@ class HistoryRow extends Component {
|
|||||||
<HistoryDetailsModal
|
<HistoryDetailsModal
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
isOpen={this.state.isDetailsModalOpen}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
|
date={date}
|
||||||
data={data}
|
data={data}
|
||||||
indexer={indexer}
|
indexer={indexer}
|
||||||
isMarkingAsFailed={isMarkingAsFailed}
|
isMarkingAsFailed={isMarkingAsFailed}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import { HistoryQueryType } from 'typings/History';
|
||||||
import styles from './HistoryRowParameter.css';
|
import styles from './HistoryRowParameter.css';
|
||||||
|
|
||||||
interface HistoryRowParameterProps {
|
interface HistoryRowParameterProps {
|
||||||
title: string;
|
title: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
queryType: HistoryQueryType;
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryRowParameter(props: HistoryRowParameterProps) {
|
function HistoryRowParameter(props: HistoryRowParameterProps) {
|
||||||
const { title, value } = props;
|
const { title, value, queryType } = props;
|
||||||
|
|
||||||
const type = title.toLowerCase();
|
const type = title.toLowerCase();
|
||||||
|
|
||||||
@@ -18,7 +20,13 @@ function HistoryRowParameter(props: HistoryRowParameterProps) {
|
|||||||
link = <Link to={`https://imdb.com/title/${value}/`}>{value}</Link>;
|
link = <Link to={`https://imdb.com/title/${value}/`}>{value}</Link>;
|
||||||
} else if (type === 'tmdb') {
|
} else if (type === 'tmdb') {
|
||||||
link = (
|
link = (
|
||||||
<Link to={`https://www.themoviedb.org/movie/${value}`}>{value}</Link>
|
<Link
|
||||||
|
to={`https://www.themoviedb.org/${
|
||||||
|
queryType === 'tvsearch' ? 'tv' : 'movie'
|
||||||
|
}/${value}`}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
} else if (type === 'tvdb') {
|
} else if (type === 'tvdb') {
|
||||||
link = (
|
link = (
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
|
|
||||||
import styles from './AddIndexerModal.css';
|
|
||||||
|
|
||||||
function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.EXTRA_LARGE}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
className={styles.modal}
|
|
||||||
>
|
|
||||||
<AddIndexerModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
onSelectIndexer={onSelectIndexer}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddIndexerModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onSelectIndexer: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddIndexerModal;
|
|
||||||
44
frontend/src/Indexer/Add/AddIndexerModal.tsx
Normal file
44
frontend/src/Indexer/Add/AddIndexerModal.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { clearIndexerSchema } from 'Store/Actions/indexerActions';
|
||||||
|
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||||
|
import styles from './AddIndexerModal.css';
|
||||||
|
|
||||||
|
interface AddIndexerModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onSelectIndexer(): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddIndexerModal({
|
||||||
|
isOpen,
|
||||||
|
onSelectIndexer,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: AddIndexerModalProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onModalClosePress = useCallback(() => {
|
||||||
|
dispatch(clearIndexerSchema());
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
|
className={styles.modal}
|
||||||
|
onModalClose={onModalClosePress}
|
||||||
|
>
|
||||||
|
<AddIndexerModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onSelectIndexer={onSelectIndexer}
|
||||||
|
onModalClose={onModalClosePress}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddIndexerModal;
|
||||||
@@ -19,14 +19,21 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.notice {
|
||||||
composes: alert from '~Components/Alert.css';
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.scroller {
|
.scroller {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
min-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterRow {
|
.filterRow {
|
||||||
@@ -51,29 +58,68 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filtersToggle {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--textColor);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersToggle:hover {
|
||||||
|
background-color: var(--hoverBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.filterInput {
|
.filterInput {
|
||||||
margin-bottom: 5px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.notice {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filtersToggle {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.filterRow {
|
.filterRow {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--cardBackgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterRowCollapsed {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filterContainer {
|
.filterContainer {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterContainer:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroller {
|
.scroller {
|
||||||
margin-right: -30px;
|
margin-right: -15px;
|
||||||
margin-bottom: -30px;
|
margin-bottom: -15px;
|
||||||
margin-left: -30px;
|
margin-left: -15px;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBody {
|
||||||
|
padding: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ interface CssExports {
|
|||||||
'filterInput': string;
|
'filterInput': string;
|
||||||
'filterLabel': string;
|
'filterLabel': string;
|
||||||
'filterRow': string;
|
'filterRow': string;
|
||||||
|
'filterRowCollapsed': string;
|
||||||
|
'filtersToggle': string;
|
||||||
'indexers': string;
|
'indexers': string;
|
||||||
'modalBody': string;
|
'modalBody': string;
|
||||||
'modalFooter': string;
|
'modalFooter': string;
|
||||||
|
'notice': string;
|
||||||
'scroller': string;
|
'scroller': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,324 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
|
|
||||||
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
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 Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import SelectIndexerRow from './SelectIndexerRow';
|
|
||||||
import styles from './AddIndexerModalContent.css';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'protocol',
|
|
||||||
label: () => translate('Protocol'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sortName',
|
|
||||||
label: () => translate('Name'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'language',
|
|
||||||
label: () => translate('Language'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'description',
|
|
||||||
label: () => translate('Description'),
|
|
||||||
isSortable: false,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'privacy',
|
|
||||||
label: () => translate('Privacy'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'categories',
|
|
||||||
label: () => translate('Categories'),
|
|
||||||
isSortable: false,
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const protocols = [
|
|
||||||
{
|
|
||||||
key: 'torrent',
|
|
||||||
value: 'torrent'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'usenet',
|
|
||||||
value: 'nzb'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const privacyLevels = [
|
|
||||||
{
|
|
||||||
key: 'private',
|
|
||||||
get value() {
|
|
||||||
return translate('Private');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'semiPrivate',
|
|
||||||
get value() {
|
|
||||||
return translate('SemiPrivate');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'public',
|
|
||||||
get value() {
|
|
||||||
return translate('Public');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class AddIndexerModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
filter: '',
|
|
||||||
filterProtocols: [],
|
|
||||||
filterLanguages: [],
|
|
||||||
filterPrivacyLevels: [],
|
|
||||||
filterCategories: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFilterChange = ({ value }) => {
|
|
||||||
this.setState({ filter: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
indexers,
|
|
||||||
onIndexerSelect,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
onSortPress,
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const languages = Array.from(new Set(indexers.map(({ language }) => language)))
|
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
.map((language) => ({ key: language, value: language }));
|
|
||||||
|
|
||||||
const filteredIndexers = indexers.filter((indexer) => {
|
|
||||||
const {
|
|
||||||
filter,
|
|
||||||
filterProtocols,
|
|
||||||
filterLanguages,
|
|
||||||
filterPrivacyLevels,
|
|
||||||
filterCategories
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterLanguages.length && !filterLanguages.includes(indexer.language)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterCategories.length) {
|
|
||||||
const { categories = [] } = indexer.capabilities || {};
|
|
||||||
const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)];
|
|
||||||
const flatCategories = categories
|
|
||||||
.filter((item) => item.id < 100000)
|
|
||||||
.flatMap(flat);
|
|
||||||
|
|
||||||
if (!filterCategories.every((item) => flatCategories.includes(item))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('AddIndexer')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody
|
|
||||||
className={styles.modalBody}
|
|
||||||
scrollDirection={scrollDirections.NONE}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
className={styles.filterInput}
|
|
||||||
placeholder={translate('FilterPlaceHolder')}
|
|
||||||
name="filter"
|
|
||||||
value={this.state.filter}
|
|
||||||
autoFocus={true}
|
|
||||||
onChange={this.onFilterChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.filterRow}>
|
|
||||||
<div className={styles.filterContainer}>
|
|
||||||
<label className={styles.filterLabel}>{translate('Protocol')}</label>
|
|
||||||
<EnhancedSelectInput
|
|
||||||
name="indexerProtocols"
|
|
||||||
value={this.state.filterProtocols}
|
|
||||||
values={protocols}
|
|
||||||
onChange={({ value }) => this.setState({ filterProtocols: value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.filterContainer}>
|
|
||||||
<label className={styles.filterLabel}>{translate('Language')}</label>
|
|
||||||
<EnhancedSelectInput
|
|
||||||
name="indexerLanguages"
|
|
||||||
value={this.state.filterLanguages}
|
|
||||||
values={languages}
|
|
||||||
onChange={({ value }) => this.setState({ filterLanguages: value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.filterContainer}>
|
|
||||||
<label className={styles.filterLabel}>{translate('Privacy')}</label>
|
|
||||||
<EnhancedSelectInput
|
|
||||||
name="indexerPrivacyLevels"
|
|
||||||
value={this.state.filterPrivacyLevels}
|
|
||||||
values={privacyLevels}
|
|
||||||
onChange={({ value }) => this.setState({ filterPrivacyLevels: value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.filterContainer}>
|
|
||||||
<label className={styles.filterLabel}>{translate('Categories')}</label>
|
|
||||||
<NewznabCategorySelectInputConnector
|
|
||||||
name="indexerCategories"
|
|
||||||
value={this.state.filterCategories}
|
|
||||||
onChange={({ value }) => this.setState({ filterCategories: value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Alert
|
|
||||||
kind={kinds.INFO}
|
|
||||||
className={styles.alert}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{translate('ProwlarrSupportsAnyIndexer')}
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<Scroller
|
|
||||||
className={styles.scroller}
|
|
||||||
autoFocus={false}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isFetching ? <LoadingIndicator /> : null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
error ? <Alert kind={kinds.DANGER}>{errorMessage}</Alert> : null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
isPopulated && !!indexers.length ?
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
onSortPress={onSortPress}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
filteredIndexers.map((indexer) => (
|
|
||||||
<SelectIndexerRow
|
|
||||||
key={`${indexer.implementation}-${indexer.name}`}
|
|
||||||
implementation={indexer.implementation}
|
|
||||||
implementationName={indexer.implementationName}
|
|
||||||
{...indexer}
|
|
||||||
onIndexerSelect={onIndexerSelect}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
isPopulated && !!indexers.length && !filteredIndexers.length ?
|
|
||||||
<Alert
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
>
|
|
||||||
{translate('NoIndexersFound')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</Scroller>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
|
||||||
<div className={styles.available}>
|
|
||||||
{
|
|
||||||
isPopulated ?
|
|
||||||
translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
|
||||||
</div>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddIndexerModalContent.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
sortDirection: PropTypes.string,
|
|
||||||
onSortPress: PropTypes.func.isRequired,
|
|
||||||
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onIndexerSelect: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddIndexerModalContent;
|
|
||||||
451
frontend/src/Indexer/Add/AddIndexerModalContent.tsx
Normal file
451
frontend/src/Indexer/Add/AddIndexerModalContent.tsx
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { some } from 'lodash';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import IndexerAppState from 'App/State/IndexerAppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
|
||||||
|
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
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 Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import { icons, kinds, scrollDirections } from 'Helpers/Props';
|
||||||
|
import Indexer, { IndexerCategory } from 'Indexer/Indexer';
|
||||||
|
import {
|
||||||
|
fetchIndexerSchema,
|
||||||
|
selectIndexerSchema,
|
||||||
|
setIndexerSchemaSort,
|
||||||
|
} from 'Store/Actions/indexerActions';
|
||||||
|
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import { SortCallback } from 'typings/callbacks';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import SelectIndexerRow from './SelectIndexerRow';
|
||||||
|
import styles from './AddIndexerModalContent.css';
|
||||||
|
|
||||||
|
const COLUMNS = [
|
||||||
|
{
|
||||||
|
name: 'protocol',
|
||||||
|
label: () => translate('Protocol'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sortName',
|
||||||
|
label: () => translate('Name'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'language',
|
||||||
|
label: () => translate('Language'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
label: () => translate('Description'),
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'privacy',
|
||||||
|
label: () => translate('Privacy'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categories',
|
||||||
|
label: () => translate('Categories'),
|
||||||
|
isSortable: false,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const PROTOCOLS = [
|
||||||
|
{
|
||||||
|
key: 'torrent',
|
||||||
|
value: 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'usenet',
|
||||||
|
value: 'nzb',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRIVACY_LEVELS = [
|
||||||
|
{
|
||||||
|
key: 'private',
|
||||||
|
get value() {
|
||||||
|
return translate('Private');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'semiPrivate',
|
||||||
|
get value() {
|
||||||
|
return translate('SemiPrivate');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'public',
|
||||||
|
get value() {
|
||||||
|
return translate('Public');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface IndexerSchema extends Indexer {
|
||||||
|
isExistingIndexer: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAddIndexersSelector() {
|
||||||
|
return createSelector(
|
||||||
|
createClientSideCollectionSelector('indexers.schema'),
|
||||||
|
createAllIndexersSelector(),
|
||||||
|
(indexers: IndexerAppState, allIndexers) => {
|
||||||
|
const { isFetching, isPopulated, error, items, sortDirection, sortKey } =
|
||||||
|
indexers;
|
||||||
|
|
||||||
|
const indexerList: IndexerSchema[] = items.map((item) => {
|
||||||
|
const { definitionName } = item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
isExistingIndexer: some(allIndexers, { definitionName }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
indexers: indexerList,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddIndexerModalContentProps {
|
||||||
|
onSelectIndexer(): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddIndexerModalContent(props: AddIndexerModalContentProps) {
|
||||||
|
const { onSelectIndexer, onModalClose } = props;
|
||||||
|
|
||||||
|
const { isFetching, isPopulated, error, indexers, sortKey, sortDirection } =
|
||||||
|
useSelector(createAddIndexersSelector());
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [filter, setFilter] = useState('');
|
||||||
|
const [filterProtocols, setFilterProtocols] = useState<string[]>([]);
|
||||||
|
const [filterLanguages, setFilterLanguages] = useState<string[]>([]);
|
||||||
|
const [filterPrivacyLevels, setFilterPrivacyLevels] = useState<string[]>([]);
|
||||||
|
const [filterCategories, setFilterCategories] = useState<number[]>([]);
|
||||||
|
const [isFiltersCollapsed, setIsFiltersCollapsed] = useState(true);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
dispatch(fetchIndexerSchema());
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterChange = useCallback(
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
setFilter(value);
|
||||||
|
},
|
||||||
|
[setFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterProtocolsChange = useCallback(
|
||||||
|
({ value }: { value: string[] }) => {
|
||||||
|
setFilterProtocols(value);
|
||||||
|
},
|
||||||
|
[setFilterProtocols]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterLanguagesChange = useCallback(
|
||||||
|
({ value }: { value: string[] }) => {
|
||||||
|
setFilterLanguages(value);
|
||||||
|
},
|
||||||
|
[setFilterLanguages]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterPrivacyLevelsChange = useCallback(
|
||||||
|
({ value }: { value: string[] }) => {
|
||||||
|
setFilterPrivacyLevels(value);
|
||||||
|
},
|
||||||
|
[setFilterPrivacyLevels]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFilterCategoriesChange = useCallback(
|
||||||
|
({ value }: { value: number[] }) => {
|
||||||
|
setFilterCategories(value);
|
||||||
|
},
|
||||||
|
[setFilterCategories]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleToggleFilters = useCallback(() => {
|
||||||
|
setIsFiltersCollapsed(!isFiltersCollapsed);
|
||||||
|
}, [isFiltersCollapsed]);
|
||||||
|
|
||||||
|
const onIndexerSelect = useCallback(
|
||||||
|
({
|
||||||
|
implementation,
|
||||||
|
implementationName,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
implementation: string;
|
||||||
|
implementationName: string;
|
||||||
|
name: string;
|
||||||
|
}) => {
|
||||||
|
dispatch(
|
||||||
|
selectIndexerSchema({
|
||||||
|
implementation,
|
||||||
|
implementationName,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
onSelectIndexer();
|
||||||
|
},
|
||||||
|
[dispatch, onSelectIndexer]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSortPress = useCallback<SortCallback>(
|
||||||
|
(sortKey, sortDirection) => {
|
||||||
|
dispatch(setIndexerSchemaSort({ sortKey, sortDirection }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const languages = useMemo(
|
||||||
|
() =>
|
||||||
|
Array.from(new Set(indexers.map(({ language }) => language)))
|
||||||
|
.map((language) => ({ key: language, value: language }))
|
||||||
|
.sort(sortByProp('value')),
|
||||||
|
[indexers]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredIndexers = useMemo(() => {
|
||||||
|
const flat = ({
|
||||||
|
id,
|
||||||
|
subCategories = [],
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
subCategories: IndexerCategory[];
|
||||||
|
}): number[] => [id, ...subCategories.flatMap(flat)];
|
||||||
|
|
||||||
|
return indexers.filter((indexer) => {
|
||||||
|
if (
|
||||||
|
filter.length &&
|
||||||
|
!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) &&
|
||||||
|
!indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterProtocols.length &&
|
||||||
|
!filterProtocols.includes(indexer.protocol)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterLanguages.length &&
|
||||||
|
!filterLanguages.includes(indexer.language)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
filterPrivacyLevels.length &&
|
||||||
|
!filterPrivacyLevels.includes(indexer.privacy)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterCategories.length) {
|
||||||
|
const { categories = [] } = indexer.capabilities || {};
|
||||||
|
|
||||||
|
const flatCategories = categories
|
||||||
|
.filter((item) => item.id < 100000)
|
||||||
|
.flatMap(flat);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!filterCategories.every((categoryId) =>
|
||||||
|
flatCategories.includes(categoryId)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
indexers,
|
||||||
|
filter,
|
||||||
|
filterProtocols,
|
||||||
|
filterLanguages,
|
||||||
|
filterPrivacyLevels,
|
||||||
|
filterCategories,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const errorMessage = getErrorMessage(
|
||||||
|
error,
|
||||||
|
translate('UnableToLoadIndexers')
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody
|
||||||
|
className={styles.modalBody}
|
||||||
|
scrollDirection={scrollDirections.NONE}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
className={styles.filterInput}
|
||||||
|
placeholder={translate('FilterPlaceHolder')}
|
||||||
|
name="filter"
|
||||||
|
value={filter}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={onFilterChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button className={styles.filtersToggle} onPress={handleToggleFilters}>
|
||||||
|
<Icon name={isFiltersCollapsed ? icons.EXPAND : icons.COLLAPSE} />
|
||||||
|
{translate('Filters')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.filterRow,
|
||||||
|
isFiltersCollapsed && styles.filterRowCollapsed
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
<label className={styles.filterLabel}>
|
||||||
|
{translate('Protocol')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<EnhancedSelectInput
|
||||||
|
name="indexerProtocols"
|
||||||
|
value={filterProtocols}
|
||||||
|
values={PROTOCOLS}
|
||||||
|
onChange={onFilterProtocolsChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
<label className={styles.filterLabel}>
|
||||||
|
{translate('Language')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<EnhancedSelectInput
|
||||||
|
name="indexerLanguages"
|
||||||
|
value={filterLanguages}
|
||||||
|
values={languages}
|
||||||
|
onChange={onFilterLanguagesChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
<label className={styles.filterLabel}>{translate('Privacy')}</label>
|
||||||
|
<EnhancedSelectInput
|
||||||
|
name="indexerPrivacyLevels"
|
||||||
|
value={filterPrivacyLevels}
|
||||||
|
values={PRIVACY_LEVELS}
|
||||||
|
onChange={onFilterPrivacyLevelsChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
<label className={styles.filterLabel}>
|
||||||
|
{translate('Categories')}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<NewznabCategorySelectInputConnector
|
||||||
|
name="indexerCategories"
|
||||||
|
value={filterCategories}
|
||||||
|
onChange={onFilterCategoriesChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert kind={kinds.INFO} className={styles.notice}>
|
||||||
|
<div>{translate('ProwlarrSupportsAnyIndexer')}</div>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Scroller className={styles.scroller} autoFocus={false}>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Alert kind={kinds.DANGER} className={styles.alert}>
|
||||||
|
{errorMessage}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !!indexers.length ? (
|
||||||
|
<Table
|
||||||
|
columns={COLUMNS}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{filteredIndexers.map((indexer) => (
|
||||||
|
<SelectIndexerRow
|
||||||
|
{...indexer}
|
||||||
|
key={`${indexer.implementation}-${indexer.name}`}
|
||||||
|
implementation={indexer.implementation}
|
||||||
|
implementationName={indexer.implementationName}
|
||||||
|
onIndexerSelect={onIndexerSelect}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !!indexers.length && !filteredIndexers.length ? (
|
||||||
|
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||||
|
{translate('NoIndexersFound')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
</Scroller>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<div className={styles.available}>
|
||||||
|
{isPopulated
|
||||||
|
? translate('CountIndexersAvailable', {
|
||||||
|
count: filteredIndexers.length,
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddIndexerModalContent;
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { some } from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
|
|
||||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createClientSideCollectionSelector('indexers.schema'),
|
|
||||||
createAllIndexersSelector(),
|
|
||||||
(indexers, allIndexers) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
sortDirection,
|
|
||||||
sortKey
|
|
||||||
} = indexers;
|
|
||||||
|
|
||||||
const indexerList = items.map((item) => {
|
|
||||||
const { definitionName } = item;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
isExistingIndexer: some(allIndexers, { definitionName })
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
indexers: indexerList,
|
|
||||||
sortKey,
|
|
||||||
sortDirection
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchIndexerSchema,
|
|
||||||
selectIndexerSchema,
|
|
||||||
setIndexerSchemaSort
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddIndexerModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.fetchIndexerSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onIndexerSelect = ({ implementation, implementationName, name }) => {
|
|
||||||
this.props.selectIndexerSchema({ implementation, implementationName, name });
|
|
||||||
this.props.onSelectIndexer();
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey, sortDirection) => {
|
|
||||||
this.props.setIndexerSchemaSort({ sortKey, sortDirection });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<AddIndexerModalContent
|
|
||||||
{...this.props}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onIndexerSelect={this.onIndexerSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddIndexerModalContentConnector.propTypes = {
|
|
||||||
fetchIndexerSchema: PropTypes.func.isRequired,
|
|
||||||
selectIndexerSchema: PropTypes.func.isRequired,
|
|
||||||
setIndexerSchemaSort: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onSelectIndexer: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);
|
|
||||||
@@ -2,18 +2,19 @@ import React, { useCallback } from 'react';
|
|||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRowButton from 'Components/Table/TableRowButton';
|
import TableRowButton from 'Components/Table/TableRowButton';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
||||||
|
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
|
||||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||||
import { IndexerCapabilities } from 'Indexer/Indexer';
|
import { IndexerCapabilities, IndexerPrivacy } from 'Indexer/Indexer';
|
||||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './SelectIndexerRow.css';
|
import styles from './SelectIndexerRow.css';
|
||||||
|
|
||||||
interface SelectIndexerRowProps {
|
interface SelectIndexerRowProps {
|
||||||
name: string;
|
name: string;
|
||||||
protocol: string;
|
protocol: DownloadProtocol;
|
||||||
privacy: string;
|
privacy: IndexerPrivacy;
|
||||||
language: string;
|
language: string;
|
||||||
description: string;
|
description: string;
|
||||||
capabilities: IndexerCapabilities;
|
capabilities: IndexerCapabilities;
|
||||||
@@ -63,7 +64,9 @@ function SelectIndexerRow(props: SelectIndexerRowProps) {
|
|||||||
|
|
||||||
<TableRowCell>{description}</TableRowCell>
|
<TableRowCell>{description}</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>{translate(firstCharToUpper(privacy))}</TableRowCell>
|
<TableRowCell>
|
||||||
|
<PrivacyLabel privacy={privacy} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
<CapabilitiesLabel capabilities={capabilities} />
|
<CapabilitiesLabel capabilities={capabilities} />
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user