mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-14 15:44:20 -04:00
Compare commits
323 Commits
v0.3.16.23
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b79d3000d | ||
|
|
0f3e716044 | ||
|
|
f53c4dc017 | ||
|
|
f48eac7e36 | ||
|
|
7cc02f95af | ||
|
|
1f92bf6679 | ||
|
|
a8d4aa6770 | ||
|
|
7661b5bc87 | ||
|
|
200ef600cd | ||
|
|
ad6228983b | ||
|
|
582ec9f7ce | ||
|
|
525e855038 | ||
|
|
7a629ed044 | ||
|
|
7f501322dd | ||
|
|
18bca0b228 | ||
|
|
9ddac60b47 | ||
|
|
bd8bc0b35b | ||
|
|
ae623f4481 | ||
|
|
e67d133bb6 | ||
|
|
772ea95ce4 | ||
|
|
5459a7bb7e | ||
|
|
614f98f9b4 | ||
|
|
55763dae43 | ||
|
|
a362dab503 | ||
|
|
dba9fbf254 | ||
|
|
59a7605385 | ||
|
|
f819e582cf | ||
|
|
0972d41bf8 | ||
|
|
05d2335bfe | ||
|
|
5b4f54a959 | ||
|
|
bb5ad605fd | ||
|
|
52c3a95e63 | ||
|
|
52c5460537 | ||
|
|
280cec3d0e | ||
|
|
f10c2c01d8 | ||
|
|
2b6a328dac | ||
|
|
4078525f67 | ||
|
|
214e4270ac | ||
|
|
5396dd3e8e | ||
|
|
d5d4996c40 | ||
|
|
8b2223a9c4 | ||
|
|
2a3e7f8dae | ||
|
|
6406ed6289 | ||
|
|
d5b0831b0f | ||
|
|
8d72d5dbab | ||
|
|
74d1ab84e2 | ||
|
|
7341d20c51 | ||
|
|
5abf4f2992 | ||
|
|
cc90050c77 | ||
|
|
7ba8f8baee | ||
|
|
70aec175ef | ||
|
|
080dd301f3 | ||
|
|
bab45481db | ||
|
|
3e1e03e0ce | ||
|
|
bb599d6dc9 | ||
|
|
c94685842f | ||
|
|
5966f6da51 | ||
|
|
229e0dfe5d | ||
|
|
5173daa265 | ||
|
|
c2f770f242 | ||
|
|
0b7ce67635 | ||
|
|
bc74456944 | ||
|
|
fa460567a7 | ||
|
|
7dfceb307b | ||
|
|
305ad235a5 | ||
|
|
74c20e41bf | ||
|
|
347289b173 | ||
|
|
0ef3d2a5cc | ||
|
|
e5519d60c9 | ||
|
|
3a85b3a060 | ||
|
|
c1cdf44322 | ||
|
|
f861e54139 | ||
|
|
279e1029e0 | ||
|
|
b9ed39175b | ||
|
|
faba3ada95 | ||
|
|
e8647aee05 | ||
|
|
eaf5ce52bc | ||
|
|
73ab2760e4 | ||
|
|
3bb036e8c6 | ||
|
|
6e05456d6a | ||
|
|
8563a42822 | ||
|
|
841d38f4a5 | ||
|
|
9326d88eb6 | ||
|
|
015da61004 | ||
|
|
d02ea4b121 | ||
|
|
7bc9d700f9 | ||
|
|
661d72ef9b | ||
|
|
258a8d1c95 | ||
|
|
d4459b9475 | ||
|
|
a550c6554f | ||
|
|
c1b26eec8d | ||
|
|
ffe5ede55d | ||
|
|
9005860899 | ||
|
|
c67f67109e | ||
|
|
51b9744e25 | ||
|
|
334d824633 | ||
|
|
ae01387ca9 | ||
|
|
4eb13e0938 | ||
|
|
6dbb826f2f | ||
|
|
52dfa57dd7 | ||
|
|
f354b3bc47 | ||
|
|
2d9e6788e6 | ||
|
|
0d121fe9c0 | ||
|
|
892c34fe35 | ||
|
|
24f6007594 | ||
|
|
5028ed4027 | ||
|
|
05f303436b | ||
|
|
5635de96a8 | ||
|
|
ce59f32023 | ||
|
|
6d675a5207 | ||
|
|
b093b23900 | ||
|
|
884ac2cb6f | ||
|
|
295a6c4255 | ||
|
|
74a59d5790 | ||
|
|
ae23e5f187 | ||
|
|
ba2add0d54 | ||
|
|
b6ebeb31c8 | ||
|
|
b8bd645560 | ||
|
|
e0d904fa69 | ||
|
|
cb532caca4 | ||
|
|
e1af8ad37f | ||
|
|
c4f30da648 | ||
|
|
b83a760873 | ||
|
|
22ab50f76d | ||
|
|
66758ca006 | ||
|
|
e7d7bc79f4 | ||
|
|
cfccb4f9c3 | ||
|
|
9312f17041 | ||
|
|
8192c22910 | ||
|
|
0b1d6b677a | ||
|
|
d666df0189 | ||
|
|
10d8f345c1 | ||
|
|
fb720b8714 | ||
|
|
e8131b5791 | ||
|
|
4f793f6b93 | ||
|
|
4215c21c94 | ||
|
|
6913789adc | ||
|
|
09e0c40792 | ||
|
|
baff805551 | ||
|
|
c885fe43cd | ||
|
|
464a777722 | ||
|
|
89e5999c85 | ||
|
|
b6fa332550 | ||
|
|
05f262dc0a | ||
|
|
699b765ee9 | ||
|
|
84beba2383 | ||
|
|
62eceb9148 | ||
|
|
f46070d4b0 | ||
|
|
73979c416a | ||
|
|
348e8f9c27 | ||
|
|
38bdb5a75d | ||
|
|
5e4c51e2f7 | ||
|
|
99a65246a9 | ||
|
|
598ce9a9d2 | ||
|
|
42d6b9e703 | ||
|
|
8f595838aa | ||
|
|
3d9d7d3582 | ||
|
|
77cf28bd78 | ||
|
|
2fb1b8af20 | ||
|
|
af1f389f8e | ||
|
|
b5334da253 | ||
|
|
68b3904382 | ||
|
|
c8b09b9e29 | ||
|
|
d910fc42ab | ||
|
|
a6db8bfe0e | ||
|
|
2033d7e411 | ||
|
|
4a04e54ceb | ||
|
|
d57a9ab9b0 | ||
|
|
d333204194 | ||
|
|
c3676f8d33 | ||
|
|
932356be61 | ||
|
|
5b1b2a2d67 | ||
|
|
c362e8c467 | ||
|
|
67c00a8cc7 | ||
|
|
27a086dfff | ||
|
|
8ee0df9c65 | ||
|
|
da30b55902 | ||
|
|
c7226fc85f | ||
|
|
84f22dbadc | ||
|
|
06a53ef9ca | ||
|
|
b5ef0cda1e | ||
|
|
1b1290efac | ||
|
|
dcbc3ea3f8 | ||
|
|
9a7b2cb818 | ||
|
|
f9cba39f0a | ||
|
|
6b6ff4fe76 | ||
|
|
05d0fe2da6 | ||
|
|
7aab2b49e2 | ||
|
|
8887df92ed | ||
|
|
9ee651d6c0 | ||
|
|
5544e169a6 | ||
|
|
11d83165e5 | ||
|
|
9e6d1c581c | ||
|
|
66e20a0aec | ||
|
|
e639b36283 | ||
|
|
c9f4fb141f | ||
|
|
29a43fc2fd | ||
|
|
f9454b5b5a | ||
|
|
9aa6d47349 | ||
|
|
e09946d946 | ||
|
|
c9c5429120 | ||
|
|
ed7bd6c66d | ||
|
|
c88fe7cae8 | ||
|
|
68642579d0 | ||
|
|
f061d70d38 | ||
|
|
fd4a609f51 | ||
|
|
9957f734a5 | ||
|
|
695b8b2ae1 | ||
|
|
420824b279 | ||
|
|
badc2567c3 | ||
|
|
c8c81927d9 | ||
|
|
f9df843789 | ||
|
|
3cd39d4ee8 | ||
|
|
8a39ef4c56 | ||
|
|
ba1195fc1b | ||
|
|
7656142db4 | ||
|
|
74c3b45ef8 | ||
|
|
f7368d3d09 | ||
|
|
5d8e2300f2 | ||
|
|
1fb54c0da5 | ||
|
|
5a9a6e593b | ||
|
|
2d5fc655c0 | ||
|
|
cfcc9a5856 | ||
|
|
8c9555f82e | ||
|
|
ee20ba1811 | ||
|
|
4cf1215cfa | ||
|
|
a8ab099177 | ||
|
|
50af8a12d4 | ||
|
|
510c39c5d8 | ||
|
|
dd4a0121f2 | ||
|
|
4fb62c072a | ||
|
|
2b100d0f72 | ||
|
|
abfdc44f92 | ||
|
|
6e76f9966a | ||
|
|
2b6ceab9d4 | ||
|
|
b636729960 | ||
|
|
8af8366575 | ||
|
|
1d31e9b9d9 | ||
|
|
37a9f670dd | ||
|
|
11eda3b11b | ||
|
|
04682c9d91 | ||
|
|
50fdc449ac | ||
|
|
b8c295727a | ||
|
|
93ee466780 | ||
|
|
77f1e8f8c9 | ||
|
|
1aa746bea1 | ||
|
|
490041d77c | ||
|
|
5dc5592c17 | ||
|
|
8fb1aff68a | ||
|
|
a397a19034 | ||
|
|
d0df761422 | ||
|
|
4781675c1a | ||
|
|
0361262bb4 | ||
|
|
3407cc9a7a | ||
|
|
4829916f0a | ||
|
|
c505eafd30 | ||
|
|
07f218f294 | ||
|
|
42751b598b | ||
|
|
5e7e0eb50b | ||
|
|
d6c631457c | ||
|
|
12ee76d222 | ||
|
|
3ea80038d3 | ||
|
|
55404cdf24 | ||
|
|
83a9cd4f3e | ||
|
|
3572d7330d | ||
|
|
a9b652a280 | ||
|
|
8efb2eb71a | ||
|
|
17094f1998 | ||
|
|
ddf5dc25a1 | ||
|
|
fa2614954b | ||
|
|
2e2894b3d3 | ||
|
|
59ff407e76 | ||
|
|
bbd7b9f92e | ||
|
|
c77d820763 | ||
|
|
3327ed0f49 | ||
|
|
44009e980b | ||
|
|
02fd733223 | ||
|
|
2fa9576d05 | ||
|
|
c7ee278ee4 | ||
|
|
d72c27ceed | ||
|
|
7a20fe2288 | ||
|
|
042b62a2a5 | ||
|
|
88141e9d63 | ||
|
|
7fa1114edf | ||
|
|
d4262532e2 | ||
|
|
a21f83aae1 | ||
|
|
d659e86a7d | ||
|
|
0b924005ec | ||
|
|
ba2fad5d9c | ||
|
|
58416cee67 | ||
|
|
38124313c7 | ||
|
|
3fc9f6c0a4 | ||
|
|
79ce5abd53 | ||
|
|
7f01d597cb | ||
|
|
31f35df71d | ||
|
|
faeb78801c | ||
|
|
bd5695f2dd | ||
|
|
5375cbe1c2 | ||
|
|
d0b797ea61 | ||
|
|
e76f160695 | ||
|
|
ef71fc1b41 | ||
|
|
14f14e5da4 | ||
|
|
bd265e47fa | ||
|
|
333d344c0b | ||
|
|
db6712f030 | ||
|
|
1065a6283c | ||
|
|
1b40c5c7ce | ||
|
|
a8de87300e | ||
|
|
f260078ac8 | ||
|
|
5a6486be21 | ||
|
|
2e9de3cb86 | ||
|
|
a259684916 | ||
|
|
5704adfbc5 | ||
|
|
6cfaab07ba | ||
|
|
b36085a3cc | ||
|
|
0afa0977b0 | ||
|
|
4a174e559f | ||
|
|
0fb8ab2280 | ||
|
|
261b0f398b | ||
|
|
d1fea384a7 | ||
|
|
9542ea0d2e | ||
|
|
e1d697c561 | ||
|
|
22ed847849 |
13
.devcontainer/Readarr.code-workspace
Normal file
13
.devcontainer/Readarr.code-workspace
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||||
|
// the frontend has vscode settings that are distinct from the backend
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../frontend"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
19
.devcontainer/devcontainer.json
Normal file
19
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||||
|
{
|
||||||
|
"name": "Readarr",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"nodeGypDependencies": true,
|
||||||
|
"version": "16",
|
||||||
|
"nvmVersion": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [8787],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
16
.github/label-actions.yml
vendored
Normal file
16
.github/label-actions.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Configuration for Label Actions - https://github.com/dessant/label-actions
|
||||||
|
|
||||||
|
'Type: Support':
|
||||||
|
comment: >
|
||||||
|
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||||
|
for bug reports and feature requests. However, this issue appears
|
||||||
|
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
|
||||||
|
close: true
|
||||||
|
close-reason: 'not planned'
|
||||||
|
|
||||||
|
'Status: Logs Needed':
|
||||||
|
comment: >
|
||||||
|
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
||||||
|
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||||
|
Guidance on how to enable trace logging can be found in
|
||||||
|
our [troubleshooting guide](https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files).
|
||||||
17
.github/workflows/label-actions.yml
vendored
Normal file
17
.github/workflows/label-actions.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: 'Label Actions'
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled, unlabeled]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/label-actions@v3
|
||||||
|
with:
|
||||||
|
process-only: 'issues'
|
||||||
31
.github/workflows/support.yml
vendored
31
.github/workflows/support.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: 'Support requests'
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled, unlabeled, reopened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
support:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: dessant/support-requests@v3
|
|
||||||
with:
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
support-label: 'Type: Support'
|
|
||||||
issue-comment: >
|
|
||||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
|
||||||
for bug reports and feature requests. However, this issue appears
|
|
||||||
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
|
|
||||||
close-issue: true
|
|
||||||
lock-issue: false
|
|
||||||
- uses: dessant/support-requests@v3
|
|
||||||
with:
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
support-label: 'Status: Logs Needed'
|
|
||||||
issue-comment: >
|
|
||||||
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
|
||||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
|
||||||
Guidance on how to enable trace logging can be found in
|
|
||||||
our [troubleshooting guide](https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files).
|
|
||||||
close-issue: false
|
|
||||||
lock-issue: false
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -120,11 +120,13 @@ _artifacts
|
|||||||
_rawPackage/
|
_rawPackage/
|
||||||
_dotTrace*
|
_dotTrace*
|
||||||
_tests/
|
_tests/
|
||||||
|
_temp*
|
||||||
*.Result.xml
|
*.Result.xml
|
||||||
coverage*.xml
|
coverage*.xml
|
||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
|
.mono
|
||||||
|
|
||||||
# .NET Core
|
# .NET Core
|
||||||
project.lock.json
|
project.lock.json
|
||||||
|
|||||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
.vscode/launch.json
vendored
Normal file
26
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||||
|
"name": "Run Readarr",
|
||||||
|
"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/Readarr",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
44
.vscode/tasks.json
vendored
Normal file
44
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build dotnet",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"-restore",
|
||||||
|
"${workspaceFolder}/src/Readarr.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/Readarr.sln",
|
||||||
|
"-property:GenerateFullPaths=true",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/src/Readarr.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 3.7 KiB |
20
README.md
20
README.md
@@ -1,3 +1,23 @@
|
|||||||
|
# Announcement: Retirement of Readarr
|
||||||
|
|
||||||
|
We would like to announce that the [Readarr project](<https://github.com/Readarr/Readarr>) has been retired. This difficult decision was made due to a combination of factors: the project's metadata has become unusable, we no longer have the time to remake or repair it, and the community effort to transition to using Open Library as the source has stalled without much progress.
|
||||||
|
|
||||||
|
Third-party metadata mirrors exist, but as we're not involved with them at all, we cannot provide support for them. Use of them is entirely at your own risk. The most popular mirror appears to be [rreading-glasses](<https://github.com/blampe/rreading-glasses>).
|
||||||
|
|
||||||
|
Without anyone to take over Readarr development, we expect it to wither away, so we still encourage you to seek alternatives to Readarr.
|
||||||
|
|
||||||
|
## Key Points:
|
||||||
|
- **Effective Immediately**: The retirement takes effect immediately. Please stay tuned for any possible further communications.
|
||||||
|
- **Support Window**: We will provide support during a brief transition period to help with troubleshooting non metadata related issues.
|
||||||
|
- **Alternative Solutions**: Users are encouraged to explore and adopt any other possible solutions as alternatives to Readarr.
|
||||||
|
- **Opportunities for Revival**: We are open to someone taking over and revitalizing the project. If you are interested, please get in touch.
|
||||||
|
- **Gratitude**: We extend our deepest gratitude to all the contributors and community members who supported Readarr over the years.
|
||||||
|
|
||||||
|
Thank you for being part of the Readarr journey. For any inquiries or assistance during this transition, please contact our team.
|
||||||
|
|
||||||
|
Sincerely,
|
||||||
|
The Servarr Team
|
||||||
|
|
||||||
# Readarr
|
# Readarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
|
|||||||
@@ -9,18 +9,18 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '0.3.16'
|
majorVersion: '0.4.19'
|
||||||
minorVersion: $[counter('minorVersion', 1)]
|
minorVersion: $[counter('minorVersion', 1)]
|
||||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.417'
|
dotnetVersion: '6.0.427'
|
||||||
nodeVersion: '16.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-22.04'
|
||||||
macImage: 'macOS-11'
|
macImage: 'macOS-13'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
@@ -166,10 +166,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1075,10 +1075,10 @@ stages:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -1102,19 +1102,19 @@ stages:
|
|||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@1
|
- task: SonarCloudPrepare@3
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'readarr'
|
organization: 'readarr'
|
||||||
scannerMode: 'CLI'
|
scannerMode: 'cli'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'readarrui'
|
cliProjectKey: 'readarrui'
|
||||||
cliProjectName: 'ReadarrUI'
|
cliProjectName: 'ReadarrUI'
|
||||||
cliProjectVersion: '$(readarrVersion)'
|
cliProjectVersion: '$(readarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@1
|
- task: SonarCloudAnalyze@3
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
@@ -1190,12 +1190,12 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@1
|
- task: SonarCloudPrepare@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'readarr'
|
organization: 'readarr'
|
||||||
scannerMode: 'MSBuild'
|
scannerMode: 'dotnet'
|
||||||
projectKey: 'Readarr_Readarr'
|
projectKey: 'Readarr_Readarr'
|
||||||
projectName: 'Readarr'
|
projectName: 'Readarr'
|
||||||
projectVersion: '$(readarrVersion)'
|
projectVersion: '$(readarrVersion)'
|
||||||
@@ -1208,21 +1208,16 @@ stages:
|
|||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@1
|
- task: SonarCloudAnalyze@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@4
|
- task: reportgenerator@5.3.11
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||||
- task: PublishCodeCoverageResults@1
|
publishCodeCoverageResults: true
|
||||||
displayName: Publish Coverage Report
|
|
||||||
inputs:
|
|
||||||
codeCoverageTool: 'cobertura'
|
|
||||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
|
||||||
reportDirectory: './CoverageResults/combined/'
|
|
||||||
|
|
||||||
- stage: Report_Out
|
- stage: Report_Out
|
||||||
dependsOn:
|
dependsOn:
|
||||||
|
|||||||
Binary file not shown.
14
docs.sh
14
docs.sh
@@ -1,3 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net6.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
@@ -21,15 +25,21 @@ slnFile=src/Readarr.sln
|
|||||||
|
|
||||||
platform=Posix
|
platform=Posix
|
||||||
|
|
||||||
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
|
application=Readarr.Console.dll
|
||||||
|
else
|
||||||
|
application=Readarr.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/Readarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/Readarr.console.dll" v1 &
|
dotnet tool run swagger tofile --output ./src/Readarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|||||||
2
frontend/.vscode/settings.json
vendored
2
frontend/.vscode/settings.json
vendored
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": true
|
"source.fixAll": "explicit"
|
||||||
},
|
},
|
||||||
|
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
|
|||||||
@@ -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 }],
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ module.exports = (env) => {
|
|||||||
const config = {
|
const config = {
|
||||||
mode: isProduction ? 'production' : 'development',
|
mode: isProduction ? 'production' : 'development',
|
||||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||||
|
target: 'web',
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
@@ -67,7 +68,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 +93,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({
|
||||||
@@ -181,7 +182,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.39'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -202,7 +203,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]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -165,7 +165,8 @@ function HistoryDetails(props) {
|
|||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
if (eventType === 'downloadFailed') {
|
||||||
const {
|
const {
|
||||||
message
|
message,
|
||||||
|
indexer
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -177,11 +178,21 @@ function HistoryDetails(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!!message &&
|
indexer ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Indexer')}
|
||||||
|
data={indexer}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
message ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('Message')}
|
title={translate('Message')}
|
||||||
data={message}
|
data={message}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -218,10 +218,12 @@ class HistoryRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.details}
|
className={styles.details}
|
||||||
>
|
>
|
||||||
<IconButton
|
<div className={styles.actionContents}>
|
||||||
name={icons.INFO}
|
<IconButton
|
||||||
onPress={this.onDetailsPress}
|
name={icons.INFO}
|
||||||
/>
|
onPress={this.onDetailsPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import selectAll from 'Utilities/Table/selectAll';
|
|||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
import QueueRowConnector from './QueueRowConnector';
|
||||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||||
|
|
||||||
class Queue extends Component {
|
class Queue extends Component {
|
||||||
|
|
||||||
@@ -289,9 +289,16 @@ class Queue extends Component {
|
|||||||
}
|
}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
|
|
||||||
<RemoveQueueItemsModal
|
<RemoveQueueItemModal
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
selectedCount={selectedCount}
|
selectedCount={selectedCount}
|
||||||
|
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.downloadClientHasPostImportCategory);
|
||||||
|
})
|
||||||
|
)}
|
||||||
canIgnore={isConfirmRemoveModalOpen && (
|
canIgnore={isConfirmRemoveModalOpen && (
|
||||||
selectedIds.every((id) => {
|
selectedIds.every((id) => {
|
||||||
const item = items.find((i) => i.id === id);
|
const item = items.find((i) => i.id === id);
|
||||||
@@ -299,7 +306,7 @@ class Queue extends Component {
|
|||||||
return !!(item && item.authorId && item.bookId);
|
return !!(item && item.authorId && item.bookId);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
allPending={isConfirmRemoveModalOpen && (
|
pending={isConfirmRemoveModalOpen && (
|
||||||
selectedIds.every((id) => {
|
selectedIds.every((id) => {
|
||||||
const item = items.find((i) => i.id === id);
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class QueueRow extends Component {
|
|||||||
indexer,
|
indexer,
|
||||||
outputPath,
|
outputPath,
|
||||||
downloadClient,
|
downloadClient,
|
||||||
|
downloadClientHasPostImportCategory,
|
||||||
downloadForced,
|
downloadForced,
|
||||||
estimatedCompletionTime,
|
estimatedCompletionTime,
|
||||||
timeleft,
|
timeleft,
|
||||||
@@ -389,6 +390,7 @@ class QueueRow extends Component {
|
|||||||
<RemoveQueueItemModal
|
<RemoveQueueItemModal
|
||||||
isOpen={isRemoveQueueItemModalOpen}
|
isOpen={isRemoveQueueItemModalOpen}
|
||||||
sourceTitle={title}
|
sourceTitle={title}
|
||||||
|
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||||
canIgnore={!!author}
|
canIgnore={!!author}
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||||
@@ -418,6 +420,7 @@ QueueRow.propTypes = {
|
|||||||
indexer: PropTypes.string,
|
indexer: PropTypes.string,
|
||||||
outputPath: PropTypes.string,
|
outputPath: PropTypes.string,
|
||||||
downloadClient: PropTypes.string,
|
downloadClient: PropTypes.string,
|
||||||
|
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||||
downloadForced: PropTypes.bool.isRequired,
|
downloadForced: PropTypes.bool.isRequired,
|
||||||
estimatedCompletionTime: PropTypes.string,
|
estimatedCompletionTime: PropTypes.string,
|
||||||
timeleft: PropTypes.string,
|
timeleft: PropTypes.string,
|
||||||
|
|||||||
@@ -1,177 +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>
|
|
||||||
Remove - {sourceTitle}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPending ?
|
|
||||||
null :
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('RemoveFromDownloadClient')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="remove"
|
|
||||||
value={remove}
|
|
||||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
|
||||||
isDisabled={!canIgnore}
|
|
||||||
onChange={this.onRemoveChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('BlocklistRelease')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="blocklist"
|
|
||||||
value={blocklist}
|
|
||||||
helpText={translate('BlocklistReleaseHelpText')}
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={this.onModalClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={this.onRemoveConfirmed}
|
|
||||||
>
|
|
||||||
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;
|
|
||||||
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistAndSearchMultipleHint')
|
||||||
|
: translate('BlocklistAndSearchHint'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'blocklistOnly',
|
||||||
|
value: translate('BlocklistOnly'),
|
||||||
|
hint: multipleSelected
|
||||||
|
? translate('BlocklistMultipleOnlyHint')
|
||||||
|
: translate('BlocklistOnlyHint'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [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,178 +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('RemoveHelpTextWarning')}
|
|
||||||
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('BlocklistReleaseHelpText')}
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
|
|
||||||
</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;
|
|
||||||
@@ -32,7 +32,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
import Status from 'System/Status/Status';
|
import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import Updates from 'System/Updates/Updates';
|
||||||
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
import UnmappedFilesTableConnector from 'UnmappedFiles/UnmappedFilesTableConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
@@ -247,7 +247,7 @@ function AppRoutes(props) {
|
|||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/system/updates"
|
path="/system/updates"
|
||||||
component={UpdatesConnector}
|
component={Updates}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import AuthorsAppState from './AuthorsAppState';
|
||||||
|
import CommandAppState from './CommandAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
|
import SystemAppState from './SystemAppState';
|
||||||
import TagsAppState from './TagsAppState';
|
import TagsAppState from './TagsAppState';
|
||||||
|
|
||||||
interface FilterBuilderPropOption {
|
interface FilterBuilderPropOption {
|
||||||
@@ -33,8 +36,24 @@ export interface CustomFilter {
|
|||||||
filers: PropertyFilter[];
|
filers: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionState {
|
||||||
|
isConnected: boolean;
|
||||||
|
isReconnecting: boolean;
|
||||||
|
version: string;
|
||||||
|
prevVersion?: string;
|
||||||
|
dimensions: {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
|
app: AppSectionState;
|
||||||
|
authors: AuthorsAppState;
|
||||||
|
commands: CommandAppState;
|
||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
|
system: SystemAppState;
|
||||||
tags: TagsAppState;
|
tags: TagsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
frontend/src/App/State/AuthorsAppState.ts
Normal file
18
frontend/src/App/State/AuthorsAppState.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import AppSectionState, {
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import Author from 'Author/Author';
|
||||||
|
|
||||||
|
interface AuthorsAppState
|
||||||
|
extends AppSectionState<Author>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {
|
||||||
|
itemMap: Record<number, number>;
|
||||||
|
|
||||||
|
deleteOptions: {
|
||||||
|
addImportListExclusion: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthorsAppState;
|
||||||
6
frontend/src/App/State/CommandAppState.ts
Normal file
6
frontend/src/App/State/CommandAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import Command from 'Commands/Command';
|
||||||
|
|
||||||
|
export type CommandAppState = AppSectionState<Command>;
|
||||||
|
|
||||||
|
export default CommandAppState;
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionItemState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import General from 'typings/Settings/General';
|
||||||
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export type GeneralAppState = AppSectionItemState<General>;
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -27,14 +32,17 @@ export interface NotificationAppState
|
|||||||
extends AppSectionState<Notification>,
|
extends AppSectionState<Notification>,
|
||||||
AppSectionDeleteState {}
|
AppSectionDeleteState {}
|
||||||
|
|
||||||
|
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
|
general: GeneralAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
uiSettings: UiSettingsAppState;
|
ui: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SettingsAppState;
|
export default SettingsAppState;
|
||||||
|
|||||||
13
frontend/src/App/State/SystemAppState.ts
Normal file
13
frontend/src/App/State/SystemAppState.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
|
import Update from 'typings/Update';
|
||||||
|
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||||
|
|
||||||
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
|
interface SystemAppState {
|
||||||
|
updates: UpdateAppState;
|
||||||
|
status: SystemStatusAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SystemAppState;
|
||||||
21
frontend/src/Author/Author.ts
Normal file
21
frontend/src/Author/Author.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
export type AuthorStatus = 'continuing' | 'ended';
|
||||||
|
|
||||||
|
interface Author extends ModelBase {
|
||||||
|
added: string;
|
||||||
|
genres: string[];
|
||||||
|
monitored: boolean;
|
||||||
|
overview: string;
|
||||||
|
path: string;
|
||||||
|
qualityProfileId: number;
|
||||||
|
metadataProfileId: number;
|
||||||
|
rootFolderPath: string;
|
||||||
|
sortName: string;
|
||||||
|
status: AuthorStatus;
|
||||||
|
tags: number[];
|
||||||
|
authorName: string;
|
||||||
|
isSaving?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Author;
|
||||||
@@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
|
||||||
function AuthorNameLink({ titleSlug, authorName }) {
|
function AuthorNameLink({ titleSlug, authorName, ...otherProps }) {
|
||||||
const link = `/author/${titleSlug}`;
|
const link = `/author/${titleSlug}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={link}>
|
<Link to={link} {...otherProps}>
|
||||||
{authorName}
|
{authorName}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
21
frontend/src/Author/AuthorStatus.ts
Normal file
21
frontend/src/Author/AuthorStatus.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { AuthorStatus } from 'Author/Author';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
export function getAuthorStatusDetails(status: AuthorStatus) {
|
||||||
|
let statusDetails = {
|
||||||
|
icon: icons.AUTHOR_CONTINUING,
|
||||||
|
title: translate('StatusEndedContinuing'),
|
||||||
|
message: translate('ContinuingMoreBooksAreExpected'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status === 'ended') {
|
||||||
|
statusDetails = {
|
||||||
|
icon: icons.AUTHOR_ENDED,
|
||||||
|
title: translate('StatusEndedEnded'),
|
||||||
|
message: translate('ContinuingNoAdditionalBooksAreExpected'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusDetails;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
|
|||||||
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||||
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -17,7 +18,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
|
||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
||||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||||
@@ -412,22 +413,25 @@ class AuthorDetails extends Component {
|
|||||||
|
|
||||||
<div className={styles.contentContainer}>
|
<div className={styles.contentContainer}>
|
||||||
{
|
{
|
||||||
!isPopulated && !booksError && !bookFilesError &&
|
!isPopulated && !booksError && !bookFilesError ?
|
||||||
<LoadingIndicator />
|
<LoadingIndicator /> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && booksError &&
|
!isFetching && booksError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingBooksFailed')}
|
{translate('LoadingBooksFailed')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && bookFilesError &&
|
!isFetching && bookFilesError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingBookFilesFailed')}
|
{translate('LoadingBookFilesFailed')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import TextTruncate from 'react-text-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
import AuthorPoster from 'Author/AuthorPoster';
|
import AuthorPoster from 'Author/AuthorPoster';
|
||||||
|
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||||
import HeartRating from 'Components/HeartRating';
|
import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
@@ -11,7 +12,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||||
import fonts from 'Styles/Variables/fonts';
|
import fonts from 'Styles/Variables/fonts';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import stripHtml from 'Utilities/String/stripHtml';
|
import stripHtml from 'Utilities/String/stripHtml';
|
||||||
@@ -87,11 +88,11 @@ class AuthorDetailsHeader extends Component {
|
|||||||
titleWidth
|
titleWidth
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const statusDetails = getAuthorStatusDetails(status);
|
||||||
|
|
||||||
const fanartUrl = getFanartUrl(images);
|
const fanartUrl = getFanartUrl(images);
|
||||||
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
|
||||||
|
|
||||||
const continuing = status === 'continuing';
|
|
||||||
|
|
||||||
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
let bookFilesCountMessage = translate('BookFilesCountMessage');
|
||||||
|
|
||||||
if (bookFileCount === 1) {
|
if (bookFileCount === 1) {
|
||||||
@@ -213,7 +214,7 @@ class AuthorDetailsHeader extends Component {
|
|||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{
|
{
|
||||||
<QualityProfileNameConnector
|
<QualityProfileName
|
||||||
qualityProfileId={qualityProfileId}
|
qualityProfileId={qualityProfileId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -236,16 +237,16 @@ class AuthorDetailsHeader extends Component {
|
|||||||
|
|
||||||
<Label
|
<Label
|
||||||
className={styles.detailsLabel}
|
className={styles.detailsLabel}
|
||||||
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
|
title={statusDetails.message}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
|
name={statusDetails.icon}
|
||||||
size={17}
|
size={17}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={styles.qualityProfileName}>
|
<span className={styles.qualityProfileName}>
|
||||||
{continuing ? 'Continuing' : 'Deceased'}
|
{statusDetails.title}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
|
|||||||
@@ -27,3 +27,9 @@
|
|||||||
|
|
||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.indexerFlags {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|||||||
1
frontend/src/Author/Details/BookRow.css.d.ts
vendored
1
frontend/src/Author/Details/BookRow.css.d.ts
vendored
@@ -1,6 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'indexerFlags': string;
|
||||||
'monitored': string;
|
'monitored': string;
|
||||||
'pageCount': string;
|
'pageCount': string;
|
||||||
'position': string;
|
'position': string;
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import BookSearchCellConnector from 'Book/BookSearchCellConnector';
|
import BookSearchCellConnector from 'Book/BookSearchCellConnector';
|
||||||
import BookTitleLink from 'Book/BookTitleLink';
|
import BookTitleLink from 'Book/BookTitleLink';
|
||||||
|
import IndexerFlags from 'Book/IndexerFlags';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import StarRating from 'Components/StarRating';
|
import StarRating from 'Components/StarRating';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import BookStatus from './BookStatus';
|
import BookStatus from './BookStatus';
|
||||||
import styles from './BookRow.css';
|
import styles from './BookRow.css';
|
||||||
|
|
||||||
@@ -59,6 +64,7 @@ class BookRow extends Component {
|
|||||||
releaseDate,
|
releaseDate,
|
||||||
title,
|
title,
|
||||||
seriesTitle,
|
seriesTitle,
|
||||||
|
authorName,
|
||||||
position,
|
position,
|
||||||
pageCount,
|
pageCount,
|
||||||
ratings,
|
ratings,
|
||||||
@@ -66,6 +72,7 @@ class BookRow extends Component {
|
|||||||
authorMonitored,
|
authorMonitored,
|
||||||
titleSlug,
|
titleSlug,
|
||||||
bookFiles,
|
bookFiles,
|
||||||
|
indexerFlags,
|
||||||
isEditorActive,
|
isEditorActive,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelectedChange,
|
onSelectedChange,
|
||||||
@@ -189,6 +196,24 @@ class BookRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'indexerFlags') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.indexerFlags}
|
||||||
|
>
|
||||||
|
{indexerFlags ? (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||||
|
title={translate('IndexerFlags')}
|
||||||
|
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'status') {
|
if (name === 'status') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
@@ -211,6 +236,7 @@ class BookRow extends Component {
|
|||||||
bookId={id}
|
bookId={id}
|
||||||
authorId={authorId}
|
authorId={authorId}
|
||||||
bookTitle={title}
|
bookTitle={title}
|
||||||
|
authorName={authorName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -229,9 +255,11 @@ BookRow.propTypes = {
|
|||||||
releaseDate: PropTypes.string,
|
releaseDate: PropTypes.string,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
seriesTitle: PropTypes.string.isRequired,
|
seriesTitle: PropTypes.string.isRequired,
|
||||||
|
authorName: PropTypes.string.isRequired,
|
||||||
position: PropTypes.string,
|
position: PropTypes.string,
|
||||||
pageCount: PropTypes.number,
|
pageCount: PropTypes.number,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
|
indexerFlags: PropTypes.number.isRequired,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
isSaving: PropTypes.bool,
|
isSaving: PropTypes.bool,
|
||||||
authorMonitored: PropTypes.bool.isRequired,
|
authorMonitored: PropTypes.bool.isRequired,
|
||||||
@@ -243,4 +271,8 @@ BookRow.propTypes = {
|
|||||||
onMonitorBookPress: PropTypes.func.isRequired
|
onMonitorBookPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BookRow.defaultProps = {
|
||||||
|
indexerFlags: 0
|
||||||
|
};
|
||||||
|
|
||||||
export default BookRow;
|
export default BookRow;
|
||||||
|
|||||||
@@ -7,21 +7,18 @@ import BookRow from './BookRow';
|
|||||||
const selectBookFiles = createSelector(
|
const selectBookFiles = createSelector(
|
||||||
(state) => state.bookFiles,
|
(state) => state.bookFiles,
|
||||||
(bookFiles) => {
|
(bookFiles) => {
|
||||||
const {
|
const { items } = bookFiles;
|
||||||
items
|
|
||||||
} = bookFiles;
|
|
||||||
|
|
||||||
const bookFileDict = items.reduce((acc, file) => {
|
return items.reduce((acc, file) => {
|
||||||
const bookId = file.bookId;
|
const bookId = file.bookId;
|
||||||
if (!acc.hasOwnProperty(bookId)) {
|
if (!acc.hasOwnProperty(bookId)) {
|
||||||
acc[bookId] = [];
|
acc[bookId] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
acc[bookId].push(file);
|
acc[bookId].push(file);
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return bookFileDict;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -31,9 +28,14 @@ function createMapStateToProps() {
|
|||||||
selectBookFiles,
|
selectBookFiles,
|
||||||
(state, { id }) => id,
|
(state, { id }) => id,
|
||||||
(author = {}, bookFiles, bookId) => {
|
(author = {}, bookFiles, bookId) => {
|
||||||
|
const files = bookFiles[bookId] ?? [];
|
||||||
|
const bookFile = files[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authorMonitored: author.monitored,
|
authorMonitored: author.monitored,
|
||||||
bookFiles: bookFiles[bookId] ?? []
|
authorName: author.authorName,
|
||||||
|
bookFiles: files,
|
||||||
|
indexerFlags: bookFile ? bookFile.indexerFlags : 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class AuthorEditorFooter extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const monitoredOptions = [
|
const monitoredOptions = [
|
||||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
|
||||||
{ key: 'monitored', value: translate('Monitored') },
|
{ key: 'monitored', value: translate('Monitored') },
|
||||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class AuthorIndexOverview extends Component {
|
|||||||
status,
|
status,
|
||||||
titleSlug,
|
titleSlug,
|
||||||
nextAiring,
|
nextAiring,
|
||||||
statistics,
|
statistics = {},
|
||||||
images,
|
images,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
posterHeight,
|
posterHeight,
|
||||||
@@ -113,10 +113,11 @@ class AuthorIndexOverview extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
bookCount,
|
bookCount = 0,
|
||||||
sizeOnDisk,
|
availableBookCount = 0,
|
||||||
bookFileCount,
|
bookFileCount = 0,
|
||||||
totalBookCount
|
totalBookCount = 0,
|
||||||
|
sizeOnDisk = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -179,6 +180,7 @@ class AuthorIndexOverview extends Component {
|
|||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
status={status}
|
status={status}
|
||||||
bookCount={bookCount}
|
bookCount={bookCount}
|
||||||
|
availableBookCount={availableBookCount}
|
||||||
bookFileCount={bookFileCount}
|
bookFileCount={bookFileCount}
|
||||||
totalBookCount={totalBookCount}
|
totalBookCount={totalBookCount}
|
||||||
posterWidth={posterWidth}
|
posterWidth={posterWidth}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
|
|||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
|
import AuthorIndexOverviewInfoRow from './AuthorIndexOverviewInfoRow';
|
||||||
import styles from './AuthorIndexOverviewInfo.css';
|
import styles from './AuthorIndexOverviewInfo.css';
|
||||||
|
|
||||||
@@ -76,9 +77,9 @@ function getInfoRowProps(row, props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'qualityProfileId') {
|
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||||
return {
|
return {
|
||||||
title: 'Quality Profile',
|
title: translate('QualityProfile'),
|
||||||
iconName: icons.PROFILE,
|
iconName: icons.PROFILE,
|
||||||
label: props.qualityProfile.name
|
label: props.qualityProfile.name
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class AuthorIndexPoster extends Component {
|
|||||||
titleSlug,
|
titleSlug,
|
||||||
status,
|
status,
|
||||||
nextAiring,
|
nextAiring,
|
||||||
statistics,
|
statistics = {},
|
||||||
images,
|
images,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
posterHeight,
|
posterHeight,
|
||||||
@@ -110,10 +110,11 @@ class AuthorIndexPoster extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
bookCount,
|
bookCount = 0,
|
||||||
sizeOnDisk,
|
availableBookCount = 0,
|
||||||
bookFileCount,
|
bookFileCount = 0,
|
||||||
totalBookCount
|
totalBookCount = 0,
|
||||||
|
sizeOnDisk = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -213,6 +214,7 @@ class AuthorIndexPoster extends Component {
|
|||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
status={status}
|
status={status}
|
||||||
bookCount={bookCount}
|
bookCount={bookCount}
|
||||||
|
availableBookCount={availableBookCount}
|
||||||
bookFileCount={bookFileCount}
|
bookFileCount={bookFileCount}
|
||||||
totalBookCount={totalBookCount}
|
totalBookCount={totalBookCount}
|
||||||
posterWidth={posterWidth}
|
posterWidth={posterWidth}
|
||||||
@@ -233,12 +235,12 @@ class AuthorIndexPoster extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{showQualityProfile && !!qualityProfile?.name ? (
|
||||||
showQualityProfile &&
|
<div className={styles.title} title={translate('QualityProfile')}>
|
||||||
<div className={styles.title}>
|
{qualityProfile.name}
|
||||||
{qualityProfile.name}
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
}
|
|
||||||
{
|
{
|
||||||
nextAiring &&
|
nextAiring &&
|
||||||
<div className={styles.nextAiring}>
|
<div className={styles.nextAiring}>
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ function AuthorIndexProgressBar(props) {
|
|||||||
monitored,
|
monitored,
|
||||||
status,
|
status,
|
||||||
bookCount,
|
bookCount,
|
||||||
|
availableBookCount,
|
||||||
bookFileCount,
|
bookFileCount,
|
||||||
totalBookCount,
|
totalBookCount,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
detailedProgressBar
|
detailedProgressBar
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const progress = bookCount ? bookCount / totalBookCount * 100 : 100;
|
const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
|
||||||
const text = `${bookCount} / ${totalBookCount}`;
|
const text = `${availableBookCount} / ${bookCount}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
@@ -29,7 +30,7 @@ function AuthorIndexProgressBar(props) {
|
|||||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||||
showText={detailedProgressBar}
|
showText={detailedProgressBar}
|
||||||
text={text}
|
text={text}
|
||||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
|
||||||
width={posterWidth}
|
width={posterWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -39,6 +40,7 @@ AuthorIndexProgressBar.propTypes = {
|
|||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
bookCount: PropTypes.number.isRequired,
|
bookCount: PropTypes.number.isRequired,
|
||||||
|
availableBookCount: PropTypes.number.isRequired,
|
||||||
bookFileCount: PropTypes.number.isRequired,
|
bookFileCount: PropTypes.number.isRequired,
|
||||||
totalBookCount: PropTypes.number.isRequired,
|
totalBookCount: PropTypes.number.isRequired,
|
||||||
posterWidth: PropTypes.number.isRequired,
|
posterWidth: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class AuthorIndexRow extends Component {
|
|||||||
nextBook,
|
nextBook,
|
||||||
lastBook,
|
lastBook,
|
||||||
added,
|
added,
|
||||||
statistics,
|
statistics = {},
|
||||||
genres,
|
genres,
|
||||||
ratings,
|
ratings,
|
||||||
path,
|
path,
|
||||||
@@ -110,10 +110,11 @@ class AuthorIndexRow extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
bookCount,
|
bookCount = 0,
|
||||||
bookFileCount,
|
availableBookCount = 0,
|
||||||
totalBookCount,
|
bookFileCount = 0,
|
||||||
sizeOnDisk
|
totalBookCount = 0,
|
||||||
|
sizeOnDisk = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -208,7 +209,7 @@ class AuthorIndexRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{qualityProfile.name}
|
{qualityProfile?.name ?? ''}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -219,7 +220,7 @@ class AuthorIndexRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{metadataProfile.name}
|
{metadataProfile?.name ?? ''}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -286,7 +287,7 @@ class AuthorIndexRow extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'bookProgress') {
|
if (name === 'bookProgress') {
|
||||||
const progress = bookCount ? bookFileCount / bookCount * 100 : 100;
|
const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell
|
<VirtualTableRowCell
|
||||||
@@ -297,8 +298,8 @@ class AuthorIndexRow extends Component {
|
|||||||
progress={progress}
|
progress={progress}
|
||||||
kind={getProgressBarKind(status, monitored, progress)}
|
kind={getProgressBarKind(status, monitored, progress)}
|
||||||
showText={true}
|
showText={true}
|
||||||
text={`${bookCount} / ${totalBookCount}`}
|
text={`${availableBookCount} / ${bookCount}`}
|
||||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
|
||||||
width={125}
|
width={125}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@@ -15,6 +16,8 @@ function AuthorStatusCell(props) {
|
|||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const statusDetails = getAuthorStatusDetails(status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
className={className}
|
className={className}
|
||||||
@@ -28,8 +31,8 @@ function AuthorStatusCell(props) {
|
|||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
name={statusDetails.icon}
|
||||||
title={status === 'ended' ? translate('StatusEndedDeceased') : translate('StatusEndedContinuing')}
|
title={`${statusDetails.title}: ${statusDetails.message}`}
|
||||||
/>
|
/>
|
||||||
</Component>
|
</Component>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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 { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './NoAuthor.css';
|
import styles from './NoAuthor.css';
|
||||||
|
|
||||||
function NoAuthor(props) {
|
function NoAuthor(props) {
|
||||||
@@ -31,7 +32,7 @@ function NoAuthor(props) {
|
|||||||
to="/settings/mediamanagement"
|
to="/settings/mediamanagement"
|
||||||
kind={kinds.PRIMARY}
|
kind={kinds.PRIMARY}
|
||||||
>
|
>
|
||||||
Add Root Folder
|
{translate('AddRootFolder')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ function NoAuthor(props) {
|
|||||||
to="/add/search"
|
to="/add/search"
|
||||||
kind={kinds.PRIMARY}
|
kind={kinds.PRIMARY}
|
||||||
>
|
>
|
||||||
Add New Author
|
{translate('AddNewAuthor')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class BookSearchCell extends Component {
|
|||||||
const {
|
const {
|
||||||
bookId,
|
bookId,
|
||||||
bookTitle,
|
bookTitle,
|
||||||
|
authorName,
|
||||||
isSearching,
|
isSearching,
|
||||||
onSearchPress,
|
onSearchPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
@@ -60,6 +61,7 @@ class BookSearchCell extends Component {
|
|||||||
isOpen={this.state.isDetailsModalOpen}
|
isOpen={this.state.isDetailsModalOpen}
|
||||||
bookId={bookId}
|
bookId={bookId}
|
||||||
bookTitle={bookTitle}
|
bookTitle={bookTitle}
|
||||||
|
authorName={authorName}
|
||||||
onModalClose={this.onDetailsModalClose}
|
onModalClose={this.onDetailsModalClose}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
@@ -73,6 +75,7 @@ BookSearchCell.propTypes = {
|
|||||||
bookId: PropTypes.number.isRequired,
|
bookId: PropTypes.number.isRequired,
|
||||||
authorId: PropTypes.number.isRequired,
|
authorId: PropTypes.number.isRequired,
|
||||||
bookTitle: PropTypes.string.isRequired,
|
bookTitle: PropTypes.string.isRequired,
|
||||||
|
authorName: PropTypes.string.isRequired,
|
||||||
isSearching: PropTypes.bool.isRequired,
|
isSearching: PropTypes.bool.isRequired,
|
||||||
onSearchPress: PropTypes.func.isRequired
|
onSearchPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,9 +84,15 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.authorLink {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
margin-right: 15px;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
.duration {
|
.duration {
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailsLabel {
|
.detailsLabel {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'alternateTitlesIconContainer': string;
|
'alternateTitlesIconContainer': string;
|
||||||
|
'authorLink': string;
|
||||||
'backdrop': string;
|
'backdrop': string;
|
||||||
'backdropOverlay': string;
|
'backdropOverlay': string;
|
||||||
'cover': string;
|
'cover': string;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import moment from 'moment';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import TextTruncate from 'react-text-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||||
import BookCover from 'Book/BookCover';
|
import BookCover from 'Book/BookCover';
|
||||||
import HeartRating from 'Components/HeartRating';
|
import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
@@ -113,7 +114,7 @@ class BookDetailsHeader extends Component {
|
|||||||
className={styles.monitorToggleButton}
|
className={styles.monitorToggleButton}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
isSaving={isSaving}
|
isSaving={isSaving}
|
||||||
size={isSmallScreen ? 30: 40}
|
size={isSmallScreen ? 30 : 40}
|
||||||
onPress={onMonitorTogglePress}
|
onPress={onMonitorTogglePress}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,7 +132,12 @@ class BookDetailsHeader extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{author.authorName}
|
<AuthorNameLink
|
||||||
|
className={styles.authorLink}
|
||||||
|
titleSlug={author.titleSlug}
|
||||||
|
authorName={author.authorName}
|
||||||
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!!pageCount &&
|
!!pageCount &&
|
||||||
<span className={styles.duration}>
|
<span className={styles.duration}>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class BookEditorFooter extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const monitoredOptions = [
|
const monitoredOptions = [
|
||||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
|
||||||
{ key: 'monitored', value: translate('Monitored') },
|
{ key: 'monitored', value: translate('Monitored') },
|
||||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dimensions from 'Styles/Variables/dimensions';
|
|||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
|
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
|
||||||
import styles from './BookIndexOverviewInfo.css';
|
import styles from './BookIndexOverviewInfo.css';
|
||||||
|
|
||||||
@@ -71,9 +72,9 @@ function getInfoRowProps(row, props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'qualityProfileId') {
|
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
|
||||||
return {
|
return {
|
||||||
title: 'Quality Profile',
|
title: translate('QualityProfile'),
|
||||||
iconName: icons.PROFILE,
|
iconName: icons.PROFILE,
|
||||||
label: props.qualityProfile.name
|
label: props.qualityProfile.name
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -250,12 +250,12 @@ class BookIndexPoster extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{showQualityProfile && !!qualityProfile?.name ? (
|
||||||
showQualityProfile &&
|
<div className={styles.title} title={translate('QualityProfile')}>
|
||||||
<div className={styles.title}>
|
{qualityProfile.name}
|
||||||
{qualityProfile.name}
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
}
|
|
||||||
{
|
{
|
||||||
nextAiring &&
|
nextAiring &&
|
||||||
<div className={styles.nextAiring}>
|
<div className={styles.nextAiring}>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ function BookIndexProgressBar(props) {
|
|||||||
detailedProgressBar
|
detailedProgressBar
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const progress = bookCount ? bookFileCount / totalBookCount * 100 : 0;
|
const progress = bookFileCount && bookCount ? (totalBookCount / bookCount) * 100 : 0;
|
||||||
const text = `${bookFileCount} / ${bookCount}`;
|
const text = `${bookFileCount ? bookCount : 0} / ${totalBookCount}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
@@ -28,7 +28,11 @@ function BookIndexProgressBar(props) {
|
|||||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||||
showText={detailedProgressBar}
|
showText={detailedProgressBar}
|
||||||
text={text}
|
text={text}
|
||||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
title={translate('BookProgressBarText', {
|
||||||
|
bookCount: bookFileCount ? bookCount : 0,
|
||||||
|
bookFileCount,
|
||||||
|
totalBookCount
|
||||||
|
})}
|
||||||
width={posterWidth}
|
width={posterWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ class BookIndexRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{qualityProfile.name}
|
{qualityProfile?.name ?? ''}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
26
frontend/src/Book/IndexerFlags.tsx
Normal file
26
frontend/src/Book/IndexerFlags.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
|
||||||
|
|
||||||
|
interface IndexerFlagsProps {
|
||||||
|
indexerFlags: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
|
||||||
|
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
|
||||||
|
|
||||||
|
const flags = allIndexerFlags.items.filter(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(item) => (indexerFlags & item.id) === item.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return flags.length ? (
|
||||||
|
<ul>
|
||||||
|
{flags.map((flag, index) => {
|
||||||
|
return <li key={index}>{flag.name}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexerFlags;
|
||||||
@@ -9,19 +9,21 @@ function BookInteractiveSearchModal(props) {
|
|||||||
isOpen,
|
isOpen,
|
||||||
bookId,
|
bookId,
|
||||||
bookTitle,
|
bookTitle,
|
||||||
|
authorName,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
size={sizes.EXTRA_LARGE}
|
size={sizes.EXTRA_EXTRA_LARGE}
|
||||||
closeOnBackgroundClick={false}
|
closeOnBackgroundClick={false}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
<BookInteractiveSearchModalContent
|
<BookInteractiveSearchModalContent
|
||||||
bookId={bookId}
|
bookId={bookId}
|
||||||
bookTitle={bookTitle}
|
bookTitle={bookTitle}
|
||||||
|
authorName={authorName}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -32,6 +34,7 @@ BookInteractiveSearchModal.propTypes = {
|
|||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
bookId: PropTypes.number.isRequired,
|
bookId: PropTypes.number.isRequired,
|
||||||
bookTitle: PropTypes.string.isRequired,
|
bookTitle: PropTypes.string.isRequired,
|
||||||
|
authorName: PropTypes.string.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,23 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function BookInteractiveSearchModalContent(props) {
|
function BookInteractiveSearchModalContent(props) {
|
||||||
const {
|
const {
|
||||||
bookId,
|
bookId,
|
||||||
bookTitle,
|
bookTitle,
|
||||||
|
authorName,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Interactive Search {bookId != null && `- ${bookTitle}`}
|
{bookId === null ?
|
||||||
|
translate('InteractiveSearchModalHeader') :
|
||||||
|
translate('InteractiveSearchModalHeaderBookAuthor', { bookTitle, authorName })
|
||||||
|
}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
@@ -32,7 +37,7 @@ function BookInteractiveSearchModalContent(props) {
|
|||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>
|
||||||
Close
|
{translate('Close')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
@@ -42,6 +47,7 @@ function BookInteractiveSearchModalContent(props) {
|
|||||||
BookInteractiveSearchModalContent.propTypes = {
|
BookInteractiveSearchModalContent.propTypes = {
|
||||||
bookId: PropTypes.number.isRequired,
|
bookId: PropTypes.number.isRequired,
|
||||||
bookTitle: PropTypes.string.isRequired,
|
bookTitle: PropTypes.string.isRequired,
|
||||||
|
authorName: PropTypes.string.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ class BookFileEditorTableContent extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]);
|
}, [{ key: 'selectQuality', value: translate('SelectQuality'), isDisabled: true }]);
|
||||||
|
|
||||||
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
||||||
|
|
||||||
|
|||||||
@@ -27,14 +27,15 @@ class BookshelfBook extends Component {
|
|||||||
title,
|
title,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
monitored,
|
monitored,
|
||||||
statistics,
|
statistics = {},
|
||||||
isSaving
|
isSaving
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
bookFileCount,
|
bookCount = 0,
|
||||||
totalBookCount,
|
bookFileCount = 0,
|
||||||
percentOfBooks
|
totalBookCount = 0,
|
||||||
|
percentOfBooks = 0
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -59,10 +60,14 @@ class BookshelfBook extends Component {
|
|||||||
percentOfBooks < 100 && monitored && styles.missingWanted,
|
percentOfBooks < 100 && monitored && styles.missingWanted,
|
||||||
percentOfBooks === 100 && styles.allBooks
|
percentOfBooks === 100 && styles.allBooks
|
||||||
)}
|
)}
|
||||||
title={translate('BookFileCounttotalBookCountBooksDownloadedInterp', [bookFileCount, totalBookCount])}
|
title={translate('BookProgressBarText', {
|
||||||
|
bookCount: bookFileCount ? bookCount : 0,
|
||||||
|
bookFileCount,
|
||||||
|
totalBookCount
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
totalBookCount === 0 ? '0/0' : `${bookFileCount}/${totalBookCount}`
|
totalBookCount === 0 ? '0/0' : `${bookFileCount ? bookCount : 0}/${totalBookCount}`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class BookshelfFooter extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const monitoredOptions = [
|
const monitoredOptions = [
|
||||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
|
||||||
{ key: 'monitored', value: translate('Monitored') },
|
{ key: 'monitored', value: translate('Monitored') },
|
||||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||||
|
import { getAuthorStatusDetails } from 'Author/AuthorStatus';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import BookshelfBook from './BookshelfBook';
|
import BookshelfBook from './BookshelfBook';
|
||||||
import styles from './BookshelfRow.css';
|
import styles from './BookshelfRow.css';
|
||||||
|
|
||||||
@@ -30,6 +29,8 @@ class BookshelfRow extends Component {
|
|||||||
onBookMonitoredPress
|
onBookMonitoredPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const statusDetails = getAuthorStatusDetails(status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VirtualTableSelectCell
|
<VirtualTableSelectCell
|
||||||
@@ -52,8 +53,8 @@ class BookshelfRow extends Component {
|
|||||||
<VirtualTableRowCell className={styles.status}>
|
<VirtualTableRowCell className={styles.status}>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={status === 'ended' ? icons.AUTHOR_ENDED : icons.AUTHOR_CONTINUING}
|
name={statusDetails.icon}
|
||||||
title={status === 'ended' ? translate('StatusEndedEnded') : translate('StatusEndedContinuing')}
|
title={statusDetails.title}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
|
|
||||||
|
|||||||
38
frontend/src/Commands/Command.ts
Normal file
38
frontend/src/Commands/Command.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
|
||||||
|
export interface CommandBody {
|
||||||
|
sendUpdatesToClient: boolean;
|
||||||
|
updateScheduledTask: boolean;
|
||||||
|
completionMessage: string;
|
||||||
|
requiresDiskAccess: boolean;
|
||||||
|
isExclusive: boolean;
|
||||||
|
isLongRunning: boolean;
|
||||||
|
name: string;
|
||||||
|
lastExecutionTime: string;
|
||||||
|
lastStartTime: string;
|
||||||
|
trigger: string;
|
||||||
|
suppressMessages: boolean;
|
||||||
|
authorId?: number;
|
||||||
|
authorIds?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Command extends ModelBase {
|
||||||
|
name: string;
|
||||||
|
commandName: string;
|
||||||
|
message: string;
|
||||||
|
body: CommandBody;
|
||||||
|
priority: string;
|
||||||
|
status: string;
|
||||||
|
result: string;
|
||||||
|
queued: string;
|
||||||
|
started: string;
|
||||||
|
ended: string;
|
||||||
|
duration: string;
|
||||||
|
trigger: string;
|
||||||
|
stateChangeTime: string;
|
||||||
|
sendUpdatesToClient: boolean;
|
||||||
|
updateScheduledTask: boolean;
|
||||||
|
lastExecutionTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Command;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { maxBy } from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
@@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
if (id) {
|
if (id) {
|
||||||
dispatchSetFilter({ selectedFilterKey: id });
|
dispatchSetFilter({ selectedFilterKey: id });
|
||||||
} else {
|
} else {
|
||||||
const last = customFilters[customFilters.length -1];
|
const last = maxBy(customFilters, 'id');
|
||||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
labelErrors: [
|
labelErrors: [
|
||||||
{
|
{
|
||||||
message: 'Label is required'
|
message: translate('LabelIsRequired')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Custom Filter
|
{translate('CustomFilter')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className={styles.labelContainer}>
|
<div className={styles.labelContainer}>
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>
|
||||||
Label
|
{translate('Label')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.labelInputContainer}>
|
<div className={styles.labelInputContainer}>
|
||||||
@@ -195,7 +196,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onCancelPress}>
|
<Button onPress={onCancelPress}>
|
||||||
Cancel
|
{translate('Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
@@ -203,7 +204,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
error={saveError}
|
error={saveError}
|
||||||
onPress={this.onSaveFilterPress}
|
onPress={this.onSaveFilterPress}
|
||||||
>
|
>
|
||||||
Save
|
{translate('Save')}
|
||||||
</SpinnerErrorButton>
|
</SpinnerErrorButton>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
|||||||
dispatchSetFilter
|
dispatchSetFilter
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// Assume that delete and then unmounting means the delete was successful.
|
// Assume that delete and then unmounting means the deletion was successful.
|
||||||
// Moving this check to a ancestor would be more accurate, but would have
|
// Moving this check to an ancestor would be more accurate, but would have
|
||||||
// more boilerplate.
|
// more boilerplate.
|
||||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ function createMapStateToProps() {
|
|||||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||||
return {
|
return {
|
||||||
key: downloadClient.id,
|
key: downloadClient.id,
|
||||||
value: downloadClient.name
|
value: downloadClient.name,
|
||||||
|
hint: `(${downloadClient.id})`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
.isDisabled {
|
.isDisabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownArrowContainer {
|
.dropdownArrowContainer {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
|||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
|
const MINIMUM_DISTANCE_FROM_EDGE = 10;
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
function isArrowKey(keyCode) {
|
||||||
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
|
||||||
}
|
}
|
||||||
@@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
|
|||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onComputeMaxHeight = (data) => {
|
onComputeMaxHeight = (data) => {
|
||||||
const {
|
|
||||||
top,
|
|
||||||
bottom
|
|
||||||
} = data.offsets.reference;
|
|
||||||
|
|
||||||
const windowHeight = window.innerHeight;
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
if ((/^botton/).test(data.placement)) {
|
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
|
||||||
data.styles.maxHeight = windowHeight - bottom;
|
|
||||||
} else {
|
|
||||||
data.styles.maxHeight = top;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -457,6 +450,10 @@ class EnhancedSelectInput extends Component {
|
|||||||
order: 851,
|
order: 851,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
fn: this.onComputeMaxHeight
|
fn: this.onComputeMaxHeight
|
||||||
|
},
|
||||||
|
preventOverflow: {
|
||||||
|
enabled: true,
|
||||||
|
boundariesElement: 'viewport'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
|
|||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
|
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
||||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
|
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
|
||||||
@@ -83,6 +84,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.INDEXER_SELECT:
|
case inputTypes.INDEXER_SELECT:
|
||||||
return IndexerSelectInputConnector;
|
return IndexerSelectInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||||
|
return IndexerFlagsSelectInput;
|
||||||
|
|
||||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||||
return DownloadClientSelectInputConnector;
|
return DownloadClientSelectInputConnector;
|
||||||
|
|
||||||
@@ -273,6 +277,7 @@ FormInputGroup.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
values: PropTypes.arrayOf(PropTypes.any),
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
min: PropTypes.number,
|
min: PropTypes.number,
|
||||||
@@ -287,6 +292,7 @@ FormInputGroup.propTypes = {
|
|||||||
includeNoChange: PropTypes.bool,
|
includeNoChange: PropTypes.bool,
|
||||||
includeNoChangeDisabled: PropTypes.bool,
|
includeNoChangeDisabled: PropTypes.bool,
|
||||||
selectedValueOptions: PropTypes.object,
|
selectedValueOptions: PropTypes.object,
|
||||||
|
indexerFlags: PropTypes.number,
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
|||||||
62
frontend/src/Components/Form/IndexerFlagsSelectInput.tsx
Normal file
62
frontend/src/Components/Form/IndexerFlagsSelectInput.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
||||||
|
createSelector(
|
||||||
|
(state: AppState) => state.settings.indexerFlags,
|
||||||
|
(indexerFlags) => {
|
||||||
|
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
if ((selectedFlags & id) === id) {
|
||||||
|
acc.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const values = indexerFlags.items.map(({ id, name }) => ({
|
||||||
|
key: id,
|
||||||
|
value: name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IndexerFlagsSelectInputProps {
|
||||||
|
name: string;
|
||||||
|
indexerFlags: number;
|
||||||
|
onChange(payload: object): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
||||||
|
const { indexerFlags, onChange } = props;
|
||||||
|
|
||||||
|
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
|
||||||
|
|
||||||
|
const onChangeWrapper = useCallback(
|
||||||
|
({ name, value }: { name: string; value: number[] }) => {
|
||||||
|
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
|
||||||
|
|
||||||
|
onChange({ name, value: indexerFlags });
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
values={values}
|
||||||
|
onChange={onChangeWrapper}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexerFlagsSelectInput;
|
||||||
@@ -39,7 +39,7 @@ function createMapStateToProps() {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
disabled: includeNoChangeDisabled
|
isDisabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ function createMapStateToProps() {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function MonitorBooksSelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ function MonitorBooksSelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function MonitorNewItemsSelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ function MonitorNewItemsSelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
.input {
|
|
||||||
composes: input from '~Components/Form/TextInput.css';
|
|
||||||
|
|
||||||
font-family: $passwordFamily;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './PasswordInput.css';
|
|
||||||
|
|
||||||
// Prevent a user from copying (or cutting) the password from the input
|
// Prevent a user from copying (or cutting) the password from the input
|
||||||
function onCopy(e) {
|
function onCopy(e) {
|
||||||
@@ -13,17 +11,14 @@ function PasswordInput(props) {
|
|||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
{...props}
|
{...props}
|
||||||
|
type="password"
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
className: PropTypes.string.isRequired
|
...TextInput.props
|
||||||
};
|
|
||||||
|
|
||||||
PasswordInput.defaultProps = {
|
|
||||||
className: styles.input
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordInput;
|
export default PasswordInput;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function createMapStateToProps() {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
disabled: includeNoChangeDisabled
|
isDisabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ function createMapStateToProps() {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ function createMapStateToProps() {
|
|||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: '',
|
value: translate('NoChange'),
|
||||||
name: translate('NoChange'),
|
|
||||||
isDisabled: includeNoChangeDisabled,
|
isDisabled: includeNoChangeDisabled,
|
||||||
isMissing: false
|
isMissing: false
|
||||||
});
|
});
|
||||||
@@ -39,7 +38,6 @@ function createMapStateToProps() {
|
|||||||
values.push({
|
values.push({
|
||||||
key: '',
|
key: '',
|
||||||
value: '',
|
value: '',
|
||||||
name: '',
|
|
||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
isHidden: true
|
isHidden: true
|
||||||
});
|
});
|
||||||
@@ -56,8 +54,7 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
values.push({
|
values.push({
|
||||||
key: ADD_NEW_KEY,
|
key: ADD_NEW_KEY,
|
||||||
value: '',
|
value: 'Add a new path'
|
||||||
name: 'Add a new path'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -105,6 +102,27 @@ class RootFolderSelectInputConnector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (prevProps.values === values) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) {
|
||||||
|
const defaultValue = values[0];
|
||||||
|
|
||||||
|
if (defaultValue.key !== ADD_NEW_KEY) {
|
||||||
|
onChange({ name, value: defaultValue.key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorFolder {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--disabledColor);
|
||||||
|
}
|
||||||
|
|
||||||
.freeSpace {
|
.freeSpace {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
color: var(--darkGray);
|
color: var(--darkGray);
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'authorFolder': string;
|
||||||
'freeSpace': string;
|
'freeSpace': string;
|
||||||
'isMissing': string;
|
'isMissing': string;
|
||||||
'isMobile': string;
|
'isMobile': string;
|
||||||
'optionText': string;
|
'optionText': string;
|
||||||
|
'value': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -7,18 +7,24 @@ import styles from './RootFolderSelectInputOption.css';
|
|||||||
|
|
||||||
function RootFolderSelectInputOption(props) {
|
function RootFolderSelectInputOption(props) {
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
value,
|
value,
|
||||||
name,
|
name,
|
||||||
freeSpace,
|
freeSpace,
|
||||||
|
authorFolder,
|
||||||
isMissing,
|
isMissing,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
isWindows,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const text = value === '' ? name : `${name} [${value}]`;
|
const slashCharacter = isWindows ? '\\' : '/';
|
||||||
|
|
||||||
|
const text = name === '' ? value : `[${name}] ${value}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputOption
|
<EnhancedSelectInputOption
|
||||||
|
id={id}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
@@ -27,7 +33,18 @@ function RootFolderSelectInputOption(props) {
|
|||||||
isMobile && styles.isMobile
|
isMobile && styles.isMobile
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>{text}</div>
|
<div className={styles.value}>
|
||||||
|
{text}
|
||||||
|
|
||||||
|
{
|
||||||
|
authorFolder && id !== 'addNew' ?
|
||||||
|
<div className={styles.authorFolder}>
|
||||||
|
{slashCharacter}
|
||||||
|
{authorFolder}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
freeSpace == null ?
|
freeSpace == null ?
|
||||||
@@ -50,11 +67,18 @@ function RootFolderSelectInputOption(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RootFolderSelectInputOption.propTypes = {
|
RootFolderSelectInputOption.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
freeSpace: PropTypes.number,
|
freeSpace: PropTypes.number,
|
||||||
|
authorFolder: PropTypes.string,
|
||||||
isMissing: PropTypes.bool,
|
isMissing: PropTypes.bool,
|
||||||
isMobile: PropTypes.bool.isRequired
|
isMobile: PropTypes.bool.isRequired,
|
||||||
|
isWindows: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
RootFolderSelectInputOption.defaultProps = {
|
||||||
|
name: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RootFolderSelectInputOption;
|
export default RootFolderSelectInputOption;
|
||||||
|
|||||||
@@ -7,12 +7,22 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.path {
|
.pathContainer {
|
||||||
@add-mixin truncate;
|
@add-mixin truncate;
|
||||||
|
display: flex;
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authorFolder {
|
||||||
|
@add-mixin truncate;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
color: var(--disabledColor);
|
||||||
|
}
|
||||||
|
|
||||||
.freeSpace {
|
.freeSpace {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'authorFolder': string;
|
||||||
'freeSpace': string;
|
'freeSpace': string;
|
||||||
'path': string;
|
'path': string;
|
||||||
|
'pathContainer': string;
|
||||||
'selectedValue': string;
|
'selectedValue': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -9,19 +9,34 @@ function RootFolderSelectInputSelectedValue(props) {
|
|||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
freeSpace,
|
freeSpace,
|
||||||
|
authorFolder,
|
||||||
includeFreeSpace,
|
includeFreeSpace,
|
||||||
|
isWindows,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const text = value === '' ? name : `${name} [${value}]`;
|
const slashCharacter = isWindows ? '\\' : '/';
|
||||||
|
|
||||||
|
const text = name === '' ? value : `[${name}] ${value}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputSelectedValue
|
<EnhancedSelectInputSelectedValue
|
||||||
className={styles.selectedValue}
|
className={styles.selectedValue}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div className={styles.path}>
|
<div className={styles.pathContainer}>
|
||||||
{text}
|
<div className={styles.path}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
authorFolder ?
|
||||||
|
<div className={styles.authorFolder}>
|
||||||
|
{slashCharacter}
|
||||||
|
{authorFolder}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -38,10 +53,13 @@ RootFolderSelectInputSelectedValue.propTypes = {
|
|||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
freeSpace: PropTypes.number,
|
freeSpace: PropTypes.number,
|
||||||
|
authorFolder: PropTypes.string,
|
||||||
|
isWindows: PropTypes.bool,
|
||||||
includeFreeSpace: PropTypes.bool.isRequired
|
includeFreeSpace: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
RootFolderSelectInputSelectedValue.defaultProps = {
|
RootFolderSelectInputSelectedValue.defaultProps = {
|
||||||
|
name: '',
|
||||||
includeFreeSpace: true
|
includeFreeSpace: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class SelectInput extends Component {
|
|||||||
const {
|
const {
|
||||||
key,
|
key,
|
||||||
value: optionValue,
|
value: optionValue,
|
||||||
|
isDisabled: optionIsDisabled = false,
|
||||||
...otherOptionProps
|
...otherOptionProps
|
||||||
} = option;
|
} = option;
|
||||||
|
|
||||||
@@ -59,6 +60,7 @@ class SelectInput extends Component {
|
|||||||
<option
|
<option
|
||||||
key={key}
|
key={key}
|
||||||
value={key}
|
value={key}
|
||||||
|
disabled={optionIsDisabled}
|
||||||
{...otherOptionProps}
|
{...otherOptionProps}
|
||||||
>
|
>
|
||||||
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
{typeof optionValue === 'function' ? optionValue() : optionValue}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class TextTagInputConnector extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<TagInput
|
<TagInput
|
||||||
|
delimiters={['Tab', 'Enter', ',']}
|
||||||
tagList={[]}
|
tagList={[]}
|
||||||
onTagAdd={this.onTagAdd}
|
onTagAdd={this.onTagAdd}
|
||||||
onTagDelete={this.onTagDelete}
|
onTagDelete={this.onTagDelete}
|
||||||
|
|||||||
@@ -2,3 +2,7 @@
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
color: var(--themeRed);
|
color: var(--themeRed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|||||||
1
frontend/src/Components/HeartRating.css.d.ts
vendored
1
frontend/src/Components/HeartRating.css.d.ts
vendored
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'heart': string;
|
'heart': string;
|
||||||
|
'rating': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import styles from './HeartRating.css';
|
|||||||
|
|
||||||
function HeartRating({ rating, iconSize }) {
|
function HeartRating({ rating, iconSize }) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className={styles.rating}>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.heart}
|
className={styles.heart}
|
||||||
name={icons.HEART}
|
name={icons.HEART}
|
||||||
|
|||||||
@@ -63,6 +63,13 @@
|
|||||||
width: 1280px;
|
width: 1280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.extraExtraLarge {
|
||||||
|
composes: modal;
|
||||||
|
|
||||||
|
width: 1600px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||||
.modal.extraLarge {
|
.modal.extraLarge {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
@@ -76,13 +83,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointMedium) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.modal.small,
|
|
||||||
.modal.medium {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
|
||||||
.modalContainer {
|
.modalContainer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,8 @@
|
|||||||
.modal.small,
|
.modal.small,
|
||||||
.modal.medium,
|
.modal.medium,
|
||||||
.modal.large,
|
.modal.large,
|
||||||
.modal.extraLarge {
|
.modal.extraLarge,
|
||||||
|
.modal.extraExtraLarge {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
|
|||||||
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
@@ -1,6 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'extraExtraLarge': string;
|
||||||
'extraLarge': string;
|
'extraLarge': string;
|
||||||
'large': string;
|
'large': string;
|
||||||
'medium': string;
|
'medium': string;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
|
||||||
|
|
||||||
.isDisabled {
|
&.isDisabled {
|
||||||
color: var(--disabledColor);
|
color: var(--disabledColor);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
|||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||||
import SignalRConnector from 'Components/SignalRConnector';
|
import SignalRConnector from 'Components/SignalRConnector';
|
||||||
|
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import PageHeader from './Header/PageHeader';
|
import PageHeader from './Header/PageHeader';
|
||||||
import PageSidebar from './Sidebar/PageSidebar';
|
import PageSidebar from './Sidebar/PageSidebar';
|
||||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
|||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
isSidebarVisible,
|
isSidebarVisible,
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
|
authenticationEnabled,
|
||||||
onSidebarToggle,
|
onSidebarToggle,
|
||||||
onSidebarVisibleChange
|
onSidebarVisibleChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -108,6 +110,10 @@ class Page extends Component {
|
|||||||
isOpen={this.state.isConnectionLostModalOpen}
|
isOpen={this.state.isConnectionLostModalOpen}
|
||||||
onModalClose={this.onConnectionLostModalClose}
|
onModalClose={this.onConnectionLostModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AuthenticationRequiredModal
|
||||||
|
isOpen={!authenticationEnabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ColorImpairedContext.Provider>
|
</ColorImpairedContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -123,6 +129,7 @@ Page.propTypes = {
|
|||||||
isUpdated: PropTypes.bool.isRequired,
|
isUpdated: PropTypes.bool.isRequired,
|
||||||
isDisconnected: PropTypes.bool.isRequired,
|
isDisconnected: PropTypes.bool.isRequired,
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
|
authenticationEnabled: PropTypes.bool.isRequired,
|
||||||
onResize: PropTypes.func.isRequired,
|
onResize: PropTypes.func.isRequired,
|
||||||
onSidebarToggle: PropTypes.func.isRequired,
|
onSidebarToggle: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -7,10 +7,18 @@ import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Ac
|
|||||||
import { fetchAuthor } from 'Store/Actions/authorActions';
|
import { fetchAuthor } from 'Store/Actions/authorActions';
|
||||||
import { fetchBooks } from 'Store/Actions/bookActions';
|
import { fetchBooks } from 'Store/Actions/bookActions';
|
||||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
fetchImportLists,
|
||||||
|
fetchIndexerFlags,
|
||||||
|
fetchLanguages,
|
||||||
|
fetchMetadataProfiles,
|
||||||
|
fetchQualityProfiles,
|
||||||
|
fetchUISettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
import LoadingPage from './LoadingPage';
|
import LoadingPage from './LoadingPage';
|
||||||
import Page from './Page';
|
import Page from './Page';
|
||||||
@@ -44,6 +52,7 @@ const selectAppProps = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectIsPopulated = createSelector(
|
const selectIsPopulated = createSelector(
|
||||||
|
(state) => state.authors.isPopulated,
|
||||||
(state) => state.customFilters.isPopulated,
|
(state) => state.customFilters.isPopulated,
|
||||||
(state) => state.tags.isPopulated,
|
(state) => state.tags.isPopulated,
|
||||||
(state) => state.settings.ui.isPopulated,
|
(state) => state.settings.ui.isPopulated,
|
||||||
@@ -51,9 +60,11 @@ const selectIsPopulated = createSelector(
|
|||||||
(state) => state.settings.qualityProfiles.isPopulated,
|
(state) => state.settings.qualityProfiles.isPopulated,
|
||||||
(state) => state.settings.metadataProfiles.isPopulated,
|
(state) => state.settings.metadataProfiles.isPopulated,
|
||||||
(state) => state.settings.importLists.isPopulated,
|
(state) => state.settings.importLists.isPopulated,
|
||||||
|
(state) => state.settings.indexerFlags.isPopulated,
|
||||||
(state) => state.system.status.isPopulated,
|
(state) => state.system.status.isPopulated,
|
||||||
(state) => state.app.translations.isPopulated,
|
(state) => state.app.translations.isPopulated,
|
||||||
(
|
(
|
||||||
|
authorsIsPopulated,
|
||||||
customFiltersIsPopulated,
|
customFiltersIsPopulated,
|
||||||
tagsIsPopulated,
|
tagsIsPopulated,
|
||||||
uiSettingsIsPopulated,
|
uiSettingsIsPopulated,
|
||||||
@@ -61,10 +72,12 @@ const selectIsPopulated = createSelector(
|
|||||||
qualityProfilesIsPopulated,
|
qualityProfilesIsPopulated,
|
||||||
metadataProfilesIsPopulated,
|
metadataProfilesIsPopulated,
|
||||||
importListsIsPopulated,
|
importListsIsPopulated,
|
||||||
|
indexerFlagsIsPopulated,
|
||||||
systemStatusIsPopulated,
|
systemStatusIsPopulated,
|
||||||
translationsIsPopulated
|
translationsIsPopulated
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
|
authorsIsPopulated &&
|
||||||
customFiltersIsPopulated &&
|
customFiltersIsPopulated &&
|
||||||
tagsIsPopulated &&
|
tagsIsPopulated &&
|
||||||
uiSettingsIsPopulated &&
|
uiSettingsIsPopulated &&
|
||||||
@@ -72,6 +85,7 @@ const selectIsPopulated = createSelector(
|
|||||||
qualityProfilesIsPopulated &&
|
qualityProfilesIsPopulated &&
|
||||||
metadataProfilesIsPopulated &&
|
metadataProfilesIsPopulated &&
|
||||||
importListsIsPopulated &&
|
importListsIsPopulated &&
|
||||||
|
indexerFlagsIsPopulated &&
|
||||||
systemStatusIsPopulated &&
|
systemStatusIsPopulated &&
|
||||||
translationsIsPopulated
|
translationsIsPopulated
|
||||||
);
|
);
|
||||||
@@ -79,6 +93,7 @@ const selectIsPopulated = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectErrors = createSelector(
|
const selectErrors = createSelector(
|
||||||
|
(state) => state.authors.error,
|
||||||
(state) => state.customFilters.error,
|
(state) => state.customFilters.error,
|
||||||
(state) => state.tags.error,
|
(state) => state.tags.error,
|
||||||
(state) => state.settings.ui.error,
|
(state) => state.settings.ui.error,
|
||||||
@@ -86,9 +101,11 @@ const selectErrors = createSelector(
|
|||||||
(state) => state.settings.qualityProfiles.error,
|
(state) => state.settings.qualityProfiles.error,
|
||||||
(state) => state.settings.metadataProfiles.error,
|
(state) => state.settings.metadataProfiles.error,
|
||||||
(state) => state.settings.importLists.error,
|
(state) => state.settings.importLists.error,
|
||||||
|
(state) => state.settings.indexerFlags.error,
|
||||||
(state) => state.system.status.error,
|
(state) => state.system.status.error,
|
||||||
(state) => state.app.translations.error,
|
(state) => state.app.translations.error,
|
||||||
(
|
(
|
||||||
|
authorsError,
|
||||||
customFiltersError,
|
customFiltersError,
|
||||||
tagsError,
|
tagsError,
|
||||||
uiSettingsError,
|
uiSettingsError,
|
||||||
@@ -96,10 +113,12 @@ const selectErrors = createSelector(
|
|||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
metadataProfilesError,
|
metadataProfilesError,
|
||||||
importListsError,
|
importListsError,
|
||||||
|
indexerFlagsError,
|
||||||
systemStatusError,
|
systemStatusError,
|
||||||
translationsError
|
translationsError
|
||||||
) => {
|
) => {
|
||||||
const hasError = !!(
|
const hasError = !!(
|
||||||
|
authorsError ||
|
||||||
customFiltersError ||
|
customFiltersError ||
|
||||||
tagsError ||
|
tagsError ||
|
||||||
uiSettingsError ||
|
uiSettingsError ||
|
||||||
@@ -107,6 +126,7 @@ const selectErrors = createSelector(
|
|||||||
qualityProfilesError ||
|
qualityProfilesError ||
|
||||||
metadataProfilesError ||
|
metadataProfilesError ||
|
||||||
importListsError ||
|
importListsError ||
|
||||||
|
indexerFlagsError ||
|
||||||
systemStatusError ||
|
systemStatusError ||
|
||||||
translationsError
|
translationsError
|
||||||
);
|
);
|
||||||
@@ -120,6 +140,7 @@ const selectErrors = createSelector(
|
|||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
metadataProfilesError,
|
metadataProfilesError,
|
||||||
importListsError,
|
importListsError,
|
||||||
|
indexerFlagsError,
|
||||||
systemStatusError,
|
systemStatusError,
|
||||||
translationsError
|
translationsError
|
||||||
};
|
};
|
||||||
@@ -133,18 +154,21 @@ function createMapStateToProps() {
|
|||||||
selectErrors,
|
selectErrors,
|
||||||
selectAppProps,
|
selectAppProps,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
|
createSystemStatusSelector(),
|
||||||
(
|
(
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
errors,
|
errors,
|
||||||
app,
|
app,
|
||||||
dimensions
|
dimensions,
|
||||||
|
systemStatus
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...app,
|
...app,
|
||||||
...errors,
|
...errors,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -177,6 +201,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatchFetchImportLists() {
|
dispatchFetchImportLists() {
|
||||||
dispatch(fetchImportLists());
|
dispatch(fetchImportLists());
|
||||||
},
|
},
|
||||||
|
dispatchFetchIndexerFlags() {
|
||||||
|
dispatch(fetchIndexerFlags());
|
||||||
|
},
|
||||||
dispatchFetchUISettings() {
|
dispatchFetchUISettings() {
|
||||||
dispatch(fetchUISettings());
|
dispatch(fetchUISettings());
|
||||||
},
|
},
|
||||||
@@ -218,6 +245,7 @@ class PageConnector extends Component {
|
|||||||
this.props.dispatchFetchQualityProfiles();
|
this.props.dispatchFetchQualityProfiles();
|
||||||
this.props.dispatchFetchMetadataProfiles();
|
this.props.dispatchFetchMetadataProfiles();
|
||||||
this.props.dispatchFetchImportLists();
|
this.props.dispatchFetchImportLists();
|
||||||
|
this.props.dispatchFetchIndexerFlags();
|
||||||
this.props.dispatchFetchUISettings();
|
this.props.dispatchFetchUISettings();
|
||||||
this.props.dispatchFetchStatus();
|
this.props.dispatchFetchStatus();
|
||||||
this.props.dispatchFetchTranslations();
|
this.props.dispatchFetchTranslations();
|
||||||
@@ -245,6 +273,7 @@ class PageConnector extends Component {
|
|||||||
dispatchFetchQualityProfiles,
|
dispatchFetchQualityProfiles,
|
||||||
dispatchFetchMetadataProfiles,
|
dispatchFetchMetadataProfiles,
|
||||||
dispatchFetchImportLists,
|
dispatchFetchImportLists,
|
||||||
|
dispatchFetchIndexerFlags,
|
||||||
dispatchFetchUISettings,
|
dispatchFetchUISettings,
|
||||||
dispatchFetchStatus,
|
dispatchFetchStatus,
|
||||||
dispatchFetchTranslations,
|
dispatchFetchTranslations,
|
||||||
@@ -287,6 +316,7 @@ PageConnector.propTypes = {
|
|||||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||||
dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
|
dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
|
||||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
|
||||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||||
dispatchFetchTranslations: PropTypes.func.isRequired,
|
dispatchFetchTranslations: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ class SignalRConnector extends Component {
|
|||||||
handleWantedCutoff = (body) => {
|
handleWantedCutoff = (body) => {
|
||||||
if (body.action === 'updated') {
|
if (body.action === 'updated') {
|
||||||
this.props.dispatchUpdateItem({
|
this.props.dispatchUpdateItem({
|
||||||
section: 'cutoffUnmet',
|
section: 'wanted.cutoffUnmet',
|
||||||
updateOnly: true,
|
updateOnly: true,
|
||||||
...body.resource
|
...body.resource
|
||||||
});
|
});
|
||||||
@@ -263,7 +263,7 @@ class SignalRConnector extends Component {
|
|||||||
handleWantedMissing = (body) => {
|
handleWantedMissing = (body) => {
|
||||||
if (body.action === 'updated') {
|
if (body.action === 'updated') {
|
||||||
this.props.dispatchUpdateItem({
|
this.props.dispatchUpdateItem({
|
||||||
section: 'missing',
|
section: 'wanted.missing',
|
||||||
updateOnly: true,
|
updateOnly: true,
|
||||||
...body.resource
|
...body.resource
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
line-height: 1.52857143;
|
line-height: 1.52857143;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
.cell {
|
.cell {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user