mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
467 Commits
v4.0.5.171
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31c7647eac | ||
|
|
97e85a908d | ||
|
|
6d91c3b62e | ||
|
|
f30207c3d1 | ||
|
|
028d2414e7 | ||
|
|
cbd7df2c91 | ||
|
|
52972e7efc | ||
|
|
8c50919499 | ||
|
|
fdc07a47b1 | ||
|
|
36225c3709 | ||
|
|
bc037ae356 | ||
|
|
77a335de30 | ||
|
|
88d56361c4 | ||
|
|
d10107739b | ||
|
|
7db7567c8e | ||
|
|
2b2b973b30 | ||
|
|
bb954a7424 | ||
|
|
640e3e5d44 | ||
|
|
1260d3c800 | ||
|
|
feeed9a7cf | ||
|
|
c8cb74a976 | ||
|
|
7193acb5ee | ||
|
|
6f1fc1686f | ||
|
|
b7407837b7 | ||
|
|
4e65669c48 | ||
|
|
fa38498db0 | ||
|
|
3b024443c5 | ||
|
|
4ba9b21bb7 | ||
|
|
e37684e045 | ||
|
|
103ccd74f3 | ||
|
|
ba22992265 | ||
|
|
963395b969 | ||
|
|
970df1a1d8 | ||
|
|
2ac139ab4d | ||
|
|
c69db1ff92 | ||
|
|
6dae2f0d84 | ||
|
|
87934c7761 | ||
|
|
fe8478f42a | ||
|
|
a840bb5423 | ||
|
|
8f5d628c55 | ||
|
|
acebe87dba | ||
|
|
7d77500667 | ||
|
|
ec73a13396 | ||
|
|
fa0f77659c | ||
|
|
1609f0c964 | ||
|
|
1fea0b3d10 | ||
|
|
3c8268c428 | ||
|
|
c589c4f85e | ||
|
|
f843107c25 | ||
|
|
035c474f10 | ||
|
|
4dcc015fb1 | ||
|
|
1969e0107f | ||
|
|
ac7c05c050 | ||
|
|
8aad79fd3e | ||
|
|
f05e552e8e | ||
|
|
8cd5cd603a | ||
|
|
ef358e6f24 | ||
|
|
fae24e98fb | ||
|
|
c885fb81f9 | ||
|
|
514c04935f | ||
|
|
4b14368736 | ||
|
|
1c30ecd66d | ||
|
|
f7b54f9d6b | ||
|
|
ce7d8a175e | ||
|
|
ab49268bac | ||
|
|
608f67a074 | ||
|
|
9a69222c9a | ||
|
|
82c526e15c | ||
|
|
983b079c82 | ||
|
|
edfc12e27a | ||
|
|
ed10b63fa0 | ||
|
|
016b571838 | ||
|
|
bfcd017012 | ||
|
|
2e83d59f61 | ||
|
|
c39fb4fe6f | ||
|
|
220b4bc257 | ||
|
|
99e25cec0f | ||
|
|
5d1d44e09e | ||
|
|
3b00112447 | ||
|
|
cb7489ce8f | ||
|
|
b552d4e9f7 | ||
|
|
c0e264cfc5 | ||
|
|
811eb36c7b | ||
|
|
1484809099 | ||
|
|
024462c52d | ||
|
|
e70aef9690 | ||
|
|
36633b5d08 | ||
|
|
1374240321 | ||
|
|
f1d54d2a9a | ||
|
|
03b8c4c28e | ||
|
|
4e4bf3507f | ||
|
|
34ae65c087 | ||
|
|
ebe23104d4 | ||
|
|
e8c3aa20bd | ||
|
|
6c231cbe6a | ||
|
|
8ce688186e | ||
|
|
04ebf03fb5 | ||
|
|
c38debab1b | ||
|
|
32f66922e7 | ||
|
|
ed536a85ad | ||
|
|
c62fc9d05b | ||
|
|
fb9a5efe05 | ||
|
|
8cb58a63d8 | ||
|
|
4c41a4f368 | ||
|
|
e039dc45e2 | ||
|
|
776143cc81 | ||
|
|
8c67a3bdee | ||
|
|
160151c6e0 | ||
|
|
efd48710e4 | ||
|
|
00c16cd06b | ||
|
|
65d07fa99e | ||
|
|
bd656ae7f6 | ||
|
|
62bcf397dd | ||
|
|
f9606518ee | ||
|
|
40f4ef27b2 | ||
|
|
93c3f6d1d6 | ||
|
|
417af2b915 | ||
|
|
4491df3ae7 | ||
|
|
a90866a73e | ||
|
|
2f62494adc | ||
|
|
e361f18837 | ||
|
|
183b8b574a | ||
|
|
12c1eb86f2 | ||
|
|
5034d83062 | ||
|
|
dba3a82439 | ||
|
|
b51a490979 | ||
|
|
8b38ccfb63 | ||
|
|
91c5e6f122 | ||
|
|
dcbef6b7b7 | ||
|
|
ca0bb14027 | ||
|
|
3e99917e9d | ||
|
|
936cf699ff | ||
|
|
202190d032 | ||
|
|
f739fd0900 | ||
|
|
88f4016fe0 | ||
|
|
78fb20282d | ||
|
|
6677fd1116 | ||
|
|
e28b7c3df6 | ||
|
|
67a1ecb0fe | ||
|
|
5bc943583c | ||
|
|
ceeec091f8 | ||
|
|
675e3cd38a | ||
|
|
45a62a2e59 | ||
|
|
ae7c07e02f | ||
|
|
4e9ef57e3d | ||
|
|
59f3be0813 | ||
|
|
fb540040ef | ||
|
|
b8af3af9f1 | ||
|
|
78cf13d341 | ||
|
|
978349e241 | ||
|
|
a77bf64352 | ||
|
|
832de3e75e | ||
|
|
8d4ba77b12 | ||
|
|
409823c7e8 | ||
|
|
8e636d7a37 | ||
|
|
38c0135d7c | ||
|
|
22005dc8c5 | ||
|
|
73208e2f60 | ||
|
|
1df0ba9e5a | ||
|
|
020ed32fcf | ||
|
|
3ddc6ac6de | ||
|
|
0f225b05c0 | ||
|
|
e006b40532 | ||
|
|
e88f25d3bf | ||
|
|
1fcfb88d2a | ||
|
|
804eaa1227 | ||
|
|
c41e3ce1e3 | ||
|
|
682d2b4e1b | ||
|
|
c114e2ddb7 | ||
|
|
f8a879f4c1 | ||
|
|
33139d4b53 | ||
|
|
de69d8ec7e | ||
|
|
03b9c957b8 | ||
|
|
41ddacc395 | ||
|
|
8a558b379a | ||
|
|
240a0339be | ||
|
|
ff724b7f40 | ||
|
|
fcf68d9259 | ||
|
|
404e6d68ea | ||
|
|
df672487cf | ||
|
|
0bc4903954 | ||
|
|
10b55bbee6 | ||
|
|
20ef22be94 | ||
|
|
57534db2f8 | ||
|
|
1e89a1a3cb | ||
|
|
f502eaffe3 | ||
|
|
fe40d83aa4 | ||
|
|
07374de747 | ||
|
|
135b5c2ddd | ||
|
|
0784f56b9a | ||
|
|
562e0dd7c0 | ||
|
|
28599f87af | ||
|
|
86446a7686 | ||
|
|
2f1793d87a | ||
|
|
a641f2897a | ||
|
|
32fa63d24d | ||
|
|
ebfa000375 | ||
|
|
39074b0b1d | ||
|
|
354ed96572 | ||
|
|
c8f419b014 | ||
|
|
a001216957 | ||
|
|
a6735e7a3f | ||
|
|
ea0bfed700 | ||
|
|
620220b269 | ||
|
|
c435fcd685 | ||
|
|
3828e475cc | ||
|
|
e6e1078c15 | ||
|
|
6660db22ec | ||
|
|
bc0fc623ee | ||
|
|
da610a1f40 | ||
|
|
6d0f10b877 | ||
|
|
4f0e1c54c1 | ||
|
|
2f0ca42341 | ||
|
|
768af433d1 | ||
|
|
8bf0298227 | ||
|
|
a7cb264cc8 | ||
|
|
10302323af | ||
|
|
dc1524c64f | ||
|
|
4d7a3d0909 | ||
|
|
30a52d11aa | ||
|
|
be4a9e9491 | ||
|
|
e196c1be69 | ||
|
|
106ffd410c | ||
|
|
c199fd05d3 | ||
|
|
75fae9262c | ||
|
|
faf9173b3b | ||
|
|
0fa8e24f48 | ||
|
|
27da041388 | ||
|
|
ca38a9b577 | ||
|
|
4b72a0a4e8 | ||
|
|
9875e550a8 | ||
|
|
c9aa59340c | ||
|
|
30c36fdc3b | ||
|
|
3976e5daf7 | ||
|
|
fca8c36156 | ||
|
|
85f53e8cb1 | ||
|
|
a73a5cc85c | ||
|
|
89d730cdfd | ||
|
|
99fc52039f | ||
|
|
e6bd58453a | ||
|
|
9603f0b086 | ||
|
|
d84c450094 | ||
|
|
97ebaf2796 | ||
|
|
31bf9e313e | ||
|
|
6cccacd4d7 | ||
|
|
3c857135c5 | ||
|
|
750a9353f8 | ||
|
|
71a19377d9 | ||
|
|
4b5ff3927d | ||
|
|
4d8a443681 | ||
|
|
6a332b40ac | ||
|
|
a929548ae3 | ||
|
|
55363f4e3d | ||
|
|
f20ac9dc34 | ||
|
|
8b20a9449c | ||
|
|
24f03fc1e9 | ||
|
|
5513d7bc5d | ||
|
|
a9072ac460 | ||
|
|
55aaaa5c40 | ||
|
|
ee99c3895d | ||
|
|
e1e10e195c | ||
|
|
0b9a212f33 | ||
|
|
0e384ee3aa | ||
|
|
d903529389 | ||
|
|
6f51e72d00 | ||
|
|
66cead6b48 | ||
|
|
7f0696c574 | ||
|
|
1584311914 | ||
|
|
278c7891a3 | ||
|
|
0a0e03dca0 | ||
|
|
546e9fd1d0 | ||
|
|
c80bd81bb9 | ||
|
|
e1cbc4a782 | ||
|
|
53d8c9ba8d | ||
|
|
9136ee4ad9 | ||
|
|
44fab9a96c | ||
|
|
66e4b7c819 | ||
|
|
98c4cbdd13 | ||
|
|
25d9f09a43 | ||
|
|
7ea1301221 | ||
|
|
f033799d7a | ||
|
|
cfa2f4d4c6 | ||
|
|
882b54be61 | ||
|
|
041fdd3929 | ||
|
|
4548dcdf97 | ||
|
|
4e14ce022c | ||
|
|
a9b93dd9c6 | ||
|
|
50d7e8fed4 | ||
|
|
402db9128c | ||
|
|
846333ddf0 | ||
|
|
dde28cbd7e | ||
|
|
8ceb306bf1 | ||
|
|
8af4246ff9 | ||
|
|
a2e06e9e65 | ||
|
|
ae7b187e41 | ||
|
|
63b4998c8e | ||
|
|
45665886d6 | ||
|
|
860424ac22 | ||
|
|
14005d8d10 | ||
|
|
da7d17f5e8 | ||
|
|
ea331feb88 | ||
|
|
7dca9060ca | ||
|
|
8af12cc4e7 | ||
|
|
aa488019cf | ||
|
|
47a05ecb36 | ||
|
|
35baebaf72 | ||
|
|
aedcd046fc | ||
|
|
f45713bff8 | ||
|
|
911a3d4c1e | ||
|
|
e16ace54a8 | ||
|
|
84710a31bd | ||
|
|
093a239e77 | ||
|
|
ee69351733 | ||
|
|
e92a67ad78 | ||
|
|
3eca63a67c | ||
|
|
8484a8beba | ||
|
|
cd3a1c18ab | ||
|
|
dc7a16a03a | ||
|
|
84338f4c50 | ||
|
|
12ac123d5a | ||
|
|
ef829c6ace | ||
|
|
592b6f7f7c | ||
|
|
be5b449de4 | ||
|
|
9b144e9ade | ||
|
|
9af2f137f4 | ||
|
|
d4bd7865f6 | ||
|
|
cf921480ec | ||
|
|
639b53887d | ||
|
|
3b29096e40 | ||
|
|
2d237ae6b7 | ||
|
|
d713b83a36 | ||
|
|
2f04b037a1 | ||
|
|
7b87de2e93 | ||
|
|
eb2fd13509 | ||
|
|
ffdb08cfe6 | ||
|
|
37c4647f24 | ||
|
|
f7a58aab33 | ||
|
|
4b186e894e | ||
|
|
35a2bc9403 | ||
|
|
cc03ce04f1 | ||
|
|
363f8fc347 | ||
|
|
0877a6718d | ||
|
|
8b253c36ea | ||
|
|
e6f82270a9 | ||
|
|
813965e6a2 | ||
|
|
0d914f4c53 | ||
|
|
ae7f73208a | ||
|
|
4c86d673ea | ||
|
|
b1527f9abb | ||
|
|
291d792810 | ||
|
|
9b528eb829 | ||
|
|
4c0b896174 | ||
|
|
4ff83f9efc | ||
|
|
217611d716 | ||
|
|
1299a97579 | ||
|
|
4c0de55672 | ||
|
|
78a0def46a | ||
|
|
11a9dcb389 | ||
|
|
4eab168267 | ||
|
|
c9b5a1258a | ||
|
|
9127a91dfc | ||
|
|
cc85a28ff7 | ||
|
|
72db8099e0 | ||
|
|
ebc5cdb335 | ||
|
|
d6d90a64a3 | ||
|
|
d46f4b2154 | ||
|
|
76650af9fd | ||
|
|
824ed0a369 | ||
|
|
ee80564dd4 | ||
|
|
3824eff5eb | ||
|
|
15e3c3efb1 | ||
|
|
f2f4a98eed | ||
|
|
bc7799139e | ||
|
|
33b62a2def | ||
|
|
5ac6c0e651 | ||
|
|
60cba74c39 | ||
|
|
5c2c490cb2 | ||
|
|
63fdf8ca8f | ||
|
|
e791f4b743 | ||
|
|
6dd85a5af9 | ||
|
|
a80f5b794b | ||
|
|
578f95546b | ||
|
|
9a613afa35 | ||
|
|
5ad3d2efcc | ||
|
|
1ad722acda | ||
|
|
bde5f68142 | ||
|
|
fbda2d54c7 | ||
|
|
2a26c6722a | ||
|
|
b7dfb8999d | ||
|
|
1662521d40 | ||
|
|
f8d75d174a | ||
|
|
80ca1a6ac2 | ||
|
|
f59c0b16ca | ||
|
|
0e95ba2021 | ||
|
|
c023fc7008 | ||
|
|
19466aa290 | ||
|
|
4b5ef4907b | ||
|
|
7b8d606a1b | ||
|
|
6a4824c029 | ||
|
|
1a1c8e6c08 | ||
|
|
e35b39b4b1 | ||
|
|
d3f14d5f5e | ||
|
|
06936c4f22 | ||
|
|
0a28ff84e8 | ||
|
|
703dee9383 | ||
|
|
dca5239420 | ||
|
|
1aaa9a14bc | ||
|
|
c6c37a408a | ||
|
|
ae4a97b4ae | ||
|
|
3afae968eb | ||
|
|
c01abbf3b5 | ||
|
|
f5ccf98162 | ||
|
|
6afd3bd344 | ||
|
|
acaf5cd353 | ||
|
|
e97e5bfe8f | ||
|
|
678872b879 | ||
|
|
10e9735c1c | ||
|
|
293a1bc618 | ||
|
|
0c883f7886 | ||
|
|
46c7de379c | ||
|
|
a83b521766 | ||
|
|
1d06e40acb | ||
|
|
bfcdc89f6a | ||
|
|
67943edfbc | ||
|
|
04f8595498 | ||
|
|
81ac73299a | ||
|
|
a779a5fad2 | ||
|
|
bfe6a740fa | ||
|
|
c9ea40b874 | ||
|
|
4ee0ae1418 | ||
|
|
ac1da45ecd | ||
|
|
5c327d5be3 | ||
|
|
55c1ce2e3d | ||
|
|
fd7f0ea973 | ||
|
|
d5dff8e8d6 | ||
|
|
8099ba10af | ||
|
|
143ccb1e2a | ||
|
|
29480d9544 | ||
|
|
6de536a7ad | ||
|
|
bce848facf | ||
|
|
ea4fe392a0 | ||
|
|
45fe585944 | ||
|
|
a0d2933134 | ||
|
|
4c622fd412 | ||
|
|
fb060730c7 | ||
|
|
6d5ff9c4d6 | ||
|
|
63bed3e670 | ||
|
|
e684c10432 | ||
|
|
d2509798e9 | ||
|
|
6c39855ebe | ||
|
|
a30e9da767 | ||
|
|
f8e81396d4 | ||
|
|
7fccf590a8 | ||
|
|
e1b937e8d5 | ||
|
|
c331c8bd11 | ||
|
|
52b72925f9 | ||
|
|
378fedcd9d | ||
|
|
a90ab1a8fd | ||
|
|
0edc5ba99a | ||
|
|
ea54ade9bf | ||
|
|
e07eb05e8b | ||
|
|
d9b771ab0b | ||
|
|
6b08e849b8 | ||
|
|
9c1f48ebc9 | ||
|
|
fd3dd1ab7d | ||
|
|
11e5c5a11b | ||
|
|
48f0291884 |
242
.github/workflows/build.yml
vendored
242
.github/workflows/build.yml
vendored
@@ -6,13 +6,13 @@ on:
|
|||||||
- develop
|
- develop
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'src/Sonarr.Api.*/openapi.json'
|
- "src/Sonarr.Api.*/openapi.json"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'src/NzbDrone.Core/Localization/Core/**'
|
- "src/NzbDrone.Core/Localization/Core/**"
|
||||||
- 'src/Sonarr.Api.*/openapi.json'
|
- "src/Sonarr.Api.*/openapi.json"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
@@ -22,7 +22,7 @@ env:
|
|||||||
FRAMEWORK: net6.0
|
FRAMEWORK: net6.0
|
||||||
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||||
SONARR_MAJOR_VERSION: 4
|
SONARR_MAJOR_VERSION: 4
|
||||||
VERSION: 4.0.5
|
VERSION: 4.0.17
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backend:
|
backend:
|
||||||
@@ -32,105 +32,105 @@ jobs:
|
|||||||
major_version: ${{ steps.variables.outputs.major_version }}
|
major_version: ${{ steps.variables.outputs.major_version }}
|
||||||
version: ${{ steps.variables.outputs.version }}
|
version: ${{ steps.variables.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
|
|
||||||
- name: Setup Environment Variables
|
- name: Setup Environment Variables
|
||||||
id: variables
|
id: variables
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
# Add 800 to the build number because GitHub won't let us pick an arbitrary starting point
|
# Add 800 to the build number because GitHub won't let us pick an arbitrary starting point
|
||||||
SONARR_VERSION="${{ env.VERSION }}.$((${{ github.run_number }}+800))"
|
SONARR_VERSION="${{ env.VERSION }}.$((${{ github.run_number }}+800))"
|
||||||
DOTNET_VERSION=$(jq -r '.sdk.version' global.json)
|
DOTNET_VERSION=$(jq -r '.sdk.version' global.json)
|
||||||
|
|
||||||
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
|
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
|
||||||
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
|
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
|
||||||
echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV"
|
echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
|
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
|
||||||
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
|
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
|
||||||
echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT"
|
echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Enable Extra Platforms In SDK
|
- name: Enable Extra Platforms In SDK
|
||||||
shell: bash
|
shell: bash
|
||||||
run: ./build.sh --enable-extra-platforms-in-sdk
|
run: ./build.sh --enable-extra-platforms-in-sdk
|
||||||
|
|
||||||
- name: Build Backend
|
- name: Build Backend
|
||||||
shell: bash
|
shell: bash
|
||||||
run: ./build.sh --backend --enable-extra-platforms --packages
|
run: ./build.sh --backend --enable-extra-platforms --packages
|
||||||
|
|
||||||
# Test Artifacts
|
# Test Artifacts
|
||||||
|
|
||||||
- name: Publish win-x64 Test Artifact
|
- name: Publish win-x64 Test Artifact
|
||||||
uses: ./.github/actions/publish-test-artifact
|
uses: ./.github/actions/publish-test-artifact
|
||||||
with:
|
with:
|
||||||
framework: ${{ env.FRAMEWORK }}
|
framework: ${{ env.FRAMEWORK }}
|
||||||
runtime: win-x64
|
runtime: win-x64
|
||||||
|
|
||||||
- name: Publish linux-x64 Test Artifact
|
- name: Publish linux-x64 Test Artifact
|
||||||
uses: ./.github/actions/publish-test-artifact
|
uses: ./.github/actions/publish-test-artifact
|
||||||
with:
|
with:
|
||||||
framework: ${{ env.FRAMEWORK }}
|
framework: ${{ env.FRAMEWORK }}
|
||||||
runtime: linux-x64
|
runtime: linux-x64
|
||||||
|
|
||||||
- name: Publish osx-arm64 Test Artifact
|
- name: Publish osx-arm64 Test Artifact
|
||||||
uses: ./.github/actions/publish-test-artifact
|
uses: ./.github/actions/publish-test-artifact
|
||||||
with:
|
with:
|
||||||
framework: ${{ env.FRAMEWORK }}
|
framework: ${{ env.FRAMEWORK }}
|
||||||
runtime: osx-arm64
|
runtime: osx-arm64
|
||||||
|
|
||||||
# Build Artifacts (grouped by OS)
|
# Build Artifacts (grouped by OS)
|
||||||
|
|
||||||
- name: Publish FreeBSD Artifact
|
- name: Publish FreeBSD Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build_freebsd
|
name: build_freebsd
|
||||||
path: _artifacts/freebsd-*/**/*
|
path: _artifacts/freebsd-*/**/*
|
||||||
- name: Publish Linux Artifact
|
- name: Publish Linux Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build_linux
|
name: build_linux
|
||||||
path: _artifacts/linux-*/**/*
|
path: _artifacts/linux-*/**/*
|
||||||
- name: Publish macOS Artifact
|
- name: Publish macOS Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build_macos
|
name: build_macos
|
||||||
path: _artifacts/osx-*/**/*
|
path: _artifacts/osx-*/**/*
|
||||||
- name: Publish Windows Artifact
|
- name: Publish Windows Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build_windows
|
name: build_windows
|
||||||
path: _artifacts/win-*/**/*
|
path: _artifacts/win-*/**/*
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Volta
|
- name: Volta
|
||||||
uses: volta-cli/action@v4
|
uses: volta-cli/action@v4
|
||||||
|
|
||||||
- name: Yarn Install
|
- name: Yarn Install
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: yarn lint
|
run: yarn lint
|
||||||
|
|
||||||
- name: Stylelint
|
- name: Stylelint
|
||||||
run: yarn stylelint -f github
|
run: yarn stylelint -f github
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: yarn build --env production
|
run: yarn build --env production
|
||||||
|
|
||||||
- name: Publish UI Artifact
|
- name: Publish UI Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build_ui
|
name: build_ui
|
||||||
path: _output/UI/**/*
|
path: _output/UI/**/*
|
||||||
|
|
||||||
unit_test:
|
unit_test:
|
||||||
needs: backend
|
needs: backend
|
||||||
@@ -150,32 +150,32 @@ jobs:
|
|||||||
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
uses: ./.github/actions/test
|
uses: ./.github/actions/test
|
||||||
with:
|
with:
|
||||||
os: ${{ matrix.os }}
|
os: ${{ matrix.os }}
|
||||||
artifact: ${{ matrix.artifact }}
|
artifact: ${{ matrix.artifact }}
|
||||||
pattern: Sonarr.*.Test.dll
|
pattern: Sonarr.*.Test.dll
|
||||||
filter: ${{ matrix.filter }}
|
filter: ${{ matrix.filter }}
|
||||||
|
|
||||||
unit_test_postgres:
|
unit_test_postgres:
|
||||||
needs: backend
|
needs: backend
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
uses: ./.github/actions/test
|
uses: ./.github/actions/test
|
||||||
with:
|
with:
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
artifact: tests-linux-x64
|
artifact: tests-linux-x64
|
||||||
pattern: Sonarr.*.Test.dll
|
pattern: Sonarr.*.Test.dll
|
||||||
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
use_postgres: true
|
use_postgres: true
|
||||||
|
|
||||||
integration_test:
|
integration_test:
|
||||||
needs: backend
|
needs: backend
|
||||||
@@ -201,19 +201,19 @@ jobs:
|
|||||||
binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out
|
- name: Check out
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
uses: ./.github/actions/test
|
uses: ./.github/actions/test
|
||||||
with:
|
with:
|
||||||
os: ${{ matrix.os }}
|
os: ${{ matrix.os }}
|
||||||
artifact: ${{ matrix.artifact }}
|
artifact: ${{ matrix.artifact }}
|
||||||
pattern: Sonarr.*.Test.dll
|
pattern: Sonarr.*.Test.dll
|
||||||
filter: ${{ matrix.filter }}
|
filter: ${{ matrix.filter }}
|
||||||
integration_tests: true
|
integration_tests: true
|
||||||
binary_artifact: ${{ matrix.binary_artifact }}
|
binary_artifact: ${{ matrix.binary_artifact }}
|
||||||
binary_path: ${{ matrix.binary_path }}
|
binary_path: ${{ matrix.binary_path }}
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }}
|
if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }}
|
||||||
@@ -228,7 +228,15 @@ jobs:
|
|||||||
|
|
||||||
notify:
|
notify:
|
||||||
name: Discord Notification
|
name: Discord Notification
|
||||||
needs: [backend, frontend, unit_test, unit_test_postgres, integration_test, deploy]
|
needs:
|
||||||
|
[
|
||||||
|
backend,
|
||||||
|
frontend,
|
||||||
|
unit_test,
|
||||||
|
unit_test_postgres,
|
||||||
|
integration_test,
|
||||||
|
deploy,
|
||||||
|
]
|
||||||
if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }}
|
if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }}
|
||||||
env:
|
env:
|
||||||
STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }}
|
STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }}
|
||||||
@@ -239,10 +247,10 @@ jobs:
|
|||||||
uses: tsickert/discord-webhook@v6.0.0
|
uses: tsickert/discord-webhook@v6.0.0
|
||||||
with:
|
with:
|
||||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
username: 'GitHub Actions'
|
username: "GitHub Actions"
|
||||||
avatar-url: 'https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png'
|
avatar-url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
|
||||||
embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}"
|
embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}"
|
||||||
embed-url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'
|
embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
embed-description: |
|
embed-description: |
|
||||||
**Branch** ${{ github.ref }}
|
**Branch** ${{ github.ref }}
|
||||||
**Build** ${{ needs.backend.outputs.version }}
|
**Build** ${{ needs.backend.outputs.version }}
|
||||||
|
|||||||
29
.github/workflows/support-requests.yml
vendored
Normal file
29
.github/workflows/support-requests.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: 'Support Requests'
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled, unlabeled, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'Sonarr/Sonarr'
|
||||||
|
steps:
|
||||||
|
- uses: dessant/support-requests@v4
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
support-label: 'support'
|
||||||
|
issue-comment: >
|
||||||
|
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||||
|
for bug reports and feature requests. However, this issue appears
|
||||||
|
to be a support request. Please use one of the support channels:
|
||||||
|
[forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/),
|
||||||
|
[discord](https://discord.gg/Ex7FmFK), or [IRC](https://web.libera.chat/?channels=#sonarr)
|
||||||
|
for support/questions.
|
||||||
|
close-issue: true
|
||||||
|
issue-close-reason: 'not planned'
|
||||||
|
lock-issue: false
|
||||||
|
issue-lock-reason: 'off-topic'
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -162,3 +162,6 @@ src/.idea/
|
|||||||
|
|
||||||
# API doc generation
|
# API doc generation
|
||||||
.config/
|
.config/
|
||||||
|
|
||||||
|
# Ignore Jetbrains IntelliJ Workspace Directories
|
||||||
|
.idea/
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||||
|
}
|
||||||
@@ -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,64 +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" 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.7738" y1="31.2729" x2="40.1662" y2="31.2729">
|
|
||||||
<stop offset="0" style="stop-color:#905CFB"/>
|
|
||||||
<stop offset="6.772543e-002" style="stop-color:#776CF9"/>
|
|
||||||
<stop offset="0.1729" style="stop-color:#5681F7"/>
|
|
||||||
<stop offset="0.2865" style="stop-color:#3B92F5"/>
|
|
||||||
<stop offset="0.4097" style="stop-color:#269FF4"/>
|
|
||||||
<stop offset="0.5474" style="stop-color:#17A9F3"/>
|
|
||||||
<stop offset="0.7111" style="stop-color:#0FAEF2"/>
|
|
||||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_1_);" d="M39.7,47.9l-6.1-34c-0.4-2.4-1.2-4.8-2.7-7.1c-2-3.2-5.2-5.4-8.8-6.3
|
|
||||||
C7.9-2.9-2.6,11.3,3.6,23.9c0,0,0,0,0,0l14.8,31.7c0.4,1,1,2,1.7,2.9c1.2,1.6,2.8,2.8,4.7,3.4C34.4,64.9,42.1,56.4,39.7,47.9z"/>
|
|
||||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="5.3113" y1="9.6691" x2="69.2278" y2="43.8664">
|
|
||||||
<stop offset="0" style="stop-color:#905CFB"/>
|
|
||||||
<stop offset="6.772543e-002" style="stop-color:#776CF9"/>
|
|
||||||
<stop offset="0.1729" style="stop-color:#5681F7"/>
|
|
||||||
<stop offset="0.2865" style="stop-color:#3B92F5"/>
|
|
||||||
<stop offset="0.4097" style="stop-color:#269FF4"/>
|
|
||||||
<stop offset="0.5474" style="stop-color:#17A9F3"/>
|
|
||||||
<stop offset="0.7111" style="stop-color:#0FAEF2"/>
|
|
||||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_2_);" d="M67.4,26.5c-1.4-2.2-3.4-3.9-5.7-4.9L25.5,1.7l0,0c-1-0.5-2.1-1-3.3-1.3
|
|
||||||
C6.7-3.2-4.4,13.8,5.5,27c1.5,2,3.6,3.6,6,4.5L48,47.9c0.8,0.5,1.6,0.8,2.5,1.1C64.5,53.4,75.1,38.6,67.4,26.5z"/>
|
|
||||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-19.2836" y1="70.8198" x2="55.9833" y2="33.1863">
|
|
||||||
<stop offset="0" style="stop-color:#3BEA62"/>
|
|
||||||
<stop offset="0.117" style="stop-color:#31DE80"/>
|
|
||||||
<stop offset="0.3025" style="stop-color:#24CEA8"/>
|
|
||||||
<stop offset="0.4844" style="stop-color:#1AC1C9"/>
|
|
||||||
<stop offset="0.6592" style="stop-color:#12B7DF"/>
|
|
||||||
<stop offset="0.8238" style="stop-color:#0EB2ED"/>
|
|
||||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_3_);" d="M67.4,26.5c-1.8-2.8-4.6-4.8-7.9-5.6c-3.5-0.8-6.8-0.5-9.6,0.7L11.4,36.1
|
|
||||||
c0,0-0.2,0.1-0.6,0.4C0.9,40.4-4,53.3,4,64c1.8,2.4,4.3,4.2,7.1,5c5.3,1.6,10.1,1,14-1.1c0,0,0.1,0,0.1,0l37.6-20.1
|
|
||||||
c0,0,0,0,0.1-0.1C69.5,43.9,72.6,34.6,67.4,26.5z"/>
|
|
||||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="38.9439" y1="5.8503" x2="5.4232" y2="77.5093">
|
|
||||||
<stop offset="0" style="stop-color:#3BEA62"/>
|
|
||||||
<stop offset="9.397750e-002" style="stop-color:#2FDB87"/>
|
|
||||||
<stop offset="0.196" style="stop-color:#24CEA8"/>
|
|
||||||
<stop offset="0.3063" style="stop-color:#1BC3C3"/>
|
|
||||||
<stop offset="0.4259" style="stop-color:#14BAD8"/>
|
|
||||||
<stop offset="0.5596" style="stop-color:#10B5E7"/>
|
|
||||||
<stop offset="0.7185" style="stop-color:#0DB1EF"/>
|
|
||||||
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
|
||||||
</linearGradient>
|
|
||||||
<path style="fill:url(#SVGID_4_);" d="M50.3,12.8c1.2-2.7,1.1-6-0.9-9c-1.1-1.8-2.9-3-4.9-3.5c-4.5-1.1-8.3,1-10.1,4.2L3.5,42
|
|
||||||
c0,0,0,0,0,0.1C-0.9,47.9-1.6,56.5,4,64c1.8,2.4,4.3,4.2,7.1,5c10.5,3.3,19.3-2.5,22.1-10.8L50.3,12.8z"/>
|
|
||||||
</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"/>
|
|
||||||
<polygon style="fill:#FFFFFF;" points="22.9,22.7 17.5,22.7 17.5,19.1 32.3,19.1 32.3,22.7 26.8,22.7 26.8,37 22.9,37 "/>
|
|
||||||
<path style="fill:#FFFFFF;" d="M32.5,28.1L32.5,28.1c0-5.1,3.8-9.3,9.3-9.3c3.4,0,5.4,1.1,7.1,2.8l-2.5,2.9c-1.4-1.3-2.8-2-4.6-2
|
|
||||||
c-3,0-5.2,2.5-5.2,5.6V28c0,3.1,2.1,5.6,5.2,5.6c2,0,3.3-0.8,4.7-2.1l2.5,2.5c-1.8,2-3.9,3.2-7.3,3.2
|
|
||||||
C36.4,37.3,32.5,33.2,32.5,28.1"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB |
24
README.md
24
README.md
@@ -1,6 +1,6 @@
|
|||||||
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
||||||
|
|
||||||
[](https://translate.servarr.com/engage/servarr/)
|
[](https://translate.servarr.com/engage/servarr/)
|
||||||
[](#backers)
|
[](#backers)
|
||||||
[](#sponsors)
|
[](#sponsors)
|
||||||
[](#mega-sponsors)
|
[](#mega-sponsors)
|
||||||
@@ -33,7 +33,7 @@ Note: GitHub Issues are for Bugs and Feature Requests Only
|
|||||||
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||||
- Automatically detects new episodes
|
- Automatically detects new episodes
|
||||||
- Can scan your existing library and download any missing episodes
|
- Can scan your existing library and download any missing episodes
|
||||||
- Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
- Can watch for better quality of the episodes you already have and do an automatic upgrade. _eg. from DVD to Blu-Ray_
|
||||||
- Automatic failed download handling will try another release if one fails
|
- Automatic failed download handling will try another release if one fails
|
||||||
- Manual search so you can pick any release or to see why a release was not downloaded automatically
|
- Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||||
- Fully configurable episode renaming
|
- Fully configurable episode renaming
|
||||||
@@ -52,7 +52,7 @@ This project exists thanks to all the people who contribute. [Contribute](CONTRI
|
|||||||
|
|
||||||
### Supporters
|
### Supporters
|
||||||
|
|
||||||
This project would not be possible without the support of our users and software providers.
|
This project would not be possible without the support of our users and software providers.
|
||||||
[**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
|
[**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
|
||||||
|
|
||||||
#### Mega Sponsors
|
#### Mega Sponsors
|
||||||
@@ -69,13 +69,17 @@ This project would not be possible without the support of our users and software
|
|||||||
|
|
||||||
#### JetBrains
|
#### JetBrains
|
||||||
|
|
||||||
Thank you to [<img src="/Logo/Jetbrains/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/Jetbrains/teamcity.svg" alt="TeamCity" width="32"> TeamCity](http://www.jetbrains.com/teamcity/)
|
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/TeamCity.png" alt="TeamCity" width="64">](http://www.jetbrains.com/teamcity/)
|
||||||
* [<img src="/Logo/Jetbrains/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
|
||||||
* [<img src="/Logo/Jetbrains/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/ReSharper.png" alt="ReSharper" width="64">](http://www.jetbrains.com/resharper/)
|
||||||
|
|
||||||
|
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/dotTrace.png" alt="dotTrace" width="64">](http://www.jetbrains.com/dottrace/)
|
||||||
|
|
||||||
|
[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/Rider.png" alt="Rider" width="64">](http://www.jetbrains.com/rider/)
|
||||||
|
|
||||||
### Licenses
|
### Licenses
|
||||||
|
|
||||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
- Copyright 2010-2023
|
- Copyright 2010-2024
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ app_guid=$(echo "$app_guid" | tr -d ' ')
|
|||||||
app_guid=${app_guid:-media}
|
app_guid=${app_guid:-media}
|
||||||
|
|
||||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||||
|
|
||||||
# Create User / Group as needed
|
# Create User / Group as needed
|
||||||
@@ -114,7 +114,7 @@ case "$ARCH" in
|
|||||||
esac
|
esac
|
||||||
echo ""
|
echo ""
|
||||||
echo "Removing previous tarballs"
|
echo "Removing previous tarballs"
|
||||||
# -f to Force so we fail if it doesnt exist
|
# -f to Force so we fail if it doesn't exist
|
||||||
rm -f "${app^}".*.tar.gz
|
rm -f "${app^}".*.tar.gz
|
||||||
echo ""
|
echo ""
|
||||||
echo "Downloading..."
|
echo "Downloading..."
|
||||||
|
|||||||
12
docs.sh
12
docs.sh
@@ -25,17 +25,23 @@ slnFile=src/Sonarr.sln
|
|||||||
|
|
||||||
platform=Posix
|
platform=Posix
|
||||||
|
|
||||||
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
|
application=Sonarr.Console.dll
|
||||||
|
else
|
||||||
|
application=Sonarr.dll
|
||||||
|
fi
|
||||||
|
|
||||||
dotnet clean $slnFile -c Debug
|
dotnet clean $slnFile -c Debug
|
||||||
dotnet clean $slnFile -c Release
|
dotnet clean $slnFile -c Release
|
||||||
|
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||||
|
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/Sonarr.dll" v3 &
|
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
|
||||||
|
|
||||||
sleep 30
|
sleep 45
|
||||||
|
|
||||||
kill %1
|
kill %1
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ module.exports = {
|
|||||||
'no-undef-init': 'off',
|
'no-undef-init': 'off',
|
||||||
'no-undefined': 'off',
|
'no-undefined': 'off',
|
||||||
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
||||||
'no-use-before-define': 'error',
|
|
||||||
|
|
||||||
// Node.js and CommonJS
|
// Node.js and CommonJS
|
||||||
|
|
||||||
@@ -359,11 +358,20 @@ 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: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true
|
||||||
|
|
||||||
|
}
|
||||||
|
],
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'react/prop-types': 'off',
|
'no-shadow': 'off',
|
||||||
'prettier/prettier': 'error',
|
'prettier/prettier': 'error',
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
'error',
|
'error',
|
||||||
@@ -376,7 +384,41 @@ module.exports = {
|
|||||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
|
||||||
|
// React Hooks
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
|
|
||||||
|
// React
|
||||||
|
'react/function-component-definition': 'error',
|
||||||
|
'react/hook-use-state': 'error',
|
||||||
|
'react/jsx-boolean-value': ['error', 'always'],
|
||||||
|
'react/jsx-curly-brace-presence': [
|
||||||
|
'error',
|
||||||
|
{ props: 'never', children: 'never' }
|
||||||
|
],
|
||||||
|
'react/jsx-fragments': 'error',
|
||||||
|
'react/jsx-handler-names': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
eventHandlerPrefix: 'on',
|
||||||
|
eventHandlerPropPrefix: 'on'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||||
|
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||||
|
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||||
|
'react/jsx-sort-props': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
callbacksLast: true,
|
||||||
|
noSortAlphabetically: true,
|
||||||
|
reservedFirst: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/self-closing-comp': 'error'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|||||||
@@ -26,6 +26,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
|
||||||
@@ -51,8 +52,7 @@ module.exports = (env) => {
|
|||||||
'node_modules'
|
'node_modules'
|
||||||
],
|
],
|
||||||
alias: {
|
alias: {
|
||||||
jquery: 'jquery/dist/jquery.min',
|
jquery: 'jquery/dist/jquery.min'
|
||||||
'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate'
|
|
||||||
},
|
},
|
||||||
fallback: {
|
fallback: {
|
||||||
buffer: false,
|
buffer: false,
|
||||||
@@ -67,7 +67,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'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -92,7 +92,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({
|
||||||
@@ -134,6 +134,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')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -181,7 +187,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.39'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -202,7 +208,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,284 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
||||||
import TablePager from 'Components/Table/TablePager';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
|
||||||
import BlocklistFilterModal from './BlocklistFilterModal';
|
|
||||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
|
||||||
|
|
||||||
class Blocklist extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: false,
|
|
||||||
lastToggled: null,
|
|
||||||
selectedState: {},
|
|
||||||
isConfirmRemoveModalOpen: false,
|
|
||||||
isConfirmClearModalOpen: false,
|
|
||||||
items: props.items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (hasDifferentItems(prevProps.items, items)) {
|
|
||||||
this.setState((state) => {
|
|
||||||
return {
|
|
||||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
|
||||||
items
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
getSelectedIds = () => {
|
|
||||||
return getSelectedIds(this.state.selectedState);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSelectAllChange = ({ value }) => {
|
|
||||||
this.setState(selectAll(this.state.selectedState, value));
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
|
||||||
this.setState((state) => {
|
|
||||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedPress = () => {
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedConfirmed = () => {
|
|
||||||
this.props.onRemoveSelected(this.getSelectedIds());
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmRemoveModalClose = () => {
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearBlocklistPress = () => {
|
|
||||||
this.setState({ isConfirmClearModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearBlocklistConfirmed = () => {
|
|
||||||
this.props.onClearBlocklistPress();
|
|
||||||
this.setState({ isConfirmClearModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmClearModalClose = () => {
|
|
||||||
this.setState({ isConfirmClearModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
totalRecords,
|
|
||||||
isRemoving,
|
|
||||||
isClearingBlocklistExecuting,
|
|
||||||
onFilterSelect,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
selectedState,
|
|
||||||
isConfirmRemoveModalOpen,
|
|
||||||
isConfirmClearModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const selectedIds = this.getSelectedIds();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Blocklist')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RemoveSelected')}
|
|
||||||
iconName={icons.REMOVE}
|
|
||||||
isDisabled={!selectedIds.length}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={this.onRemoveSelectedPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Clear')}
|
|
||||||
iconName={icons.CLEAR}
|
|
||||||
isDisabled={!items.length}
|
|
||||||
isSpinning={isClearingBlocklistExecuting}
|
|
||||||
onPress={this.onClearBlocklistPress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
{...otherProps}
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={BlocklistFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('BlocklistLoadError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error && !items.length &&
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{
|
|
||||||
selectedFilterKey === 'all' ?
|
|
||||||
translate('NoHistoryBlocklist') :
|
|
||||||
translate('BlocklistFilterHasNoItems')
|
|
||||||
}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error && !!items.length &&
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
selectAll={true}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
columns={columns}
|
|
||||||
{...otherProps}
|
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<BlocklistRowConnector
|
|
||||||
key={item.id}
|
|
||||||
isSelected={selectedState[item.id] || false}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
onSelectedChange={this.onSelectedChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isFetching}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('RemoveSelected')}
|
|
||||||
message={translate('RemoveSelectedBlocklistMessageText')}
|
|
||||||
confirmLabel={translate('RemoveSelected')}
|
|
||||||
onConfirm={this.onRemoveSelectedConfirmed}
|
|
||||||
onCancel={this.onConfirmRemoveModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isConfirmClearModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('ClearBlocklist')}
|
|
||||||
message={translate('ClearBlocklistMessageText')}
|
|
||||||
confirmLabel={translate('Clear')}
|
|
||||||
onConfirm={this.onClearBlocklistConfirmed}
|
|
||||||
onCancel={this.onConfirmClearModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Blocklist.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
|
||||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
|
||||||
onRemoveSelected: PropTypes.func.isRequired,
|
|
||||||
onClearBlocklistPress: PropTypes.func.isRequired,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Blocklist;
|
|
||||||
329
frontend/src/Activity/Blocklist/Blocklist.tsx
Normal file
329
frontend/src/Activity/Blocklist/Blocklist.tsx
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { SelectProvider } from 'App/SelectContext';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import usePaging from 'Components/Table/usePaging';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
clearBlocklist,
|
||||||
|
fetchBlocklist,
|
||||||
|
gotoBlocklistPage,
|
||||||
|
removeBlocklistItems,
|
||||||
|
setBlocklistFilter,
|
||||||
|
setBlocklistSort,
|
||||||
|
setBlocklistTableOption,
|
||||||
|
} from 'Store/Actions/blocklistActions';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||||
|
import BlocklistRow from './BlocklistRow';
|
||||||
|
|
||||||
|
function Blocklist() {
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalRecords,
|
||||||
|
isRemoving,
|
||||||
|
} = useSelector((state: AppState) => state.blocklist);
|
||||||
|
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
|
||||||
|
const isClearingBlocklistExecuting = useSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
|
||||||
|
);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [isConfirmClearModalOpen, setIsConfirmClearModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const wasClearingBlocklistExecuting = usePrevious(
|
||||||
|
isClearingBlocklistExecuting
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectAllChange = useCallback(
|
||||||
|
({ value }: CheckInputChanged) => {
|
||||||
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectedChange = useCallback(
|
||||||
|
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||||
|
setSelectState({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
items,
|
||||||
|
id,
|
||||||
|
isSelected: value,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveSelectedPress = useCallback(() => {
|
||||||
|
setIsConfirmRemoveModalOpen(true);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const handleRemoveSelectedConfirmed = useCallback(() => {
|
||||||
|
dispatch(removeBlocklistItems({ ids: selectedIds }));
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
}, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]);
|
||||||
|
|
||||||
|
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const handleClearBlocklistPress = useCallback(() => {
|
||||||
|
setIsConfirmClearModalOpen(true);
|
||||||
|
}, [setIsConfirmClearModalOpen]);
|
||||||
|
|
||||||
|
const handleClearBlocklistConfirmed = useCallback(() => {
|
||||||
|
dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST }));
|
||||||
|
setIsConfirmClearModalOpen(false);
|
||||||
|
}, [setIsConfirmClearModalOpen, dispatch]);
|
||||||
|
|
||||||
|
const handleConfirmClearModalClose = useCallback(() => {
|
||||||
|
setIsConfirmClearModalOpen(false);
|
||||||
|
}, [setIsConfirmClearModalOpen]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
} = usePaging({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
gotoPage: gotoBlocklistPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(selectedFilterKey: string) => {
|
||||||
|
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortPress = useCallback(
|
||||||
|
(sortKey: string) => {
|
||||||
|
dispatch(setBlocklistSort({ sortKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableOptionChange = useCallback(
|
||||||
|
(payload: TableOptionsChangePayload) => {
|
||||||
|
dispatch(setBlocklistTableOption(payload));
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchBlocklist());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearBlocklist());
|
||||||
|
};
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchBlocklist());
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) {
|
||||||
|
dispatch(gotoBlocklistPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
}, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectProvider items={items}>
|
||||||
|
<PageContent title={translate('Blocklist')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RemoveSelected')}
|
||||||
|
iconName={icons.REMOVE}
|
||||||
|
isDisabled={!selectedIds.length}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={handleRemoveSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Clear')}
|
||||||
|
iconName={icons.CLEAR}
|
||||||
|
isDisabled={!items.length}
|
||||||
|
isSpinning={isClearingBlocklistExecuting}
|
||||||
|
onPress={handleClearBlocklistPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={BlocklistFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && !!error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !items.length ? (
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{selectedFilterKey === 'all'
|
||||||
|
? translate('NoBlocklistItems')
|
||||||
|
: translate('BlocklistFilterHasNoItems')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !!items.length ? (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
onSelectAllChange={handleSelectAllChange}
|
||||||
|
onSortPress={handleSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<BlocklistRow
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id] || false}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={handleSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<TablePager
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('RemoveSelected')}
|
||||||
|
message={translate('RemoveSelectedBlocklistMessageText')}
|
||||||
|
confirmLabel={translate('RemoveSelected')}
|
||||||
|
onConfirm={handleRemoveSelectedConfirmed}
|
||||||
|
onCancel={handleConfirmRemoveModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmClearModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('ClearBlocklist')}
|
||||||
|
message={translate('ClearBlocklistMessageText')}
|
||||||
|
confirmLabel={translate('Clear')}
|
||||||
|
onConfirm={handleClearBlocklistConfirmed}
|
||||||
|
onCancel={handleConfirmClearModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
</SelectProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blocklist;
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import Blocklist from './Blocklist';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.blocklist,
|
|
||||||
createCustomFiltersSelector('blocklist'),
|
|
||||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
|
||||||
(blocklist, customFilters, isClearingBlocklistExecuting) => {
|
|
||||||
return {
|
|
||||||
isClearingBlocklistExecuting,
|
|
||||||
customFilters,
|
|
||||||
...blocklist
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...blocklistActions,
|
|
||||||
executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class BlocklistConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchBlocklist,
|
|
||||||
gotoBlocklistFirstPage
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchBlocklist();
|
|
||||||
} else {
|
|
||||||
gotoBlocklistFirstPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
|
|
||||||
this.props.gotoBlocklistFirstPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.clearBlocklist();
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
this.props.fetchBlocklist();
|
|
||||||
};
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFirstPagePress = () => {
|
|
||||||
this.props.gotoBlocklistFirstPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPagePress = () => {
|
|
||||||
this.props.gotoBlocklistPreviousPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPagePress = () => {
|
|
||||||
this.props.gotoBlocklistNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onLastPagePress = () => {
|
|
||||||
this.props.gotoBlocklistLastPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelect = (page) => {
|
|
||||||
this.props.gotoBlocklistPage({ page });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelected = (ids) => {
|
|
||||||
this.props.removeBlocklistItems({ ids });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
|
||||||
this.props.setBlocklistSort({ sortKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFilterSelect = (selectedFilterKey) => {
|
|
||||||
this.props.setBlocklistFilter({ selectedFilterKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearBlocklistPress = () => {
|
|
||||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
|
||||||
this.props.setBlocklistTableOption(payload);
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
this.props.gotoBlocklistFirstPage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Blocklist
|
|
||||||
onFirstPagePress={this.onFirstPagePress}
|
|
||||||
onPreviousPagePress={this.onPreviousPagePress}
|
|
||||||
onNextPagePress={this.onNextPagePress}
|
|
||||||
onLastPagePress={this.onLastPagePress}
|
|
||||||
onPageSelect={this.onPageSelect}
|
|
||||||
onRemoveSelected={this.onRemoveSelected}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onFilterSelect={this.onFilterSelect}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BlocklistConnector.propTypes = {
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
fetchBlocklist: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistFirstPage: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistNextPage: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistLastPage: PropTypes.func.isRequired,
|
|
||||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
|
||||||
removeBlocklistItems: PropTypes.func.isRequired,
|
|
||||||
setBlocklistSort: PropTypes.func.isRequired,
|
|
||||||
setBlocklistFilter: PropTypes.func.isRequired,
|
|
||||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
|
||||||
clearBlocklist: PropTypes.func.isRequired,
|
|
||||||
executeCommand: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
|
|
||||||
);
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class BlocklistDetailsModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
sourceTitle,
|
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
message,
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
Details
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Protocol')}
|
|
||||||
data={protocol}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
!!message &&
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!message &&
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BlocklistDetailsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
indexer: PropTypes.string,
|
|
||||||
message: PropTypes.string,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlocklistDetailsModal;
|
|
||||||
64
frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
Normal file
64
frontend/src/Activity/Blocklist/BlocklistDetailsModal.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
interface BlocklistDetailsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
sourceTitle: string;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
indexer?: string;
|
||||||
|
message?: string;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
|
||||||
|
const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>Details</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Protocol')}
|
||||||
|
data={protocol}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Indexer')}
|
||||||
|
data={indexer}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Message')}
|
||||||
|
data={message}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlocklistDetailsModal;
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
|
||||||
import styles from './BlocklistRow.css';
|
|
||||||
|
|
||||||
class BlocklistRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isDetailsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onDetailsPress = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDetailsModalClose = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
series,
|
|
||||||
sourceTitle,
|
|
||||||
languages,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
date,
|
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
message,
|
|
||||||
isSelected,
|
|
||||||
columns,
|
|
||||||
onSelectedChange,
|
|
||||||
onRemovePress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableSelectCell
|
|
||||||
id={id}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'series.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<SeriesTitleLink
|
|
||||||
titleSlug={series.titleSlug}
|
|
||||||
title={series.title}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sourceTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{sourceTitle}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.languages}
|
|
||||||
>
|
|
||||||
<EpisodeLanguages
|
|
||||||
languages={languages}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.quality}
|
|
||||||
>
|
|
||||||
<EpisodeQuality
|
|
||||||
quality={quality}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'date') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={date}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.indexer}
|
|
||||||
>
|
|
||||||
{indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.actions}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
name={icons.INFO}
|
|
||||||
onPress={this.onDetailsPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
title={translate('RemoveFromBlocklist')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={onRemovePress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<BlocklistDetailsModal
|
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
protocol={protocol}
|
|
||||||
indexer={indexer}
|
|
||||||
message={message}
|
|
||||||
onModalClose={this.onDetailsModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
BlocklistRow.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
series: PropTypes.object.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
indexer: PropTypes.string,
|
|
||||||
message: PropTypes.string,
|
|
||||||
isSelected: PropTypes.bool.isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BlocklistRow;
|
|
||||||
163
frontend/src/Activity/Blocklist/BlocklistRow.tsx
Normal file
163
frontend/src/Activity/Blocklist/BlocklistRow.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
|
import useSeries from 'Series/useSeries';
|
||||||
|
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||||
|
import Blocklist from 'typings/Blocklist';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||||
|
import styles from './BlocklistRow.css';
|
||||||
|
|
||||||
|
interface BlocklistRowProps extends Blocklist {
|
||||||
|
isSelected: boolean;
|
||||||
|
columns: Column[];
|
||||||
|
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BlocklistRow(props: BlocklistRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
seriesId,
|
||||||
|
sourceTitle,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
customFormats,
|
||||||
|
date,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
message,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const series = useSeries(seriesId);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleDetailsPress = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(true);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleDetailsModalClose = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(false);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleRemovePress = useCallback(() => {
|
||||||
|
dispatch(removeBlocklistItem({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
if (!series) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'series.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<SeriesTitleLink
|
||||||
|
titleSlug={series.titleSlug}
|
||||||
|
title={series.title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.languages}>
|
||||||
|
<EpisodeLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.quality}>
|
||||||
|
<EpisodeQuality quality={quality} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'date') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore ts(2739)
|
||||||
|
return <RelativeDateCell key={name} date={date} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.indexer}>
|
||||||
|
{indexer}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.actions}>
|
||||||
|
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
title={translate('RemoveFromBlocklist')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={handleRemovePress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<BlocklistDetailsModal
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
protocol={protocol}
|
||||||
|
indexer={indexer}
|
||||||
|
message={message}
|
||||||
|
onModalClose={handleDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlocklistRow;
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
|
||||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
|
||||||
import BlocklistRow from './BlocklistRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSeriesSelector(),
|
|
||||||
(series) => {
|
|
||||||
return {
|
|
||||||
series
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onRemovePress() {
|
|
||||||
dispatch(removeBlocklistItem({ id: props.id }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
|
||||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './HistoryDetails.css';
|
|
||||||
|
|
||||||
function HistoryDetails(props) {
|
|
||||||
const {
|
|
||||||
eventType,
|
|
||||||
sourceTitle,
|
|
||||||
data,
|
|
||||||
downloadId,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (eventType === 'grabbed') {
|
|
||||||
const {
|
|
||||||
indexer,
|
|
||||||
releaseGroup,
|
|
||||||
seriesMatchType,
|
|
||||||
customFormatScore,
|
|
||||||
nzbInfoUrl,
|
|
||||||
downloadClient,
|
|
||||||
downloadClientName,
|
|
||||||
age,
|
|
||||||
ageHours,
|
|
||||||
ageMinutes,
|
|
||||||
publishedDate
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
indexer ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
releaseGroup ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('ReleaseGroup')}
|
|
||||||
data={releaseGroup}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
seriesMatchType ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('SeriesMatchType')}
|
|
||||||
data={seriesMatchType}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
nzbInfoUrl ?
|
|
||||||
<span>
|
|
||||||
<DescriptionListItemTitle>
|
|
||||||
{translate('InfoUrl')}
|
|
||||||
</DescriptionListItemTitle>
|
|
||||||
|
|
||||||
<DescriptionListItemDescription>
|
|
||||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
|
||||||
</DescriptionListItemDescription>
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadClientNameInfo ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DownloadClient')}
|
|
||||||
data={downloadClientNameInfo}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
age || ageHours || ageMinutes ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('AgeWhenGrabbed')}
|
|
||||||
data={formatAge(age, ageHours, ageMinutes)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
publishedDate ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('PublishedDate')}
|
|
||||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
|
||||||
const {
|
|
||||||
message
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
message ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadFolderImported') {
|
|
||||||
const {
|
|
||||||
customFormatScore,
|
|
||||||
droppedPath,
|
|
||||||
importedPath
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
droppedPath ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Source')}
|
|
||||||
data={droppedPath}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
importedPath ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('ImportedTo')}
|
|
||||||
data={importedPath}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'episodeFileDeleted') {
|
|
||||||
const {
|
|
||||||
reason,
|
|
||||||
customFormatScore
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
let reasonMessage = '';
|
|
||||||
|
|
||||||
switch (reason) {
|
|
||||||
case 'Manual':
|
|
||||||
reasonMessage = translate('DeletedReasonManual');
|
|
||||||
break;
|
|
||||||
case 'MissingFromDisk':
|
|
||||||
reasonMessage = translate('DeletedReasonEpisodeMissingFromDisk');
|
|
||||||
break;
|
|
||||||
case 'Upgrade':
|
|
||||||
reasonMessage = translate('DeletedReasonUpgrade');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
reasonMessage = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Reason')}
|
|
||||||
data={reasonMessage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'episodeFileRenamed') {
|
|
||||||
const {
|
|
||||||
sourcePath,
|
|
||||||
sourceRelativePath,
|
|
||||||
path,
|
|
||||||
relativePath
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('SourcePath')}
|
|
||||||
data={sourcePath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('SourceRelativePath')}
|
|
||||||
data={sourceRelativePath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DestinationPath')}
|
|
||||||
data={path}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DestinationRelativePath')}
|
|
||||||
data={relativePath}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadIgnored') {
|
|
||||||
const {
|
|
||||||
message
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
message ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryDetails.propTypes = {
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryDetails;
|
|
||||||
321
frontend/src/Activity/History/Details/HistoryDetails.tsx
Normal file
321
frontend/src/Activity/History/Details/HistoryDetails.tsx
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||||
|
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import {
|
||||||
|
DownloadFailedHistory,
|
||||||
|
DownloadFolderImportedHistory,
|
||||||
|
DownloadIgnoredHistory,
|
||||||
|
EpisodeFileDeletedHistory,
|
||||||
|
EpisodeFileRenamedHistory,
|
||||||
|
GrabbedHistoryData,
|
||||||
|
HistoryData,
|
||||||
|
HistoryEventType,
|
||||||
|
} from 'typings/History';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './HistoryDetails.css';
|
||||||
|
|
||||||
|
interface HistoryDetailsProps {
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDetails(props: HistoryDetailsProps) {
|
||||||
|
const { eventType, sourceTitle, data, downloadId } = props;
|
||||||
|
|
||||||
|
const { shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventType === 'grabbed') {
|
||||||
|
const {
|
||||||
|
indexer,
|
||||||
|
releaseGroup,
|
||||||
|
seriesMatchType,
|
||||||
|
releaseSource,
|
||||||
|
customFormatScore,
|
||||||
|
nzbInfoUrl,
|
||||||
|
downloadClient,
|
||||||
|
downloadClientName,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishedDate,
|
||||||
|
} = data as GrabbedHistoryData;
|
||||||
|
|
||||||
|
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||||
|
|
||||||
|
let releaseSourceMessage = '';
|
||||||
|
|
||||||
|
switch (releaseSource) {
|
||||||
|
case 'Unknown':
|
||||||
|
releaseSourceMessage = translate('Unknown');
|
||||||
|
break;
|
||||||
|
case 'Rss':
|
||||||
|
releaseSourceMessage = translate('Rss');
|
||||||
|
break;
|
||||||
|
case 'Search':
|
||||||
|
releaseSourceMessage = translate('Search');
|
||||||
|
break;
|
||||||
|
case 'UserInvokedSearch':
|
||||||
|
releaseSourceMessage = translate('UserInvokedSearch');
|
||||||
|
break;
|
||||||
|
case 'InteractiveSearch':
|
||||||
|
releaseSourceMessage = translate('InteractiveSearch');
|
||||||
|
break;
|
||||||
|
case 'ReleasePush':
|
||||||
|
releaseSourceMessage = translate('ReleasePush');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
releaseSourceMessage = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{indexer ? (
|
||||||
|
<DescriptionListItem title={translate('Indexer')} data={indexer} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{releaseGroup ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ReleaseGroup')}
|
||||||
|
data={releaseGroup}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{seriesMatchType ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('SeriesMatchType')}
|
||||||
|
data={seriesMatchType}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{releaseSource ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ReleaseSource')}
|
||||||
|
data={releaseSourceMessage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{nzbInfoUrl ? (
|
||||||
|
<span>
|
||||||
|
<DescriptionListItemTitle>
|
||||||
|
{translate('InfoUrl')}
|
||||||
|
</DescriptionListItemTitle>
|
||||||
|
|
||||||
|
<DescriptionListItemDescription>
|
||||||
|
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||||
|
</DescriptionListItemDescription>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadClientNameInfo ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DownloadClient')}
|
||||||
|
data={downloadClientNameInfo}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{age || ageHours || ageMinutes ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('AgeWhenGrabbed')}
|
||||||
|
data={formatAge(age, ageHours, ageMinutes)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{publishedDate ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('PublishedDate')}
|
||||||
|
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFailed') {
|
||||||
|
const { message } = data as DownloadFailedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem title={translate('Message')} data={message} />
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFolderImported') {
|
||||||
|
const { customFormatScore, droppedPath, importedPath } =
|
||||||
|
data as DownloadFolderImportedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{droppedPath ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Source')}
|
||||||
|
data={droppedPath}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{importedPath ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ImportedTo')}
|
||||||
|
data={importedPath}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'episodeFileDeleted') {
|
||||||
|
const { reason, customFormatScore } = data as EpisodeFileDeletedHistory;
|
||||||
|
|
||||||
|
let reasonMessage = '';
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case 'Manual':
|
||||||
|
reasonMessage = translate('DeletedReasonManual');
|
||||||
|
break;
|
||||||
|
case 'MissingFromDisk':
|
||||||
|
reasonMessage = translate('DeletedReasonEpisodeMissingFromDisk');
|
||||||
|
break;
|
||||||
|
case 'Upgrade':
|
||||||
|
reasonMessage = translate('DeletedReasonUpgrade');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reasonMessage = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
|
||||||
|
|
||||||
|
<DescriptionListItem title={translate('Reason')} data={reasonMessage} />
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'episodeFileRenamed') {
|
||||||
|
const { sourcePath, sourceRelativePath, path, relativePath } =
|
||||||
|
data as EpisodeFileRenamedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('SourcePath')}
|
||||||
|
data={sourcePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('SourceRelativePath')}
|
||||||
|
data={sourceRelativePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem title={translate('DestinationPath')} data={path} />
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DestinationRelativePath')}
|
||||||
|
data={relativePath}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadIgnored') {
|
||||||
|
const { message } = data as DownloadIgnoredHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem title={translate('Message')} data={message} />
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryDetails;
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import HistoryDetails from './HistoryDetails';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(uiSettings) => {
|
|
||||||
return _.pick(uiSettings, [
|
|
||||||
'shortDateFormat',
|
|
||||||
'timeFormat'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(HistoryDetails);
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
@@ -8,11 +7,12 @@ 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 { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import HistoryDetails from './HistoryDetails';
|
import HistoryDetails from './HistoryDetails';
|
||||||
import styles from './HistoryDetailsModal.css';
|
import styles from './HistoryDetailsModal.css';
|
||||||
|
|
||||||
function getHeaderTitle(eventType) {
|
function getHeaderTitle(eventType: HistoryEventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return translate('Grabbed');
|
return translate('Grabbed');
|
||||||
@@ -31,29 +31,33 @@ function getHeaderTitle(eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryDetailsModal(props) {
|
interface HistoryDetailsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
isMarkingAsFailed: boolean;
|
||||||
|
onMarkAsFailedPress: () => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDetailsModal(props: HistoryDetailsModalProps) {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
eventType,
|
eventType,
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
data,
|
data,
|
||||||
downloadId,
|
downloadId,
|
||||||
isMarkingAsFailed,
|
isMarkingAsFailed = false,
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
onMarkAsFailedPress,
|
onMarkAsFailedPress,
|
||||||
onModalClose
|
onModalClose,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>{getHeaderTitle(eventType)}</ModalHeader>
|
||||||
{getHeaderTitle(eventType)}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<HistoryDetails
|
<HistoryDetails
|
||||||
@@ -61,50 +65,26 @@ function HistoryDetailsModal(props) {
|
|||||||
sourceTitle={sourceTitle}
|
sourceTitle={sourceTitle}
|
||||||
data={data}
|
data={data}
|
||||||
downloadId={downloadId}
|
downloadId={downloadId}
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{
|
{eventType === 'grabbed' && (
|
||||||
eventType === 'grabbed' &&
|
<SpinnerButton
|
||||||
<SpinnerButton
|
className={styles.markAsFailedButton}
|
||||||
className={styles.markAsFailedButton}
|
kind={kinds.DANGER}
|
||||||
kind={kinds.DANGER}
|
isSpinning={isMarkingAsFailed}
|
||||||
isSpinning={isMarkingAsFailed}
|
onPress={onMarkAsFailedPress}
|
||||||
onPress={onMarkAsFailedPress}
|
>
|
||||||
>
|
{translate('MarkAsFailed')}
|
||||||
{translate('MarkAsFailed')}
|
</SpinnerButton>
|
||||||
</SpinnerButton>
|
)}
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryDetailsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
isMarkingAsFailed: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryDetailsModal.defaultProps = {
|
|
||||||
isMarkingAsFailed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryDetailsModal;
|
export default HistoryDetailsModal;
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
||||||
import TablePager from 'Components/Table/TablePager';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import HistoryFilterModal from './HistoryFilterModal';
|
|
||||||
import HistoryRowConnector from './HistoryRowConnector';
|
|
||||||
|
|
||||||
class History extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
|
||||||
// Don't update when fetching has completed if items have changed,
|
|
||||||
// before episodes start fetching or when episodes start fetching.
|
|
||||||
|
|
||||||
if (
|
|
||||||
(
|
|
||||||
this.props.isFetching &&
|
|
||||||
nextProps.isPopulated &&
|
|
||||||
hasDifferentItems(this.props.items, nextProps.items)
|
|
||||||
) ||
|
|
||||||
(!this.props.isEpisodesFetching && nextProps.isEpisodesFetching)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
totalRecords,
|
|
||||||
isEpisodesFetching,
|
|
||||||
isEpisodesPopulated,
|
|
||||||
episodesError,
|
|
||||||
onFilterSelect,
|
|
||||||
onFirstPagePress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isFetchingAny = isFetching || isEpisodesFetching;
|
|
||||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
|
|
||||||
const hasError = error || episodesError;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('History')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Refresh')}
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isFetching}
|
|
||||||
onPress={onFirstPagePress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
{...otherProps}
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={HistoryFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isFetchingAny && !isAllPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetchingAny && hasError &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('HistoryLoadError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// If history isPopulated and it's empty show no history found and don't
|
|
||||||
// wait for the episodes to populate because they are never coming.
|
|
||||||
|
|
||||||
isPopulated && !hasError && !items.length &&
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{translate('NoHistoryFound')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !!items.length &&
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<HistoryRowConnector
|
|
||||||
key={item.id}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isFetchingAny}
|
|
||||||
onFirstPagePress={onFirstPagePress}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
History.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
isEpisodesFetching: PropTypes.bool.isRequired,
|
|
||||||
isEpisodesPopulated: PropTypes.bool.isRequired,
|
|
||||||
episodesError: PropTypes.object,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
|
||||||
onFirstPagePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default History;
|
|
||||||
231
frontend/src/Activity/History/History.tsx
Normal file
231
frontend/src/Activity/History/History.tsx
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import usePaging from 'Components/Table/usePaging';
|
||||||
|
import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||||
|
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||||
|
import {
|
||||||
|
clearHistory,
|
||||||
|
fetchHistory,
|
||||||
|
gotoHistoryPage,
|
||||||
|
setHistoryFilter,
|
||||||
|
setHistorySort,
|
||||||
|
setHistoryTableOption,
|
||||||
|
} from 'Store/Actions/historyActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import HistoryItem from 'typings/History';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import HistoryFilterModal from './HistoryFilterModal';
|
||||||
|
import HistoryRow from './HistoryRow';
|
||||||
|
|
||||||
|
function History() {
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalRecords,
|
||||||
|
} = useSelector((state: AppState) => state.history);
|
||||||
|
|
||||||
|
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
|
||||||
|
useSelector(createEpisodesFetchingSelector());
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('history'));
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const isFetchingAny = isFetching || isEpisodesFetching;
|
||||||
|
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
|
||||||
|
const hasError = error || episodesError;
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
} = usePaging({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
gotoPage: gotoHistoryPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(selectedFilterKey: string) => {
|
||||||
|
dispatch(setHistoryFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortPress = useCallback(
|
||||||
|
(sortKey: string) => {
|
||||||
|
dispatch(setHistorySort({ sortKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableOptionChange = useCallback(
|
||||||
|
(payload: TableOptionsChangePayload) => {
|
||||||
|
dispatch(setHistoryTableOption(payload));
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
dispatch(gotoHistoryPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchHistory());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoHistoryPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearHistory());
|
||||||
|
dispatch(clearEpisodes());
|
||||||
|
dispatch(clearEpisodeFiles());
|
||||||
|
};
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const episodeIds = selectUniqueIds<HistoryItem, number>(items, 'episodeId');
|
||||||
|
|
||||||
|
if (episodeIds.length) {
|
||||||
|
dispatch(fetchEpisodes({ episodeIds }));
|
||||||
|
} else {
|
||||||
|
dispatch(clearEpisodes());
|
||||||
|
}
|
||||||
|
}, [items, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchHistory());
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('History')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Refresh')}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isFetching}
|
||||||
|
onPress={handleFirstPagePress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={HistoryFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{isFetchingAny && !isAllPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetchingAny && hasError ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{
|
||||||
|
// If history isPopulated and it's empty show no history found and don't
|
||||||
|
// wait for the episodes to populate because they are never coming.
|
||||||
|
|
||||||
|
isPopulated && !hasError && !items.length ? (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoHistoryFound')}</Alert>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && items.length ? (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
onSortPress={handleSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<HistoryRow key={item.id} columns={columns} {...item} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default History;
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
|
||||||
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
|
||||||
import * as historyActions from 'Store/Actions/historyActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import History from './History';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.history,
|
|
||||||
(state) => state.episodes,
|
|
||||||
createCustomFiltersSelector('history'),
|
|
||||||
(history, episodes, customFilters) => {
|
|
||||||
return {
|
|
||||||
isEpisodesFetching: episodes.isFetching,
|
|
||||||
isEpisodesPopulated: episodes.isPopulated,
|
|
||||||
episodesError: episodes.error,
|
|
||||||
customFilters,
|
|
||||||
...history
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...historyActions,
|
|
||||||
fetchEpisodes,
|
|
||||||
clearEpisodes,
|
|
||||||
clearEpisodeFiles
|
|
||||||
};
|
|
||||||
|
|
||||||
class HistoryConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchHistory,
|
|
||||||
gotoHistoryFirstPage
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchHistory();
|
|
||||||
} else {
|
|
||||||
gotoHistoryFirstPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
|
||||||
const episodeIds = selectUniqueIds(this.props.items, 'episodeId');
|
|
||||||
|
|
||||||
if (episodeIds.length) {
|
|
||||||
this.props.fetchEpisodes({ episodeIds });
|
|
||||||
} else {
|
|
||||||
this.props.clearEpisodes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
this.props.clearHistory();
|
|
||||||
this.props.clearEpisodes();
|
|
||||||
this.props.clearEpisodeFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
this.props.fetchHistory();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFirstPagePress = () => {
|
|
||||||
this.props.gotoHistoryFirstPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPagePress = () => {
|
|
||||||
this.props.gotoHistoryPreviousPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPagePress = () => {
|
|
||||||
this.props.gotoHistoryNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onLastPagePress = () => {
|
|
||||||
this.props.gotoHistoryLastPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelect = (page) => {
|
|
||||||
this.props.gotoHistoryPage({ page });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
|
||||||
this.props.setHistorySort({ sortKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFilterSelect = (selectedFilterKey) => {
|
|
||||||
this.props.setHistoryFilter({ selectedFilterKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
|
||||||
this.props.setHistoryTableOption(payload);
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
this.props.gotoHistoryFirstPage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<History
|
|
||||||
onFirstPagePress={this.onFirstPagePress}
|
|
||||||
onPreviousPagePress={this.onPreviousPagePress}
|
|
||||||
onNextPagePress={this.onNextPagePress}
|
|
||||||
onLastPagePress={this.onLastPagePress}
|
|
||||||
onPageSelect={this.onPageSelect}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onFilterSelect={this.onFilterSelect}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryConnector.propTypes = {
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
fetchHistory: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryPreviousPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryNextPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryLastPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryPage: PropTypes.func.isRequired,
|
|
||||||
setHistorySort: PropTypes.func.isRequired,
|
|
||||||
setHistoryFilter: PropTypes.func.isRequired,
|
|
||||||
setHistoryTableOption: PropTypes.func.isRequired,
|
|
||||||
clearHistory: PropTypes.func.isRequired,
|
|
||||||
fetchEpisodes: PropTypes.func.isRequired,
|
|
||||||
clearEpisodes: PropTypes.func.isRequired,
|
|
||||||
clearEpisodeFiles: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
|
|
||||||
);
|
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React 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 { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
EpisodeFileDeletedHistory,
|
||||||
|
GrabbedHistoryData,
|
||||||
|
HistoryData,
|
||||||
|
HistoryEventType,
|
||||||
|
} from 'typings/History';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType, data) {
|
function getIconName(eventType: HistoryEventType, data: HistoryData) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
@@ -17,7 +22,9 @@ function getIconName(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'episodeFileDeleted':
|
case 'episodeFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
return (data as EpisodeFileDeletedHistory).reason === 'MissingFromDisk'
|
||||||
|
? icons.FILE_MISSING
|
||||||
|
: icons.DELETE;
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -27,7 +34,7 @@ function getIconName(eventType, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconKind(eventType) {
|
function getIconKind(eventType: HistoryEventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return kinds.DANGER;
|
return kinds.DANGER;
|
||||||
@@ -36,10 +43,13 @@ function getIconKind(eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTooltip(eventType, data) {
|
function getTooltip(eventType: HistoryEventType, data: HistoryData) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return translate('EpisodeGrabbedTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
|
return translate('EpisodeGrabbedTooltip', {
|
||||||
|
indexer: (data as GrabbedHistoryData).indexer,
|
||||||
|
downloadClient: (data as GrabbedHistoryData).downloadClient,
|
||||||
|
});
|
||||||
case 'seriesFolderImported':
|
case 'seriesFolderImported':
|
||||||
return translate('SeriesFolderImportedTooltip');
|
return translate('SeriesFolderImportedTooltip');
|
||||||
case 'downloadFolderImported':
|
case 'downloadFolderImported':
|
||||||
@@ -47,7 +57,9 @@ function getTooltip(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return translate('DownloadFailedEpisodeTooltip');
|
return translate('DownloadFailedEpisodeTooltip');
|
||||||
case 'episodeFileDeleted':
|
case 'episodeFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? translate('EpisodeFileMissingTooltip') : translate('EpisodeFileDeletedTooltip');
|
return (data as EpisodeFileDeletedHistory).reason === 'MissingFromDisk'
|
||||||
|
? translate('EpisodeFileMissingTooltip')
|
||||||
|
: translate('EpisodeFileDeletedTooltip');
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return translate('EpisodeFileRenamedTooltip');
|
return translate('EpisodeFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -57,31 +69,21 @@ function getTooltip(eventType, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, data }) {
|
interface HistoryEventTypeCellProps {
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
data: HistoryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryEventTypeCell({ eventType, data }: HistoryEventTypeCellProps) {
|
||||||
const iconName = getIconName(eventType, data);
|
const iconName = getIconName(eventType, data);
|
||||||
const iconKind = getIconKind(eventType);
|
const iconKind = getIconKind(eventType);
|
||||||
const tooltip = getTooltip(eventType, data);
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell className={styles.cell} title={tooltip}>
|
||||||
className={styles.cell}
|
<Icon name={iconName} kind={iconKind} />
|
||||||
title={tooltip}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
kind={iconKind}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryEventTypeCell.propTypes = {
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryEventTypeCell.defaultProps = {
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryEventTypeCell;
|
export default HistoryEventTypeCell;
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import episodeEntities from 'Episode/episodeEntities';
|
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
|
||||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
|
||||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
|
||||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
|
||||||
import styles from './HistoryRow.css';
|
|
||||||
|
|
||||||
class HistoryRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isDetailsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (
|
|
||||||
prevProps.isMarkingAsFailed &&
|
|
||||||
!this.props.isMarkingAsFailed &&
|
|
||||||
!this.props.markAsFailedError
|
|
||||||
) {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onDetailsPress = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDetailsModalClose = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
episodeId,
|
|
||||||
series,
|
|
||||||
episode,
|
|
||||||
languages,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
customFormatScore,
|
|
||||||
qualityCutoffNotMet,
|
|
||||||
eventType,
|
|
||||||
sourceTitle,
|
|
||||||
date,
|
|
||||||
data,
|
|
||||||
downloadId,
|
|
||||||
isMarkingAsFailed,
|
|
||||||
columns,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
onMarkAsFailedPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!episode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'eventType') {
|
|
||||||
return (
|
|
||||||
<HistoryEventTypeCell
|
|
||||||
key={name}
|
|
||||||
eventType={eventType}
|
|
||||||
data={data}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'series.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<SeriesTitleLink
|
|
||||||
titleSlug={series.titleSlug}
|
|
||||||
title={series.title}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episode') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<SeasonEpisodeNumber
|
|
||||||
seasonNumber={episode.seasonNumber}
|
|
||||||
episodeNumber={episode.episodeNumber}
|
|
||||||
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
|
||||||
seriesType={series.seriesType}
|
|
||||||
alternateTitles={series.alternateTitles}
|
|
||||||
sceneSeasonNumber={episode.sceneSeasonNumber}
|
|
||||||
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
|
||||||
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episodes.title') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeTitleLink
|
|
||||||
episodeId={episodeId}
|
|
||||||
episodeEntity={episodeEntities.EPISODES}
|
|
||||||
seriesId={series.id}
|
|
||||||
episodeTitle={episode.title}
|
|
||||||
showOpenSeriesButton={true}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeLanguages languages={languages} />
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeQuality
|
|
||||||
quality={quality}
|
|
||||||
isCutoffMet={qualityCutoffNotMet}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'date') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={date}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.downloadClient}
|
|
||||||
>
|
|
||||||
{data.downloadClient}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.indexer}
|
|
||||||
>
|
|
||||||
{data.indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormatScore') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.customFormatScore}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
anchor={formatCustomFormatScore(
|
|
||||||
customFormatScore,
|
|
||||||
customFormats.length
|
|
||||||
)}
|
|
||||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'releaseGroup') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.releaseGroup}
|
|
||||||
>
|
|
||||||
{data.releaseGroup}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sourceTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
>
|
|
||||||
{sourceTitle}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'details') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.details}
|
|
||||||
>
|
|
||||||
<div className={styles.actionContents}>
|
|
||||||
<IconButton
|
|
||||||
name={icons.INFO}
|
|
||||||
onPress={this.onDetailsPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<HistoryDetailsModal
|
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
|
||||||
eventType={eventType}
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
data={data}
|
|
||||||
downloadId={downloadId}
|
|
||||||
isMarkingAsFailed={isMarkingAsFailed}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
|
||||||
onModalClose={this.onDetailsModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryRow.propTypes = {
|
|
||||||
episodeId: PropTypes.number,
|
|
||||||
series: PropTypes.object.isRequired,
|
|
||||||
episode: PropTypes.object,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
|
||||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
isMarkingAsFailed: PropTypes.bool,
|
|
||||||
markAsFailedError: PropTypes.object,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryRow.defaultProps = {
|
|
||||||
customFormats: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryRow;
|
|
||||||
270
frontend/src/Activity/History/HistoryRow.tsx
Normal file
270
frontend/src/Activity/History/HistoryRow.tsx
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import episodeEntities from 'Episode/episodeEntities';
|
||||||
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
|
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||||
|
import useEpisode from 'Episode/useEpisode';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
|
import useSeries from 'Series/useSeries';
|
||||||
|
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||||
|
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||||
|
import styles from './HistoryRow.css';
|
||||||
|
|
||||||
|
interface HistoryRowProps {
|
||||||
|
id: number;
|
||||||
|
episodeId: number;
|
||||||
|
seriesId: number;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats?: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
|
qualityCutoffNotMet: boolean;
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
date: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
isMarkingAsFailed?: boolean;
|
||||||
|
markAsFailedError?: object;
|
||||||
|
columns: Column[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryRow(props: HistoryRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
episodeId,
|
||||||
|
seriesId,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
customFormats = [],
|
||||||
|
customFormatScore,
|
||||||
|
qualityCutoffNotMet,
|
||||||
|
eventType,
|
||||||
|
sourceTitle,
|
||||||
|
date,
|
||||||
|
data,
|
||||||
|
downloadId,
|
||||||
|
isMarkingAsFailed = false,
|
||||||
|
markAsFailedError,
|
||||||
|
columns,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const wasMarkingAsFailed = usePrevious(isMarkingAsFailed);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const series = useSeries(seriesId);
|
||||||
|
const episode = useEpisode(episodeId, 'episodes');
|
||||||
|
|
||||||
|
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleDetailsPress = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(true);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleDetailsModalClose = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(false);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleMarkAsFailedPress = useCallback(() => {
|
||||||
|
dispatch(markAsFailed({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasMarkingAsFailed && !isMarkingAsFailed && !markAsFailedError) {
|
||||||
|
setIsDetailsModalOpen(false);
|
||||||
|
dispatch(fetchHistory());
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
wasMarkingAsFailed,
|
||||||
|
isMarkingAsFailed,
|
||||||
|
markAsFailedError,
|
||||||
|
setIsDetailsModalOpen,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!series || !episode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'eventType') {
|
||||||
|
return (
|
||||||
|
<HistoryEventTypeCell
|
||||||
|
key={name}
|
||||||
|
eventType={eventType}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'series.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<SeriesTitleLink
|
||||||
|
titleSlug={series.titleSlug}
|
||||||
|
title={series.title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episode') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<SeasonEpisodeNumber
|
||||||
|
seasonNumber={episode.seasonNumber}
|
||||||
|
episodeNumber={episode.episodeNumber}
|
||||||
|
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
||||||
|
seriesType={series.seriesType}
|
||||||
|
alternateTitles={series.alternateTitles}
|
||||||
|
sceneSeasonNumber={episode.sceneSeasonNumber}
|
||||||
|
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
||||||
|
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episodes.title') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeTitleLink
|
||||||
|
episodeId={episodeId}
|
||||||
|
episodeEntity={episodeEntities.EPISODES}
|
||||||
|
seriesId={series.id}
|
||||||
|
episodeTitle={episode.title}
|
||||||
|
showOpenSeriesButton={true}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeQuality
|
||||||
|
quality={quality}
|
||||||
|
isCutoffNotMet={qualityCutoffNotMet}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'date') {
|
||||||
|
return <RelativeDateCell key={name} date={date} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
const downloadClientName =
|
||||||
|
'downloadClientName' in data ? data.downloadClientName : null;
|
||||||
|
const downloadClient =
|
||||||
|
'downloadClient' in data ? data.downloadClient : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.downloadClient}>
|
||||||
|
{downloadClientName ?? downloadClient ?? ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.indexer}>
|
||||||
|
{'indexer' in data ? data.indexer : ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'releaseGroup') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.releaseGroup}>
|
||||||
|
{'releaseGroup' in data ? data.releaseGroup : ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'details') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.details}>
|
||||||
|
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<HistoryDetailsModal
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
eventType={eventType}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
data={data}
|
||||||
|
downloadId={downloadId}
|
||||||
|
isMarkingAsFailed={isMarkingAsFailed}
|
||||||
|
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||||
|
onModalClose={handleDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryRow;
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
|
||||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
|
||||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import HistoryRow from './HistoryRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSeriesSelector(),
|
|
||||||
createEpisodeSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(series, episode, uiSettings) => {
|
|
||||||
return {
|
|
||||||
series,
|
|
||||||
episode,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchHistory,
|
|
||||||
markAsFailed
|
|
||||||
};
|
|
||||||
|
|
||||||
class HistoryRowConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (
|
|
||||||
prevProps.isMarkingAsFailed &&
|
|
||||||
!this.props.isMarkingAsFailed &&
|
|
||||||
!this.props.markAsFailedError
|
|
||||||
) {
|
|
||||||
this.props.fetchHistory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMarkAsFailedPress = () => {
|
|
||||||
this.props.markAsFailed({ id: this.props.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<HistoryRow
|
|
||||||
{...this.props}
|
|
||||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryRowConnector.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
isMarkingAsFailed: PropTypes.bool,
|
|
||||||
markAsFailedError: PropTypes.object,
|
|
||||||
fetchHistory: PropTypes.func.isRequired,
|
|
||||||
markAsFailed: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector);
|
|
||||||
@@ -11,3 +11,7 @@
|
|||||||
border-color: var(--usenetColor);
|
border-color: var(--usenetColor);
|
||||||
background-color: var(--usenetColor);
|
background-color: var(--usenetColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unknown {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'torrent': string;
|
'torrent': string;
|
||||||
|
'unknown': string;
|
||||||
'usenet': string;
|
'usenet': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import styles from './ProtocolLabel.css';
|
|
||||||
|
|
||||||
function ProtocolLabel({ protocol }) {
|
|
||||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label className={styles[protocol]}>
|
|
||||||
{protocolName}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProtocolLabel.propTypes = {
|
|
||||||
protocol: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProtocolLabel;
|
|
||||||
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import styles from './ProtocolLabel.css';
|
||||||
|
|
||||||
|
interface ProtocolLabelProps {
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
|
||||||
|
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||||
|
|
||||||
|
return <Label className={styles[protocol]}>{protocolName}</Label>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProtocolLabel;
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
||||||
import TablePager from 'Components/Table/TablePager';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
|
||||||
import QueueFilterModal from './QueueFilterModal';
|
|
||||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
|
||||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
|
||||||
|
|
||||||
class Queue extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._shouldBlockRefresh = false;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: false,
|
|
||||||
lastToggled: null,
|
|
||||||
selectedState: {},
|
|
||||||
isPendingSelected: false,
|
|
||||||
isConfirmRemoveModalOpen: false,
|
|
||||||
items: props.items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
if (this._shouldBlockRefresh) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
isEpisodesFetching
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(!isEpisodesFetching && prevProps.isEpisodesFetching) ||
|
|
||||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.episodeId))
|
|
||||||
) {
|
|
||||||
this.setState((state) => {
|
|
||||||
return {
|
|
||||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
|
||||||
items
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextState = {};
|
|
||||||
|
|
||||||
if (prevProps.items !== items) {
|
|
||||||
nextState.items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedIds = this.getSelectedIds();
|
|
||||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
|
||||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
|
||||||
nextState.isPendingSelected = isPendingSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(nextState)) {
|
|
||||||
this.setState(nextState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
getSelectedIds = () => {
|
|
||||||
return getSelectedIds(this.state.selectedState);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
|
||||||
this._shouldBlockRefresh = isOpen;
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectAllChange = ({ value }) => {
|
|
||||||
this.setState(selectAll(this.state.selectedState, value));
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
|
||||||
this.setState((state) => {
|
|
||||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onGrabSelectedPress = () => {
|
|
||||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedPress = () => {
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
|
||||||
this._shouldBlockRefresh = true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedConfirmed = (payload) => {
|
|
||||||
this._shouldBlockRefresh = false;
|
|
||||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmRemoveModalClose = () => {
|
|
||||||
this._shouldBlockRefresh = false;
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isEpisodesFetching,
|
|
||||||
isEpisodesPopulated,
|
|
||||||
episodesError,
|
|
||||||
columns,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
count,
|
|
||||||
totalRecords,
|
|
||||||
isGrabbing,
|
|
||||||
isRemoving,
|
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
|
||||||
onRefreshPress,
|
|
||||||
onFilterSelect,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
selectedState,
|
|
||||||
isConfirmRemoveModalOpen,
|
|
||||||
isPendingSelected,
|
|
||||||
items
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
|
|
||||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
|
|
||||||
const hasError = error || episodesError;
|
|
||||||
const selectedIds = this.getSelectedIds();
|
|
||||||
const selectedCount = selectedIds.length;
|
|
||||||
const disableSelectedActions = selectedCount === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Queue')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label="Refresh"
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isRefreshing}
|
|
||||||
onPress={onRefreshPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('GrabSelected')}
|
|
||||||
iconName={icons.DOWNLOAD}
|
|
||||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={this.onGrabSelectedPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RemoveSelected')}
|
|
||||||
iconName={icons.REMOVE}
|
|
||||||
isDisabled={disableSelectedActions}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={this.onRemoveSelectedPress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection
|
|
||||||
alignContent={align.RIGHT}
|
|
||||||
>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
columns={columns}
|
|
||||||
{...otherProps}
|
|
||||||
optionsComponent={QueueOptionsConnector}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={QueueFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isRefreshing && !isAllPopulated ?
|
|
||||||
<LoadingIndicator /> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isRefreshing && hasError ?
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('QueueLoadError')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !items.length ?
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{
|
|
||||||
selectedFilterKey !== 'all' && count > 0 ?
|
|
||||||
translate('QueueFilterHasNoItems') :
|
|
||||||
translate('QueueIsEmpty')
|
|
||||||
}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !!items.length ?
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
selectAll={true}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
{...otherProps}
|
|
||||||
optionsComponent={QueueOptionsConnector}
|
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<QueueRowConnector
|
|
||||||
key={item.id}
|
|
||||||
episodeId={item.episodeId}
|
|
||||||
isSelected={selectedState[item.id]}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
onSelectedChange={this.onSelectedChange}
|
|
||||||
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isRefreshing}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
<RemoveQueueItemModal
|
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
|
||||||
selectedCount={selectedCount}
|
|
||||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
return !!(item && item.downloadClientHasPostImportCategory);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
canIgnore={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
return !!(item && item.seriesId && item.episodeId);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
pending={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
|
||||||
onModalClose={this.onConfirmRemoveModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Queue.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isEpisodesFetching: PropTypes.bool.isRequired,
|
|
||||||
isEpisodesPopulated: PropTypes.bool.isRequired,
|
|
||||||
episodesError: PropTypes.object,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
count: PropTypes.number.isRequired,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
|
||||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
|
||||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
|
||||||
onRemoveSelectedPress: PropTypes.func.isRequired,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.defaultProps = {
|
|
||||||
count: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Queue;
|
|
||||||
415
frontend/src/Activity/Queue/Queue.tsx
Normal file
415
frontend/src/Activity/Queue/Queue.tsx
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
import React, {
|
||||||
|
ReactElement,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import usePaging from 'Components/Table/usePaging';
|
||||||
|
import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||||
|
import {
|
||||||
|
clearQueue,
|
||||||
|
fetchQueue,
|
||||||
|
gotoQueuePage,
|
||||||
|
grabQueueItems,
|
||||||
|
removeQueueItems,
|
||||||
|
setQueueFilter,
|
||||||
|
setQueueSort,
|
||||||
|
setQueueTableOption,
|
||||||
|
} from 'Store/Actions/queueActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import QueueItem from 'typings/Queue';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import QueueFilterModal from './QueueFilterModal';
|
||||||
|
import QueueOptions from './QueueOptions';
|
||||||
|
import QueueRow from './QueueRow';
|
||||||
|
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||||
|
import createQueueStatusSelector from './Status/createQueueStatusSelector';
|
||||||
|
|
||||||
|
function Queue() {
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalRecords,
|
||||||
|
isGrabbing,
|
||||||
|
isRemoving,
|
||||||
|
} = useSelector((state: AppState) => state.queue.paged);
|
||||||
|
|
||||||
|
const { count } = useSelector(createQueueStatusSelector());
|
||||||
|
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
|
||||||
|
useSelector(createEpisodesFetchingSelector());
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('queue'));
|
||||||
|
|
||||||
|
const isRefreshMonitoredDownloadsExecuting = useSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldBlockRefresh = useRef(false);
|
||||||
|
const currentQueue = useRef<ReactElement | null>(null);
|
||||||
|
|
||||||
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const isPendingSelected = useMemo(() => {
|
||||||
|
return items.some((item) => {
|
||||||
|
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||||
|
});
|
||||||
|
}, [items, selectedIds]);
|
||||||
|
|
||||||
|
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const isRefreshing =
|
||||||
|
isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||||
|
const isAllPopulated =
|
||||||
|
isPopulated &&
|
||||||
|
(isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
|
||||||
|
const hasError = error || episodesError;
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
const disableSelectedActions = selectedCount === 0;
|
||||||
|
|
||||||
|
const handleSelectAllChange = useCallback(
|
||||||
|
({ value }: CheckInputChanged) => {
|
||||||
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectedChange = useCallback(
|
||||||
|
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||||
|
setSelectState({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
items,
|
||||||
|
id,
|
||||||
|
isSelected: value,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRefreshPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: commandNames.REFRESH_MONITORED_DOWNLOADS,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => {
|
||||||
|
shouldBlockRefresh.current = isOpen;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGrabSelectedPress = useCallback(() => {
|
||||||
|
dispatch(grabQueueItems({ ids: selectedIds }));
|
||||||
|
}, [selectedIds, dispatch]);
|
||||||
|
|
||||||
|
const handleRemoveSelectedPress = useCallback(() => {
|
||||||
|
shouldBlockRefresh.current = true;
|
||||||
|
setIsConfirmRemoveModalOpen(true);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const handleRemoveSelectedConfirmed = useCallback(
|
||||||
|
(payload: RemovePressProps) => {
|
||||||
|
shouldBlockRefresh.current = false;
|
||||||
|
dispatch(removeQueueItems({ ids: selectedIds, ...payload }));
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
},
|
||||||
|
[selectedIds, setIsConfirmRemoveModalOpen, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||||
|
shouldBlockRefresh.current = false;
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
} = usePaging({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
gotoPage: gotoQueuePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(selectedFilterKey: string) => {
|
||||||
|
dispatch(setQueueFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortPress = useCallback(
|
||||||
|
(sortKey: string) => {
|
||||||
|
dispatch(setQueueSort({ sortKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableOptionChange = useCallback(
|
||||||
|
(payload: TableOptionsChangePayload) => {
|
||||||
|
dispatch(setQueueTableOption(payload));
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
dispatch(gotoQueuePage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchQueue());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoQueuePage({ page: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearQueue());
|
||||||
|
};
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const episodeIds = selectUniqueIds<QueueItem, number | undefined>(
|
||||||
|
items,
|
||||||
|
'episodeId'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (episodeIds.length) {
|
||||||
|
dispatch(fetchEpisodes({ episodeIds }));
|
||||||
|
} else {
|
||||||
|
dispatch(clearEpisodes());
|
||||||
|
}
|
||||||
|
}, [items, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchQueue());
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (!shouldBlockRefresh.current) {
|
||||||
|
currentQueue.current = (
|
||||||
|
<PageContentBody>
|
||||||
|
{isRefreshing && !isAllPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isRefreshing && hasError ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('QueueLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && !items.length ? (
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{selectedFilterKey !== 'all' && count > 0
|
||||||
|
? translate('QueueFilterHasNoItems')
|
||||||
|
: translate('QueueIsEmpty')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && !!items.length ? (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
optionsComponent={QueueOptions}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
onSelectAllChange={handleSelectAllChange}
|
||||||
|
onSortPress={handleSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<QueueRow
|
||||||
|
key={item.id}
|
||||||
|
episodeId={item.episodeId}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={handleSelectedChange}
|
||||||
|
onQueueRowModalOpenOrClose={
|
||||||
|
handleQueueRowModalOpenOrClose
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Queue')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Refresh"
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isRefreshing}
|
||||||
|
onPress={handleRefreshPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('GrabSelected')}
|
||||||
|
iconName={icons.DOWNLOAD}
|
||||||
|
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={handleGrabSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RemoveSelected')}
|
||||||
|
iconName={icons.REMOVE}
|
||||||
|
isDisabled={disableSelectedActions}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={handleRemoveSelectedPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
maxPageSize={200}
|
||||||
|
optionsComponent={QueueOptions}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={QueueFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
{currentQueue.current}
|
||||||
|
|
||||||
|
<RemoveQueueItemModal
|
||||||
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
|
selectedCount={selectedCount}
|
||||||
|
canChangeCategory={
|
||||||
|
isConfirmRemoveModalOpen &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.downloadClientHasPostImportCategory);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
canIgnore={
|
||||||
|
isConfirmRemoveModalOpen &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.seriesId && item.episodeId);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isPending={
|
||||||
|
isConfirmRemoveModalOpen &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
item.status === 'delay' ||
|
||||||
|
item.status === 'downloadClientUnavailable'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onRemovePress={handleRemoveSelectedConfirmed}
|
||||||
|
onModalClose={handleConfirmRemoveModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Queue;
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
|
||||||
import * as queueActions from 'Store/Actions/queueActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import Queue from './Queue';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.episodes,
|
|
||||||
(state) => state.queue.options,
|
|
||||||
(state) => state.queue.paged,
|
|
||||||
(state) => state.queue.status.item,
|
|
||||||
createCustomFiltersSelector('queue'),
|
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
|
||||||
(episodes, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
|
||||||
return {
|
|
||||||
count: options.includeUnknownSeriesItems ? status.totalCount : status.count,
|
|
||||||
isEpisodesFetching: episodes.isFetching,
|
|
||||||
isEpisodesPopulated: episodes.isPopulated,
|
|
||||||
episodesError: episodes.error,
|
|
||||||
customFilters,
|
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
|
||||||
...options,
|
|
||||||
...queue
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...queueActions,
|
|
||||||
fetchEpisodes,
|
|
||||||
clearEpisodes,
|
|
||||||
executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueueConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchQueue,
|
|
||||||
fetchQueueStatus,
|
|
||||||
gotoQueueFirstPage
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchQueue();
|
|
||||||
} else {
|
|
||||||
gotoQueueFirstPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchQueueStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
|
||||||
const episodeIds = selectUniqueIds(this.props.items, 'episodeId');
|
|
||||||
|
|
||||||
if (episodeIds.length) {
|
|
||||||
this.props.fetchEpisodes({ episodeIds });
|
|
||||||
} else {
|
|
||||||
this.props.clearEpisodes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.props.includeUnknownSeriesItems !==
|
|
||||||
prevProps.includeUnknownSeriesItems
|
|
||||||
) {
|
|
||||||
this.repopulate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
this.props.clearQueue();
|
|
||||||
this.props.clearEpisodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
this.props.fetchQueue();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFirstPagePress = () => {
|
|
||||||
this.props.gotoQueueFirstPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPagePress = () => {
|
|
||||||
this.props.gotoQueuePreviousPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPagePress = () => {
|
|
||||||
this.props.gotoQueueNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onLastPagePress = () => {
|
|
||||||
this.props.gotoQueueLastPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelect = (page) => {
|
|
||||||
this.props.gotoQueuePage({ page });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
|
||||||
this.props.setQueueSort({ sortKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFilterSelect = (selectedFilterKey) => {
|
|
||||||
this.props.setQueueFilter({ selectedFilterKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
|
||||||
this.props.setQueueTableOption(payload);
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
this.props.gotoQueueFirstPage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onRefreshPress = () => {
|
|
||||||
this.props.executeCommand({
|
|
||||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onGrabSelectedPress = (ids) => {
|
|
||||||
this.props.grabQueueItems({ ids });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedPress = (payload) => {
|
|
||||||
this.props.removeQueueItems(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Queue
|
|
||||||
onFirstPagePress={this.onFirstPagePress}
|
|
||||||
onPreviousPagePress={this.onPreviousPagePress}
|
|
||||||
onNextPagePress={this.onNextPagePress}
|
|
||||||
onLastPagePress={this.onLastPagePress}
|
|
||||||
onPageSelect={this.onPageSelect}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onFilterSelect={this.onFilterSelect}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
onRefreshPress={this.onRefreshPress}
|
|
||||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
|
||||||
onRemoveSelectedPress={this.onRemoveSelectedPress}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueConnector.propTypes = {
|
|
||||||
includeUnknownSeriesItems: PropTypes.bool.isRequired,
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
fetchQueue: PropTypes.func.isRequired,
|
|
||||||
fetchQueueStatus: PropTypes.func.isRequired,
|
|
||||||
gotoQueueFirstPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueueNextPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueuePage: PropTypes.func.isRequired,
|
|
||||||
setQueueSort: PropTypes.func.isRequired,
|
|
||||||
setQueueFilter: PropTypes.func.isRequired,
|
|
||||||
setQueueTableOption: PropTypes.func.isRequired,
|
|
||||||
clearQueue: PropTypes.func.isRequired,
|
|
||||||
grabQueueItems: PropTypes.func.isRequired,
|
|
||||||
removeQueueItems: PropTypes.func.isRequired,
|
|
||||||
fetchEpisodes: PropTypes.func.isRequired,
|
|
||||||
clearEpisodes: PropTypes.func.isRequired,
|
|
||||||
executeCommand: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
|
||||||
);
|
|
||||||
@@ -1,36 +1,49 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QueueStatus from './QueueStatus';
|
import QueueStatus from './QueueStatus';
|
||||||
import styles from './QueueDetails.css';
|
import styles from './QueueDetails.css';
|
||||||
|
|
||||||
function QueueDetails(props) {
|
interface QueueDetailsProps {
|
||||||
|
title: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
progressBar: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueDetails(props: QueueDetailsProps) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
size,
|
size,
|
||||||
sizeleft,
|
sizeleft,
|
||||||
status,
|
status,
|
||||||
trackedDownloadState,
|
trackedDownloadState = 'downloading',
|
||||||
trackedDownloadStatus,
|
trackedDownloadStatus = 'ok',
|
||||||
statusMessages,
|
statusMessages,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
progressBar
|
progressBar,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const progress = (100 - sizeleft / size * 100);
|
const progress = 100 - (sizeleft / size) * 100;
|
||||||
const isDownloading = status === 'downloading';
|
const isDownloading = status === 'downloading';
|
||||||
const isPaused = status === 'paused';
|
const isPaused = status === 'paused';
|
||||||
const hasWarning = trackedDownloadStatus === 'warning';
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
const hasError = trackedDownloadStatus === 'error';
|
const hasError = trackedDownloadStatus === 'error';
|
||||||
|
|
||||||
if (
|
if ((isDownloading || isPaused) && !hasWarning && !hasError) {
|
||||||
(isDownloading || isPaused) &&
|
|
||||||
!hasWarning &&
|
|
||||||
!hasError
|
|
||||||
) {
|
|
||||||
const state = isPaused ? translate('Paused') : translate('Downloading');
|
const state = isPaused ? translate('Paused') : translate('Downloading');
|
||||||
|
|
||||||
if (progress < 5) {
|
if (progress < 5) {
|
||||||
@@ -45,11 +58,9 @@ function QueueDetails(props) {
|
|||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
className={styles.progressBarContainer}
|
className={styles.progressBarContainer}
|
||||||
anchor={progressBar}
|
anchor={progressBar!}
|
||||||
title={`${state} - ${progress.toFixed(1)}%`}
|
title={`${state} - ${progress.toFixed(1)}%`}
|
||||||
body={
|
body={<div>{title}</div>}
|
||||||
<div>{title}</div>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -68,22 +79,4 @@ function QueueDetails(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueDetails.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
sizeleft: PropTypes.number.isRequired,
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
progressBar: PropTypes.node.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueDetails.defaultProps = {
|
|
||||||
trackedDownloadStatus: 'ok',
|
|
||||||
trackedDownloadState: 'downloading'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueDetails;
|
export default QueueDetails;
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component, Fragment } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class QueueOptions extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
includeUnknownSeriesItems: props.includeUnknownSeriesItems
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
includeUnknownSeriesItems
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (includeUnknownSeriesItems !== prevProps.includeUnknownSeriesItems) {
|
|
||||||
this.setState({
|
|
||||||
includeUnknownSeriesItems
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onOptionChange = ({ name, value }) => {
|
|
||||||
this.setState({
|
|
||||||
[name]: value
|
|
||||||
}, () => {
|
|
||||||
this.props.onOptionChange({
|
|
||||||
[name]: value
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
includeUnknownSeriesItems
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="includeUnknownSeriesItems"
|
|
||||||
value={includeUnknownSeriesItems}
|
|
||||||
helpText={translate('ShowUnknownSeriesItemsHelpText')}
|
|
||||||
onChange={this.onOptionChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueOptions.propTypes = {
|
|
||||||
includeUnknownSeriesItems: PropTypes.bool.isRequired,
|
|
||||||
onOptionChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueOptions;
|
|
||||||
48
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
48
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function QueueOptions() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { includeUnknownSeriesItems } = useSelector(
|
||||||
|
(state: AppState) => state.queue.options
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOptionChange = useCallback(
|
||||||
|
({ name, value }: CheckInputChanged) => {
|
||||||
|
dispatch(
|
||||||
|
setQueueOption({
|
||||||
|
[name]: value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (name === 'includeUnknownSeriesItems') {
|
||||||
|
dispatch(gotoQueuePage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeUnknownSeriesItems"
|
||||||
|
value={includeUnknownSeriesItems}
|
||||||
|
helpText={translate('ShowUnknownSeriesItemsHelpText')}
|
||||||
|
onChange={handleOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueOptions;
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { setQueueOption } from 'Store/Actions/queueActions';
|
|
||||||
import QueueOptions from './QueueOptions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.queue.options,
|
|
||||||
(options) => {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
onOptionChange: setQueueOption
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
|
||||||
@@ -26,4 +26,5 @@
|
|||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 70px;
|
width: 70px;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,481 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import ProgressBar from 'Components/ProgressBar';
|
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
|
||||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
|
||||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
|
||||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QueueStatusCell from './QueueStatusCell';
|
|
||||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
|
||||||
import TimeleftCell from './TimeleftCell';
|
|
||||||
import styles from './QueueRow.css';
|
|
||||||
|
|
||||||
class QueueRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isRemoveQueueItemModalOpen: false,
|
|
||||||
isInteractiveImportModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveQueueItemPress = () => {
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
|
|
||||||
const {
|
|
||||||
onRemoveQueueItemPress,
|
|
||||||
onQueueRowModalOpenOrClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onQueueRowModalOpenOrClose(false);
|
|
||||||
onRemoveQueueItemPress(blocklist, skipRedownload);
|
|
||||||
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveQueueItemModalClose = () => {
|
|
||||||
this.props.onQueueRowModalOpenOrClose(false);
|
|
||||||
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInteractiveImportPress = () => {
|
|
||||||
this.props.onQueueRowModalOpenOrClose(true);
|
|
||||||
|
|
||||||
this.setState({ isInteractiveImportModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInteractiveImportModalClose = () => {
|
|
||||||
this.props.onQueueRowModalOpenOrClose(false);
|
|
||||||
|
|
||||||
this.setState({ isInteractiveImportModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
downloadId,
|
|
||||||
title,
|
|
||||||
status,
|
|
||||||
trackedDownloadStatus,
|
|
||||||
trackedDownloadState,
|
|
||||||
statusMessages,
|
|
||||||
errorMessage,
|
|
||||||
series,
|
|
||||||
episode,
|
|
||||||
languages,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
customFormatScore,
|
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
outputPath,
|
|
||||||
downloadClient,
|
|
||||||
downloadClientHasPostImportCategory,
|
|
||||||
estimatedCompletionTime,
|
|
||||||
added,
|
|
||||||
timeleft,
|
|
||||||
size,
|
|
||||||
sizeleft,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
isGrabbing,
|
|
||||||
grabError,
|
|
||||||
isRemoving,
|
|
||||||
isSelected,
|
|
||||||
columns,
|
|
||||||
onSelectedChange,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isRemoveQueueItemModalOpen,
|
|
||||||
isInteractiveImportModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const progress = 100 - (sizeleft / size * 100);
|
|
||||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
|
||||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableSelectCell
|
|
||||||
id={id}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'status') {
|
|
||||||
return (
|
|
||||||
<QueueStatusCell
|
|
||||||
key={name}
|
|
||||||
sourceTitle={title}
|
|
||||||
status={status}
|
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
|
||||||
trackedDownloadState={trackedDownloadState}
|
|
||||||
statusMessages={statusMessages}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'series.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
series ?
|
|
||||||
<SeriesTitleLink
|
|
||||||
titleSlug={series.titleSlug}
|
|
||||||
title={series.title}
|
|
||||||
/> :
|
|
||||||
title
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episode') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
episode ?
|
|
||||||
<SeasonEpisodeNumber
|
|
||||||
seasonNumber={episode.seasonNumber}
|
|
||||||
episodeNumber={episode.episodeNumber}
|
|
||||||
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
|
||||||
seriesType={series.seriesType}
|
|
||||||
alternateTitles={series.alternateTitles}
|
|
||||||
sceneSeasonNumber={episode.sceneSeasonNumber}
|
|
||||||
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
|
||||||
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
|
|
||||||
unverifiedSceneNumbering={episode.unverifiedSceneNumbering}
|
|
||||||
/> :
|
|
||||||
'-'
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episodes.title') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
episode ?
|
|
||||||
<EpisodeTitleLink
|
|
||||||
episodeId={episode.id}
|
|
||||||
seriesId={series.id}
|
|
||||||
episodeFileId={episode.episodeFileId}
|
|
||||||
episodeTitle={episode.title}
|
|
||||||
showOpenSeriesButton={true}
|
|
||||||
/> :
|
|
||||||
'-'
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episodes.airDateUtc') {
|
|
||||||
if (episode) {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={episode.airDateUtc}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
-
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeLanguages
|
|
||||||
languages={languages}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
quality ?
|
|
||||||
<EpisodeQuality
|
|
||||||
quality={quality}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormatScore') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.customFormatScore}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
anchor={formatCustomFormatScore(
|
|
||||||
customFormatScore,
|
|
||||||
customFormats.length
|
|
||||||
)}
|
|
||||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'protocol') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{downloadClient}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'title') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{title}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'size') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>{formatBytes(size)}</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'outputPath') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{outputPath}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'estimatedCompletionTime') {
|
|
||||||
return (
|
|
||||||
<TimeleftCell
|
|
||||||
key={name}
|
|
||||||
status={status}
|
|
||||||
estimatedCompletionTime={estimatedCompletionTime}
|
|
||||||
timeleft={timeleft}
|
|
||||||
size={size}
|
|
||||||
sizeleft={sizeleft}
|
|
||||||
showRelativeDates={showRelativeDates}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'progress') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.progress}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
!!progress &&
|
|
||||||
<ProgressBar
|
|
||||||
progress={progress}
|
|
||||||
title={`${progress.toFixed(1)}%`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'added') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCellConnector
|
|
||||||
key={name}
|
|
||||||
date={added}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.actions}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
showInteractiveImport &&
|
|
||||||
<IconButton
|
|
||||||
name={icons.INTERACTIVE}
|
|
||||||
onPress={this.onInteractiveImportPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPending &&
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={icons.DOWNLOAD}
|
|
||||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={onGrabPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<SpinnerIconButton
|
|
||||||
title={translate('RemoveFromQueue')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={this.onRemoveQueueItemPress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<InteractiveImportModal
|
|
||||||
isOpen={isInteractiveImportModalOpen}
|
|
||||||
downloadId={downloadId}
|
|
||||||
title={title}
|
|
||||||
onModalClose={this.onInteractiveImportModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RemoveQueueItemModal
|
|
||||||
isOpen={isRemoveQueueItemModalOpen}
|
|
||||||
sourceTitle={title}
|
|
||||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
|
||||||
canIgnore={!!series}
|
|
||||||
isPending={isPending}
|
|
||||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
|
||||||
onModalClose={this.onRemoveQueueItemModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueRow.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string,
|
|
||||||
trackedDownloadState: PropTypes.string,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
series: PropTypes.object,
|
|
||||||
episode: PropTypes.object,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
indexer: PropTypes.string,
|
|
||||||
outputPath: PropTypes.string,
|
|
||||||
downloadClient: PropTypes.string,
|
|
||||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
added: PropTypes.string,
|
|
||||||
timeleft: PropTypes.string,
|
|
||||||
size: PropTypes.number,
|
|
||||||
sizeleft: PropTypes.number,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
grabError: PropTypes.object,
|
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
|
||||||
onRemoveQueueItemPress: PropTypes.func.isRequired,
|
|
||||||
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueRow.defaultProps = {
|
|
||||||
customFormats: [],
|
|
||||||
isGrabbing: false,
|
|
||||||
isRemoving: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueRow;
|
|
||||||
411
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
411
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||||
|
import { Error } from 'App/State/AppSectionState';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
|
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||||
|
import useEpisode from 'Episode/useEpisode';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
|
import useSeries from 'Series/useSeries';
|
||||||
|
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueueStatusCell from './QueueStatusCell';
|
||||||
|
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||||
|
import TimeleftCell from './TimeleftCell';
|
||||||
|
import styles from './QueueRow.css';
|
||||||
|
|
||||||
|
interface QueueRowProps {
|
||||||
|
id: number;
|
||||||
|
seriesId?: number;
|
||||||
|
episodeId?: number;
|
||||||
|
downloadId?: string;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats?: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
indexer?: string;
|
||||||
|
outputPath?: string;
|
||||||
|
downloadClient?: string;
|
||||||
|
downloadClientHasPostImportCategory?: boolean;
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
added?: string;
|
||||||
|
timeleft?: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
isGrabbing?: boolean;
|
||||||
|
grabError?: Error;
|
||||||
|
isRemoving?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
columns: Column[];
|
||||||
|
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||||
|
onQueueRowModalOpenOrClose: (isOpen: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueRow(props: QueueRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
seriesId,
|
||||||
|
episodeId,
|
||||||
|
downloadId,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus,
|
||||||
|
trackedDownloadState,
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
customFormats = [],
|
||||||
|
customFormatScore,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
outputPath,
|
||||||
|
downloadClient,
|
||||||
|
downloadClientHasPostImportCategory,
|
||||||
|
estimatedCompletionTime,
|
||||||
|
added,
|
||||||
|
timeleft,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
isGrabbing = false,
|
||||||
|
grabError,
|
||||||
|
isRemoving = false,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange,
|
||||||
|
onQueueRowModalOpenOrClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const series = useSeries(seriesId);
|
||||||
|
const episode = useEpisode(episodeId, 'episodes');
|
||||||
|
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const handleGrabPress = useCallback(() => {
|
||||||
|
dispatch(grabQueueItem({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
const handleInteractiveImportPress = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(true);
|
||||||
|
setIsInteractiveImportModalOpen(true);
|
||||||
|
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const handleInteractiveImportModalClose = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
setIsInteractiveImportModalOpen(false);
|
||||||
|
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const handleRemoveQueueItemPress = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(true);
|
||||||
|
setIsRemoveQueueItemModalOpen(true);
|
||||||
|
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const handleRemoveQueueItemModalConfirmed = useCallback(
|
||||||
|
(payload: RemovePressProps) => {
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
dispatch(removeQueueItem({ id, ...payload }));
|
||||||
|
setIsRemoveQueueItemModalOpen(false);
|
||||||
|
},
|
||||||
|
[id, setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveQueueItemModalClose = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
setIsRemoveQueueItemModalOpen(false);
|
||||||
|
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const progress = 100 - (sizeleft / size) * 100;
|
||||||
|
const showInteractiveImport =
|
||||||
|
status === 'completed' && trackedDownloadStatus === 'warning';
|
||||||
|
const isPending =
|
||||||
|
status === 'delay' || status === 'downloadClientUnavailable';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<QueueStatusCell
|
||||||
|
key={name}
|
||||||
|
sourceTitle={title}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'series.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{series ? (
|
||||||
|
<SeriesTitleLink
|
||||||
|
titleSlug={series.titleSlug}
|
||||||
|
title={series.title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episode') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{episode ? (
|
||||||
|
<SeasonEpisodeNumber
|
||||||
|
seasonNumber={episode.seasonNumber}
|
||||||
|
episodeNumber={episode.episodeNumber}
|
||||||
|
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
||||||
|
seriesType={series?.seriesType}
|
||||||
|
alternateTitles={series?.alternateTitles}
|
||||||
|
sceneSeasonNumber={episode.sceneSeasonNumber}
|
||||||
|
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
||||||
|
sceneAbsoluteEpisodeNumber={
|
||||||
|
episode.sceneAbsoluteEpisodeNumber
|
||||||
|
}
|
||||||
|
unverifiedSceneNumbering={episode.unverifiedSceneNumbering}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episodes.title') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{series && episode ? (
|
||||||
|
<EpisodeTitleLink
|
||||||
|
episodeId={episode.id}
|
||||||
|
seriesId={series.id}
|
||||||
|
episodeTitle={episode.title}
|
||||||
|
episodeEntity="episodes"
|
||||||
|
showOpenSeriesButton={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episodes.airDateUtc') {
|
||||||
|
if (episode) {
|
||||||
|
return <RelativeDateCell key={name} date={episode.airDateUtc} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TableRowCell key={name}>-</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{quality ? <EpisodeQuality quality={quality} /> : null}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'protocol') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<ProtocolLabel protocol={protocol} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return <TableRowCell key={name}>{indexer}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
return <TableRowCell key={name}>{downloadClient}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'title') {
|
||||||
|
return <TableRowCell key={name}>{title}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'size') {
|
||||||
|
return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'outputPath') {
|
||||||
|
return <TableRowCell key={name}>{outputPath}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'estimatedCompletionTime') {
|
||||||
|
return (
|
||||||
|
<TimeleftCell
|
||||||
|
key={name}
|
||||||
|
status={status}
|
||||||
|
estimatedCompletionTime={estimatedCompletionTime}
|
||||||
|
timeleft={timeleft}
|
||||||
|
size={size}
|
||||||
|
sizeleft={sizeleft}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'progress') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.progress}>
|
||||||
|
{!!progress && (
|
||||||
|
<ProgressBar
|
||||||
|
progress={progress}
|
||||||
|
title={`${progress.toFixed(1)}%`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'added') {
|
||||||
|
return <RelativeDateCell key={name} date={added} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.actions}>
|
||||||
|
{showInteractiveImport ? (
|
||||||
|
<IconButton
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
onPress={handleInteractiveImportPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPending ? (
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.DOWNLOAD}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={handleGrabPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SpinnerIconButton
|
||||||
|
title={translate('RemoveFromQueue')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={handleRemoveQueueItemPress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<InteractiveImportModal
|
||||||
|
isOpen={isInteractiveImportModalOpen}
|
||||||
|
downloadId={downloadId}
|
||||||
|
modalTitle={title}
|
||||||
|
onModalClose={handleInteractiveImportModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RemoveQueueItemModal
|
||||||
|
isOpen={isRemoveQueueItemModalOpen}
|
||||||
|
sourceTitle={title}
|
||||||
|
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||||
|
canIgnore={!!series}
|
||||||
|
isPending={isPending}
|
||||||
|
onRemovePress={handleRemoveQueueItemModalConfirmed}
|
||||||
|
onModalClose={handleRemoveQueueItemModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueRow;
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
|
||||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
|
||||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import QueueRow from './QueueRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSeriesSelector(),
|
|
||||||
createEpisodeSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(series, episode, uiSettings) => {
|
|
||||||
const result = {
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
|
|
||||||
result.series = series;
|
|
||||||
result.episode = episode;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
grabQueueItem,
|
|
||||||
removeQueueItem
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueueRowConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onGrabPress = () => {
|
|
||||||
this.props.grabQueueItem({ id: this.props.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveQueueItemPress = (payload) => {
|
|
||||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<QueueRow
|
|
||||||
{...this.props}
|
|
||||||
onGrabPress={this.onGrabPress}
|
|
||||||
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueRowConnector.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
episode: PropTypes.object,
|
|
||||||
grabQueueItem: PropTypes.func.isRequired,
|
|
||||||
removeQueueItem: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);
|
|
||||||
@@ -1,51 +1,59 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon, { IconProps } from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import { TooltipPosition } from 'Helpers/Props/tooltipPositions';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './QueueStatus.css';
|
import styles from './QueueStatus.css';
|
||||||
|
|
||||||
function getDetailedPopoverBody(statusMessages) {
|
function getDetailedPopoverBody(statusMessages: StatusMessage[]) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{
|
{statusMessages.map(({ title, messages }) => {
|
||||||
statusMessages.map(({ title, messages }) => {
|
return (
|
||||||
return (
|
<div
|
||||||
<div
|
key={title}
|
||||||
key={title}
|
className={messages.length ? undefined : styles.noMessages}
|
||||||
className={messages.length ? undefined: styles.noMessages}
|
>
|
||||||
>
|
{title}
|
||||||
{title}
|
<ul>
|
||||||
<ul>
|
{messages.map((message) => {
|
||||||
{
|
return <li key={message}>{message}</li>;
|
||||||
messages.map((message) => {
|
})}
|
||||||
return (
|
</ul>
|
||||||
<li key={message}>
|
</div>
|
||||||
{message}
|
);
|
||||||
</li>
|
})}
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function QueueStatus(props) {
|
interface QueueStatusProps {
|
||||||
|
sourceTitle: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
position: TooltipPosition;
|
||||||
|
canFlip?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueStatus(props: QueueStatusProps) {
|
||||||
const {
|
const {
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
status,
|
status,
|
||||||
trackedDownloadStatus,
|
trackedDownloadStatus = 'ok',
|
||||||
trackedDownloadState,
|
trackedDownloadState = 'downloading',
|
||||||
statusMessages,
|
statusMessages = [],
|
||||||
errorMessage,
|
errorMessage,
|
||||||
position,
|
position,
|
||||||
canFlip
|
canFlip = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const hasWarning = trackedDownloadStatus === 'warning';
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
@@ -53,7 +61,7 @@ function QueueStatus(props) {
|
|||||||
|
|
||||||
// status === 'downloading'
|
// status === 'downloading'
|
||||||
let iconName = icons.DOWNLOADING;
|
let iconName = icons.DOWNLOADING;
|
||||||
let iconKind = kinds.DEFAULT;
|
let iconKind: IconProps['kind'] = kinds.DEFAULT;
|
||||||
let title = translate('Downloading');
|
let title = translate('Downloading');
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
@@ -70,6 +78,11 @@ function QueueStatus(props) {
|
|||||||
iconName = icons.DOWNLOADED;
|
iconName = icons.DOWNLOADED;
|
||||||
title = translate('Downloaded');
|
title = translate('Downloaded');
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'importBlocked') {
|
||||||
|
title += ` - ${translate('UnableToImportAutomatically')}`;
|
||||||
|
iconKind = kinds.WARNING;
|
||||||
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'importPending') {
|
if (trackedDownloadState === 'importPending') {
|
||||||
title += ` - ${translate('WaitingToImport')}`;
|
title += ` - ${translate('WaitingToImport')}`;
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PURPLE;
|
||||||
@@ -110,7 +123,8 @@ function QueueStatus(props) {
|
|||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
const warningMessage =
|
||||||
|
errorMessage || translate('CheckDownloadClientForDetails');
|
||||||
title = translate('DownloadWarning', { warningMessage });
|
title = translate('DownloadWarning', { warningMessage });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,35 +142,17 @@ function QueueStatus(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={<Icon name={iconName} kind={iconKind} />}
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
kind={iconKind}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={title}
|
title={title}
|
||||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
body={
|
||||||
|
hasWarning || hasError
|
||||||
|
? getDetailedPopoverBody(statusMessages)
|
||||||
|
: sourceTitle
|
||||||
|
}
|
||||||
position={position}
|
position={position}
|
||||||
canFlip={canFlip}
|
canFlip={canFlip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueStatus.propTypes = {
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
|
|
||||||
canFlip: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueStatus.defaultProps = {
|
|
||||||
trackedDownloadStatus: 'ok',
|
|
||||||
trackedDownloadState: 'downloading',
|
|
||||||
canFlip: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueStatus;
|
export default QueueStatus;
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import { tooltipPositions } from 'Helpers/Props';
|
|
||||||
import QueueStatus from './QueueStatus';
|
|
||||||
import styles from './QueueStatusCell.css';
|
|
||||||
|
|
||||||
function QueueStatusCell(props) {
|
|
||||||
const {
|
|
||||||
sourceTitle,
|
|
||||||
status,
|
|
||||||
trackedDownloadStatus,
|
|
||||||
trackedDownloadState,
|
|
||||||
statusMessages,
|
|
||||||
errorMessage
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell className={styles.status}>
|
|
||||||
<QueueStatus
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
status={status}
|
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
|
||||||
trackedDownloadState={trackedDownloadState}
|
|
||||||
statusMessages={statusMessages}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
position={tooltipPositions.RIGHT}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueStatusCell.propTypes = {
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueStatusCell.defaultProps = {
|
|
||||||
trackedDownloadStatus: 'ok',
|
|
||||||
trackedDownloadState: 'downloading'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueStatusCell;
|
|
||||||
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
|
import QueueStatus from './QueueStatus';
|
||||||
|
import styles from './QueueStatusCell.css';
|
||||||
|
|
||||||
|
interface QueueStatusCellProps {
|
||||||
|
sourceTitle: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueStatusCell(props: QueueStatusCellProps) {
|
||||||
|
const {
|
||||||
|
sourceTitle,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus = 'ok',
|
||||||
|
trackedDownloadState = 'downloading',
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.status}>
|
||||||
|
<QueueStatus
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
position="right"
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueStatusCell;
|
||||||
@@ -12,7 +12,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './RemoveQueueItemModal.css';
|
import styles from './RemoveQueueItemModal.css';
|
||||||
|
|
||||||
interface RemovePressProps {
|
export interface RemovePressProps {
|
||||||
remove: boolean;
|
remove: boolean;
|
||||||
changeCategory: boolean;
|
changeCategory: boolean;
|
||||||
blocklist: boolean;
|
blocklist: boolean;
|
||||||
@@ -21,7 +21,7 @@ interface RemovePressProps {
|
|||||||
|
|
||||||
interface RemoveQueueItemModalProps {
|
interface RemoveQueueItemModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
sourceTitle: string;
|
sourceTitle?: string;
|
||||||
canChangeCategory: boolean;
|
canChangeCategory: boolean;
|
||||||
canIgnore: boolean;
|
canIgnore: boolean;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
@@ -39,7 +39,7 @@ type BlocklistMethod =
|
|||||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
sourceTitle,
|
sourceTitle = '',
|
||||||
canIgnore,
|
canIgnore,
|
||||||
canChangeCategory,
|
canChangeCategory,
|
||||||
isPending,
|
isPending,
|
||||||
@@ -118,6 +118,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
|||||||
{
|
{
|
||||||
key: 'blocklistAndSearch',
|
key: 'blocklistAndSearch',
|
||||||
value: translate('BlocklistAndSearch'),
|
value: translate('BlocklistAndSearch'),
|
||||||
|
isDisabled: isPending,
|
||||||
hint: multipleSelected
|
hint: multipleSelected
|
||||||
? translate('BlocklistAndSearchMultipleHint')
|
? translate('BlocklistAndSearchMultipleHint')
|
||||||
: translate('BlocklistAndSearchHint'),
|
: translate('BlocklistAndSearchHint'),
|
||||||
@@ -130,7 +131,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
|||||||
: translate('BlocklistOnlyHint'),
|
: translate('BlocklistOnlyHint'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [multipleSelected]);
|
}, [isPending, multipleSelected]);
|
||||||
|
|
||||||
const handleRemovalMethodChange = useCallback(
|
const handleRemovalMethodChange = useCallback(
|
||||||
({ value }: { value: RemovalMethod }) => {
|
({ value }: { value: RemovalMethod }) => {
|
||||||
|
|||||||
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||||
|
import createQueueStatusSelector from './createQueueStatusSelector';
|
||||||
|
|
||||||
|
function QueueStatus() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isConnected, isReconnecting } = useSelector(
|
||||||
|
(state: AppState) => state.app
|
||||||
|
);
|
||||||
|
const { isPopulated, count, errors, warnings } = useSelector(
|
||||||
|
createQueueStatusSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasReconnecting = usePrevious(isReconnecting);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPopulated) {
|
||||||
|
dispatch(fetchQueueStatus());
|
||||||
|
}
|
||||||
|
}, [isPopulated, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isConnected && wasReconnecting) {
|
||||||
|
dispatch(fetchQueueStatus());
|
||||||
|
}
|
||||||
|
}, [isConnected, wasReconnecting, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSidebarStatus count={count} errors={errors} warnings={warnings} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueStatus;
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
|
||||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.app,
|
|
||||||
(state) => state.queue.status,
|
|
||||||
(state) => state.queue.options.includeUnknownSeriesItems,
|
|
||||||
(app, status, includeUnknownSeriesItems) => {
|
|
||||||
const {
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
unknownErrors,
|
|
||||||
unknownWarnings,
|
|
||||||
count,
|
|
||||||
totalCount
|
|
||||||
} = status.item;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isConnected: app.isConnected,
|
|
||||||
isReconnecting: app.isReconnecting,
|
|
||||||
isPopulated: status.isPopulated,
|
|
||||||
...status.item,
|
|
||||||
count: includeUnknownSeriesItems ? totalCount : count,
|
|
||||||
errors: includeUnknownSeriesItems ? errors || unknownErrors : errors,
|
|
||||||
warnings: includeUnknownSeriesItems ? warnings || unknownWarnings : warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchQueueStatus
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueueStatusConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.props.isPopulated) {
|
|
||||||
this.props.fetchQueueStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this.props.isConnected && prevProps.isReconnecting) {
|
|
||||||
this.props.fetchQueueStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PageSidebarStatus
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueStatusConnector.propTypes = {
|
|
||||||
isConnected: PropTypes.bool.isRequired,
|
|
||||||
isReconnecting: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
fetchQueueStatus: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function createQueueStatusSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.status.isPopulated,
|
||||||
|
(state: AppState) => state.queue.status.item,
|
||||||
|
(state: AppState) => state.queue.options.includeUnknownSeriesItems,
|
||||||
|
(isPopulated, status, includeUnknownSeriesItems) => {
|
||||||
|
const {
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
unknownErrors,
|
||||||
|
unknownWarnings,
|
||||||
|
count,
|
||||||
|
totalCount,
|
||||||
|
} = status;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...status,
|
||||||
|
isPopulated,
|
||||||
|
count: includeUnknownSeriesItems ? totalCount : count,
|
||||||
|
errors: includeUnknownSeriesItems ? errors || unknownErrors : errors,
|
||||||
|
warnings: includeUnknownSeriesItems
|
||||||
|
? warnings || unknownWarnings
|
||||||
|
: warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createQueueStatusSelector;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React 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';
|
||||||
@@ -11,7 +10,18 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './TimeleftCell.css';
|
import styles from './TimeleftCell.css';
|
||||||
|
|
||||||
function TimeleftCell(props) {
|
interface TimeleftCellProps {
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
timeleft?: string;
|
||||||
|
status: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
showRelativeDates: boolean;
|
||||||
|
shortDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimeleftCell(props: TimeleftCellProps) {
|
||||||
const {
|
const {
|
||||||
estimatedCompletionTime,
|
estimatedCompletionTime,
|
||||||
timeleft,
|
timeleft,
|
||||||
@@ -20,16 +30,18 @@ function TimeleftCell(props) {
|
|||||||
sizeleft,
|
sizeleft,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat
|
timeFormat,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (status === 'delay') {
|
if (status === 'delay') {
|
||||||
const date = getRelativeDate({
|
const date = getRelativeDate({
|
||||||
date: estimatedCompletionTime,
|
date: estimatedCompletionTime,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
showRelativeDates
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||||
|
includeMinuteZero: true,
|
||||||
});
|
});
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell className={styles.timeleft}>
|
<TableRowCell className={styles.timeleft}>
|
||||||
@@ -47,9 +59,11 @@ function TimeleftCell(props) {
|
|||||||
const date = getRelativeDate({
|
const date = getRelativeDate({
|
||||||
date: estimatedCompletionTime,
|
date: estimatedCompletionTime,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
showRelativeDates
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||||
|
includeMinuteZero: true,
|
||||||
});
|
});
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell className={styles.timeleft}>
|
<TableRowCell className={styles.timeleft}>
|
||||||
@@ -64,11 +78,7 @@ function TimeleftCell(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||||
return (
|
return <TableRowCell className={styles.timeleft}>-</TableRowCell>;
|
||||||
<TableRowCell className={styles.timeleft}>
|
|
||||||
-
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalSize = formatBytes(size);
|
const totalSize = formatBytes(size);
|
||||||
@@ -84,15 +94,4 @@ function TimeleftCell(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeleftCell.propTypes = {
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
timeleft: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
sizeleft: PropTypes.number.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TimeleftCell;
|
export default TimeleftCell;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
@@ -129,7 +130,8 @@ class AddNewSeries extends Component {
|
|||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('AddNewSeriesError')}
|
{translate('AddNewSeriesError')}
|
||||||
</div>
|
</div>
|
||||||
<div>{getErrorMessage(error)}</div>
|
|
||||||
|
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,16 @@
|
|||||||
height: 55px;
|
height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.originalLanguageName,
|
||||||
|
.network,
|
||||||
|
.genres {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genres {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
.tvdbLink {
|
.tvdbLink {
|
||||||
composes: link from '~Components/Link/Link.css';
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
interface CssExports {
|
interface CssExports {
|
||||||
'alreadyExistsIcon': string;
|
'alreadyExistsIcon': string;
|
||||||
'content': string;
|
'content': string;
|
||||||
|
'genres': string;
|
||||||
'icons': string;
|
'icons': string;
|
||||||
|
'network': string;
|
||||||
|
'originalLanguageName': string;
|
||||||
'overlay': string;
|
'overlay': string;
|
||||||
'overview': string;
|
'overview': string;
|
||||||
'poster': string;
|
'poster': string;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Label from 'Components/Label';
|
|||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import MetadataAttribution from 'Components/MetadataAttribution';
|
import MetadataAttribution from 'Components/MetadataAttribution';
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import SeriesGenres from 'Series/SeriesGenres';
|
||||||
import SeriesPoster from 'Series/SeriesPoster';
|
import SeriesPoster from 'Series/SeriesPoster';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AddNewSeriesModal from './AddNewSeriesModal';
|
import AddNewSeriesModal from './AddNewSeriesModal';
|
||||||
@@ -55,6 +56,8 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
titleSlug,
|
titleSlug,
|
||||||
year,
|
year,
|
||||||
network,
|
network,
|
||||||
|
originalLanguage,
|
||||||
|
genres,
|
||||||
status,
|
status,
|
||||||
overview,
|
overview,
|
||||||
statistics,
|
statistics,
|
||||||
@@ -145,14 +148,49 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
<Label size={sizes.LARGE}>
|
<Label size={sizes.LARGE}>
|
||||||
<HeartRating
|
<HeartRating
|
||||||
rating={ratings.value}
|
rating={ratings.value}
|
||||||
|
votes={ratings.votes}
|
||||||
iconSize={13}
|
iconSize={13}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
|
{
|
||||||
|
originalLanguage?.name ?
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<Icon
|
||||||
|
name={icons.LANGUAGE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={styles.originalLanguageName}>
|
||||||
|
{originalLanguage.name}
|
||||||
|
</span>
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
network ?
|
network ?
|
||||||
<Label size={sizes.LARGE}>
|
<Label size={sizes.LARGE}>
|
||||||
{network}
|
<Icon
|
||||||
|
name={icons.NETWORK}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className={styles.network}>
|
||||||
|
{network}
|
||||||
|
</span>
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
genres.length > 0 ?
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<Icon
|
||||||
|
name={icons.GENRE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<SeriesGenres className={styles.genres} genres={genres} />
|
||||||
</Label> :
|
</Label> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -218,6 +256,8 @@ AddNewSeriesSearchResult.propTypes = {
|
|||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
network: PropTypes.string,
|
network: PropTypes.string,
|
||||||
|
originalLanguage: PropTypes.object,
|
||||||
|
genres: PropTypes.arrayOf(PropTypes.string),
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
statistics: PropTypes.object.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
@@ -229,4 +269,8 @@ AddNewSeriesSearchResult.propTypes = {
|
|||||||
isSmallScreen: PropTypes.bool.isRequired
|
isSmallScreen: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AddNewSeriesSearchResult.defaultProps = {
|
||||||
|
genres: []
|
||||||
|
};
|
||||||
|
|
||||||
export default AddNewSeriesSearchResult;
|
export default AddNewSeriesSearchResult;
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
.inputContainer {
|
.inputContainer {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
|
|
||||||
div {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { ConnectedRouter } from 'connected-react-router';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import DocumentTitle from 'react-document-title';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import PageConnector from 'Components/Page/PageConnector';
|
|
||||||
import ApplyTheme from './ApplyTheme';
|
|
||||||
import AppRoutes from './AppRoutes';
|
|
||||||
|
|
||||||
function App({ store, history }) {
|
|
||||||
return (
|
|
||||||
<DocumentTitle title={window.Sonarr.instanceName}>
|
|
||||||
<Provider store={store}>
|
|
||||||
<ConnectedRouter history={history}>
|
|
||||||
<ApplyTheme />
|
|
||||||
<PageConnector>
|
|
||||||
<AppRoutes app={App} />
|
|
||||||
</PageConnector>
|
|
||||||
</ConnectedRouter>
|
|
||||||
</Provider>
|
|
||||||
</DocumentTitle>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
App.propTypes = {
|
|
||||||
store: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
35
frontend/src/App/App.tsx
Normal file
35
frontend/src/App/App.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||||
|
import React from 'react';
|
||||||
|
import DocumentTitle from 'react-document-title';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { Store } from 'redux';
|
||||||
|
import PageConnector from 'Components/Page/PageConnector';
|
||||||
|
import ApplyTheme from './ApplyTheme';
|
||||||
|
import AppRoutes from './AppRoutes';
|
||||||
|
|
||||||
|
interface AppProps {
|
||||||
|
store: Store;
|
||||||
|
history: ConnectedRouterProps['history'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
function App({ store, history }: AppProps) {
|
||||||
|
return (
|
||||||
|
<DocumentTitle title={window.Sonarr.instanceName}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<ApplyTheme />
|
||||||
|
<PageConnector>
|
||||||
|
<AppRoutes />
|
||||||
|
</PageConnector>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</DocumentTitle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
|
||||||
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
|
||||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
|
||||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
|
||||||
import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
|
|
||||||
import ImportSeries from 'AddSeries/ImportSeries/ImportSeries';
|
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
|
||||||
import NotFound from 'Components/NotFound';
|
|
||||||
import Switch from 'Components/Router/Switch';
|
|
||||||
import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector';
|
|
||||||
import SeriesIndex from 'Series/Index/SeriesIndex';
|
|
||||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
|
||||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
|
||||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
|
||||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
|
||||||
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
|
||||||
import MetadataSourceSettings from 'Settings/MetadataSource/MetadataSourceSettings';
|
|
||||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
|
||||||
import Profiles from 'Settings/Profiles/Profiles';
|
|
||||||
import QualityConnector from 'Settings/Quality/QualityConnector';
|
|
||||||
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';
|
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
|
||||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
|
||||||
|
|
||||||
function AppRoutes(props) {
|
|
||||||
const {
|
|
||||||
app
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{/*
|
|
||||||
Series
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
component={SeriesIndex}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
window.Sonarr.urlBase &&
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
addUrlBase={false}
|
|
||||||
render={() => {
|
|
||||||
return (
|
|
||||||
<Redirect
|
|
||||||
to={getPathWithUrlBase('/')}
|
|
||||||
component={app}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/add/new"
|
|
||||||
component={AddNewSeriesConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/add/import"
|
|
||||||
component={ImportSeries}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/serieseditor"
|
|
||||||
exact={true}
|
|
||||||
render={() => {
|
|
||||||
return (
|
|
||||||
<Redirect
|
|
||||||
to={getPathWithUrlBase('/')}
|
|
||||||
component={app}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/seasonpass"
|
|
||||||
exact={true}
|
|
||||||
render={() => {
|
|
||||||
return (
|
|
||||||
<Redirect
|
|
||||||
to={getPathWithUrlBase('/')}
|
|
||||||
component={app}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/series/:titleSlug"
|
|
||||||
component={SeriesDetailsPageConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Calendar
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/calendar"
|
|
||||||
component={CalendarPageConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Activity
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/activity/history"
|
|
||||||
component={HistoryConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/activity/queue"
|
|
||||||
component={QueueConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/activity/blocklist"
|
|
||||||
component={BlocklistConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Wanted
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/wanted/missing"
|
|
||||||
component={MissingConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/wanted/cutoffunmet"
|
|
||||||
component={CutoffUnmetConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Settings
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/settings"
|
|
||||||
component={Settings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/mediamanagement"
|
|
||||||
component={MediaManagementConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/profiles"
|
|
||||||
component={Profiles}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/quality"
|
|
||||||
component={QualityConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/customformats"
|
|
||||||
component={CustomFormatSettingsPage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/indexers"
|
|
||||||
component={IndexerSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/downloadclients"
|
|
||||||
component={DownloadClientSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/importlists"
|
|
||||||
component={ImportListSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/connect"
|
|
||||||
component={NotificationSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/metadata"
|
|
||||||
component={MetadataSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/metadatasource"
|
|
||||||
component={MetadataSourceSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/tags"
|
|
||||||
component={TagSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/general"
|
|
||||||
component={GeneralSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/ui"
|
|
||||||
component={UISettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
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;
|
|
||||||
167
frontend/src/App/AppRoutes.tsx
Normal file
167
frontend/src/App/AppRoutes.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
|
import Blocklist from 'Activity/Blocklist/Blocklist';
|
||||||
|
import History from 'Activity/History/History';
|
||||||
|
import Queue from 'Activity/Queue/Queue';
|
||||||
|
import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
|
||||||
|
import ImportSeries from 'AddSeries/ImportSeries/ImportSeries';
|
||||||
|
import CalendarPage from 'Calendar/CalendarPage';
|
||||||
|
import NotFound from 'Components/NotFound';
|
||||||
|
import Switch from 'Components/Router/Switch';
|
||||||
|
import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector';
|
||||||
|
import SeriesIndex from 'Series/Index/SeriesIndex';
|
||||||
|
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||||
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
|
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||||
|
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||||
|
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
||||||
|
import MetadataSourceSettings from 'Settings/MetadataSource/MetadataSourceSettings';
|
||||||
|
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||||
|
import Profiles from 'Settings/Profiles/Profiles';
|
||||||
|
import QualityConnector from 'Settings/Quality/QualityConnector';
|
||||||
|
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';
|
||||||
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||||
|
|
||||||
|
function RedirectWithUrlBase() {
|
||||||
|
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
{/*
|
||||||
|
Series
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/" component={SeriesIndex} />
|
||||||
|
|
||||||
|
{window.Sonarr.urlBase && (
|
||||||
|
<Route
|
||||||
|
exact={true}
|
||||||
|
path="/"
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
addUrlBase={false}
|
||||||
|
render={RedirectWithUrlBase}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Route path="/add/new" component={AddNewSeriesConnector} />
|
||||||
|
|
||||||
|
<Route path="/add/import" component={ImportSeries} />
|
||||||
|
|
||||||
|
<Route path="/serieseditor" exact={true} render={RedirectWithUrlBase} />
|
||||||
|
|
||||||
|
<Route path="/seasonpass" exact={true} render={RedirectWithUrlBase} />
|
||||||
|
|
||||||
|
<Route path="/series/:titleSlug" component={SeriesDetailsPageConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Calendar
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/calendar" component={CalendarPage} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Activity
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/activity/history" component={History} />
|
||||||
|
|
||||||
|
<Route path="/activity/queue" component={Queue} />
|
||||||
|
|
||||||
|
<Route path="/activity/blocklist" component={Blocklist} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Wanted
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/wanted/missing" component={MissingConnector} />
|
||||||
|
|
||||||
|
<Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Settings
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/settings" component={Settings} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/mediamanagement"
|
||||||
|
component={MediaManagementConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/profiles" component={Profiles} />
|
||||||
|
|
||||||
|
<Route path="/settings/quality" component={QualityConnector} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/customformats"
|
||||||
|
component={CustomFormatSettingsPage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/downloadclients"
|
||||||
|
component={DownloadClientSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/importlists"
|
||||||
|
component={ImportListSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/connect" component={NotificationSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/metadata" component={MetadataSettings} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/metadatasource"
|
||||||
|
component={MetadataSourceSettings}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/tags" component={TagSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/general" component={GeneralSettingsConnector} />
|
||||||
|
|
||||||
|
<Route path="/settings/ui" component={UISettingsConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
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.Sonarr.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.Sonarr.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.Sonarr.theme,
|
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.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,10 +1,16 @@
|
|||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import Column from 'Components/Table/Column';
|
||||||
import { FilterBuilderProp } from './AppState';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
|
import { ValidationFailure } from 'typings/pending';
|
||||||
|
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
status?: number;
|
||||||
message: string;
|
responseJSON:
|
||||||
};
|
| {
|
||||||
|
message: string | undefined;
|
||||||
|
}
|
||||||
|
| ValidationFailure[]
|
||||||
|
| undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionDeleteState {
|
export interface AppSectionDeleteState {
|
||||||
@@ -18,11 +24,18 @@ export interface AppSectionSaveState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PagedAppSectionState {
|
export interface PagedAppSectionState {
|
||||||
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
totalRecords?: 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>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +48,13 @@ export interface AppSectionSchemaState<T> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionItemSchemaState<T> {
|
||||||
|
isSchemaFetching: boolean;
|
||||||
|
isSchemaPopulated: boolean;
|
||||||
|
schemaError: Error;
|
||||||
|
schema: T;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppSectionItemState<T> {
|
export interface AppSectionItemState<T> {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
@@ -43,6 +63,16 @@ export interface AppSectionItemState<T> {
|
|||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionProviderState<T>
|
||||||
|
extends AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
items: T[];
|
||||||
|
pendingChanges: Partial<T>;
|
||||||
|
}
|
||||||
|
|
||||||
interface AppSectionState<T> {
|
interface AppSectionState<T> {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
|
import CaptchaAppState from './CaptchaAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||||
import EpisodesAppState from './EpisodesAppState';
|
import EpisodesAppState from './EpisodesAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
|
import OAuthAppState from './OAuthAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
|
import PathsAppState from './PathsAppState';
|
||||||
|
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
|
import ReleasesAppState from './ReleasesAppState';
|
||||||
import RootFolderAppState from './RootFolderAppState';
|
import RootFolderAppState from './RootFolderAppState';
|
||||||
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
import SystemAppState from './SystemAppState';
|
import SystemAppState from './SystemAppState';
|
||||||
import TagsAppState from './TagsAppState';
|
import TagsAppState from './TagsAppState';
|
||||||
|
import WantedAppState from './WantedAppState';
|
||||||
|
|
||||||
interface FilterBuilderPropOption {
|
interface FilterBuilderPropOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -35,19 +41,25 @@ export interface PropertyFilter {
|
|||||||
export interface Filter {
|
export interface Filter {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
filers: PropertyFilter[];
|
filters: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomFilter {
|
export interface CustomFilter {
|
||||||
id: number;
|
id: number;
|
||||||
type: string;
|
type: string;
|
||||||
label: string;
|
label: string;
|
||||||
filers: PropertyFilter[];
|
filters: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
|
isConnected: boolean;
|
||||||
|
isReconnecting: boolean;
|
||||||
|
isSidebarVisible: boolean;
|
||||||
|
version: string;
|
||||||
|
prevVersion?: string;
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
|
isLargeScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
@@ -57,19 +69,27 @@ interface AppState {
|
|||||||
app: AppSectionState;
|
app: AppSectionState;
|
||||||
blocklist: BlocklistAppState;
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
|
captcha: CaptchaAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
episodeFiles: EpisodeFilesAppState;
|
episodeFiles: EpisodeFilesAppState;
|
||||||
|
episodeHistory: HistoryAppState;
|
||||||
|
episodes: EpisodesAppState;
|
||||||
episodesSelection: EpisodesAppState;
|
episodesSelection: EpisodesAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
|
oAuth: OAuthAppState;
|
||||||
parse: ParseAppState;
|
parse: ParseAppState;
|
||||||
|
paths: PathsAppState;
|
||||||
|
providerOptions: ProviderOptionsAppState;
|
||||||
queue: QueueAppState;
|
queue: QueueAppState;
|
||||||
|
releases: ReleasesAppState;
|
||||||
rootFolders: RootFolderAppState;
|
rootFolders: RootFolderAppState;
|
||||||
series: SeriesAppState;
|
series: SeriesAppState;
|
||||||
seriesIndex: SeriesIndexAppState;
|
seriesIndex: SeriesIndexAppState;
|
||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
system: SystemAppState;
|
system: SystemAppState;
|
||||||
tags: TagsAppState;
|
tags: TagsAppState;
|
||||||
|
wanted: WantedAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppState;
|
export default AppState;
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import Blocklist from 'typings/Blocklist';
|
import Blocklist from 'typings/Blocklist';
|
||||||
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
|
} from './AppSectionState';
|
||||||
|
|
||||||
interface BlocklistAppState
|
interface BlocklistAppState
|
||||||
extends AppSectionState<Blocklist>,
|
extends AppSectionState<Blocklist>,
|
||||||
AppSectionFilterState<Blocklist> {}
|
AppSectionFilterState<Blocklist>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {
|
||||||
|
isRemoving: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default BlocklistAppState;
|
export default BlocklistAppState;
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
|
import moment from 'moment';
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Episode from 'Episode/Episode';
|
import { CalendarView } from 'Calendar/calendarViews';
|
||||||
|
import { CalendarItem } from 'typings/Calendar';
|
||||||
|
|
||||||
|
interface CalendarOptions {
|
||||||
|
showEpisodeInformation: boolean;
|
||||||
|
showFinaleIcon: boolean;
|
||||||
|
showSpecialIcon: boolean;
|
||||||
|
showCutoffUnmetIcon: boolean;
|
||||||
|
collapseMultipleEpisodes: boolean;
|
||||||
|
fullColorEvents: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface CalendarAppState
|
interface CalendarAppState
|
||||||
extends AppSectionState<Episode>,
|
extends AppSectionState<CalendarItem>,
|
||||||
AppSectionFilterState<Episode> {}
|
AppSectionFilterState<CalendarItem> {
|
||||||
|
searchMissingCommandId: number | null;
|
||||||
|
start: moment.Moment;
|
||||||
|
end: moment.Moment;
|
||||||
|
dates: string[];
|
||||||
|
time: string;
|
||||||
|
view: CalendarView;
|
||||||
|
options: CalendarOptions;
|
||||||
|
}
|
||||||
|
|
||||||
export default CalendarAppState;
|
export default CalendarAppState;
|
||||||
|
|||||||
11
frontend/src/App/State/CaptchaAppState.ts
Normal file
11
frontend/src/App/State/CaptchaAppState.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
interface CaptchaAppState {
|
||||||
|
refreshing: false;
|
||||||
|
token: string;
|
||||||
|
siteKey: unknown;
|
||||||
|
secretToken: unknown;
|
||||||
|
ray: unknown;
|
||||||
|
stoken: unknown;
|
||||||
|
responseUrl: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CaptchaAppState;
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import History from 'typings/History';
|
import History from 'typings/History';
|
||||||
|
|
||||||
interface HistoryAppState
|
interface HistoryAppState
|
||||||
extends AppSectionState<History>,
|
extends AppSectionState<History>,
|
||||||
AppSectionFilterState<History> {}
|
AppSectionFilterState<History>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {}
|
||||||
|
|
||||||
export default HistoryAppState;
|
export default HistoryAppState;
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
|
|
||||||
import ImportMode from 'InteractiveImport/ImportMode';
|
import ImportMode from 'InteractiveImport/ImportMode';
|
||||||
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
||||||
|
|
||||||
|
interface FavoriteFolder {
|
||||||
|
folder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecentFolder {
|
||||||
|
folder: string;
|
||||||
|
lastUsed: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
||||||
originalItems: InteractiveImport[];
|
originalItems: InteractiveImport[];
|
||||||
importMode: ImportMode;
|
importMode: ImportMode;
|
||||||
|
favoriteFolders: FavoriteFolder[];
|
||||||
recentFolders: RecentFolder[];
|
recentFolders: RecentFolder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
frontend/src/App/State/MetadataAppState.ts
Normal file
6
frontend/src/App/State/MetadataAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { AppSectionProviderState } from 'App/State/AppSectionState';
|
||||||
|
import Metadata from 'typings/Metadata';
|
||||||
|
|
||||||
|
type MetadataAppState = AppSectionProviderState<Metadata>;
|
||||||
|
|
||||||
|
export default MetadataAppState;
|
||||||
9
frontend/src/App/State/OAuthAppState.ts
Normal file
9
frontend/src/App/State/OAuthAppState.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Error } from './AppSectionState';
|
||||||
|
|
||||||
|
interface OAuthAppState {
|
||||||
|
authorizing: boolean;
|
||||||
|
result: Record<string, unknown> | null;
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OAuthAppState;
|
||||||
29
frontend/src/App/State/PathsAppState.ts
Normal file
29
frontend/src/App/State/PathsAppState.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
interface BasePath {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
lastModified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface File extends BasePath {
|
||||||
|
type: 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Folder extends BasePath {
|
||||||
|
type: 'folder';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathType = 'file' | 'folder' | 'drive' | 'computer' | 'parent';
|
||||||
|
export type Path = File | Folder;
|
||||||
|
|
||||||
|
interface PathsAppState {
|
||||||
|
currentPath: string;
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
directories: Folder[];
|
||||||
|
files: File[];
|
||||||
|
parent: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PathsAppState;
|
||||||
22
frontend/src/App/State/ProviderOptionsAppState.ts
Normal file
22
frontend/src/App/State/ProviderOptionsAppState.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import Field, { FieldSelectOption } from 'typings/Field';
|
||||||
|
|
||||||
|
export interface ProviderOptions {
|
||||||
|
fields?: Field[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsDevice {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsAppState {
|
||||||
|
devices: AppSectionState<ProviderOptionsDevice>;
|
||||||
|
servers: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
newznabCategories: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getProfiles: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getTags: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getRootFolders: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProviderOptionsAppState;
|
||||||
@@ -3,15 +3,29 @@ import AppSectionState, {
|
|||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
Error,
|
Error,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
} from './AppSectionState';
|
} from './AppSectionState';
|
||||||
|
|
||||||
|
export interface QueueStatus {
|
||||||
|
totalCount: number;
|
||||||
|
count: number;
|
||||||
|
unknownCount: number;
|
||||||
|
errors: boolean;
|
||||||
|
warnings: boolean;
|
||||||
|
unknownErrors: boolean;
|
||||||
|
unknownWarnings: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||||
params: unknown;
|
params: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueuePagedAppState
|
export interface QueuePagedAppState
|
||||||
extends AppSectionState<Queue>,
|
extends AppSectionState<Queue>,
|
||||||
AppSectionFilterState<Queue> {
|
AppSectionFilterState<Queue>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {
|
||||||
isGrabbing: boolean;
|
isGrabbing: boolean;
|
||||||
grabError: Error;
|
grabError: Error;
|
||||||
isRemoving: boolean;
|
isRemoving: boolean;
|
||||||
@@ -19,9 +33,12 @@ export interface QueuePagedAppState
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface QueueAppState {
|
interface QueueAppState {
|
||||||
status: AppSectionItemState<Queue>;
|
status: AppSectionItemState<QueueStatus>;
|
||||||
details: QueueDetailsAppState;
|
details: QueueDetailsAppState;
|
||||||
paged: QueuePagedAppState;
|
paged: QueuePagedAppState;
|
||||||
|
options: {
|
||||||
|
includeUnknownSeriesItems: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default QueueAppState;
|
export default QueueAppState;
|
||||||
|
|||||||
10
frontend/src/App/State/ReleasesAppState.ts
Normal file
10
frontend/src/App/State/ReleasesAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import Release from 'typings/Release';
|
||||||
|
|
||||||
|
interface ReleasesAppState
|
||||||
|
extends AppSectionState<Release>,
|
||||||
|
AppSectionFilterState<Release> {}
|
||||||
|
|
||||||
|
export default ReleasesAppState;
|
||||||
@@ -3,7 +3,7 @@ import AppSectionState, {
|
|||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import Series from 'Series/Series';
|
import Series from 'Series/Series';
|
||||||
import { Filter, FilterBuilderProp } from './AppState';
|
import { Filter, FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ export interface SeriesIndexAppState {
|
|||||||
showTitle: boolean;
|
showTitle: boolean;
|
||||||
showMonitored: boolean;
|
showMonitored: boolean;
|
||||||
showQualityProfile: boolean;
|
showQualityProfile: boolean;
|
||||||
|
showTags: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export interface SeriesIndexAppState {
|
|||||||
showSeasonCount: boolean;
|
showSeasonCount: boolean;
|
||||||
showPath: boolean;
|
showPath: boolean;
|
||||||
showSizeOnDisk: boolean;
|
showSizeOnDisk: boolean;
|
||||||
|
showTags: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -57,6 +59,8 @@ interface SeriesAppState
|
|||||||
deleteOptions: {
|
deleteOptions: {
|
||||||
addImportListExclusion: boolean;
|
addImportListExclusion: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pendingChanges: Partial<Series>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SeriesAppState;
|
export default SeriesAppState;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionItemSchemaState,
|
||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
|
||||||
PagedAppSectionState,
|
PagedAppSectionState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||||
@@ -14,13 +15,30 @@ import Indexer from 'typings/Indexer';
|
|||||||
import IndexerFlag from 'typings/IndexerFlag';
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import General from 'typings/Settings/General';
|
||||||
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
import MetadataAppState from './MetadataAppState';
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {
|
||||||
|
isTestingAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GeneralAppState
|
||||||
|
extends AppSectionItemState<General>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NamingAppState
|
||||||
|
extends AppSectionItemState<NamingConfig>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -29,7 +47,9 @@ export interface ImportListAppState
|
|||||||
export interface IndexerAppState
|
export interface IndexerAppState
|
||||||
extends AppSectionState<Indexer>,
|
extends AppSectionState<Indexer>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {
|
||||||
|
isTestingAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NotificationAppState
|
export interface NotificationAppState
|
||||||
extends AppSectionState<Notification>,
|
extends AppSectionState<Notification>,
|
||||||
@@ -37,7 +57,18 @@ export interface NotificationAppState
|
|||||||
|
|
||||||
export interface QualityProfilesAppState
|
export interface QualityProfilesAppState
|
||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionItemSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
|
export interface ReleaseProfilesAppState
|
||||||
|
extends AppSectionState<ReleaseProfile>,
|
||||||
|
AppSectionSaveState {
|
||||||
|
pendingChanges: Partial<ReleaseProfile>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomFormatAppState
|
||||||
|
extends AppSectionState<CustomFormat>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface ImportListOptionsSettingsAppState
|
export interface ImportListOptionsSettingsAppState
|
||||||
extends AppSectionItemState<ImportListOptionsSettings>,
|
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||||
@@ -57,15 +88,21 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
|||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
advancedSettings: boolean;
|
||||||
|
customFormats: CustomFormatAppState;
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
|
general: GeneralAppState;
|
||||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||||
importListOptions: ImportListOptionsSettingsAppState;
|
importListOptions: ImportListOptionsSettingsAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
|
metadata: MetadataAppState;
|
||||||
|
naming: NamingAppState;
|
||||||
|
namingExamples: NamingExamplesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
qualityProfiles: QualityProfilesAppState;
|
qualityProfiles: QualityProfilesAppState;
|
||||||
|
releaseProfiles: ReleaseProfilesAppState;
|
||||||
ui: UiSettingsAppState;
|
ui: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
|
import DiskSpace from 'typings/DiskSpace';
|
||||||
|
import Health from 'typings/Health';
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import { AppSectionItemState } from './AppSectionState';
|
import Task from 'typings/Task';
|
||||||
|
import Update from 'typings/Update';
|
||||||
|
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||||
|
|
||||||
|
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
||||||
|
export type HealthAppState = AppSectionState<Health>;
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
|
export type TaskAppState = AppSectionState<Task>;
|
||||||
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
|
diskSpace: DiskSpaceAppState;
|
||||||
|
health: HealthAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
|
tasks: TaskAppState;
|
||||||
|
updates: UpdateAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemAppState;
|
export default SystemAppState;
|
||||||
|
|||||||
13
frontend/src/App/State/WantedAppState.ts
Normal file
13
frontend/src/App/State/WantedAppState.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import Episode from 'Episode/Episode';
|
||||||
|
|
||||||
|
type WantedCutoffUnmetAppState = AppSectionState<Episode>;
|
||||||
|
|
||||||
|
type WantedMissingAppState = AppSectionState<Episode>;
|
||||||
|
|
||||||
|
interface WantedAppState {
|
||||||
|
cutoffUnmet: WantedCutoffUnmetAppState;
|
||||||
|
missing: WantedMissingAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WantedAppState;
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import AgendaEventConnector from './AgendaEventConnector';
|
|
||||||
import styles from './Agenda.css';
|
|
||||||
|
|
||||||
function Agenda(props) {
|
|
||||||
const {
|
|
||||||
items
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.agenda}>
|
|
||||||
{
|
|
||||||
items.map((item, index) => {
|
|
||||||
const momentDate = moment(item.airDateUtc);
|
|
||||||
const showDate = index === 0 ||
|
|
||||||
!moment(items[index - 1].airDateUtc).isSame(momentDate, 'day');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgendaEventConnector
|
|
||||||
key={item.id}
|
|
||||||
episodeId={item.id}
|
|
||||||
showDate={showDate}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Agenda.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Agenda;
|
|
||||||
25
frontend/src/Calendar/Agenda/Agenda.tsx
Normal file
25
frontend/src/Calendar/Agenda/Agenda.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import AgendaEvent from './AgendaEvent';
|
||||||
|
import styles from './Agenda.css';
|
||||||
|
|
||||||
|
function Agenda() {
|
||||||
|
const { items } = useSelector((state: AppState) => state.calendar);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.agenda}>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
const momentDate = moment(item.airDateUtc);
|
||||||
|
const showDate =
|
||||||
|
index === 0 ||
|
||||||
|
!moment(items[index - 1].airDateUtc).isSame(momentDate, 'day');
|
||||||
|
|
||||||
|
return <AgendaEvent key={item.id} showDate={showDate} {...item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Agenda;
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import Agenda from './Agenda';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
(calendar) => {
|
|
||||||
return calendar;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(Agenda);
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
|
||||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
|
||||||
import episodeEntities from 'Episode/episodeEntities';
|
|
||||||
import getFinaleTypeName from 'Episode/getFinaleTypeName';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import formatTime from 'Utilities/Date/formatTime';
|
|
||||||
import padNumber from 'Utilities/Number/padNumber';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AgendaEvent.css';
|
|
||||||
|
|
||||||
class AgendaEvent extends Component {
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isDetailsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDetailsModalClose = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
series,
|
|
||||||
episodeFile,
|
|
||||||
title,
|
|
||||||
seasonNumber,
|
|
||||||
episodeNumber,
|
|
||||||
absoluteEpisodeNumber,
|
|
||||||
airDateUtc,
|
|
||||||
monitored,
|
|
||||||
unverifiedSceneNumbering,
|
|
||||||
finaleType,
|
|
||||||
hasFile,
|
|
||||||
grabbed,
|
|
||||||
queueItem,
|
|
||||||
showDate,
|
|
||||||
showEpisodeInformation,
|
|
||||||
showFinaleIcon,
|
|
||||||
showSpecialIcon,
|
|
||||||
showCutoffUnmetIcon,
|
|
||||||
timeFormat,
|
|
||||||
longDateFormat,
|
|
||||||
colorImpairedMode
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const startTime = moment(airDateUtc);
|
|
||||||
const endTime = moment(airDateUtc).add(series.runtime, 'minutes');
|
|
||||||
const downloading = !!(queueItem || grabbed);
|
|
||||||
const isMonitored = series.monitored && monitored;
|
|
||||||
const statusStyle = getStatusStyle(hasFile, downloading, startTime, endTime, isMonitored);
|
|
||||||
const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.event}>
|
|
||||||
<Link
|
|
||||||
className={styles.underlay}
|
|
||||||
onPress={this.onPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.overlay}>
|
|
||||||
<div className={styles.date}>
|
|
||||||
{
|
|
||||||
showDate &&
|
|
||||||
startTime.format(longDateFormat)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.eventWrapper,
|
|
||||||
styles[statusStyle],
|
|
||||||
colorImpairedMode && 'colorImpaired'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={styles.time}>
|
|
||||||
{formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.seriesTitle}>
|
|
||||||
{series.title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
showEpisodeInformation &&
|
|
||||||
<div className={styles.seasonEpisodeNumber}>
|
|
||||||
{seasonNumber}x{padNumber(episodeNumber, 2)}
|
|
||||||
|
|
||||||
{
|
|
||||||
series.seriesType === 'anime' && absoluteEpisodeNumber &&
|
|
||||||
<span className={styles.absoluteEpisodeNumber}>({absoluteEpisodeNumber})</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.episodeSeparator}> - </div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.episodeTitle}>
|
|
||||||
{
|
|
||||||
showEpisodeInformation &&
|
|
||||||
title
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
missingAbsoluteNumber &&
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.WARNING}
|
|
||||||
title={translate('EpisodeMissingAbsoluteNumber')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
unverifiedSceneNumbering && !missingAbsoluteNumber ?
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.WARNING}
|
|
||||||
title={translate('SceneNumberNotVerified')}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!queueItem &&
|
|
||||||
<span className={styles.statusIcon}>
|
|
||||||
<CalendarEventQueueDetails
|
|
||||||
seriesType={series.seriesType}
|
|
||||||
seasonNumber={seasonNumber}
|
|
||||||
absoluteEpisodeNumber={absoluteEpisodeNumber}
|
|
||||||
{...queueItem}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!queueItem && grabbed &&
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.DOWNLOADING}
|
|
||||||
title={translate('EpisodeIsDownloading')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showCutoffUnmetIcon &&
|
|
||||||
!!episodeFile &&
|
|
||||||
episodeFile.qualityCutoffNotMet &&
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.EPISODE_FILE}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={translate('QualityCutoffNotMet')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
episodeNumber === 1 && seasonNumber > 0 &&
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.INFO}
|
|
||||||
kind={kinds.INFO}
|
|
||||||
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showFinaleIcon &&
|
|
||||||
finaleType ?
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.INFO}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={getFinaleTypeName(finaleType)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showSpecialIcon &&
|
|
||||||
(episodeNumber === 0 || seasonNumber === 0) &&
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.INFO}
|
|
||||||
kind={kinds.PINK}
|
|
||||||
title={translate('Special')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EpisodeDetailsModal
|
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
|
||||||
episodeId={id}
|
|
||||||
episodeEntity={episodeEntities.CALENDAR}
|
|
||||||
seriesId={series.id}
|
|
||||||
episodeTitle={title}
|
|
||||||
showOpenSeriesButton={true}
|
|
||||||
onModalClose={this.onDetailsModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AgendaEvent.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
series: PropTypes.object.isRequired,
|
|
||||||
episodeFile: PropTypes.object,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
seasonNumber: PropTypes.number.isRequired,
|
|
||||||
episodeNumber: PropTypes.number.isRequired,
|
|
||||||
absoluteEpisodeNumber: PropTypes.number,
|
|
||||||
airDateUtc: PropTypes.string.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
unverifiedSceneNumbering: PropTypes.bool,
|
|
||||||
finaleType: PropTypes.string,
|
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
grabbed: PropTypes.bool,
|
|
||||||
queueItem: PropTypes.object,
|
|
||||||
showDate: PropTypes.bool.isRequired,
|
|
||||||
showEpisodeInformation: PropTypes.bool.isRequired,
|
|
||||||
showFinaleIcon: PropTypes.bool.isRequired,
|
|
||||||
showSpecialIcon: PropTypes.bool.isRequired,
|
|
||||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AgendaEvent;
|
|
||||||
227
frontend/src/Calendar/Agenda/AgendaEvent.tsx
Normal file
227
frontend/src/Calendar/Agenda/AgendaEvent.tsx
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
||||||
|
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
||||||
|
import episodeEntities from 'Episode/episodeEntities';
|
||||||
|
import getFinaleTypeName from 'Episode/getFinaleTypeName';
|
||||||
|
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import useSeries from 'Series/useSeries';
|
||||||
|
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import formatTime from 'Utilities/Date/formatTime';
|
||||||
|
import padNumber from 'Utilities/Number/padNumber';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AgendaEvent.css';
|
||||||
|
|
||||||
|
interface AgendaEventProps {
|
||||||
|
id: number;
|
||||||
|
seriesId: number;
|
||||||
|
episodeFileId: number;
|
||||||
|
title: string;
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeNumber: number;
|
||||||
|
absoluteEpisodeNumber?: number;
|
||||||
|
airDateUtc: string;
|
||||||
|
monitored: boolean;
|
||||||
|
unverifiedSceneNumbering?: boolean;
|
||||||
|
finaleType?: string;
|
||||||
|
hasFile: boolean;
|
||||||
|
grabbed?: boolean;
|
||||||
|
showDate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgendaEvent(props: AgendaEventProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
seriesId,
|
||||||
|
episodeFileId,
|
||||||
|
title,
|
||||||
|
seasonNumber,
|
||||||
|
episodeNumber,
|
||||||
|
absoluteEpisodeNumber,
|
||||||
|
airDateUtc,
|
||||||
|
monitored,
|
||||||
|
unverifiedSceneNumbering,
|
||||||
|
finaleType,
|
||||||
|
hasFile,
|
||||||
|
grabbed,
|
||||||
|
showDate,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const series = useSeries(seriesId)!;
|
||||||
|
const episodeFile = useEpisodeFile(episodeFileId);
|
||||||
|
const queueItem = useSelector(createQueueItemSelectorForHook(id));
|
||||||
|
const { timeFormat, longDateFormat, enableColorImpairedMode } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
showEpisodeInformation,
|
||||||
|
showFinaleIcon,
|
||||||
|
showSpecialIcon,
|
||||||
|
showCutoffUnmetIcon,
|
||||||
|
} = useSelector((state: AppState) => state.calendar.options);
|
||||||
|
|
||||||
|
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const startTime = moment(airDateUtc);
|
||||||
|
const endTime = moment(airDateUtc).add(series.runtime, 'minutes');
|
||||||
|
const downloading = !!(queueItem || grabbed);
|
||||||
|
const isMonitored = series.monitored && monitored;
|
||||||
|
const statusStyle = getStatusStyle(
|
||||||
|
hasFile,
|
||||||
|
downloading,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
isMonitored
|
||||||
|
);
|
||||||
|
const missingAbsoluteNumber =
|
||||||
|
series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDetailsModalClose = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.event}>
|
||||||
|
<Link className={styles.underlay} onPress={handlePress} />
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.date}>
|
||||||
|
{showDate && startTime.format(longDateFormat)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.eventWrapper,
|
||||||
|
styles[statusStyle],
|
||||||
|
enableColorImpairedMode && 'colorImpaired'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.time}>
|
||||||
|
{formatTime(airDateUtc, timeFormat)} -{' '}
|
||||||
|
{formatTime(endTime.toISOString(), timeFormat, {
|
||||||
|
includeMinuteZero: true,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.seriesTitle}>{series.title}</div>
|
||||||
|
|
||||||
|
{showEpisodeInformation ? (
|
||||||
|
<div className={styles.seasonEpisodeNumber}>
|
||||||
|
{seasonNumber}x{padNumber(episodeNumber, 2)}
|
||||||
|
{series.seriesType === 'anime' && absoluteEpisodeNumber && (
|
||||||
|
<span className={styles.absoluteEpisodeNumber}>
|
||||||
|
({absoluteEpisodeNumber})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className={styles.episodeSeparator}> - </div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className={styles.episodeTitle}>
|
||||||
|
{showEpisodeInformation ? title : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{missingAbsoluteNumber ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.WARNING}
|
||||||
|
title={translate('EpisodeMissingAbsoluteNumber')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{unverifiedSceneNumbering && !missingAbsoluteNumber ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.WARNING}
|
||||||
|
title={translate('SceneNumberNotVerified')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{queueItem ? (
|
||||||
|
<span className={styles.statusIcon}>
|
||||||
|
<CalendarEventQueueDetails
|
||||||
|
seasonNumber={seasonNumber}
|
||||||
|
{...queueItem}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!queueItem && grabbed ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
title={translate('EpisodeIsDownloading')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showCutoffUnmetIcon &&
|
||||||
|
episodeFile &&
|
||||||
|
episodeFile.qualityCutoffNotMet ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.EPISODE_FILE}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title={translate('QualityCutoffNotMet')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{episodeNumber === 1 && seasonNumber > 0 && (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
kind={kinds.INFO}
|
||||||
|
title={
|
||||||
|
seasonNumber === 1
|
||||||
|
? translate('SeriesPremiere')
|
||||||
|
: translate('SeasonPremiere')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showFinaleIcon && finaleType ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title={getFinaleTypeName(finaleType)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showSpecialIcon && (episodeNumber === 0 || seasonNumber === 0) ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
kind={kinds.PINK}
|
||||||
|
title={translate('Special')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EpisodeDetailsModal
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
episodeId={id}
|
||||||
|
episodeEntity={episodeEntities.CALENDAR}
|
||||||
|
seriesId={series.id}
|
||||||
|
episodeTitle={title}
|
||||||
|
showOpenSeriesButton={true}
|
||||||
|
onModalClose={handleDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AgendaEvent;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
|
|
||||||
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
|
||||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import AgendaEvent from './AgendaEvent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.options,
|
|
||||||
createSeriesSelector(),
|
|
||||||
createEpisodeFileSelector(),
|
|
||||||
createQueueItemSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(calendarOptions, series, episodeFile, queueItem, uiSettings) => {
|
|
||||||
return {
|
|
||||||
series,
|
|
||||||
episodeFile,
|
|
||||||
queueItem,
|
|
||||||
...calendarOptions,
|
|
||||||
timeFormat: uiSettings.timeFormat,
|
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(AgendaEvent);
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AgendaConnector from './Agenda/AgendaConnector';
|
|
||||||
import * as calendarViews from './calendarViews';
|
|
||||||
import CalendarDaysConnector from './Day/CalendarDaysConnector';
|
|
||||||
import DaysOfWeekConnector from './Day/DaysOfWeekConnector';
|
|
||||||
import CalendarHeaderConnector from './Header/CalendarHeaderConnector';
|
|
||||||
import styles from './Calendar.css';
|
|
||||||
|
|
||||||
class Calendar extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
view
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.calendar}>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && isPopulated && view === calendarViews.AGENDA &&
|
|
||||||
<div className={styles.calendarContent}>
|
|
||||||
<CalendarHeaderConnector />
|
|
||||||
<AgendaConnector />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && isPopulated && view !== calendarViews.AGENDA &&
|
|
||||||
<div className={styles.calendarContent}>
|
|
||||||
<CalendarHeaderConnector />
|
|
||||||
<DaysOfWeekConnector />
|
|
||||||
<CalendarDaysConnector />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Calendar.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
view: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Calendar;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user