Compare commits
487 Commits
v4.0.0.741
...
develop
Author | SHA1 | Date |
---|---|---|
|
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 | |
|
af0e55aef4 | |
|
39a439eb4c | |
|
66940b283b | |
|
2a662afaef | |
|
62a9c2519b | |
|
ca372bee25 | |
|
0904a0737e | |
|
70bc26dc19 | |
|
a2e0002a08 | |
|
d7ceb11a64 | |
|
cc5b5463f2 | |
|
9b4ff657af | |
|
aea50fa47e | |
|
05edd44ed6 | |
|
4440aa3cac | |
|
084fcc2295 | |
|
536ff142c3 | |
|
627b2a4289 | |
|
9734c2d144 | |
|
c7c1e3ac9e | |
|
429444d085 | |
|
5cb649e9d8 | |
|
cac7d239ea | |
|
3940059ea3 | |
|
20d00fe88c | |
|
b4d05214ae | |
|
cc0a284660 | |
|
f50a263f4f | |
|
29176c8367 | |
|
1eddf3a152 | |
|
8360dd7a7b | |
|
7e8d8500f2 | |
|
cae134ec7b | |
|
f81bb3ec19 | |
|
128309068d | |
|
73a4bdea52 | |
|
47ba002806 | |
|
ba88185dea | |
|
e24ce40eb8 | |
|
8be8c7f89c | |
|
7166a6c019 | |
|
3fbe436138 | |
|
92eab4b2e2 | |
|
23c741fd00 | |
|
8ddf46113b | |
|
c81ae65461 | |
|
efb3fa93e4 | |
|
04bd535cfc | |
|
9738101042 | |
|
1df7cdc65e | |
|
d051dac12c | |
|
5d01ecd30e | |
|
316b5cbf75 | |
|
2440672179 | |
|
a97fbcc40a | |
|
d738035fed | |
|
dc3e932102 | |
|
aded9d95f7 | |
|
b81c3ee4a8 | |
|
cf6748a80c | |
|
ef6cc7fa3a | |
|
f9b013a8bf | |
|
e966254462 | |
|
016c4b353b | |
|
d71c619f1a | |
|
6c232b062c | |
|
d6278fced4 | |
|
317ce39aa2 | |
|
941985f65b | |
|
10daf97d81 | |
|
6b08117d7d | |
|
9afe1c4b3f | |
|
0fdbbd018c | |
|
8a7b67c593 | |
|
4b8afe3d33 | |
|
476e7a7b94 | |
|
1fcd2b492c | |
|
1aef91041e | |
|
fc06e51352 | |
|
f4c19a384b | |
|
5061dc4b5e | |
|
37863a8deb | |
|
5c42935eb3 | |
|
dac69445e4 | |
|
aca10f6f4f | |
|
74cdf01e49 | |
|
a169ebff2a | |
|
7fc3bebc91 | |
|
e672996dbb | |
|
238ba85f0a | |
|
1562d3bae3 | |
|
7776ec9955 | |
|
af5a681ab7 | |
|
0a7f3a12c2 | |
|
2ef46e5b90 | |
|
6003ca1696 | |
|
0937ee6fef | |
|
60ee7cc716 | |
|
4e83820511 | |
|
5a66b949cf | |
|
f010f56290 | |
|
060b789bc6 | |
|
7353fe479d | |
|
1ec1ce58e9 | |
|
35d0e6a6f8 | |
|
588372fd95 | |
|
13c925b341 | |
|
1335efd487 | |
|
d338425951 | |
|
fc6494c569 | |
|
c403b2cdd5 | |
|
cf3d51bab2 | |
|
dec3fc6889 | |
|
40bac23698 | |
|
88de927435 | |
|
29204c93a3 | |
|
c641733781 | |
|
58de0310fd | |
|
172b1a82d1 | |
|
e14568adef | |
|
381ce61aef | |
|
9f705e4161 | |
|
063dba22a8 | |
|
6d552f2a60 | |
|
4d4d63921b | |
|
6584d95331 | |
|
86034beccd | |
|
4aa56e3f91 | |
|
2ec071a5ec | |
|
d86aeb7472 | |
|
48cb5d2271 | |
|
a0329adeba | |
|
89bef4af99 | |
|
a12cdb34bc | |
|
13e29bd257 | |
|
61a7515041 | |
|
2c25245860 | |
|
18aadb544e | |
|
c7dd7abf89 | |
|
d0e9504af0 | |
|
e81bb3b993 | |
|
f211433b77 | |
|
2068c5393e | |
|
0183812cc5 | |
|
7f09903a06 | |
|
fa4c11a943 | |
|
653963a247 | |
|
32c32e2f88 | |
|
07bd159436 | |
|
20273b07ad | |
|
e5f19f01fa | |
|
13af6f5779 | |
|
71c2c0570b | |
|
64c6a8879b | |
|
7f061a9583 | |
|
4285691064 | |
|
de9899c60e | |
|
6c8758c27a | |
|
086d3b5afa | |
|
f8a0751775 | |
|
c99d81e79b | |
|
9fd193d2a8 | |
|
64f4365fe9 | |
|
2773f77e1c | |
|
0a84b4a8e9 | |
|
236d8e4c50 | |
|
16d3827dbd | |
|
fa600e62e0 | |
|
fb6fc568c5 | |
|
1f97679868 | |
|
b34e0f8259 | |
|
4c170d0452 | |
|
6dc0a88004 | |
|
33b44a8a53 | |
|
cb72e752f9 | |
|
a11ee7bc11 | |
|
98d60e1a8e | |
|
6377c688fc | |
|
7a37f130f9 | |
|
724dd7e733 | |
|
e1be3b20e9 | |
|
2f041f9ec1 | |
|
f10ccf587d | |
|
0242b40eda | |
|
7a768b5d0f | |
|
a57254640f | |
|
1a6f45bafd | |
|
c6071f6d81 | |
|
2a47a237d4 | |
|
a7607ac7d6 | |
|
43797b326d | |
|
5c4f829993 | |
|
8dd8c95f36 | |
|
6f6036a199 | |
|
625e500132 | |
|
39575b1248 | |
|
f1d343218c | |
|
b0829d5537 | |
|
965e7c22d9 | |
|
75535e61d9 | |
|
c0b17d9345 | |
|
84e657482d | |
|
ed27bcf213 | |
|
9ee2fe6f5c | |
|
d5e19b8c3c | |
|
2957b40512 | |
|
9f46fc923d | |
|
7dc1e47504 | |
|
d15c116f13 | |
|
dd704579df | |
|
bd9d4b484c | |
|
913b845faa | |
|
6e81517d51 | |
|
34e74eecd7 | |
|
895eccebc5 | |
|
f722d49b3a | |
|
cac97c057f | |
|
63e132d257 | |
|
6ab1d8e16b | |
|
80630bf97f | |
|
904285045b | |
|
1006ec6b52 | |
|
4cb1100704 | |
|
745b92daf4 | |
|
9eafdbd1af | |
|
200396ef7a | |
|
c5a724f14e | |
|
42b11528b4 | |
|
e2210228b3 | |
|
ded7c3c6e2 | |
|
e1c6722aad | |
|
e17655c26a | |
|
e66c628241 | |
|
8f0514a91d | |
|
d7aea82e45 | |
|
19db75b36b | |
|
11a18b534a | |
|
70807a9dcf | |
|
350600607d | |
|
e9f0c96249 | |
|
d9acbf5682 | |
|
07cbd7c8d2 | |
|
0ea189d03c | |
|
9e3f9f9618 | |
|
68c326ae27 | |
|
46367d2023 | |
|
b64c52a846 | |
|
345854d0fe | |
|
31baed4b2c | |
|
7d0d503a5e | |
|
9f50166fa6 | |
|
3c1ca6ea4e | |
|
3cd4c67ba1 | |
|
fc3a2e9ab2 | |
|
a71d40edba | |
|
9ba5850fca | |
|
0d06418194 | |
|
f95dd00b51 | |
|
271266b10a | |
|
cab93249ec | |
|
8921c5d7a0 | |
|
dbbf1a7f58 | |
|
69f99373e5 | |
|
7be5732a3a | |
|
e66ba84fc0 | |
|
c0b30a5028 | |
|
3cf4d2907e | |
|
ae96ebca57 | |
|
d336aaf3f0 | |
|
ec40bc6eea | |
|
75bb34afaa | |
|
de1cc25c90 | |
|
9884f6f282 | |
|
e792db4d33 | |
|
2dbf5b5a71 | |
|
c6bb6ad878 | |
|
bfd24da2d9 | |
|
8dd3b45c90 | |
|
0b0f21d0ac | |
|
738f5c58ad | |
|
9a7b5bf14e | |
|
e1260d504e | |
|
736651324f | |
|
489f03441b | |
|
e4b5d559df | |
|
07fbb0d1f4 | |
|
666455f9b1 | |
|
091449d9bf | |
|
f87a66fcba | |
|
1bba7e177b | |
|
57445bbe57 | |
|
ec91142c85 | |
|
0685896ed8 | |
|
ee0048c768 | |
|
16e5ffa467 | |
|
431c66c7c1 | |
|
57bd6539c8 | |
|
637cb1711d | |
|
7e011df2b2 | |
|
79907c881c | |
|
db6a627983 | |
|
d76a489be6 | |
|
fd17df0dd0 | |
|
53cf530893 | |
|
91f33c670e | |
|
d322619733 | |
|
1182798929 | |
|
ad0249c7db | |
|
3259e6dc10 | |
|
541d3307e1 | |
|
ad60352bae | |
|
02e051580c | |
|
b8964f8bba | |
|
53f6f64448 | |
|
9044ecc59b | |
|
eff0408b0e | |
|
fef525ddb8 | |
|
3454f1c9ed | |
|
21666df8f1 | |
|
fd58e9671c | |
|
8971ac2e14 | |
|
a99fc6fa66 | |
|
587b600d6c | |
|
06b86d4fad | |
|
6b92b556bb | |
|
a49cf72869 | |
|
070919a7e6 | |
|
8f7f23c938 | |
|
cd2ce34f10 | |
|
d172f2e6d9 | |
|
46517e4397 | |
|
b7ce5a8318 | |
|
87d0a6bdf6 | |
|
b6ffe935e8 | |
|
0673374e97 | |
|
84c95f5a9d | |
|
72401759b5 | |
|
4e19fec123 | |
|
5ce98fd83d | |
|
bc20ef73bd | |
|
16d60a6586 | |
|
0a5200766e | |
|
be937ec581 | |
|
1f82b9fb78 | |
|
c8a1118679 | |
|
91053ca51d | |
|
4085771602 | |
|
efb000529b | |
|
4b22200708 | |
|
f9148c36db | |
|
7a82e7b365 |
|
@ -0,0 +1,13 @@
|
||||||
|
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||||
|
// the frontend has vscode settings that are distinct from the backend
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../frontend"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||||
|
{
|
||||||
|
"name": "Sonarr",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"nodeGypDependencies": true,
|
||||||
|
"version": "16",
|
||||||
|
"nvmVersion": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [8989],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -203,6 +203,7 @@ dotnet_diagnostic.CA1819.severity = suggestion
|
||||||
dotnet_diagnostic.CA1822.severity = suggestion
|
dotnet_diagnostic.CA1822.severity = suggestion
|
||||||
dotnet_diagnostic.CA1823.severity = suggestion
|
dotnet_diagnostic.CA1823.severity = suggestion
|
||||||
dotnet_diagnostic.CA1824.severity = suggestion
|
dotnet_diagnostic.CA1824.severity = suggestion
|
||||||
|
dotnet_diagnostic.CA1825.severity = suggestion
|
||||||
dotnet_diagnostic.CA2000.severity = suggestion
|
dotnet_diagnostic.CA2000.severity = suggestion
|
||||||
dotnet_diagnostic.CA2002.severity = suggestion
|
dotnet_diagnostic.CA2002.severity = suggestion
|
||||||
dotnet_diagnostic.CA2007.severity = suggestion
|
dotnet_diagnostic.CA2007.severity = suggestion
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: 'Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Sonarr v2 is EOL & unsupported.'
|
description: 'Only bug reports for v4 will be accepted, older versions are no longer receiving bug fixes and support issues will be closed immediately.'
|
||||||
labels: ['needs-triage']
|
labels: ['needs-triage']
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
|
@ -37,8 +37,8 @@ body:
|
||||||
label: Environment
|
label: Environment
|
||||||
description: |
|
description: |
|
||||||
examples:
|
examples:
|
||||||
- **OS**: Ubuntu 20.04
|
- **OS**: Ubuntu 22.04
|
||||||
- **Sonarr**: Sonarr 3.0.6.1265
|
- **Sonarr**: Sonarr 4.0.0.766
|
||||||
- **Docker Install**: Yes
|
- **Docker Install**: Yes
|
||||||
- **Using Reverse Proxy**: No
|
- **Using Reverse Proxy**: No
|
||||||
- **Browser**: Firefox 90 (If UI related)
|
- **Browser**: Firefox 90 (If UI related)
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
#### Database Migration
|
|
||||||
YES | NO
|
|
||||||
|
|
||||||
#### Description
|
#### Description
|
||||||
A few sentences describing the overall goals of the pull request's commits.
|
A few sentences describing the overall goals of the pull request's commits.
|
||||||
|
|
||||||
#### Todos
|
<!-- Remove any of the following sections if they are not used -->
|
||||||
- [ ] Tests
|
|
||||||
- [ ] Wiki Updates
|
#### Screenshots for UI Changes
|
||||||
|
|
||||||
|
|
||||||
|
#### Database Migration
|
||||||
|
YES - ###
|
||||||
|
|
||||||
|
|
||||||
#### Issues Fixed or Closed by this PR
|
#### Issues Fixed or Closed by this PR
|
||||||
|
* Closes #
|
||||||
|
|
||||||
*
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: Archive
|
||||||
|
description: Archive binaries for deployment
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
os:
|
||||||
|
description: 'OS that the packaging is running on'
|
||||||
|
required: true
|
||||||
|
artifact:
|
||||||
|
description: 'Binary artifact'
|
||||||
|
required: true
|
||||||
|
archive_type:
|
||||||
|
description: 'File type to use for the final package'
|
||||||
|
required: true
|
||||||
|
branch:
|
||||||
|
description: 'Git branch used for this build'
|
||||||
|
required: true
|
||||||
|
major_version:
|
||||||
|
description: 'Sonarr major version'
|
||||||
|
required: true
|
||||||
|
version:
|
||||||
|
description: 'Sonarr version'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Archive Artifact
|
||||||
|
uses: thedoctor0/zip-release@0.7.5
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
name: Package
|
||||||
|
description: Packages binaries for deployment
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
platform:
|
||||||
|
description: 'Binary platform'
|
||||||
|
required: true
|
||||||
|
framework:
|
||||||
|
description: '.net framework'
|
||||||
|
required: true
|
||||||
|
artifact:
|
||||||
|
description: 'Binary artifact'
|
||||||
|
required: true
|
||||||
|
branch:
|
||||||
|
description: 'Git branch used for this build'
|
||||||
|
required: true
|
||||||
|
major_version:
|
||||||
|
description: 'Sonarr major version'
|
||||||
|
required: true
|
||||||
|
version:
|
||||||
|
description: 'Sonarr version'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Download Artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact }}
|
||||||
|
path: _output
|
||||||
|
|
||||||
|
- name: Download UI Artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_ui
|
||||||
|
path: _output/UI
|
||||||
|
|
||||||
|
- name: Configure Environment Variables
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "FRAMEWORK=${{ inputs.framework }}" >> "$GITHUB_ENV"
|
||||||
|
echo "BRANCH=${{ inputs.branch }}" >> "$GITHUB_ENV"
|
||||||
|
echo "SONARR_MAJOR_VERSION=${{ inputs.major_version }}" >> "$GITHUB_ENV"
|
||||||
|
echo "SONARR_VERSION=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Create Packages
|
||||||
|
shell: bash
|
||||||
|
run: $GITHUB_ACTION_PATH/package.sh
|
||||||
|
|
||||||
|
- name: Create Windows Installer (x64)
|
||||||
|
if: ${{ inputs.platform == 'windows' }}
|
||||||
|
working-directory: distribution/windows/setup
|
||||||
|
shell: cmd
|
||||||
|
run: |
|
||||||
|
SET RUNTIME=win-x64
|
||||||
|
|
||||||
|
build.bat
|
||||||
|
|
||||||
|
- name: Create Windows Installer (x86)
|
||||||
|
if: ${{ inputs.platform == 'windows' }}
|
||||||
|
working-directory: distribution/windows/setup
|
||||||
|
shell: cmd
|
||||||
|
run: |
|
||||||
|
SET RUNTIME=win-x86
|
||||||
|
|
||||||
|
build.bat
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: release_${{ inputs.platform }}
|
||||||
|
compression-level: 0
|
||||||
|
if-no-files-found: error
|
||||||
|
path: |
|
||||||
|
_artifacts/*.exe
|
||||||
|
_artifacts/*.tar.gz
|
||||||
|
_artifacts/*.zip
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
outputFolder=_output
|
||||||
|
artifactsFolder=_artifacts
|
||||||
|
uiFolder="$outputFolder/UI"
|
||||||
|
framework="${FRAMEWORK:=net6.0}"
|
||||||
|
|
||||||
|
rm -rf $artifactsFolder
|
||||||
|
mkdir $artifactsFolder
|
||||||
|
|
||||||
|
for runtime in _output/*
|
||||||
|
do
|
||||||
|
name="${runtime##*/}"
|
||||||
|
folderName="$runtime/$framework"
|
||||||
|
sonarrFolder="$folderName/Sonarr"
|
||||||
|
archiveName="Sonarr.$BRANCH.$SONARR_VERSION.$name"
|
||||||
|
|
||||||
|
if [[ "$name" == 'UI' ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating package for $name"
|
||||||
|
|
||||||
|
echo "Copying UI"
|
||||||
|
cp -r $uiFolder $sonarrFolder
|
||||||
|
|
||||||
|
echo "Setting permissions"
|
||||||
|
find $sonarrFolder -name "ffprobe" -exec chmod a+x {} \;
|
||||||
|
find $sonarrFolder -name "Sonarr" -exec chmod a+x {} \;
|
||||||
|
find $sonarrFolder -name "Sonarr.Update" -exec chmod a+x {} \;
|
||||||
|
|
||||||
|
if [[ "$name" == *"osx"* ]]; then
|
||||||
|
echo "Creating macOS package"
|
||||||
|
|
||||||
|
packageName="$name-app"
|
||||||
|
packageFolder="$outputFolder/$packageName"
|
||||||
|
|
||||||
|
rm -rf $packageFolder
|
||||||
|
mkdir $packageFolder
|
||||||
|
|
||||||
|
cp -r distribution/macOS/Sonarr.app $packageFolder
|
||||||
|
mkdir -p $packageFolder/Sonarr.app/Contents/MacOS
|
||||||
|
|
||||||
|
echo "Copying Binaries"
|
||||||
|
cp -r $sonarrFolder/* $packageFolder/Sonarr.app/Contents/MacOS
|
||||||
|
|
||||||
|
echo "Removing Update Folder"
|
||||||
|
rm -r $packageFolder/Sonarr.app/Contents/MacOS/Sonarr.Update
|
||||||
|
|
||||||
|
echo "Packaging macOS app Artifact"
|
||||||
|
(cd $packageFolder; zip -rq "../../$artifactsFolder/$archiveName-app.zip" ./Sonarr.app)
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Packaging Artifact"
|
||||||
|
if [[ "$name" == *"linux"* ]] || [[ "$name" == *"osx"* ]] || [[ "$name" == *"freebsd"* ]]; then
|
||||||
|
tar -zcf "./$artifactsFolder/$archiveName.tar.gz" -C $folderName Sonarr
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$name" == *"win"* ]]; then
|
||||||
|
if [ "$RUNNER_OS" = "Windows" ]
|
||||||
|
then
|
||||||
|
(cd $folderName; 7z a -tzip "../../../$artifactsFolder/$archiveName.zip" ./Sonarr)
|
||||||
|
else
|
||||||
|
(cd $folderName; zip -rq "../../../$artifactsFolder/$archiveName.zip" ./Sonarr)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
|
@ -0,0 +1,18 @@
|
||||||
|
name: Publish Test Artifact
|
||||||
|
description: Publishes a test artifact
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
framework:
|
||||||
|
description: '.net framework'
|
||||||
|
required: true
|
||||||
|
runtime:
|
||||||
|
description: '.net runtime'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: tests-${{ inputs.runtime }}
|
||||||
|
path: _tests/${{ inputs.framework }}/${{ inputs.runtime }}/publish/**/*
|
|
@ -0,0 +1,87 @@
|
||||||
|
name: Test
|
||||||
|
description: Runs unit/integration tests
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
use_postgres:
|
||||||
|
description: 'Whether postgres should be used for the database'
|
||||||
|
os:
|
||||||
|
description: 'OS that the tests are running on'
|
||||||
|
required: true
|
||||||
|
artifact:
|
||||||
|
description: 'Test binary artifact'
|
||||||
|
required: true
|
||||||
|
pattern:
|
||||||
|
description: 'Pattern for DLLs'
|
||||||
|
required: true
|
||||||
|
filter:
|
||||||
|
description: 'Filter for tests'
|
||||||
|
required: true
|
||||||
|
integration_tests:
|
||||||
|
description: 'True if running integration tests'
|
||||||
|
binary_artifact:
|
||||||
|
description: 'Binary artifact for integration tests'
|
||||||
|
binary_path:
|
||||||
|
description: 'Path witin binary artifact for integration tests'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
|
||||||
|
- name: Setup Postgres
|
||||||
|
if: ${{ inputs.use_postgres }}
|
||||||
|
uses: ikalnytskyi/action-setup-postgres@v4
|
||||||
|
|
||||||
|
- name: Setup Test Variables
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "RESULTS_NAME=${{ inputs.integration_tests && 'integation-' || 'unit-' }}${{ inputs.artifact }}${{ inputs.use_postgres && '-postgres' }}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Setup Postgres Environment Variables
|
||||||
|
if: ${{ inputs.use_postgres }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "Sonarr__Postgres__Host=localhost" >> "$GITHUB_ENV"
|
||||||
|
echo "Sonarr__Postgres__Port=5432" >> "$GITHUB_ENV"
|
||||||
|
echo "Sonarr__Postgres__User=postgres" >> "$GITHUB_ENV"
|
||||||
|
echo "Sonarr__Postgres__Password=postgres" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Download Artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact }}
|
||||||
|
path: _tests
|
||||||
|
|
||||||
|
- name: Download Binary Artifact
|
||||||
|
if: ${{ inputs.integration_tests }}
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.binary_artifact }}
|
||||||
|
path: _output
|
||||||
|
|
||||||
|
- name: Set up binary artifact
|
||||||
|
if: ${{ inputs.binary_path != '' }}
|
||||||
|
shell: bash
|
||||||
|
run: mv ./_output/${{inputs.binary_path}} _tests/bin
|
||||||
|
|
||||||
|
- name: Make executable
|
||||||
|
if: startsWith(inputs.os, 'windows') != true
|
||||||
|
shell: bash
|
||||||
|
run: chmod +x ./_tests/Sonarr.Test.Dummy && chmod +x ./_tests/ffprobe
|
||||||
|
|
||||||
|
- name: Make Sonarr binary executable
|
||||||
|
if: ${{ inputs.integration_tests && !startsWith(inputs.os, 'windows') }}
|
||||||
|
shell: bash
|
||||||
|
run: chmod +x ./_tests/bin/Sonarr
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger "trx;LogFileName=${{ env.RESULTS_NAME }}.trx" --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true"
|
||||||
|
|
||||||
|
- name: Upload Test Results
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: results-${{ env.RESULTS_NAME }}
|
||||||
|
path: TestResults/*.trx
|
|
@ -0,0 +1,12 @@
|
||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
|
@ -1,17 +1,23 @@
|
||||||
'connection':
|
'connection':
|
||||||
- src/NzbDrone.Core/Notifications/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: src/NzbDrone.Core/Notifications/**/*
|
||||||
|
|
||||||
'db-migration':
|
'db-migration':
|
||||||
- src/NzbDrone.Core/Datastore/Migration/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: src/NzbDrone.Core/Datastore/Migration/*
|
||||||
|
|
||||||
'download-client':
|
'download-client':
|
||||||
- src/NzbDrone.Core/Download/Clients/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: src/NzbDrone.Core/Download/Clients/**/*
|
||||||
|
|
||||||
'indexer':
|
'indexer':
|
||||||
- src/NzbDrone.Core/Indexers/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: src/NzbDrone.Core/Indexers/**/*
|
||||||
|
|
||||||
'parsing':
|
'parsing':
|
||||||
- src/NzbDrone.Core/Parser/**/*
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: src/NzbDrone.Core/Parser/**/*
|
||||||
|
|
||||||
'ui-only':
|
'ui-only':
|
||||||
- all: ['frontend/**/*']
|
- changed-files:
|
||||||
|
- any-glob-to-all-files: frontend/**/*
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
authors:
|
||||||
|
- Weblate
|
||||||
|
- SonarrBot
|
||||||
|
categories:
|
||||||
|
- title: Changes
|
||||||
|
labels:
|
||||||
|
- '*'
|
|
@ -26,17 +26,11 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup dotnet
|
- name: Setup dotnet
|
||||||
uses: actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
id: setup-dotnet
|
id: setup-dotnet
|
||||||
with:
|
|
||||||
dotnet-version: '6.0.x'
|
|
||||||
|
|
||||||
- name: Create temporary global.json
|
|
||||||
run: |
|
|
||||||
echo '{"sdk":{"version": "${{ steps.setup-dotnet.outputs.dotnet-version }}" } }' > ./global.json
|
|
||||||
|
|
||||||
- name: Create openapi.json
|
- name: Create openapi.json
|
||||||
run: ./docs.sh Linux
|
run: ./docs.sh Linux
|
||||||
|
|
|
@ -0,0 +1,257 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- "src/Sonarr.Api.*/openapi.json"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths-ignore:
|
||||||
|
- "src/NzbDrone.Core/Localization/Core/**"
|
||||||
|
- "src/Sonarr.Api.*/openapi.json"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
FRAMEWORK: net6.0
|
||||||
|
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||||
|
SONARR_MAJOR_VERSION: 4
|
||||||
|
VERSION: 4.0.8
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend:
|
||||||
|
runs-on: windows-latest
|
||||||
|
outputs:
|
||||||
|
framework: ${{ steps.variables.outputs.framework }}
|
||||||
|
major_version: ${{ steps.variables.outputs.major_version }}
|
||||||
|
version: ${{ steps.variables.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
|
||||||
|
- name: Setup Environment Variables
|
||||||
|
id: variables
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# 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))"
|
||||||
|
DOTNET_VERSION=$(jq -r '.sdk.version' global.json)
|
||||||
|
|
||||||
|
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
|
||||||
|
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
|
||||||
|
echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Enable Extra Platforms In SDK
|
||||||
|
shell: bash
|
||||||
|
run: ./build.sh --enable-extra-platforms-in-sdk
|
||||||
|
|
||||||
|
- name: Build Backend
|
||||||
|
shell: bash
|
||||||
|
run: ./build.sh --backend --enable-extra-platforms --packages
|
||||||
|
|
||||||
|
# Test Artifacts
|
||||||
|
|
||||||
|
- name: Publish win-x64 Test Artifact
|
||||||
|
uses: ./.github/actions/publish-test-artifact
|
||||||
|
with:
|
||||||
|
framework: ${{ env.FRAMEWORK }}
|
||||||
|
runtime: win-x64
|
||||||
|
|
||||||
|
- name: Publish linux-x64 Test Artifact
|
||||||
|
uses: ./.github/actions/publish-test-artifact
|
||||||
|
with:
|
||||||
|
framework: ${{ env.FRAMEWORK }}
|
||||||
|
runtime: linux-x64
|
||||||
|
|
||||||
|
- name: Publish osx-arm64 Test Artifact
|
||||||
|
uses: ./.github/actions/publish-test-artifact
|
||||||
|
with:
|
||||||
|
framework: ${{ env.FRAMEWORK }}
|
||||||
|
runtime: osx-arm64
|
||||||
|
|
||||||
|
# Build Artifacts (grouped by OS)
|
||||||
|
|
||||||
|
- name: Publish FreeBSD Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_freebsd
|
||||||
|
path: _artifacts/freebsd-*/**/*
|
||||||
|
- name: Publish Linux Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_linux
|
||||||
|
path: _artifacts/linux-*/**/*
|
||||||
|
- name: Publish macOS Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_macos
|
||||||
|
path: _artifacts/osx-*/**/*
|
||||||
|
- name: Publish Windows Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_windows
|
||||||
|
path: _artifacts/win-*/**/*
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Volta
|
||||||
|
uses: volta-cli/action@v4
|
||||||
|
|
||||||
|
- name: Yarn Install
|
||||||
|
run: yarn install
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: yarn lint
|
||||||
|
|
||||||
|
- name: Stylelint
|
||||||
|
run: yarn stylelint -f github
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: yarn build --env production
|
||||||
|
|
||||||
|
- name: Publish UI Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build_ui
|
||||||
|
path: _output/UI/**/*
|
||||||
|
|
||||||
|
unit_test:
|
||||||
|
needs: backend
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
artifact: tests-linux-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
|
- os: macos-latest
|
||||||
|
artifact: tests-osx-arm64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
|
- os: windows-latest
|
||||||
|
artifact: tests-win-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
with:
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
artifact: ${{ matrix.artifact }}
|
||||||
|
pattern: Sonarr.*.Test.dll
|
||||||
|
filter: ${{ matrix.filter }}
|
||||||
|
|
||||||
|
unit_test_postgres:
|
||||||
|
needs: backend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
with:
|
||||||
|
os: ubuntu-latest
|
||||||
|
artifact: tests-linux-x64
|
||||||
|
pattern: Sonarr.*.Test.dll
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||||
|
use_postgres: true
|
||||||
|
|
||||||
|
integration_test:
|
||||||
|
needs: backend
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
artifact: tests-linux-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory=IntegrationTest
|
||||||
|
binary_artifact: build_linux
|
||||||
|
binary_path: linux-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||||
|
- os: macos-latest
|
||||||
|
artifact: tests-osx-arm64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory=IntegrationTest
|
||||||
|
binary_artifact: build_macos
|
||||||
|
binary_path: osx-arm64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||||
|
- os: windows-latest
|
||||||
|
artifact: tests-win-x64
|
||||||
|
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory=IntegrationTest
|
||||||
|
binary_artifact: build_windows
|
||||||
|
binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
uses: ./.github/actions/test
|
||||||
|
with:
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
artifact: ${{ matrix.artifact }}
|
||||||
|
pattern: Sonarr.*.Test.dll
|
||||||
|
filter: ${{ matrix.filter }}
|
||||||
|
integration_tests: true
|
||||||
|
binary_artifact: ${{ matrix.binary_artifact }}
|
||||||
|
binary_path: ${{ matrix.binary_path }}
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }}
|
||||||
|
needs: [backend, frontend, unit_test, unit_test_postgres, integration_test]
|
||||||
|
secrets: inherit
|
||||||
|
uses: ./.github/workflows/deploy.yml
|
||||||
|
with:
|
||||||
|
framework: ${{ needs.backend.outputs.framework }}
|
||||||
|
branch: ${{ github.ref_name }}
|
||||||
|
major_version: ${{ needs.backend.outputs.major_version }}
|
||||||
|
version: ${{ needs.backend.outputs.version }}
|
||||||
|
|
||||||
|
notify:
|
||||||
|
name: Discord Notification
|
||||||
|
needs:
|
||||||
|
[
|
||||||
|
backend,
|
||||||
|
frontend,
|
||||||
|
unit_test,
|
||||||
|
unit_test_postgres,
|
||||||
|
integration_test,
|
||||||
|
deploy,
|
||||||
|
]
|
||||||
|
if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }}
|
||||||
|
env:
|
||||||
|
STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Notify
|
||||||
|
uses: tsickert/discord-webhook@v6.0.0
|
||||||
|
with:
|
||||||
|
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
username: "GitHub Actions"
|
||||||
|
avatar-url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
|
||||||
|
embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}"
|
||||||
|
embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
embed-description: |
|
||||||
|
**Branch** ${{ github.ref }}
|
||||||
|
**Build** ${{ needs.backend.outputs.version }}
|
||||||
|
embed-color: ${{ env.STATUS == 'success' && '3066993' || '15158332' }}
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: Merge Conflict Labeler
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
pull_request_target:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
types: [synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label:
|
||||||
|
name: Labeling
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'Sonarr/Sonarr' }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Apply label
|
||||||
|
uses: eps1lon/actions-label-merge-conflict@v3
|
||||||
|
with:
|
||||||
|
dirtyLabel: 'merge-conflict'
|
||||||
|
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
framework:
|
||||||
|
description: '.net framework'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
branch:
|
||||||
|
description: 'Git branch used for this build'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
major_version:
|
||||||
|
description: 'Sonarr major version'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
version:
|
||||||
|
description: 'Sonarr version'
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
secrets:
|
||||||
|
SERVICES_API_KEY:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [freebsd, linux, macos, windows]
|
||||||
|
include:
|
||||||
|
- platform: freebsd
|
||||||
|
os: ubuntu-latest
|
||||||
|
- platform: linux
|
||||||
|
os: ubuntu-latest
|
||||||
|
- platform: macos
|
||||||
|
os: ubuntu-latest
|
||||||
|
- platform: windows
|
||||||
|
os: windows-latest
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
uses: ./.github/actions/package
|
||||||
|
with:
|
||||||
|
framework: ${{ inputs.framework }}
|
||||||
|
platform: ${{ matrix.platform }}
|
||||||
|
artifact: build_${{ matrix.platform }}
|
||||||
|
branch: ${{ inputs.branch }}
|
||||||
|
major_version: ${{ inputs.major_version }}
|
||||||
|
version: ${{ inputs.version }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download release artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: _artifacts
|
||||||
|
pattern: release_*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Get Previous Release
|
||||||
|
id: previous-release
|
||||||
|
uses: cardinalby/git-get-release-action@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
with:
|
||||||
|
latest: true
|
||||||
|
prerelease: ${{ inputs.branch != 'main' }}
|
||||||
|
|
||||||
|
- name: Generate Release Notes
|
||||||
|
id: generate-release-notes
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
result-encoding: string
|
||||||
|
script: |
|
||||||
|
const { data } = await github.rest.repos.generateReleaseNotes({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
tag_name: 'v${{ inputs.version }}',
|
||||||
|
target_commitish: '${{ github.sha }}',
|
||||||
|
previous_tag_name: '${{ steps.previous-release.outputs.tag_name }}',
|
||||||
|
})
|
||||||
|
return data.body
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
artifacts: _artifacts/Sonarr.*
|
||||||
|
commit: ${{ github.sha }}
|
||||||
|
generateReleaseNotes: false
|
||||||
|
body: ${{ steps.generate-release-notes.outputs.result }}
|
||||||
|
name: ${{ inputs.version }}
|
||||||
|
prerelease: ${{ inputs.branch != 'main' }}
|
||||||
|
skipIfReleaseExists: true
|
||||||
|
tag: v${{ inputs.version }}
|
||||||
|
|
||||||
|
- name: Publish to Services
|
||||||
|
shell: bash
|
||||||
|
working-directory: _artifacts
|
||||||
|
run: |
|
||||||
|
branch=${{ inputs.branch }}
|
||||||
|
version=${{ inputs.version }}
|
||||||
|
lastCommit=${{ github.sha }}
|
||||||
|
|
||||||
|
hashes="["
|
||||||
|
|
||||||
|
addHash() {
|
||||||
|
path=$1
|
||||||
|
os=$2
|
||||||
|
arch=$3
|
||||||
|
type=$4
|
||||||
|
|
||||||
|
local hash=$(sha256sum *.$version.$path | awk '{ print $1; }')
|
||||||
|
echo "{ \""Os\"": \""$os\"", \""Arch\"": \""$arch\"", \""Type\"": \""$type\"", \""Hash\"": \""$hash\"" }"
|
||||||
|
}
|
||||||
|
|
||||||
|
hashes="$hashes $(addHash "linux-arm.tar.gz" "linux" "arm" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "linux-arm64.tar.gz" "linux" "arm64" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "linux-x64.tar.gz" "linux" "x64" "archive")"
|
||||||
|
# hashes="$hashes, $(addHash "linux-x86.tar.gz" "linux" "x86" "archive")"
|
||||||
|
|
||||||
|
# hashes="$hashes, $(addHash "linux-musl-arm.tar.gz" "linuxmusl" "arm" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "linux-musl-arm64.tar.gz" "linuxmusl" "arm64" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "linux-musl-x64.tar.gz" "linuxmusl" "x64" "archive")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "osx-arm64.tar.gz" "osx" "arm64" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "osx-x64.tar.gz" "osx" "x64" "archive")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "osx-arm64-app.zip" "osx" "arm64" "installer")"
|
||||||
|
hashes="$hashes, $(addHash "osx-x64-app.zip" "osx" "x64" "installer")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "win-x64.zip" "windows" "x64" "archive")"
|
||||||
|
hashes="$hashes, $(addHash "win-x86.zip" "windows" "x86" "archive")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "win-x64-installer.exe" "windows" "x64" "installer")"
|
||||||
|
hashes="$hashes, $(addHash "win-x86-installer.exe" "windows" "x86" "installer")"
|
||||||
|
|
||||||
|
hashes="$hashes, $(addHash "freebsd-x64.tar.gz" "freebsd" "x64" "archive")"
|
||||||
|
|
||||||
|
hashes="$hashes ]"
|
||||||
|
|
||||||
|
json="{\""branch\"":\""$branch\"", \""version\"":\""$version\"", \""lastCommit\"":\""$lastCommit\"", \""hashes\"":$hashes, \""gitHubRelease\"":true}"
|
||||||
|
url="https://services.sonarr.tv/v1/update"
|
||||||
|
|
||||||
|
echo "Publishing update $version ($branch) to: $url"
|
||||||
|
echo "$json"
|
||||||
|
|
||||||
|
curl -H "Content-Type: application/json" -H "X-Api-Key: ${{ secrets.SERVICES_API_KEY }}" -X POST -d "$json" --fail-with-body $url
|
|
@ -8,5 +8,6 @@ jobs:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'Sonarr/Sonarr'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v4
|
- uses: actions/labeler@v5
|
||||||
|
|
|
@ -8,14 +8,15 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
lock:
|
lock:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'Sonarr/Sonarr'
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v2
|
- uses: dessant/lock-threads@v5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-lock-inactive-days: '90'
|
issue-inactive-days: '90'
|
||||||
issue-exclude-created-before: ''
|
exclude-issue-created-before: ''
|
||||||
issue-exclude-labels: 'one-day-maybe'
|
exclude-any-issue-labels: 'one-day-maybe'
|
||||||
issue-lock-labels: ''
|
add-issue-labels: ''
|
||||||
issue-lock-comment: ''
|
issue-comment: ''
|
||||||
issue-lock-reason: 'resolved'
|
issue-lock-reason: 'resolved'
|
||||||
process-only: ''
|
process-only: ''
|
||||||
|
|
|
@ -127,6 +127,7 @@ coverage*.xml
|
||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
|
.mono
|
||||||
|
|
||||||
#VS outout folders
|
#VS outout folders
|
||||||
bin
|
bin
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||||
|
"name": "Run Sonarr",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build dotnet",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/_output/net6.0/Sonarr",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build dotnet",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"-restore",
|
||||||
|
"${workspaceFolder}/src/Sonarr.sln",
|
||||||
|
"-p:GenerateFullPaths=true",
|
||||||
|
"-p:Configuration=Debug",
|
||||||
|
"-p:Platform=Posix",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/src/Sonarr.sln",
|
||||||
|
"-property:GenerateFullPaths=true",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/src/Sonarr.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
47
build.sh
47
build.sh
|
@ -4,19 +4,18 @@ set -e
|
||||||
outputFolder='_output'
|
outputFolder='_output'
|
||||||
testPackageFolder='_tests'
|
testPackageFolder='_tests'
|
||||||
artifactsFolder="_artifacts";
|
artifactsFolder="_artifacts";
|
||||||
|
framework="${FRAMEWORK:=net6.0}"
|
||||||
|
|
||||||
ProgressStart()
|
ProgressStart()
|
||||||
{
|
{
|
||||||
echo "##teamcity[blockOpened name='$1']"
|
echo "::group::$1"
|
||||||
echo "##teamcity[progressStart '$1']"
|
|
||||||
echo "Start '$1'"
|
echo "Start '$1'"
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressEnd()
|
ProgressEnd()
|
||||||
{
|
{
|
||||||
echo "Finish '$1'"
|
echo "Finish '$1'"
|
||||||
echo "##teamcity[progressFinish '$1']"
|
echo "::endgroup::"
|
||||||
echo "##teamcity[blockClosed name='$1']"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateVersionNumber()
|
UpdateVersionNumber()
|
||||||
|
@ -140,7 +139,7 @@ PackageLinux()
|
||||||
|
|
||||||
echo "Adding Sonarr.Mono to UpdatePackage"
|
echo "Adding Sonarr.Mono to UpdatePackage"
|
||||||
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
||||||
if [ "$framework" = "net6.0" ]; then
|
if [ "$framework" = "$framework" ]; then
|
||||||
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
||||||
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
||||||
fi
|
fi
|
||||||
|
@ -168,7 +167,7 @@ PackageMacOS()
|
||||||
|
|
||||||
echo "Adding Sonarr.Mono to UpdatePackage"
|
echo "Adding Sonarr.Mono to UpdatePackage"
|
||||||
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
||||||
if [ "$framework" = "net6.0" ]; then
|
if [ "$framework" = "$framework" ]; then
|
||||||
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
||||||
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
||||||
fi
|
fi
|
||||||
|
@ -400,20 +399,20 @@ then
|
||||||
|
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "win-x64"
|
PackageTests "$framework" "win-x64"
|
||||||
PackageTests "net6.0" "win-x86"
|
PackageTests "$framework" "win-x86"
|
||||||
PackageTests "net6.0" "linux-x64"
|
PackageTests "$framework" "linux-x64"
|
||||||
PackageTests "net6.0" "linux-musl-x64"
|
PackageTests "$framework" "linux-musl-x64"
|
||||||
PackageTests "net6.0" "osx-x64"
|
PackageTests "$framework" "osx-x64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "freebsd-x64"
|
PackageTests "$framework" "freebsd-x64"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
PackageTests "$FRAMEWORK" "$RID"
|
PackageTests "$FRAMEWORK" "$RID"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
UploadTestArtifacts "net6.0"
|
UploadTestArtifacts "$framework"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$FRONTEND" = "YES" ];
|
if [ "$FRONTEND" = "YES" ];
|
||||||
|
@ -435,22 +434,22 @@ then
|
||||||
|
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
Package "net6.0" "win-x64"
|
Package "$framework" "win-x64"
|
||||||
Package "net6.0" "win-x86"
|
Package "$framework" "win-x86"
|
||||||
Package "net6.0" "linux-x64"
|
Package "$framework" "linux-x64"
|
||||||
Package "net6.0" "linux-musl-x64"
|
Package "$framework" "linux-musl-x64"
|
||||||
Package "net6.0" "linux-arm64"
|
Package "$framework" "linux-arm64"
|
||||||
Package "net6.0" "linux-musl-arm64"
|
Package "$framework" "linux-musl-arm64"
|
||||||
Package "net6.0" "linux-arm"
|
Package "$framework" "linux-arm"
|
||||||
Package "net6.0" "osx-x64"
|
Package "$framework" "osx-x64"
|
||||||
Package "net6.0" "osx-arm64"
|
Package "$framework" "osx-arm64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
Package "net6.0" "freebsd-x64"
|
Package "$framework" "freebsd-x64"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
Package "$FRAMEWORK" "$RID"
|
Package "$FRAMEWORK" "$RID"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
UploadArtifacts "net6.0"
|
UploadArtifacts "$framework"
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
fromdos ./debian/*
|
|
||||||
chmod ugo-x ./debian/*
|
|
||||||
cp -r ./debian ./debian_backup
|
|
||||||
|
|
||||||
BuildVersion=${dependent_build_number:-4.10.0.999}
|
|
||||||
BuildBranch=${dependent_build_branch:-main}
|
|
||||||
BootstrapVersion=`echo "$BuildVersion" | cut -d. -f1,2,3`
|
|
||||||
BootstrapUpdater="BuiltIn"
|
|
||||||
PackageUpdater="apt"
|
|
||||||
|
|
||||||
echo Version: "$BuildVersion" Branch: "$BuildBranch"
|
|
||||||
|
|
||||||
rm -r ./sonarr_bin/Sonarr.Update
|
|
||||||
chmod -R ugo-x,ugo+rwX,go-w ./sonarr_bin/*
|
|
||||||
|
|
||||||
echo Updating changelog for $BuildVersion
|
|
||||||
sed -i "s:{version}:$BuildVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
|
||||||
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$PackageUpdater:g" debian/preinst debian/postinst debian/postrm
|
|
||||||
sed -i '/#BEGIN BUILTIN UPDATER/,/#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
|
||||||
echo "# Do Not Edit\nPackageVersion=$BuildVersion\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=$BuildVersion\nUpdateMethod=$PackageUpdater\nBranch=$BuildBranch" > package_info
|
|
||||||
|
|
||||||
echo Running debuild for $BuildVersion
|
|
||||||
if [ -z "${TEST_OUTPUT}" ]; then
|
|
||||||
debuild -b
|
|
||||||
else
|
|
||||||
debuild -us -uc -b
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore debian directory to the original files
|
|
||||||
rm -rf ./debian
|
|
||||||
mv ./debian_backup ./debian
|
|
||||||
|
|
||||||
echo Updating changelog for $BootstrapVersion
|
|
||||||
sed -i "s:{version}:$BootstrapVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
|
||||||
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$BootstrapUpdater:g" debian/preinst debian/postinst debian/postrm
|
|
||||||
sed -i '/#BEGIN BUILTIN UPDATER/d; /#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
|
||||||
echo "# Do Not Edit\nPackageVersion=$BootstrapVersion\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=$BuildVersion\nUpdateMethod=$BootstrapUpdater\nBranch=$BuildBranch" > package_info
|
|
||||||
|
|
||||||
echo Running debuild for $BootstrapVersion
|
|
||||||
if [ -z "${TEST_OUTPUT}" ]; then
|
|
||||||
debuild -b
|
|
||||||
else
|
|
||||||
debuild -us -uc -b
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo Moving stuff around
|
|
||||||
mv ../sonarr_*.deb ./
|
|
||||||
mv ../sonarr_*.changes ./
|
|
||||||
rm ../sonarr_*.build
|
|
||||||
|
|
||||||
if [ -z "${TEST_OUTPUT}" ]; then
|
|
||||||
echo Signing Package
|
|
||||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BuildVersion}_all.deb"
|
|
||||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BootstrapVersion}_all.deb"
|
|
||||||
|
|
||||||
echo running alien
|
|
||||||
alien -r -v ./*.deb
|
|
||||||
else
|
|
||||||
echo "Exporting packages to ${TEST_OUTPUT}"
|
|
||||||
dpkg -e "sonarr_${BuildVersion}_all.deb" ${TEST_OUTPUT}/sonarr-build
|
|
||||||
dpkg -e "sonarr_${BootstrapVersion}_all.deb" ${TEST_OUTPUT}/sonarr-release
|
|
||||||
|
|
||||||
cp *.deb ${TEST_OUTPUT}/
|
|
||||||
fi
|
|
|
@ -1,6 +0,0 @@
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
|
@ -1,5 +0,0 @@
|
||||||
sonarr ({version}) {branch}; urgency=low
|
|
||||||
|
|
||||||
* Automatic Release.
|
|
||||||
|
|
||||||
-- Sonarr <hello@sonarr.tv> Sun, 28 Jan 2018 00:00:00 -0700
|
|
|
@ -1 +0,0 @@
|
||||||
10
|
|
|
@ -1,17 +0,0 @@
|
||||||
#!/bin/sh -e
|
|
||||||
|
|
||||||
. /usr/share/debconf/confmodule
|
|
||||||
|
|
||||||
db_beginblock
|
|
||||||
db_input high sonarr/owning_user || true
|
|
||||||
db_input high sonarr/owning_group || true
|
|
||||||
db_endblock
|
|
||||||
db_go
|
|
||||||
|
|
||||||
db_beginblock
|
|
||||||
db_input low sonarr/owning_umask || true
|
|
||||||
db_input low sonarr/config_directory || true
|
|
||||||
db_endblock
|
|
||||||
db_go
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,18 +0,0 @@
|
||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Sonarr <hello@sonarr.tv>
|
|
||||||
Source: sonarr
|
|
||||||
Homepage: https://sonarr.tv
|
|
||||||
Vcs-Git: git@github.com:Sonarr/Sonarr.git
|
|
||||||
Vcs-Browser: https://github.com/Sonarr/Sonarr
|
|
||||||
Build-Depends: debhelper (>= 9),
|
|
||||||
dh-systemd (>= 1.5)
|
|
||||||
|
|
||||||
Package: sonarr
|
|
||||||
Architecture: all
|
|
||||||
Provides: nzbdrone
|
|
||||||
Conflicts: nzbdrone
|
|
||||||
Replaces: nzbdrone
|
|
||||||
Depends: adduser, libsqlite3-0 (>= 3.7), ${cli:Depends}, ${misc:Depends}
|
|
||||||
Suggests: sqlite3 (>= 3.7)
|
|
||||||
Description: Internet PVR
|
|
|
@ -1,24 +0,0 @@
|
||||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: sonarr
|
|
||||||
Source: https://github.com/Sonarr/Sonarr
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2010-2016 Sonarr <hello@sonarr.tv>
|
|
||||||
|
|
||||||
License: GPL-3.0+
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
.
|
|
||||||
This package is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
.
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
.
|
|
||||||
On Debian systems, the complete text of the GNU General
|
|
||||||
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
|
|
|
@ -1 +0,0 @@
|
||||||
sonarr_3.0.0.0_all.deb web optional
|
|
|
@ -1,2 +0,0 @@
|
||||||
sonarr_bin/* usr/lib/sonarr/bin
|
|
||||||
package_info usr/lib/sonarr
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
#!/bin/bash
|
||||||
|
### Description: Sonarr .NET Debian install
|
||||||
|
### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0
|
||||||
|
### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN
|
||||||
|
### Version v1.0.0 2023-12-29 - StevieTV - adapted from servarr script for Sonarr installs
|
||||||
|
### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM
|
||||||
|
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
|
||||||
|
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
|
||||||
|
|
||||||
|
### Boilerplate Warning
|
||||||
|
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
scriptversion="1.0.3"
|
||||||
|
scriptdate="2024-01-06"
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Running Sonarr Install Script - Version [$scriptversion] as of [$scriptdate]"
|
||||||
|
|
||||||
|
# Am I root?, need root!
|
||||||
|
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "Please run as root."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
app="sonarr"
|
||||||
|
app_port="8989"
|
||||||
|
app_prereq="curl sqlite3 wget"
|
||||||
|
app_umask="0002"
|
||||||
|
branch="main"
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
### Update these variables as required for your specific instance
|
||||||
|
installdir="/opt" # {Update me if needed} Install Location
|
||||||
|
bindir="${installdir}/${app^}" # Full Path to Install Location
|
||||||
|
datadir="/var/lib/$app/" # {Update me if needed} AppData directory to use
|
||||||
|
app_bin=${app^} # Binary Name of the app
|
||||||
|
|
||||||
|
# This script should not be ran from installdir, otherwise later in the script the extracted files will be removed before they can be moved to installdir.
|
||||||
|
if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ]; then
|
||||||
|
echo "You should not run this script from the intended install directory. The script will exit. Please re-run it from another directory"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prompt User
|
||||||
|
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
|
||||||
|
app_uid=$(echo "$app_uid" | tr -d ' ')
|
||||||
|
app_uid=${app_uid:-$app}
|
||||||
|
# Prompt Group
|
||||||
|
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
|
||||||
|
app_guid=$(echo "$app_guid" | tr -d ' ')
|
||||||
|
app_guid=${app_guid:-media}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
# Create User / Group as needed
|
||||||
|
if [ "$app_guid" != "$app_uid" ]; then
|
||||||
|
if ! getent group "$app_guid" >/dev/null; then
|
||||||
|
groupadd "$app_guid"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if ! getent passwd "$app_uid" >/dev/null; then
|
||||||
|
adduser --system --no-create-home --ingroup "$app_guid" "$app_uid"
|
||||||
|
echo "Created and added User [$app_uid] to Group [$app_guid]"
|
||||||
|
fi
|
||||||
|
if ! getent group "$app_guid" | grep -qw "$app_uid"; then
|
||||||
|
echo "User [$app_uid] did not exist in Group [$app_guid]"
|
||||||
|
usermod -a -G "$app_guid" "$app_uid"
|
||||||
|
echo "Added User [$app_uid] to Group [$app_guid]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop the App if running
|
||||||
|
if service --status-all | grep -Fq "$app"; then
|
||||||
|
systemctl stop "$app"
|
||||||
|
systemctl disable "$app".service
|
||||||
|
echo "Stopped existing $app"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create Appdata Directory
|
||||||
|
|
||||||
|
# AppData
|
||||||
|
mkdir -p "$datadir"
|
||||||
|
chown -R "$app_uid":"$app_guid" "$datadir"
|
||||||
|
chmod 775 "$datadir"
|
||||||
|
echo "Directories created"
|
||||||
|
# Download and install the App
|
||||||
|
|
||||||
|
# prerequisite packages
|
||||||
|
echo ""
|
||||||
|
echo "Installing pre-requisite Packages"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
apt update && apt install -y $app_prereq
|
||||||
|
echo ""
|
||||||
|
ARCH=$(dpkg --print-architecture)
|
||||||
|
# get arch
|
||||||
|
dlbase="https://services.sonarr.tv/v1/download/$branch/latest?version=4&os=linux"
|
||||||
|
case "$ARCH" in
|
||||||
|
"amd64") DLURL="${dlbase}&arch=x64" ;;
|
||||||
|
"armhf") DLURL="${dlbase}&arch=arm" ;;
|
||||||
|
"arm64") DLURL="${dlbase}&arch=arm64" ;;
|
||||||
|
*)
|
||||||
|
echo "Arch not supported"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo ""
|
||||||
|
echo "Removing previous tarballs"
|
||||||
|
# -f to Force so we fail if it doesn't exist
|
||||||
|
rm -f "${app^}".*.tar.gz
|
||||||
|
echo ""
|
||||||
|
echo "Downloading..."
|
||||||
|
wget --content-disposition "$DLURL"
|
||||||
|
tar -xvzf "${app^}".*.tar.gz
|
||||||
|
echo ""
|
||||||
|
echo "Installation files downloaded and extracted"
|
||||||
|
|
||||||
|
# remove existing installs
|
||||||
|
echo "Removing existing installation"
|
||||||
|
rm -rf "$bindir"
|
||||||
|
echo "Installing..."
|
||||||
|
mv "${app^}" $installdir
|
||||||
|
chown "$app_uid":"$app_guid" -R "$bindir"
|
||||||
|
chmod 775 "$bindir"
|
||||||
|
rm -rf "${app^}.*.tar.gz"
|
||||||
|
# Ensure we check for an update in case user installs older version or different branch
|
||||||
|
touch "$datadir"/update_required
|
||||||
|
chown "$app_uid":"$app_guid" "$datadir"/update_required
|
||||||
|
echo "App Installed"
|
||||||
|
# Configure Autostart
|
||||||
|
|
||||||
|
# Remove any previous app .service
|
||||||
|
echo "Removing old service file"
|
||||||
|
rm -rf /etc/systemd/system/"$app".service
|
||||||
|
|
||||||
|
# Create app .service with correct user startup
|
||||||
|
echo "Creating service file"
|
||||||
|
cat <<EOF | tee /etc/systemd/system/"$app".service >/dev/null
|
||||||
|
[Unit]
|
||||||
|
Description=${app^} Daemon
|
||||||
|
After=syslog.target network.target
|
||||||
|
[Service]
|
||||||
|
User=$app_uid
|
||||||
|
Group=$app_guid
|
||||||
|
UMask=$app_umask
|
||||||
|
Type=simple
|
||||||
|
ExecStart=$bindir/$app_bin -nobrowser -data=$datadir
|
||||||
|
TimeoutStopSec=20
|
||||||
|
KillMode=process
|
||||||
|
Restart=on-failure
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Start the App
|
||||||
|
echo "Service file created. Attempting to start the app"
|
||||||
|
systemctl -q daemon-reload
|
||||||
|
systemctl enable --now -q "$app"
|
||||||
|
|
||||||
|
# Finish Update/Installation
|
||||||
|
host=$(hostname -I)
|
||||||
|
ip_local=$(grep -oP '^\S*' <<<"$host")
|
||||||
|
echo ""
|
||||||
|
echo "Install complete"
|
||||||
|
sleep 10
|
||||||
|
STATUS="$(systemctl is-active "$app")"
|
||||||
|
if [ "${STATUS}" = "active" ]; then
|
||||||
|
echo "Browse to http://$ip_local:$app_port for the ${app^} GUI"
|
||||||
|
else
|
||||||
|
echo "${app^} failed to start"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exit
|
||||||
|
exit 0
|
|
@ -1,111 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BUILD_VERSION={version}
|
|
||||||
UPDATER={updater}
|
|
||||||
|
|
||||||
. /usr/share/debconf/confmodule
|
|
||||||
db_get sonarr/owning_user
|
|
||||||
USER="$RET"
|
|
||||||
db_get sonarr/owning_group
|
|
||||||
GROUP="$RET"
|
|
||||||
db_get sonarr/owning_umask
|
|
||||||
UMASK="$RET"
|
|
||||||
db_get sonarr/config_directory
|
|
||||||
CONFDIR="$RET"
|
|
||||||
|
|
||||||
# Add User and Group
|
|
||||||
if ! getent group "$GROUP" >/dev/null; then
|
|
||||||
groupadd "$GROUP"
|
|
||||||
fi
|
|
||||||
if ! getent passwd "$USER" >/dev/null; then
|
|
||||||
adduser --system --no-create-home --ingroup "$GROUP" "$USER"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $1 = "configure" ]; then
|
|
||||||
# Migrate old Sonarr v3 alpha data dir from /var/opt/sonarr or user home
|
|
||||||
if [ -d "/var/opt/sonarr" ] && [ "$CONFDIR" != "/var/opt/sonarr" ] && [ ! -d "$CONFDIR" ]; then
|
|
||||||
varoptRoot="/var/opt/sonarr"
|
|
||||||
varoptAppData="$varoptRoot/.config/Sonarr"
|
|
||||||
sonarrUserHome=`getent passwd $USER | cut -d ':' -f 6`
|
|
||||||
sonarrAppData="$sonarrUserHome/.config/Sonarr"
|
|
||||||
if [ -f "$varoptRoot/sonarr.db" ]; then
|
|
||||||
# Handle /var/opt/sonarr/sonarr.db
|
|
||||||
mv "$varoptRoot" "$CONFDIR"
|
|
||||||
elif [ -f "$varoptAppData/sonarr.db" ]; then
|
|
||||||
# Handle /var/opt/sonarr/.config/Sonarr/sonarr.db
|
|
||||||
mv "$varoptAppData" "$CONFDIR"
|
|
||||||
rm -rf "$varoptRoot"
|
|
||||||
elif [ -f "$sonarrAppData/sonarr.db" ]; then
|
|
||||||
# Handle ~/.config/Sonarr/sonarr.db
|
|
||||||
mv "$sonarrAppData" "$CONFDIR"
|
|
||||||
rm -rf "$sonarrAppData"
|
|
||||||
else
|
|
||||||
mv "$varoptRoot" "$CONFDIR"
|
|
||||||
fi
|
|
||||||
chown -R $USER:$GROUP "$CONFDIR"
|
|
||||||
chmod -R 775 "$CONFDIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Migrate old NzbDrone data dir
|
|
||||||
if [ -d "/usr/lib/sonarr/nzbdrone-appdata" ] && [ ! -d "$CONFDIR" ]; then
|
|
||||||
NZBDRONE_DATA=`readlink /usr/lib/sonarr/nzbdrone-appdata`
|
|
||||||
if [ -f "$NZBDRONE_DATA/config.xml" ] && [ -f "$NZBDRONE_DATA/nzbdrone.db" ]; then
|
|
||||||
echo "Found NzbDrone database in $NZBDRONE_DATA, copying to $CONFDIR."
|
|
||||||
mkdir -p "$CONFDIR"
|
|
||||||
cp $NZBDRONE_DATA/config.xml $NZBDRONE_DATA/nzbdrone.db* "$CONFDIR/"
|
|
||||||
chown -R $USER:$GROUP "$CONFDIR"
|
|
||||||
chmod -R 775 "$CONFDIR"
|
|
||||||
else
|
|
||||||
echo "Missing NzbDrone database in $NZBDRONE_DATA, skipping migration."
|
|
||||||
fi
|
|
||||||
rm /usr/lib/sonarr/nzbdrone-appdata
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create data directory
|
|
||||||
if [ ! -d "$CONFDIR" ]; then
|
|
||||||
mkdir -p "$CONFDIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set permissions on data directory (always do this instead only on creation in case user was changed via dpkg-reconfigure)
|
|
||||||
chown -R $USER:$GROUP "$CONFDIR"
|
|
||||||
|
|
||||||
#BEGIN BUILTIN UPDATER
|
|
||||||
# Apply patch if present
|
|
||||||
if [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin_patch/release_info ]; then
|
|
||||||
# It shouldn't be possible to get a wrong bin_patch, but let's check anyway and throw it away if it's wrong
|
|
||||||
currentVersion=`cat /usr/lib/sonarr/bin_patch/release_info | grep 'ReleaseVersion=' | cut -d= -f 2`
|
|
||||||
currentRelease=`echo "$currentVersion" | cut -d. -f1,2,3`
|
|
||||||
currentBuild=`echo "$currentVersion" | cut -d. -f4`
|
|
||||||
targetVersion=$BUILD_VERSION
|
|
||||||
targetRelease=`echo "$targetVersion" | cut -d. -f1,2,3`
|
|
||||||
targetBuild=`echo "$targetVersion" | cut -d. -f4`
|
|
||||||
|
|
||||||
if [ "$currentRelease" = "$targetRelease" ] && [ "$currentBuild" -gt "$targetBuild" ]; then
|
|
||||||
echo "Applying $currentVersion from BuiltIn updater instead of downgrading to $targetVersion"
|
|
||||||
rm -rf /usr/lib/sonarr/bin
|
|
||||||
mv /usr/lib/sonarr/bin_patch /usr/lib/sonarr/bin
|
|
||||||
else
|
|
||||||
rm -rf /usr/lib/sonarr/bin_patch
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
#END BUILTIN UPDATER
|
|
||||||
|
|
||||||
# Set permissions on /usr/lib/sonarr
|
|
||||||
chown -R $USER:$GROUP /usr/lib/sonarr
|
|
||||||
|
|
||||||
# Update sonarr.service file
|
|
||||||
sed -i "s:User=\w*:User=$USER:g; s:Group=\w*:Group=$GROUP:g; s:UMask=[0-9]*:UMask=$UMASK:g; s:-data=.*$:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
|
|
||||||
|
|
||||||
#BEGIN BUILTIN UPDATER
|
|
||||||
if [ "$UPDATER" = "BuiltIn" ]; then
|
|
||||||
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
|
|
||||||
touch $CONFDIR/update_required
|
|
||||||
chown $USER:$GROUP $CONFDIR/update_required
|
|
||||||
fi
|
|
||||||
#END BUILTIN UPDATER
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,36 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BUILD_VERSION={version}
|
|
||||||
UPDATER={updater}
|
|
||||||
|
|
||||||
if [ $1 = "abort-install" ]; then
|
|
||||||
# preinst was aborted, possibly due to NzbDrone still running.
|
|
||||||
# Nothing to do here
|
|
||||||
:
|
|
||||||
fi
|
|
||||||
|
|
||||||
# The bin directory is expected to be empty, unless the BuiltIn updater added files.
|
|
||||||
if [ $1 = "remove" ] && [ -d /usr/lib/sonarr/bin ]; then
|
|
||||||
rm -rf /usr/lib/sonarr/bin
|
|
||||||
fi
|
|
||||||
|
|
||||||
#BEGIN BUILTIN UPDATER
|
|
||||||
# Remove any existing patch if still present
|
|
||||||
if [ $1 = "remove" ] && [ -d /usr/lib/sonarr/bin_patch ]; then
|
|
||||||
rm -rf /usr/lib/sonarr/bin_patch
|
|
||||||
fi
|
|
||||||
#END BUILTIN UPDATER
|
|
||||||
|
|
||||||
# Purge the entire sonarr configuration directory.
|
|
||||||
# TODO: Maybe move a minimal backup to tmp?
|
|
||||||
if [ $1 = "purge" ] && [ -e /usr/share/debconf/confmodule ]; then
|
|
||||||
. /usr/share/debconf/confmodule
|
|
||||||
db_get sonarr/config_directory
|
|
||||||
CONFDIR="$RET"
|
|
||||||
if [ -d "$CONFDIR" ]; then
|
|
||||||
rm -rf "$CONFDIR"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
|
@ -1,91 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BUILD_VERSION={version}
|
|
||||||
UPDATER={updater}
|
|
||||||
|
|
||||||
# Deal with existing nzbdrone installs
|
|
||||||
#
|
|
||||||
# Existing nzbdrone packages do not have startup scripts and the process might still be running.
|
|
||||||
# If the user manually installed nzbdrone then the process might still be running too.
|
|
||||||
if [ $1 = "install" ]; then
|
|
||||||
psNzbDrone=`ps ax -o'user:20,pid,ppid,unit,args' | grep mono.*NzbDrone\\\\.exe || true`
|
|
||||||
if [ ! -z "$psNzbDrone" ]; then
|
|
||||||
# Get the user and optional systemd unit
|
|
||||||
psNzbDroneUser=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 1`
|
|
||||||
psNzbDroneUnit=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 4`
|
|
||||||
# Get the appdata from the cmdline or get it from the user dir
|
|
||||||
droneAppData=`echo "$psNzbDrone" | tr ' ' '\n' | grep -- "-data=" | cut -d= -f 2`
|
|
||||||
if [ "$droneAppData" = "" ]; then
|
|
||||||
droneUserHome=`getent passwd $psNzbDroneUser | cut -d ':' -f 6`
|
|
||||||
droneAppData="$droneUserHome/.config/NzbDrone"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$psNzbDroneUnit" != "-" ] && [ -d /run/systemd/system ]; then
|
|
||||||
if [ "$psNzbDroneUnit" = "sonarr.service" ]; then
|
|
||||||
# Conflicts with our new sonarr.service so we have to remove it
|
|
||||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and removing..."
|
|
||||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
|
||||||
if [ -f "/etc/systemd/system/$psNzbDroneUnit" ]; then
|
|
||||||
rm /etc/systemd/system/$psNzbDroneUnit
|
|
||||||
fi
|
|
||||||
if [ -f "/usr/lib/systemd/system/$psNzbDroneUnit" ]; then
|
|
||||||
rm /usr/lib/systemd/system/$psNzbDroneUnit
|
|
||||||
fi
|
|
||||||
deb-systemd-helper purge $psNzbDroneUnit >/dev/null
|
|
||||||
deb-systemd-helper unmask $psNzbDroneUnit >/dev/null
|
|
||||||
systemctl --system daemon-reload >/dev/null || true
|
|
||||||
else
|
|
||||||
# Just disable it, so the user can revisit the settings later
|
|
||||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and disabling..."
|
|
||||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
|
||||||
deb-systemd-invoke mask $psNzbDroneUnit >/dev/null
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# We don't support auto migration for other startup methods, so bail.
|
|
||||||
# This leaves the sonarr package in an incomplete state.
|
|
||||||
echo "ps: $psNzbDrone"
|
|
||||||
echo "Error: An existing Sonarr v2 (NzbDrone) process is running. Remove the NzbDrone auto-startup prior to installing sonarr."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# We don't have the debconf configuration yet so we can't migrate the data.
|
|
||||||
# Instead we symlink so postinst knows where it's at.
|
|
||||||
if [ -f "/usr/lib/sonarr/nzbdrone-appdata" ]; then
|
|
||||||
rm "/usr/lib/sonarr/nzbdrone-appdata"
|
|
||||||
else
|
|
||||||
mkdir -p "/usr/lib/sonarr"
|
|
||||||
fi
|
|
||||||
ln -s $droneAppData /usr/lib/sonarr/nzbdrone-appdata
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
#BEGIN BUILTIN UPDATER
|
|
||||||
# Check for supported upgrade paths
|
|
||||||
if [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin/release_info ]; then
|
|
||||||
# If we allow the Built-In updater to upgrade from 3.0.1.123 to 3.0.2.500 and now apt is catching up to 3.0.2.425
|
|
||||||
# then we need to deal with that 500->425 'downgrade'.
|
|
||||||
# We do that by preserving the binaries and using those instead for postinst.
|
|
||||||
|
|
||||||
currentVersion=`cat /usr/lib/sonarr/bin/release_info | grep 'ReleaseVersion=' | cut -d= -f 2`
|
|
||||||
currentRelease=`echo "$currentVersion" | cut -d. -f1,2,3`
|
|
||||||
currentBuild=`echo "$currentVersion" | cut -d. -f4`
|
|
||||||
targetVersion=$BUILD_VERSION
|
|
||||||
targetRelease=`echo "$targetVersion" | cut -d. -f1,2,3`
|
|
||||||
targetBuild=`echo "$targetVersion" | cut -d. -f4`
|
|
||||||
|
|
||||||
if [ -d /usr/lib/sonarr/bin_patch ]; then
|
|
||||||
rm -rf /usr/lib/sonarr/bin_patch
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the existing version is already an upgrade for the included build
|
|
||||||
if [ "$currentRelease" = "$targetRelease" ] && [ "$currentBuild" -gt "$targetBuild" ]; then
|
|
||||||
echo "Preserving $currentVersion from BuiltIn updater instead of downgrading to $targetVersion"
|
|
||||||
cp -r /usr/lib/sonarr/bin /usr/lib/sonarr/bin_patch
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
#END BUILTIN UPDATER
|
|
||||||
|
|
||||||
#DEBHELPER#
|
|
||||||
|
|
||||||
exit 0
|
|
|
@ -1,21 +0,0 @@
|
||||||
#!/usr/bin/make -f
|
|
||||||
|
|
||||||
# Uncomment this to turn on verbose mode.
|
|
||||||
#export DH_VERBOSE=1
|
|
||||||
|
|
||||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@ --with=systemd --with=cli
|
|
||||||
|
|
||||||
# No init script, only systemd
|
|
||||||
override_dh_installinit:
|
|
||||||
true
|
|
||||||
|
|
||||||
# Sonarr likes debug symbols for logging
|
|
||||||
override_dh_clistrip:
|
|
||||||
|
|
||||||
override_dh_makeclilibs:
|
|
||||||
|
|
||||||
override_dh_clideps:
|
|
||||||
dh_clideps -d -r $(patsubst %,--exclude-moduleref=%,$(EXCLUDE_MODULEREFS))
|
|
|
@ -1,2 +0,0 @@
|
||||||
ignores msbuild
|
|
||||||
ignores libc6
|
|
|
@ -11,7 +11,7 @@ Group=sonarr
|
||||||
UMask=002
|
UMask=002
|
||||||
|
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/lib/sonarr/bin/Sonarr -nobrowser -data=/var/lib/sonarr
|
ExecStart=/opt/Sonarr/Sonarr -nobrowser -data=/var/lib/sonarr
|
||||||
TimeoutStopSec=20
|
TimeoutStopSec=20
|
||||||
KillMode=process
|
KillMode=process
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
Template: sonarr/owning_user
|
|
||||||
Type: string
|
|
||||||
Default: sonarr
|
|
||||||
Description: Sonarr user:
|
|
||||||
Specify the user that is used to run Sonarr. The user will be created if it does not already exist.
|
|
||||||
The default 'sonarr' should work fine for most users. You can specify the user group next.
|
|
||||||
|
|
||||||
Template: sonarr/owning_group
|
|
||||||
Type: string
|
|
||||||
Default: sonarr
|
|
||||||
Description: Sonarr group:
|
|
||||||
Specify the group that is used to run Sonarr. The group will be created if it does not already exist.
|
|
||||||
If the user doesn't already exist then this group will be used as the user's primary group.
|
|
||||||
Any media files created by Sonarr will be writeable by this group.
|
|
||||||
It's advisable to keep the group the same between download client, Sonarr and media centers.
|
|
||||||
|
|
||||||
Template: sonarr/owning_umask
|
|
||||||
Type: string
|
|
||||||
Default: 0002
|
|
||||||
Description: Sonarr umask:
|
|
||||||
Specifies the umask of the files created by Sonarr. 0002 means the files will be created with 664 as permissions.
|
|
||||||
|
|
||||||
Template: sonarr/config_directory
|
|
||||||
Type: string
|
|
||||||
Default: /var/lib/sonarr
|
|
||||||
Description: Config directory:
|
|
||||||
Specify the directory where Sonarr stores the internal database and metadata. Media content will be stored elsewhere.
|
|
|
@ -23,11 +23,11 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>3.0</string>
|
<string>4.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>xmmd</string>
|
<string>xmmd</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>3.0</string>
|
<string>4.0</string>
|
||||||
<key>NSAppleScriptEnabled</key>
|
<key>NSAppleScriptEnabled</key>
|
||||||
<string>YES</string>
|
<string>YES</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
|
@ -3,8 +3,5 @@
|
||||||
@REM SET BRANCH=develop
|
@REM SET BRANCH=develop
|
||||||
@REM SET FRAMEWORK=net6.0
|
@REM SET FRAMEWORK=net6.0
|
||||||
@REM SET RUNTIME=win-x64
|
@REM SET RUNTIME=win-x64
|
||||||
echo ##teamcity[progressStart 'Building setup file']
|
|
||||||
inno\ISCC.exe sonarr.iss
|
|
||||||
echo ##teamcity[progressFinish 'Building setup file']
|
|
||||||
|
|
||||||
echo ##teamcity[publishArtifacts 'distribution\windows\setup\output\*%RUNTIME%*.exe']
|
inno\ISCC.exe sonarr.iss
|
||||||
|
|
|
@ -40,7 +40,7 @@ Compression=lzma2/normal
|
||||||
AppContact={#ForumsURL}
|
AppContact={#ForumsURL}
|
||||||
VersionInfoVersion={#MajorVersion}
|
VersionInfoVersion={#MajorVersion}
|
||||||
SetupLogging=yes
|
SetupLogging=yes
|
||||||
OutputDir=output
|
OutputDir="..\..\..\_artifacts"
|
||||||
AppverName={#AppName}
|
AppverName={#AppName}
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
|
@ -53,8 +53,8 @@ Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts
|
||||||
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Sonarr\Sonarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\..\..\_output\{#Runtime}\{#Framework}\Sonarr\Sonarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Sonarr\*"; Excludes: "Sonarr.Update"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\..\..\_output\{#Runtime}\{#Framework}\Sonarr\*"; Excludes: "Sonarr.Update"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
|
|
13
docs.sh
13
docs.sh
|
@ -1,6 +1,7 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net6.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
|
@ -24,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/net6.0/$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
|
||||||
|
|
||||||
|
|
|
@ -359,11 +359,16 @@ module.exports = {
|
||||||
],
|
],
|
||||||
|
|
||||||
rules: Object.assign(typescriptEslintRecommended.rules, {
|
rules: Object.assign(typescriptEslintRecommended.rules, {
|
||||||
'no-shadow': 'off',
|
'@typescript-eslint/no-unused-vars': [
|
||||||
// These should be enabled after cleaning things up
|
'error',
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
{
|
||||||
|
args: 'after-used',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true
|
||||||
|
}
|
||||||
|
],
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
'react/prop-types': 'off',
|
'no-shadow': 'off',
|
||||||
'prettier/prettier': 'error',
|
'prettier/prettier': 'error',
|
||||||
'simple-import-sort/imports': [
|
'simple-import-sort/imports': [
|
||||||
'error',
|
'error',
|
||||||
|
@ -376,7 +381,41 @@ module.exports = {
|
||||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
|
||||||
|
// React Hooks
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'error',
|
||||||
|
|
||||||
|
// React
|
||||||
|
'react/function-component-definition': 'error',
|
||||||
|
'react/hook-use-state': 'error',
|
||||||
|
'react/jsx-boolean-value': ['error', 'always'],
|
||||||
|
'react/jsx-curly-brace-presence': [
|
||||||
|
'error',
|
||||||
|
{ props: 'never', children: 'never' }
|
||||||
|
],
|
||||||
|
'react/jsx-fragments': 'error',
|
||||||
|
'react/jsx-handler-names': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
eventHandlerPrefix: 'on',
|
||||||
|
eventHandlerPropPrefix: 'on'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
|
||||||
|
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
|
||||||
|
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
|
||||||
|
'react/jsx-sort-props': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
callbacksLast: true,
|
||||||
|
noSortAlphabetically: true,
|
||||||
|
reservedFirst: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/self-closing-comp': 'error'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,8 @@ const loose = true;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'@babel/plugin-transform-logical-assignment-operators',
|
||||||
|
|
||||||
// Stage 1
|
// Stage 1
|
||||||
'@babel/plugin-proposal-export-default-from',
|
'@babel/plugin-proposal-export-default-from',
|
||||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||||
|
|
|
@ -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({
|
||||||
|
@ -202,7 +202,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]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,261 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
||||||
import TablePager from 'Components/Table/TablePager';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
|
||||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
|
||||||
|
|
||||||
class Blocklist extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: false,
|
|
||||||
lastToggled: null,
|
|
||||||
selectedState: {},
|
|
||||||
isConfirmRemoveModalOpen: false,
|
|
||||||
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,
|
|
||||||
totalRecords,
|
|
||||||
isRemoving,
|
|
||||||
isClearingBlocklistExecuting,
|
|
||||||
...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>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('BlocklistLoadError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error && !items.length &&
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{translate('NoHistoryBlocklist')}
|
|
||||||
</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,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
|
||||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
|
||||||
onRemoveSelected: PropTypes.func.isRequired,
|
|
||||||
onClearBlocklistPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Blocklist;
|
|
|
@ -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,152 +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 createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import Blocklist from './Blocklist';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.blocklist,
|
|
||||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
|
||||||
(blocklist, isClearingBlocklistExecuting) => {
|
|
||||||
return {
|
|
||||||
isClearingBlocklistExecuting,
|
|
||||||
...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 });
|
|
||||||
};
|
|
||||||
|
|
||||||
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}
|
|
||||||
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,
|
|
||||||
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;
|
|
|
@ -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;
|
|
@ -0,0 +1,54 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setBlocklistFilter } from 'Store/Actions/blocklistActions';
|
||||||
|
|
||||||
|
function createBlocklistSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.blocklist.items,
|
||||||
|
(blocklistItems) => {
|
||||||
|
return blocklistItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.blocklist.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlocklistFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createBlocklistSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'blocklist';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setBlocklistFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
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;
|
||||||
|
shortDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDetails(props: HistoryDetailsProps) {
|
||||||
|
const { eventType, sourceTitle, data, downloadId } = props;
|
||||||
|
|
||||||
|
const { shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventType === 'grabbed') {
|
||||||
|
const {
|
||||||
|
indexer,
|
||||||
|
releaseGroup,
|
||||||
|
seriesMatchType,
|
||||||
|
customFormatScore,
|
||||||
|
nzbInfoUrl,
|
||||||
|
downloadClient,
|
||||||
|
downloadClientName,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishedDate,
|
||||||
|
} = data as GrabbedHistoryData;
|
||||||
|
|
||||||
|
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(parseInt(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 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,7 +31,20 @@ function getHeaderTitle(eventType) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryDetailsModal(props) {
|
interface HistoryDetailsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
isMarkingAsFailed: boolean;
|
||||||
|
shortDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
onMarkAsFailedPress: () => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDetailsModal(props: HistoryDetailsModalProps) {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
eventType,
|
eventType,
|
||||||
|
@ -42,18 +55,13 @@ function HistoryDetailsModal(props) {
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat,
|
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
|
||||||
|
@ -67,44 +75,26 @@ function HistoryDetailsModal(props) {
|
||||||
</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 = {
|
HistoryDetailsModal.defaultProps = {
|
||||||
isMarkingAsFailed: false
|
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;
|
|
|
@ -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) {
|
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) {
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'episodeFileDeleted':
|
case 'episodeFileDeleted':
|
||||||
return 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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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 {
|
||||||
const iconName = getIconName(eventType);
|
eventType: HistoryEventType;
|
||||||
|
data: HistoryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryEventTypeCell({ eventType, data }: HistoryEventTypeCellProps) {
|
||||||
|
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;
|
|
|
@ -0,0 +1,275 @@
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } 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 { QualityModel } from 'Quality/Quality';
|
||||||
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
|
import useSeries from 'Series/useSeries';
|
||||||
|
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
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: object[];
|
||||||
|
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,
|
||||||
|
markAsFailedError,
|
||||||
|
columns,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const wasMarkingAsFailed = usePrevious(isMarkingAsFailed);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const series = useSeries(seriesId);
|
||||||
|
const episode = useEpisode(episodeId, 'episodes');
|
||||||
|
|
||||||
|
const { shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
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') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.downloadClient}>
|
||||||
|
{'downloadClient' in data ? data.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}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||||
|
onModalClose={handleDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryRow.defaultProps = {
|
||||||
|
customFormats: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
|
|
@ -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,364 +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 RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
<RemoveQueueItemsModal
|
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
|
||||||
selectedCount={selectedCount}
|
|
||||||
canIgnore={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
return !!(item && item.seriesId && item.episodeId);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
allPending={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
|
||||||
onModalClose={this.onConfirmRemoveModalClose}
|
|
||||||
/>
|
|
||||||
</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;
|
|
|
@ -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;
|
|
|
@ -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,467 +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,
|
|
||||||
estimatedCompletionTime,
|
|
||||||
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 === '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}
|
|
||||||
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,
|
|
||||||
estimatedCompletionTime: 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;
|
|
|
@ -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 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/TooltipPosition';
|
||||||
|
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';
|
||||||
|
@ -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,23 @@ 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 = {
|
QueueStatus.defaultProps = {
|
||||||
trackedDownloadStatus: 'ok',
|
trackedDownloadStatus: 'ok',
|
||||||
trackedDownloadState: 'downloading',
|
trackedDownloadState: 'downloading',
|
||||||
canFlip: false
|
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;
|
|
|
@ -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;
|
|
@ -1,171 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
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 { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class RemoveQueueItemModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = function() {
|
|
||||||
this.setState({
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveChange = ({ value }) => {
|
|
||||||
this.setState({ remove: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlocklistChange = ({ value }) => {
|
|
||||||
this.setState({ blocklist: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSkipRedownloadChange = ({ value }) => {
|
|
||||||
this.setState({ skipRedownload: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
|
||||||
const state = this.state;
|
|
||||||
|
|
||||||
this.resetState();
|
|
||||||
this.props.onRemovePress(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.resetState();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
sourceTitle,
|
|
||||||
canIgnore,
|
|
||||||
isPending
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { remove, blocklist, skipRedownload } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('RemoveQueueItem', { sourceTitle })}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPending ?
|
|
||||||
null :
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="remove"
|
|
||||||
value={remove}
|
|
||||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
|
||||||
isDisabled={!canIgnore}
|
|
||||||
onChange={this.onRemoveChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="blocklist"
|
|
||||||
value={blocklist}
|
|
||||||
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
|
||||||
onChange={this.onBlocklistChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
blocklist ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="skipRedownload"
|
|
||||||
value={skipRedownload}
|
|
||||||
helpText={translate('SkipRedownloadHelpText')}
|
|
||||||
onChange={this.onSkipRedownloadChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={this.onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={this.onRemoveConfirmed}
|
|
||||||
>
|
|
||||||
{translate('Remove')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveQueueItemModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
canIgnore: PropTypes.bool.isRequired,
|
|
||||||
isPending: PropTypes.bool.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RemoveQueueItemModal;
|
|
|
@ -0,0 +1,231 @@
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
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 { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './RemoveQueueItemModal.css';
|
||||||
|
|
||||||
|
export interface RemovePressProps {
|
||||||
|
remove: boolean;
|
||||||
|
changeCategory: boolean;
|
||||||
|
blocklist: boolean;
|
||||||
|
skipRedownload: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RemoveQueueItemModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
sourceTitle?: string;
|
||||||
|
canChangeCategory: boolean;
|
||||||
|
canIgnore: boolean;
|
||||||
|
isPending: boolean;
|
||||||
|
selectedCount?: number;
|
||||||
|
onRemovePress(props: RemovePressProps): void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||||
|
type BlocklistMethod =
|
||||||
|
| 'doNotBlocklist'
|
||||||
|
| 'blocklistAndSearch'
|
||||||
|
| 'blocklistOnly';
|
||||||
|
|
||||||
|
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
sourceTitle = '',
|
||||||
|
canIgnore,
|
||||||
|
canChangeCategory,
|
||||||
|
isPending,
|
||||||
|
selectedCount,
|
||||||
|
onRemovePress,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const multipleSelected = selectedCount && selectedCount > 1;
|
||||||
|
|
||||||
|
const [removalMethod, setRemovalMethod] =
|
||||||
|
useState<RemovalMethod>('removeFromClient');
|
||||||
|
const [blocklistMethod, setBlocklistMethod] =
|
||||||
|
useState<BlocklistMethod>('doNotBlocklist');
|
||||||
|
|
||||||
|
const { title, message } = useMemo(() => {
|
||||||
|
if (!selectedCount) {
|
||||||
|
return {
|
||||||
|
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||||
|
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedCount === 1) {
|
||||||
|
return {
|
||||||
|
title: translate('RemoveSelectedItem'),
|
||||||
|
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: translate('RemoveSelectedItems'),
|
||||||
|
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||||
|
selectedCount,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}, [sourceTitle, selectedCount]);
|
||||||
|
|
||||||
|
const removalMethodOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'removeFromClient',
|
||||||
|
value: translate('RemoveFromDownloadClient'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('RemoveMultipleFromDownloadClientHint')
|
||||||
|
: translate('RemoveFromDownloadClientHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'changeCategory',
|
||||||
|
value: translate('ChangeCategory'),
|
||||||
|
isDisabled: !canChangeCategory,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('ChangeCategoryMultipleHint')
|
||||||
|
: translate('ChangeCategoryHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ignore',
|
||||||
|
value: multipleSelected
|
||||||
|
? translate('IgnoreDownloads')
|
||||||
|
: translate('IgnoreDownload'),
|
||||||
|
isDisabled: !canIgnore,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('IgnoreDownloadsHint')
|
||||||
|
: translate('IgnoreDownloadHint'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||||
|
|
||||||
|
const blocklistMethodOptions = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'doNotBlocklist',
|
||||||
|
value: translate('DoNotBlocklist'),
|
||||||
|
hint: translate('DoNotBlocklistHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocklistAndSearch',
|
||||||
|
value: translate('BlocklistAndSearch'),
|
||||||
|
isDisabled: isPending,
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistAndSearchMultipleHint')
|
||||||
|
: translate('BlocklistAndSearchHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocklistOnly',
|
||||||
|
value: translate('BlocklistOnly'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistMultipleOnlyHint')
|
||||||
|
: translate('BlocklistOnlyHint'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [isPending, multipleSelected]);
|
||||||
|
|
||||||
|
const handleRemovalMethodChange = useCallback(
|
||||||
|
({ value }: { value: RemovalMethod }) => {
|
||||||
|
setRemovalMethod(value);
|
||||||
|
},
|
||||||
|
[setRemovalMethod]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlocklistMethodChange = useCallback(
|
||||||
|
({ value }: { value: BlocklistMethod }) => {
|
||||||
|
setBlocklistMethod(value);
|
||||||
|
},
|
||||||
|
[setBlocklistMethod]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirmRemove = useCallback(() => {
|
||||||
|
onRemovePress({
|
||||||
|
remove: removalMethod === 'removeFromClient',
|
||||||
|
changeCategory: removalMethod === 'changeCategory',
|
||||||
|
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||||
|
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemovalMethod('removeFromClient');
|
||||||
|
setBlocklistMethod('doNotBlocklist');
|
||||||
|
}, [
|
||||||
|
removalMethod,
|
||||||
|
blocklistMethod,
|
||||||
|
setRemovalMethod,
|
||||||
|
setBlocklistMethod,
|
||||||
|
onRemovePress,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
setRemovalMethod('removeFromClient');
|
||||||
|
setBlocklistMethod('doNotBlocklist');
|
||||||
|
|
||||||
|
onModalClose();
|
||||||
|
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||||
|
<ModalContent onModalClose={handleModalClose}>
|
||||||
|
<ModalHeader>{title}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.message}>{message}</div>
|
||||||
|
|
||||||
|
{isPending ? null : (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="removalMethod"
|
||||||
|
value={removalMethod}
|
||||||
|
values={removalMethodOptions}
|
||||||
|
isDisabled={!canChangeCategory && !canIgnore}
|
||||||
|
helpTextWarning={translate(
|
||||||
|
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||||
|
)}
|
||||||
|
onChange={handleRemovalMethodChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{multipleSelected
|
||||||
|
? translate('BlocklistReleases')
|
||||||
|
: translate('BlocklistRelease')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="blocklistMethod"
|
||||||
|
value={blocklistMethod}
|
||||||
|
values={blocklistMethodOptions}
|
||||||
|
helpText={translate('BlocklistReleaseHelpText')}
|
||||||
|
onChange={handleBlocklistMethodChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||||
|
{translate('Remove')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RemoveQueueItemModal;
|
|
@ -1,174 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
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 { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './RemoveQueueItemsModal.css';
|
|
||||||
|
|
||||||
class RemoveQueueItemsModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
resetState = function() {
|
|
||||||
this.setState({
|
|
||||||
remove: true,
|
|
||||||
blocklist: false,
|
|
||||||
skipRedownload: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveChange = ({ value }) => {
|
|
||||||
this.setState({ remove: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlocklistChange = ({ value }) => {
|
|
||||||
this.setState({ blocklist: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSkipRedownloadChange = ({ value }) => {
|
|
||||||
this.setState({ skipRedownload: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveConfirmed = () => {
|
|
||||||
const state = this.state;
|
|
||||||
|
|
||||||
this.resetState();
|
|
||||||
this.props.onRemovePress(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.resetState();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
selectedCount,
|
|
||||||
canIgnore,
|
|
||||||
allPending
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { remove, blocklist, skipRedownload } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div className={styles.message}>
|
|
||||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
allPending ?
|
|
||||||
null :
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="remove"
|
|
||||||
value={remove}
|
|
||||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
|
||||||
isDisabled={!canIgnore}
|
|
||||||
onChange={this.onRemoveChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="blocklist"
|
|
||||||
value={blocklist}
|
|
||||||
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
|
||||||
onChange={this.onBlocklistChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
blocklist ?
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="skipRedownload"
|
|
||||||
value={skipRedownload}
|
|
||||||
helpText={translate('SkipRedownloadHelpText')}
|
|
||||||
onChange={this.onSkipRedownloadChange}
|
|
||||||
/>
|
|
||||||
</FormGroup> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={this.onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={this.onRemoveConfirmed}
|
|
||||||
>
|
|
||||||
{translate('Remove')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoveQueueItemsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
selectedCount: PropTypes.number.isRequired,
|
|
||||||
canIgnore: PropTypes.bool.isRequired,
|
|
||||||
allPending: PropTypes.bool.isRequired,
|
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RemoveQueueItemsModal;
|
|
|
@ -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,12 +30,18 @@ function TimeleftCell(props) {
|
||||||
sizeleft,
|
sizeleft,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat
|
timeFormat,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (status === 'delay') {
|
if (status === 'delay') {
|
||||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
const date = getRelativeDate({
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
date: estimatedCompletionTime,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||||
|
includeMinuteZero: true,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell className={styles.timeleft}>
|
<TableRowCell className={styles.timeleft}>
|
||||||
|
@ -40,8 +56,14 @@ function TimeleftCell(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'downloadClientUnavailable') {
|
if (status === 'downloadClientUnavailable') {
|
||||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
const date = getRelativeDate({
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
date: estimatedCompletionTime,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||||
|
includeMinuteZero: true,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell className={styles.timeleft}>
|
<TableRowCell className={styles.timeleft}>
|
||||||
|
@ -56,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);
|
||||||
|
@ -76,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;
|
|
@ -69,6 +69,11 @@
|
||||||
height: 55px;
|
height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.originalLanguageName,
|
||||||
|
.network {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.tvdbLink {
|
.tvdbLink {
|
||||||
composes: link from '~Components/Link/Link.css';
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ interface CssExports {
|
||||||
'alreadyExistsIcon': string;
|
'alreadyExistsIcon': string;
|
||||||
'content': string;
|
'content': string;
|
||||||
'icons': string;
|
'icons': string;
|
||||||
|
'network': string;
|
||||||
|
'originalLanguageName': string;
|
||||||
'overlay': string;
|
'overlay': string;
|
||||||
'overview': string;
|
'overview': string;
|
||||||
'poster': string;
|
'poster': string;
|
||||||
|
|
|
@ -55,6 +55,7 @@ class AddNewSeriesSearchResult extends Component {
|
||||||
titleSlug,
|
titleSlug,
|
||||||
year,
|
year,
|
||||||
network,
|
network,
|
||||||
|
originalLanguage,
|
||||||
status,
|
status,
|
||||||
overview,
|
overview,
|
||||||
statistics,
|
statistics,
|
||||||
|
@ -145,14 +146,37 @@ 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> :
|
</Label> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -218,6 +242,7 @@ 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,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
statistics: PropTypes.object.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -118,7 +118,7 @@ class ImportSeriesSelectFolder extends Component {
|
||||||
className={styles.addErrorAlert}
|
className={styles.addErrorAlert}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
>
|
>
|
||||||
{translate('RootFolderLoadError')}
|
{translate('AddRootFolderError')}
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue