mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-11 15:20:03 -04:00
Compare commits
414 Commits
sonarr-pul
...
sonarr-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edc522e412 | ||
|
|
d77aa82961 | ||
|
|
c7a4060c4c | ||
|
|
64e6f98683 | ||
|
|
dcc2a14c60 | ||
|
|
22781b62e6 | ||
|
|
d93329a3fd | ||
|
|
ef20abba7a | ||
|
|
d647b47e88 | ||
|
|
e22f284a14 | ||
|
|
7ac8b5600e | ||
|
|
5039ba823c | ||
|
|
427176d7d2 | ||
|
|
6a2fd3a4e6 | ||
|
|
e03390c8d4 | ||
|
|
4081f3efc2 | ||
|
|
9cec9ac428 | ||
|
|
8bd7194121 | ||
|
|
8c97df1be6 | ||
|
|
d556b77f9d | ||
|
|
8b9cada59e | ||
|
|
a18bbeee5a | ||
|
|
6f17057f31 | ||
|
|
4d3e83ec15 | ||
|
|
4034250a33 | ||
|
|
d83c02fe9f | ||
|
|
b4112dc4bb | ||
|
|
a463166bb6 | ||
|
|
d7d57f0162 | ||
|
|
8258323307 | ||
|
|
d15c42957a | ||
|
|
df0a5f004d | ||
|
|
d8f11bc3cb | ||
|
|
71c2b1aeec | ||
|
|
6f7c6721db | ||
|
|
388fb52644 | ||
|
|
de46816cdb | ||
|
|
dbb6ef7664 | ||
|
|
4a3062deae | ||
|
|
bc7bf6b269 | ||
|
|
01b4ee1a02 | ||
|
|
3825ecd393 | ||
|
|
bc63587428 | ||
|
|
1caa49c895 | ||
|
|
fbdc9f3a13 | ||
|
|
f5847e9e5b | ||
|
|
874b4fc401 | ||
|
|
ec75aa6378 | ||
|
|
31aaec4b10 | ||
|
|
6199cb2999 | ||
|
|
adb22868b6 | ||
|
|
20cf7c1ffc | ||
|
|
13fd31b67d | ||
|
|
95dbfb6e4a | ||
|
|
31c8092960 | ||
|
|
06fbd5f93d | ||
|
|
44c37b3f47 | ||
|
|
5726df841c | ||
|
|
590b203bb6 | ||
|
|
9982df9d2b | ||
|
|
d1741c8b75 | ||
|
|
20a477f95d | ||
|
|
e346eb6c64 | ||
|
|
49710b8863 | ||
|
|
2699f7c0d7 | ||
|
|
ed1b29f8e4 | ||
|
|
8a4d309d57 | ||
|
|
7dc061cc8a | ||
|
|
cef06d11a5 | ||
|
|
9235ae85bc | ||
|
|
b123952010 | ||
|
|
e62d00103d | ||
|
|
13dad33e31 | ||
|
|
be8e50a41f | ||
|
|
6e38df366f | ||
|
|
91b8565629 | ||
|
|
7d02c00ca8 | ||
|
|
b83ccc19b0 | ||
|
|
93086abf58 | ||
|
|
f68dc04273 | ||
|
|
5664054f95 | ||
|
|
f16bd435db | ||
|
|
2bde9d13dd | ||
|
|
f448481460 | ||
|
|
70856c217c | ||
|
|
4db158e0c9 | ||
|
|
55ab909fde | ||
|
|
61c9779022 | ||
|
|
8299c8e13e | ||
|
|
da5e35fc25 | ||
|
|
a6a2219bc4 | ||
|
|
aa2855a62b | ||
|
|
599f52e72f | ||
|
|
c5c2b94b9a | ||
|
|
b016b36435 | ||
|
|
d93a0c27e9 | ||
|
|
eecf08e063 | ||
|
|
fb1643f630 | ||
|
|
a061179f6b | ||
|
|
b441f6c05b | ||
|
|
0904eac300 | ||
|
|
29e3d8f477 | ||
|
|
71bd88e531 | ||
|
|
0689bec779 | ||
|
|
117a5c8010 | ||
|
|
d10d91439f | ||
|
|
ed0722bae4 | ||
|
|
e69371deca | ||
|
|
5ae8deb9d6 | ||
|
|
291674fc18 | ||
|
|
774cd04d32 | ||
|
|
3eb0533b17 | ||
|
|
2fe11ca1a9 | ||
|
|
c8ae6b0299 | ||
|
|
2086ae1e2c | ||
|
|
2aeec97d5e | ||
|
|
45e9d14916 | ||
|
|
ded45a53f3 | ||
|
|
d5cbb8f84f | ||
|
|
cac0fca0d2 | ||
|
|
9b5a050c40 | ||
|
|
9fa67dbe5f | ||
|
|
505ef5ee63 | ||
|
|
87a2d7382e | ||
|
|
7c74a8df01 | ||
|
|
f8324b3c92 | ||
|
|
cb03f3bf6b | ||
|
|
d6f85ec4f9 | ||
|
|
dd0dd921b0 | ||
|
|
1f02423148 | ||
|
|
7a4fba851f | ||
|
|
2d5efc268f | ||
|
|
cad5762f31 | ||
|
|
bcc8370d05 | ||
|
|
3481168df5 | ||
|
|
337a30ac0f | ||
|
|
eaad4de4dc | ||
|
|
b98ae37abf | ||
|
|
5dac84f9d8 | ||
|
|
ef21c73619 | ||
|
|
14d74f2eca | ||
|
|
c3cbbb7627 | ||
|
|
ad3a58c422 | ||
|
|
347b154882 | ||
|
|
63ccc155d6 | ||
|
|
76613316fa | ||
|
|
f33e9f2bbc | ||
|
|
e4a3d7b273 | ||
|
|
46c2e0ba82 | ||
|
|
8616373f96 | ||
|
|
b3f99d8c20 | ||
|
|
c0c0847963 | ||
|
|
c92f9d03c0 | ||
|
|
f7bf1e243d | ||
|
|
20b1f41ac1 | ||
|
|
f703db1e00 | ||
|
|
20f67c8035 | ||
|
|
56ae497bfa | ||
|
|
e760dc56c6 | ||
|
|
7ad26b386c | ||
|
|
199085249f | ||
|
|
b84041d95f | ||
|
|
615facb3c4 | ||
|
|
b4fd1340c2 | ||
|
|
1da27b0978 | ||
|
|
b0a3ddef9c | ||
|
|
a59706ceb4 | ||
|
|
ce58e6ecdb | ||
|
|
d01ce8b908 | ||
|
|
575f995916 | ||
|
|
c17c8815ef | ||
|
|
8445941510 | ||
|
|
32ae18ae6f | ||
|
|
b91ec241a6 | ||
|
|
17ce8187fd | ||
|
|
41615fe026 | ||
|
|
9cff8f31e9 | ||
|
|
21538b972d | ||
|
|
5a7b4d41d8 | ||
|
|
16f67265c9 | ||
|
|
03d2a85821 | ||
|
|
fa68a9559f | ||
|
|
c4a2a432d2 | ||
|
|
0b3eda4de3 | ||
|
|
a1d4b3a0cf | ||
|
|
28f055e838 | ||
|
|
8d91fb81bd | ||
|
|
085913a852 | ||
|
|
8599fc4ee6 | ||
|
|
68f017cd84 | ||
|
|
f34171444e | ||
|
|
10760471e0 | ||
|
|
4ecabcd3d6 | ||
|
|
1af2e14474 | ||
|
|
fe836c56ee | ||
|
|
ae611cca74 | ||
|
|
c446f9835e | ||
|
|
2ff8bf204f | ||
|
|
5aa17e7a67 | ||
|
|
312cf2d364 | ||
|
|
a9852ded2f | ||
|
|
4286f546e9 | ||
|
|
482fe04161 | ||
|
|
cbdc2c51c4 | ||
|
|
f55fda9bac | ||
|
|
3841f7708e | ||
|
|
f2bfed0252 | ||
|
|
bc7e4ea622 | ||
|
|
38f16e6b85 | ||
|
|
af57024c63 | ||
|
|
54faa58a4d | ||
|
|
8ca35a709c | ||
|
|
72a0e38b36 | ||
|
|
b25f8449a2 | ||
|
|
cf0a15a308 | ||
|
|
a677098f0f | ||
|
|
a40cce9c71 | ||
|
|
6d94136273 | ||
|
|
f6d620f969 | ||
|
|
8d9302da77 | ||
|
|
f5cbab6ac9 | ||
|
|
cb0ae36033 | ||
|
|
93a42e2fe9 | ||
|
|
0f9ab46dd6 | ||
|
|
3a360d7a1e | ||
|
|
8e37aa2e78 | ||
|
|
000022a927 | ||
|
|
2bfc50b59b | ||
|
|
4f46ab9a9a | ||
|
|
e7410959fe | ||
|
|
ee1112026a | ||
|
|
65681cad10 | ||
|
|
9653f9bbca | ||
|
|
6f97ca9a55 | ||
|
|
c0e193dd1f | ||
|
|
ecf1e1a130 | ||
|
|
967dae5132 | ||
|
|
f4b96dc45e | ||
|
|
8181749629 | ||
|
|
9bca258541 | ||
|
|
77d2db3c47 | ||
|
|
1dd61e826b | ||
|
|
857a5f5005 | ||
|
|
a321288a71 | ||
|
|
62a3355546 | ||
|
|
6db135877a | ||
|
|
fa1985509d | ||
|
|
01a5c456c4 | ||
|
|
3ec451e85e | ||
|
|
bf7fc6fa19 | ||
|
|
cbf2f0f4fb | ||
|
|
565fb77713 | ||
|
|
3ea0c8e5fa | ||
|
|
61454955be | ||
|
|
34506025ac | ||
|
|
ef35efb127 | ||
|
|
2a4f681b17 | ||
|
|
24e2ff56dd | ||
|
|
c5787b0c87 | ||
|
|
d6ba6bbfd1 | ||
|
|
de72cfcaaa | ||
|
|
90d671d68d | ||
|
|
b4b14a5359 | ||
|
|
759c132797 | ||
|
|
dc1fbb3a7e | ||
|
|
eb431f09fd | ||
|
|
0031214fb3 | ||
|
|
94adb4d582 | ||
|
|
bc3764dcda | ||
|
|
64fafc599c | ||
|
|
53f66606bc | ||
|
|
706f8310bb | ||
|
|
ae4994571a | ||
|
|
258847a83b | ||
|
|
43f1d77b9f | ||
|
|
f451796cf5 | ||
|
|
620fb57b30 | ||
|
|
bca70e931f | ||
|
|
c7f0a8578a | ||
|
|
c92563ea10 | ||
|
|
66e7ce6f27 | ||
|
|
b1da9a1934 | ||
|
|
1491788081 | ||
|
|
33e1c4a537 | ||
|
|
8c3dd3730a | ||
|
|
02927dc37d | ||
|
|
be46d5ae8f | ||
|
|
e4fdf71eee | ||
|
|
9a2260a00f | ||
|
|
399a370e7a | ||
|
|
ff27ad2096 | ||
|
|
76bcf94f1d | ||
|
|
472da10149 | ||
|
|
8011112919 | ||
|
|
6d8adec7dc | ||
|
|
29c7404185 | ||
|
|
f6ff53ca31 | ||
|
|
2dff18490e | ||
|
|
6d7657e8b3 | ||
|
|
0db6cf272f | ||
|
|
5827644300 | ||
|
|
d1f1052d7d | ||
|
|
341d64ebf7 | ||
|
|
c817d6c0d8 | ||
|
|
735fceb074 | ||
|
|
0e43f67a9f | ||
|
|
eb95fe265a | ||
|
|
3e32f7d49f | ||
|
|
4b8c739b5c | ||
|
|
eeb172d3bf | ||
|
|
e2745c5956 | ||
|
|
ede981f737 | ||
|
|
e2c1d76516 | ||
|
|
05a8a3d764 | ||
|
|
da262f3d95 | ||
|
|
a5b1711827 | ||
|
|
7b0802cfd6 | ||
|
|
16fcba02ba | ||
|
|
b3c217d713 | ||
|
|
975ff0baf3 | ||
|
|
0bb8672119 | ||
|
|
9ebeee8b4f | ||
|
|
894adbe91e | ||
|
|
e7628476f7 | ||
|
|
3e5045e496 | ||
|
|
95d93dfa09 | ||
|
|
842f80d567 | ||
|
|
fc8b6014ec | ||
|
|
6fb24128f8 | ||
|
|
8dbd006978 | ||
|
|
fed67044f0 | ||
|
|
f7c05d98a9 | ||
|
|
25b37ace34 | ||
|
|
e1eb9a0ba7 | ||
|
|
b6055c3339 | ||
|
|
0e78d8d535 | ||
|
|
0cf7953cab | ||
|
|
0d3cd24fb2 | ||
|
|
41d7a1f355 | ||
|
|
608c0f268f | ||
|
|
8c38e4ede8 | ||
|
|
4f791f965a | ||
|
|
0b0bebf3aa | ||
|
|
05a0508d78 | ||
|
|
6cecf23b88 | ||
|
|
fb0128e3be | ||
|
|
6947e6e34c | ||
|
|
c51ae664aa | ||
|
|
1d694af98e | ||
|
|
bc678e1976 | ||
|
|
c0db20a860 | ||
|
|
621bb20756 | ||
|
|
326d395da4 | ||
|
|
7be2effa05 | ||
|
|
8f965a7cd5 | ||
|
|
15dee29057 | ||
|
|
61617ff5bc | ||
|
|
27e3b5e630 | ||
|
|
1b6694739e | ||
|
|
a7c0eabb56 | ||
|
|
7c5188638f | ||
|
|
4ce405728a | ||
|
|
615acdaebe | ||
|
|
d460cbf319 | ||
|
|
4887ed0d2f | ||
|
|
bf852cadbe | ||
|
|
05d24821f7 | ||
|
|
04e575903f | ||
|
|
dfb9558868 | ||
|
|
808fb5f2aa | ||
|
|
9f62ee34f1 | ||
|
|
847a9eae93 | ||
|
|
a9e181425b | ||
|
|
357245a9eb | ||
|
|
8280908c39 | ||
|
|
b05bd685bc | ||
|
|
33e5351add | ||
|
|
a1c2986af8 | ||
|
|
2983b60026 | ||
|
|
0d9e98cf51 | ||
|
|
5d63ef01b6 | ||
|
|
0bb391235f | ||
|
|
c4d02c1666 | ||
|
|
7c6adb0da7 | ||
|
|
8f2ae2beff | ||
|
|
f53a1d7942 | ||
|
|
cef15887a4 | ||
|
|
5f946c0aa3 | ||
|
|
7528ed4084 | ||
|
|
e8ddaf6ccf | ||
|
|
c5281d04f1 | ||
|
|
ec0e930a54 | ||
|
|
fb7ec5c61e | ||
|
|
cb2bd0273f | ||
|
|
d6e55e2913 | ||
|
|
a3a914adfe | ||
|
|
1835f15460 | ||
|
|
d1caed5c7d | ||
|
|
6bdfe01fbc | ||
|
|
225f0b310a | ||
|
|
6a0c99d32b | ||
|
|
04cf0594f6 | ||
|
|
2001035257 | ||
|
|
355dcaed6c | ||
|
|
3d3292fd3d | ||
|
|
78d073a0a8 | ||
|
|
ca8f768f84 | ||
|
|
b57ba3d6e2 | ||
|
|
d48f2a1cb6 | ||
|
|
d8e4d388dc | ||
|
|
47ae2032e5 | ||
|
|
89319f9833 | ||
|
|
07829a19b8 | ||
|
|
27259c1ebd |
@@ -19,10 +19,10 @@ indent_size = 4
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:refactoring
|
||||
dotnet_style_qualification_for_property = false:refactoring
|
||||
dotnet_style_qualification_for_method = false:refactoring
|
||||
dotnet_style_qualification_for_event = false:refactoring
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
@@ -32,10 +32,6 @@ csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||
|
||||
@@ -208,9 +204,6 @@ dotnet_diagnostic.CA2000.severity = suggestion
|
||||
dotnet_diagnostic.CA2002.severity = suggestion
|
||||
dotnet_diagnostic.CA2007.severity = suggestion
|
||||
dotnet_diagnostic.CA2008.severity = suggestion
|
||||
dotnet_diagnostic.CA2009.severity = suggestion
|
||||
dotnet_diagnostic.CA2010.severity = suggestion
|
||||
dotnet_diagnostic.CA2011.severity = suggestion
|
||||
dotnet_diagnostic.CA2012.severity = suggestion
|
||||
dotnet_diagnostic.CA2013.severity = suggestion
|
||||
dotnet_diagnostic.CA2100.severity = suggestion
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
12
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,14 +1,13 @@
|
||||
name: Bug Report
|
||||
title: "[BUG]: "
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -43,12 +42,14 @@ body:
|
||||
- **Docker Install**: Yes
|
||||
- **Using Reverse Proxy**: No
|
||||
- **Browser**: Firefox 90 (If UI related)
|
||||
- **Database**: Sqlite 3.36.0
|
||||
value: |
|
||||
- OS:
|
||||
- Readarr:
|
||||
- Docker Install:
|
||||
- Using Reverse Proxy:
|
||||
- Browser:
|
||||
- Database:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
@@ -64,12 +65,11 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
label: Trace Logs?
|
||||
description: |
|
||||
Trace Logs (https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files)
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
***Generally speaking, all bug reports must have trace logs provided.***
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,14 +1,13 @@
|
||||
name: Feature Request
|
||||
title: "[FEAT]: "
|
||||
description: 'Suggest an idea for Readarr'
|
||||
labels: ['Type: Feature Request', 'Status: Needs Triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
||||
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
41
.github/workflows/azuresync.yml
vendored
41
.github/workflows/azuresync.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Sync issue to Azure DevOps work item
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
|
||||
|
||||
concurrency: azuresync-${{ github.event.issue.number }}
|
||||
|
||||
jobs:
|
||||
alert:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Readarr"
|
||||
ado_wit: "Bug"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
|
||||
env:
|
||||
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
|
||||
github_token: "${{ github.token }}"
|
||||
ado_organization: "Servarr"
|
||||
ado_project: "Servarr"
|
||||
ado_area_path: "Servarr\\Readarr"
|
||||
ado_wit: "User Story"
|
||||
ado_new_state: "New"
|
||||
ado_active_state: "Active"
|
||||
ado_close_state: "Closed"
|
||||
ado_bypassrules: true
|
||||
log_level: 100
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
}
|
||||
}
|
||||
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
<development@readarr.com>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
@@ -1,51 +1,13 @@
|
||||
# How to Contribute #
|
||||
# How to Contribute
|
||||
|
||||
We're always looking for people to help make Readarr even better, there are a number of ways to contribute.
|
||||
|
||||
This file is updated on an ad-hoc basis, for the latest details please see the [contributing wiki page](https://wiki.servarr.com/readarr/contributing).
|
||||
This file has been moved to the wiki for the latest details please see the [contributing wiki page](https://wiki.servarr.com/readarr/contributing).
|
||||
|
||||
## Documentation ##
|
||||
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/readarr) the better.
|
||||
## Documentation
|
||||
|
||||
## Development ##
|
||||
Setup guides, [FAQ](https://wiki.servarr.com/readarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/readarr) the better.
|
||||
|
||||
### Tools required ###
|
||||
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
|
||||
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS](https://nodejs.org/en/download/) (Node 12.X.X or higher)
|
||||
- [Yarn](https://yarnpkg.com/)
|
||||
- .NET Core 5.0.
|
||||
## Development
|
||||
|
||||
### Getting started ###
|
||||
|
||||
1. Fork Readarr
|
||||
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
|
||||
3. Install the required Node Packages `yarn install`
|
||||
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
|
||||
5. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `net5.0`
|
||||
6. Debug the project in Visual Studio
|
||||
7. Open http://localhost:8787
|
||||
|
||||
### Contributing Code ###
|
||||
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Readarr/Readarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
|
||||
- Rebase from Readarr's develop branch, don't merge
|
||||
- Make meaningful commits, or squash them
|
||||
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
|
||||
- Reach out to us on the discord if you have any questions
|
||||
- Add tests (unit/integration)
|
||||
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
|
||||
- One feature/bug fix per pull request to keep things clean and easy to understand
|
||||
- Use 4 spaces instead of tabs, this is the default for VS 2019 and WebStorm (to my knowledge)
|
||||
|
||||
### Pull Requesting ###
|
||||
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
|
||||
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
|
||||
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
|
||||
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
|
||||
- new-feature (Good)
|
||||
- fix-bug (Good)
|
||||
- patch (Bad)
|
||||
- develop (Bad)
|
||||
|
||||
If you have any questions about any of this, please let us know.
|
||||
See the [Wiki Page](https://wiki.servarr.com/readarr/contributing)
|
||||
|
||||
57
README.md
57
README.md
@@ -1,56 +1,77 @@
|
||||
# Readarr
|
||||
|
||||
[](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://hub.docker.com/r/hotio/readarr)
|
||||
[](#backers) [](#sponsors)
|
||||
[](https://translate.servarr.com/engage/readarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/readarr/installation#docker)
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
[](#mega-sponsors)
|
||||
|
||||
### Readarr is currently in beta testing and is generally still in a work in progress. Features may be broken, incomplete, or cause spontaneous combustion.
|
||||
### Readarr is currently in beta testing and is generally still in a work in progress. Features may be broken, incomplete, or cause spontaneous combustion
|
||||
|
||||
Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort and rename them.
|
||||
Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort, and rename them.
|
||||
Note that only one type of a given book is supported. If you want both an audiobook and ebook of a given book you will need multiple instances.
|
||||
|
||||
## Major Features Include:
|
||||
## Major Features Include
|
||||
|
||||
* Can watch for better quality of the ebooks and audiobooks you have and do an automatic upgrade. *e.g. from PDF to AZW3*
|
||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
* Automatically detects new books
|
||||
* Can scan your existing library and download any missing books
|
||||
* Automatic failed download handling will try another release if one fails
|
||||
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||
* Advanced customization for profiles, such that Readarr will always download the copy you want
|
||||
* Fully configurable book renaming
|
||||
* Full integration with SABnzbd and NZBGet
|
||||
* Full integration with Calibre (add to library, conversion)
|
||||
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
|
||||
* Full integration with Calibre (add to library, conversion) (Requires Calibre Content Server)
|
||||
* And a beautiful UI
|
||||
|
||||
## Support
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
[](https://wiki.servarr.com/readarr)
|
||||
[](https://readarr.com/discord)
|
||||
[](https://www.reddit.com/r/readarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
[](https://github.com/Readarr/Readarr/issues)
|
||||
[](https://wiki.servarr.com/readarr)
|
||||
|
||||
## Contributors
|
||||
## Contributors & Developers
|
||||
|
||||
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||
<a href="https://github.com/Readarr/Readarr/graphs/contributors"><img src="https://opencollective.com/Readarr/contributors.svg?width=890&button=false" /></a>
|
||||
[API Documentation](https://readarr.com/docs/api/)
|
||||
|
||||
This project exists thanks to all the people who contribute.
|
||||
- [Contribute (GitHub)](CONTRIBUTING.md)
|
||||
- [Contribution (Wiki Article)](https://wiki.servarr.com/readarr/contributing)
|
||||
|
||||
[](https://github.com/Readarr/Readarr/graphs/contributors)
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/readarr#backer)
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Readarr#backer)
|
||||
|
||||
<img src="https://opencollective.com/readarr/backers.svg?width=890"></a>
|
||||
[](https://opencollective.com/Readarr#backer)
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/readarr#sponsor)
|
||||
|
||||
<img src="https://opencollective.com/readarr/sponsors.svg?width=890"></a>
|
||||
[](https://opencollective.com/readarr#sponsor)
|
||||
|
||||
## Mega Sponsors
|
||||
<img src="https://opencollective.com/readarr/tiers/mega-sponsor.svg?width=890"></a>
|
||||
|
||||
[](https://opencollective.com/readarr#mega-sponsor)
|
||||
|
||||
## DigitalOcean
|
||||
|
||||
This project is also supported by DigitalOcean
|
||||
<p>
|
||||
<a href="https://www.digitalocean.com/">
|
||||
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2021
|
||||
* Copyright 2010-2022
|
||||
|
||||
8
SECURITY.md
Normal file
8
SECURITY.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities on Discord (preferred) to
|
||||
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
|
||||
us within 72 hours. If the issue is confirmed, we will release a patch as soon
|
||||
as possible depending on complexity/severity.
|
||||
@@ -7,15 +7,19 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '0.1.0'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.1.5'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '5.0.302'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
dotnetVersion: '6.0.302'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
@@ -42,7 +46,7 @@ stages:
|
||||
matrix:
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
enableAnalysis: 'false'
|
||||
|
||||
pool:
|
||||
@@ -62,16 +66,13 @@ stages:
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- bash: |
|
||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||
echo $BUNDLEDVERSIONS
|
||||
grep osx-x64 $BUNDLEDVERSIONS
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "BSD already enabled"
|
||||
else
|
||||
echo "Enabling BSD support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
|
||||
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
|
||||
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
displayName: Enable FreeBSD Support
|
||||
displayName: Extra Platform Support
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
|
||||
@@ -81,31 +82,29 @@ stages:
|
||||
displayName: Build Readarr Backend
|
||||
env:
|
||||
NUGET_PACKAGES: $(nugetCacheFolder)
|
||||
- powershell: Get-ChildItem _output\net5.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
|
||||
- powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
|
||||
displayName: Clean up intermediate output
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
path: $(outputFolder)
|
||||
artifact: '$(osName)Backend'
|
||||
artifactType: 'pipeline'
|
||||
parallel: true
|
||||
parallelCount: 100
|
||||
- publish: $(outputFolder)
|
||||
artifact: '$(osName)Backend'
|
||||
displayName: Publish Backend
|
||||
- publish: '$(testsFolder)/net5.0/win-x64/publish'
|
||||
artifact: WindowsCoreTests
|
||||
displayName: Publish Windows Test Package
|
||||
- publish: '$(testsFolder)/net5.0/linux-x64/publish'
|
||||
artifact: LinuxCoreTests
|
||||
displayName: Publish Linux Test Package
|
||||
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
|
||||
artifact: LinuxMuslCoreTests
|
||||
displayName: Publish Linux Musl Test Package
|
||||
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
|
||||
artifact: FreebsdCoreTests
|
||||
displayName: Publish FreeBSD Test Package
|
||||
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
|
||||
artifact: MacCoreTests
|
||||
displayName: Publish MacOS Test Package
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
artifact: win-x64-tests
|
||||
displayName: Publish win-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
artifact: linux-x64-tests
|
||||
displayName: Publish linux-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||
artifact: linux-x86-tests
|
||||
displayName: Publish linux-x86 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
artifact: linux-musl-x64-tests
|
||||
displayName: Publish linux-musl-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
artifact: freebsd-x64-tests
|
||||
displayName: Publish freebsd-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
artifact: osx-x64-tests
|
||||
displayName: Publish osx-x64 Test Package
|
||||
|
||||
- stage: Build_Backend_Other
|
||||
displayName: Build Backend (Other OS)
|
||||
@@ -116,11 +115,11 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
enableAnalysis: 'true'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: ${{ variables.macImage }}
|
||||
enableAnalysis: 'false'
|
||||
|
||||
pool:
|
||||
@@ -137,25 +136,29 @@ stages:
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- bash: |
|
||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||
echo $BUNDLEDVERSIONS
|
||||
grep osx-x64 $BUNDLEDVERSIONS
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "BSD already enabled"
|
||||
else
|
||||
echo "Enabling BSD support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
|
||||
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
|
||||
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
displayName: Enable FreeBSD Support
|
||||
displayName: Extra Platform Support
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
|
||||
path: $(nugetCacheFolder)
|
||||
displayName: Cache NuGet packages
|
||||
- bash: ./build.sh --backend --enable-bsd
|
||||
- bash: ./build.sh --backend --enable-extra-platforms
|
||||
displayName: Build Readarr Backend
|
||||
env:
|
||||
NUGET_PACKAGES: $(nugetCacheFolder)
|
||||
- bash: |
|
||||
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
|
||||
find ${TESTSFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
|
||||
displayName: Clean up intermediate output
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
|
||||
- stage: Build_Frontend
|
||||
displayName: Frontend
|
||||
@@ -166,13 +169,13 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: ${{ variables.macImage }}
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
@@ -209,7 +212,7 @@ stages:
|
||||
- job: Windows_Installer
|
||||
displayName: Create Installer
|
||||
pool:
|
||||
vmImage: 'windows-2019'
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
@@ -225,16 +228,11 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
setup/inno/ISCC.exe setup/readarr.iss //DFramework=net5.0 //DRuntime=win-x86
|
||||
cp setup/output/Readarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
- bash: |
|
||||
setup/inno/ISCC.exe setup/readarr.iss //DFramework=net5.0 //DRuntime=win-x64
|
||||
cp setup/output/Readarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
./build.sh --packages --installer
|
||||
cp setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
cp setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create Installers
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'WindowsInstaller'
|
||||
displayName: Publish Installer
|
||||
@@ -247,7 +245,7 @@ stages:
|
||||
- job: Other_Packages
|
||||
displayName: Create Standard Packages
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
@@ -263,90 +261,120 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages --enable-bsd
|
||||
- bash: ./build.sh --packages --enable-extra-platforms
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
find . -name "fpcalc" -exec chmod a+x {} \;
|
||||
find . -name "Readarr" -exec chmod a+x {} \;
|
||||
find . -name "Readarr.Update" -exec chmod a+x {} \;
|
||||
displayName: Set executable bits
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows Core zip
|
||||
displayName: Create win-x64 zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows x86 Core zip
|
||||
displayName: Create win-x86 zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS Core app
|
||||
displayName: Create osx-x64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS Core tar
|
||||
displayName: Create osx-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/macos/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Core tar
|
||||
displayName: Create osx-arm64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-arm64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create osx-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Musl Core tar
|
||||
displayName: Create linux-musl-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Core tar
|
||||
displayName: Create linux-x86 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x86.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-arm tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Core tar
|
||||
displayName: Create linux-musl-arm tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Musl Core tar
|
||||
displayName: Create linux-musl-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create FreeBSD Core Core tar
|
||||
displayName: Create freebsd-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'Packages'
|
||||
displayName: Publish Packages
|
||||
@@ -392,22 +420,22 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
testName: 'osx-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: ${{ variables.macImage }}
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
testName: 'win-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
testName: 'linux-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
FreebsdCore:
|
||||
osName: 'Linux'
|
||||
testName: 'FreebsdCore'
|
||||
testName: 'freebsd-x64'
|
||||
poolName: 'FreeBSD'
|
||||
imageName:
|
||||
|
||||
@@ -426,15 +454,11 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: '$(testName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
chmod a+x _tests/fpcalc
|
||||
displayName: Make fpcalc Executable
|
||||
condition: and(succeeded(), or(eq(variables['osName'], 'Mac'), eq(variables['testName'], 'LinuxCore')))
|
||||
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
@@ -458,11 +482,15 @@ stages:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
@@ -470,9 +498,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -495,6 +529,57 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres
|
||||
displayName: Unit Native LinuxCore with Postgres Database
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
artifactName: LinuxCoreTests
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
Readarr__Postgres__Port: '5432'
|
||||
Readarr__Postgres__User: 'readarr'
|
||||
Readarr__Postgres__Password: 'readarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'linux-x64-Tests'
|
||||
targetPath: $(testsFolder)
|
||||
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
docker run -d --name=postgres14 \
|
||||
-e POSTGRES_PASSWORD=readarr \
|
||||
-e POSTGRES_USER=readarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
ls -lR ${TESTSFOLDER}
|
||||
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||
displayName: Run Tests
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration
|
||||
@@ -506,18 +591,18 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
imageName: 'macos-10.14'
|
||||
testName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Readarr.*.osx-core-x64.tar.gz'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
imageName: 'windows-2019'
|
||||
testName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Readarr.*.windows-core-x64.zip'
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
imageName: 'ubuntu-18.04'
|
||||
testName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
|
||||
pool:
|
||||
@@ -533,7 +618,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: '$(testName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -563,6 +648,66 @@ stages:
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_LinuxCore_Postgres
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
variables:
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
Readarr__Postgres__Host: 'localhost'
|
||||
Readarr__Postgres__Port: '5432'
|
||||
Readarr__Postgres__User: 'readarr'
|
||||
Readarr__Postgres__Password: 'readarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'linux-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
mkdir -p ./bin/
|
||||
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Readarr/. ./bin/
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
docker run -d --name=postgres14 \
|
||||
-e POSTGRES_PASSWORD=readarr \
|
||||
-e POSTGRES_USER=readarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||
displayName: Run Integration Tests
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_FreeBSD
|
||||
displayName: Integration Native FreeBSD
|
||||
workspace:
|
||||
@@ -578,14 +723,14 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'FreebsdCoreTests'
|
||||
artifactName: 'freebsd-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '/$(pattern)'
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- bash: |
|
||||
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
||||
@@ -612,13 +757,17 @@ stages:
|
||||
strategy:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
testName: 'linux-musl-x64'
|
||||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
pattern: 'Readarr.*.linux-musl-core-x64.tar.gz'
|
||||
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
pattern: 'Readarr.*.linux-core-x86.tar.gz'
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
@@ -626,9 +775,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -674,15 +829,18 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
artifactName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
artifactName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Readarr.*.osx-core-x64.tar.gz'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
artifactName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Readarr.*.windows-core-x64.zip'
|
||||
|
||||
pool:
|
||||
@@ -698,7 +856,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(osName)CoreTests'
|
||||
artifactName: '$(artifactName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -749,10 +907,10 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
@@ -781,7 +939,7 @@ stages:
|
||||
displayName: Frontend
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@1
|
||||
@@ -804,7 +962,7 @@ stages:
|
||||
variables:
|
||||
disable.coverage.autogenerate: 'true'
|
||||
pool:
|
||||
vmImage: ubuntu-18.04
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
@@ -842,8 +1000,8 @@ stages:
|
||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||
- bash: |
|
||||
./build.sh --backend -f net5.0 -r linux-x64
|
||||
TEST_DIR=_tests/net5.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
|
||||
./build.sh --backend -f net6.0 -r linux-x64
|
||||
TEST_DIR=_tests/net6.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
env:
|
||||
NUGET_PACKAGES: $(nugetCacheFolder)
|
||||
@@ -876,7 +1034,7 @@ stages:
|
||||
- job:
|
||||
displayName: Discord Notification
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
continueOnError: true
|
||||
@@ -892,3 +1050,4 @@ stages:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
DISCORDCHANNELID: $(discordChannelId)
|
||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||
DISCORDTHREADID: $(discordThreadId)
|
||||
|
||||
138
build.sh
138
build.sh
@@ -27,15 +27,22 @@ UpdateVersionNumber()
|
||||
fi
|
||||
}
|
||||
|
||||
EnableBsdSupport()
|
||||
EnableExtraPlatformsInSDK()
|
||||
{
|
||||
#todo enable sdk with
|
||||
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
}
|
||||
|
||||
EnableExtraPlatforms()
|
||||
{
|
||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -129,7 +136,7 @@ PackageLinux()
|
||||
|
||||
echo "Adding Readarr.Mono to UpdatePackage"
|
||||
cp $folder/Readarr.Mono.* $folder/Readarr.Update
|
||||
if [ "$framework" = "net5.0" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
|
||||
fi
|
||||
@@ -140,12 +147,13 @@ PackageLinux()
|
||||
PackageMacOS()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating MacOS Package for $framework"
|
||||
ProgressStart "Creating MacOS Package for $framework $runtime"
|
||||
|
||||
local folder=$artifactsFolder/macos/$framework/Readarr
|
||||
local folder=$artifactsFolder/$runtime/$framework/Readarr
|
||||
|
||||
PackageFiles "$folder" "$framework" "osx-x64"
|
||||
PackageFiles "$folder" "$framework" "$runtime"
|
||||
|
||||
echo "Removing Service helpers"
|
||||
rm -f $folder/ServiceUninstall.*
|
||||
@@ -156,7 +164,7 @@ PackageMacOS()
|
||||
|
||||
echo "Adding Readarr.Mono to UpdatePackage"
|
||||
cp $folder/Readarr.Mono.* $folder/Readarr.Update
|
||||
if [ "$framework" = "net5.0" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
|
||||
fi
|
||||
@@ -167,10 +175,11 @@ PackageMacOS()
|
||||
PackageMacOSApp()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating macOS App Package for $framework"
|
||||
ProgressStart "Creating macOS App Package for $framework $runtime"
|
||||
|
||||
local folder=$artifactsFolder/macos-app/$framework
|
||||
local folder="$artifactsFolder/$runtime-app/$framework"
|
||||
|
||||
rm -rf $folder
|
||||
mkdir -p $folder
|
||||
@@ -178,7 +187,7 @@ PackageMacOSApp()
|
||||
mkdir -p $folder/Readarr.app/Contents/MacOS
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $artifactsFolder/macos/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
|
||||
cp -r $artifactsFolder/$runtime/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $folder/Readarr.app/Contents/MacOS/Readarr.Update
|
||||
@@ -225,12 +234,38 @@ Package()
|
||||
PackageWindows "$framework" "$runtime"
|
||||
;;
|
||||
osx)
|
||||
PackageMacOS "$framework"
|
||||
PackageMacOSApp "$framework"
|
||||
PackageMacOS "$framework" "$runtime"
|
||||
PackageMacOSApp "$framework" "$runtime"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
BuildInstaller()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
./_inno/ISCC.exe setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
|
||||
}
|
||||
|
||||
InstallInno()
|
||||
{
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
ProgressEnd "Installed portable Inno Setup"
|
||||
}
|
||||
|
||||
RemoveInno()
|
||||
{
|
||||
rm -rf _inno
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
{
|
||||
local framework="$1"
|
||||
@@ -262,8 +297,10 @@ if [ $# -eq 0 ]; then
|
||||
BACKEND=YES
|
||||
FRONTEND=YES
|
||||
PACKAGES=YES
|
||||
INSTALLER=NO
|
||||
LINT=YES
|
||||
ENABLE_BSD=NO
|
||||
ENABLE_EXTRA_PLATFORMS=NO
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
@@ -275,8 +312,12 @@ case $key in
|
||||
BACKEND=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-bsd)
|
||||
ENABLE_BSD=YES
|
||||
--enable-bsd|--enable-extra-platforms)
|
||||
ENABLE_EXTRA_PLATFORMS=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-extra-platforms-in-sdk)
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
|
||||
shift # past argument
|
||||
;;
|
||||
-r|--runtime)
|
||||
@@ -297,6 +338,10 @@ case $key in
|
||||
PACKAGES=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--installer)
|
||||
INSTALLER=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--lint)
|
||||
LINT=YES
|
||||
shift # past argument
|
||||
@@ -316,24 +361,30 @@ esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
|
||||
then
|
||||
EnableExtraPlatformsInSDK
|
||||
fi
|
||||
|
||||
if [ "$BACKEND" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
EnableBsdSupport
|
||||
EnableExtraPlatforms
|
||||
fi
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
PackageTests "net5.0" "win-x64"
|
||||
PackageTests "net5.0" "win-x86"
|
||||
PackageTests "net5.0" "linux-x64"
|
||||
PackageTests "net5.0" "linux-musl-x64"
|
||||
PackageTests "net5.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
PackageTests "net6.0" "win-x64"
|
||||
PackageTests "net6.0" "win-x86"
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
PackageTests "net5.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
@@ -362,19 +413,30 @@ then
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
Package "net5.0" "win-x64"
|
||||
Package "net5.0" "win-x86"
|
||||
Package "net5.0" "linux-x64"
|
||||
Package "net5.0" "linux-musl-x64"
|
||||
Package "net5.0" "linux-arm64"
|
||||
Package "net5.0" "linux-musl-arm64"
|
||||
Package "net5.0" "linux-arm"
|
||||
Package "net5.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
Package "net6.0" "win-x64"
|
||||
Package "net6.0" "win-x86"
|
||||
Package "net6.0" "linux-x64"
|
||||
Package "net6.0" "linux-musl-x64"
|
||||
Package "net6.0" "linux-arm64"
|
||||
Package "net6.0" "linux-musl-arm64"
|
||||
Package "net6.0" "linux-arm"
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
Package "net5.0" "freebsd-x64"
|
||||
Package "net6.0" "freebsd-x64"
|
||||
Package "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$INSTALLER" = "YES" ];
|
||||
then
|
||||
InstallInno
|
||||
BuildInstaller "net6.0" "win-x64"
|
||||
BuildInstaller "net6.0" "win-x86"
|
||||
RemoveInno
|
||||
fi
|
||||
|
||||
5
debian/changelog
vendored
5
debian/changelog
vendored
@@ -1,5 +0,0 @@
|
||||
nzbdrone {version} {branch}; urgency=low
|
||||
|
||||
* Automatic Release.
|
||||
|
||||
-- NzbDrone <contact@nzbdrone.com> Mon, 26 Aug 2013 00:00:00 -0700
|
||||
1
debian/compat
vendored
1
debian/compat
vendored
@@ -1 +0,0 @@
|
||||
8
|
||||
12
debian/control
vendored
12
debian/control
vendored
@@ -1,12 +0,0 @@
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Sonarr <contact@nzbdrone.com>
|
||||
Source: nzbdrone
|
||||
Homepage: https://readarr.com
|
||||
Vcs-Git: git@github.com:readarr/Readarr.git
|
||||
Vcs-Browser: https://github.com/readarr/Readarr
|
||||
|
||||
Package: nzbdrone
|
||||
Architecture: all
|
||||
Depends: libmono-cil-dev (>= 3.2), sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
|
||||
Description: Readarr is a music collection manager
|
||||
24
debian/copyright
vendored
24
debian/copyright
vendored
@@ -1,24 +0,0 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: nzbdrone
|
||||
Source: https://github.com/readarr/Readarr
|
||||
|
||||
Files: *
|
||||
Copyright: 2010-2016 Readarr <hello@readarr.com>
|
||||
|
||||
License: GPL-3.0+
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
|
||||
1
debian/install
vendored
1
debian/install
vendored
@@ -1 +0,0 @@
|
||||
nzbdrone_bin/* opt/NzbDrone
|
||||
13
debian/rules
vendored
13
debian/rules
vendored
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
# Sample debian/rules that uses debhelper.
|
||||
# This file was originally written by Joey Hess and Craig Small.
|
||||
# As a special exception, when this file is copied by dh-make into a
|
||||
# dh-make output file, you may use that output file without restriction.
|
||||
# This special exception was added by Craig Small in version 0.37 of dh-make.
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
%:
|
||||
dh $@
|
||||
@@ -39,6 +39,7 @@ module.exports = {
|
||||
plugins: [
|
||||
'filenames',
|
||||
'react',
|
||||
'react-hooks',
|
||||
'simple-import-sort',
|
||||
'import'
|
||||
],
|
||||
@@ -308,7 +309,9 @@ module.exports = {
|
||||
'react/react-in-jsx-scope': 2,
|
||||
'react/self-closing-comp': 2,
|
||||
'react/sort-comp': 2,
|
||||
'react/jsx-wrap-multilines': 2
|
||||
'react/jsx-wrap-multilines': 2,
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'error'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
@@ -44,7 +44,8 @@ module.exports = (env) => {
|
||||
'node_modules'
|
||||
],
|
||||
alias: {
|
||||
jquery: 'jquery/src/jquery'
|
||||
jquery: 'jquery/src/jquery',
|
||||
'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate'
|
||||
},
|
||||
fallback: {
|
||||
buffer: false,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const reload = require('require-nocache')(module);
|
||||
|
||||
const cssVarsFiles = [
|
||||
'./src/Styles/Variables/colors',
|
||||
'./src/Styles/Variables/dimensions',
|
||||
'./src/Styles/Variables/fonts',
|
||||
'./src/Styles/Variables/animations',
|
||||
|
||||
@@ -19,9 +19,9 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import BlacklistRowConnector from './BlacklistRowConnector';
|
||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
||||
|
||||
class Blacklist extends Component {
|
||||
class Blocklist extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -61,33 +61,33 @@ class Blacklist extends Component {
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = () => {
|
||||
this.props.onRemoveSelected(this.getSelectedIds());
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -103,8 +103,8 @@ class Blacklist extends Component {
|
||||
columns,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlacklistExecuting,
|
||||
onClearBlacklistPress,
|
||||
isClearingBlocklistExecuting,
|
||||
onClearBlocklistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -121,7 +121,7 @@ class Blacklist extends Component {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Blacklist')}>
|
||||
<PageContent title={translate('Blocklist')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
@@ -135,8 +135,8 @@ class Blacklist extends Component {
|
||||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
isSpinning={isClearingBlacklistExecuting}
|
||||
onPress={onClearBlacklistPress}
|
||||
isSpinning={isClearingBlocklistExecuting}
|
||||
onPress={onClearBlocklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
@@ -162,14 +162,14 @@ class Blacklist extends Component {
|
||||
{
|
||||
!isAnyFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToLoadBlacklist')}
|
||||
{translate('UnableToLoadBlocklist')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !error && !items.length &&
|
||||
<div>
|
||||
No history blacklist
|
||||
{translate('NoHistoryBlocklist')}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ class Blacklist extends Component {
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlacklistRowConnector
|
||||
<BlocklistRowConnector
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
@@ -224,7 +224,7 @@ class Blacklist extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
Blacklist.propTypes = {
|
||||
Blocklist.propTypes = {
|
||||
isAuthorFetching: PropTypes.bool.isRequired,
|
||||
isAuthorPopulated: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
@@ -234,9 +234,9 @@ Blacklist.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
onRemoveSelected: PropTypes.func.isRequired,
|
||||
onClearBlacklistPress: PropTypes.func.isRequired
|
||||
onClearBlocklistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Blacklist;
|
||||
export default Blocklist;
|
||||
@@ -4,34 +4,34 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Blacklist from './Blacklist';
|
||||
import Blocklist from './Blocklist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blacklist,
|
||||
(state) => state.blocklist,
|
||||
(state) => state.authors,
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
|
||||
(blacklist, authors, isClearingBlacklistExecuting) => {
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||
(blocklist, authors, isClearingBlocklistExecuting) => {
|
||||
return {
|
||||
isAuthorFetching: authors.isFetching,
|
||||
isAuthorPopulated: authors.isPopulated,
|
||||
isClearingBlacklistExecuting,
|
||||
...blacklist
|
||||
isClearingBlocklistExecuting,
|
||||
...blocklist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...blacklistActions,
|
||||
...blocklistActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class BlacklistConnector extends Component {
|
||||
class BlocklistConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -39,27 +39,27 @@ class BlacklistConnector extends Component {
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchBlacklist,
|
||||
gotoBlacklistFirstPage
|
||||
fetchBlocklist,
|
||||
gotoBlocklistFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchBlacklist();
|
||||
fetchBlocklist();
|
||||
} else {
|
||||
gotoBlacklistFirstPage();
|
||||
gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearBlacklist();
|
||||
this.props.clearBlocklist();
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
}
|
||||
|
||||
@@ -67,57 +67,57 @@ class BlacklistConnector extends Component {
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchBlacklist();
|
||||
}
|
||||
this.props.fetchBlocklist();
|
||||
};
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoBlacklistPreviousPage();
|
||||
}
|
||||
this.props.gotoBlocklistPreviousPage();
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoBlacklistNextPage();
|
||||
}
|
||||
this.props.gotoBlocklistNextPage();
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoBlacklistLastPage();
|
||||
}
|
||||
this.props.gotoBlocklistLastPage();
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoBlacklistPage({ page });
|
||||
}
|
||||
this.props.gotoBlocklistPage({ page });
|
||||
};
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlacklistItems({ ids });
|
||||
}
|
||||
this.props.removeBlocklistItems({ ids });
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlacklistSort({ sortKey });
|
||||
}
|
||||
this.props.setBlocklistSort({ sortKey });
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlacklistTableOption(payload);
|
||||
this.props.setBlocklistTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClearBlacklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
|
||||
}
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Blacklist
|
||||
<Blocklist
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
@@ -126,30 +126,30 @@ class BlacklistConnector extends Component {
|
||||
onRemoveSelected={this.onRemoveSelected}
|
||||
onSortPress={this.onSortPress}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlacklistPress={this.onClearBlacklistPress}
|
||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlacklistConnector.propTypes = {
|
||||
BlocklistConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchBlacklist: PropTypes.func.isRequired,
|
||||
gotoBlacklistFirstPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||
removeBlacklistItems: PropTypes.func.isRequired,
|
||||
setBlacklistSort: PropTypes.func.isRequired,
|
||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlacklist: PropTypes.func.isRequired,
|
||||
fetchBlocklist: PropTypes.func.isRequired,
|
||||
gotoBlocklistFirstPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
||||
removeBlocklistItems: PropTypes.func.isRequired,
|
||||
setBlocklistSort: PropTypes.func.isRequired,
|
||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlocklist: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
|
||||
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
|
||||
);
|
||||
@@ -10,7 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class BlacklistDetailsModal extends Component {
|
||||
class BlocklistDetailsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -78,7 +78,7 @@ class BlacklistDetailsModal extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
BlacklistDetailsModal.propTypes = {
|
||||
BlocklistDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
@@ -87,4 +87,4 @@ BlacklistDetailsModal.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlacklistDetailsModal;
|
||||
export default BlocklistDetailsModal;
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
import BookFormats from 'Book/BookFormats';
|
||||
import BookQuality from 'Book/BookQuality';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
@@ -9,10 +10,10 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||
import styles from './BlacklistRow.css';
|
||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||
import styles from './BlocklistRow.css';
|
||||
|
||||
class BlacklistRow extends Component {
|
||||
class BlocklistRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -30,11 +31,11 @@ class BlacklistRow extends Component {
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -45,6 +46,7 @@ class BlacklistRow extends Component {
|
||||
author,
|
||||
sourceTitle,
|
||||
quality,
|
||||
customFormats,
|
||||
date,
|
||||
protocol,
|
||||
indexer,
|
||||
@@ -110,6 +112,16 @@ class BlacklistRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<BookFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
@@ -142,7 +154,7 @@ class BlacklistRow extends Component {
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlacklist')}
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRemovePress}
|
||||
@@ -155,7 +167,7 @@ class BlacklistRow extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
<BlacklistDetailsModal
|
||||
<BlocklistDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
@@ -169,11 +181,12 @@ class BlacklistRow extends Component {
|
||||
|
||||
}
|
||||
|
||||
BlacklistRow.propTypes = {
|
||||
BlocklistRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
author: PropTypes.object.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
@@ -184,4 +197,4 @@ BlacklistRow.propTypes = {
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlacklistRow;
|
||||
export default BlocklistRow;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
|
||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import BlacklistRow from './BlacklistRow';
|
||||
import BlocklistRow from './BlocklistRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
@@ -18,9 +18,9 @@ function createMapStateToProps() {
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemovePress() {
|
||||
dispatch(removeBlacklistItem({ id: props.id }));
|
||||
dispatch(removeBlocklistItem({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
|
||||
@@ -9,6 +9,7 @@ import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryDetails.css';
|
||||
|
||||
@@ -65,8 +66,10 @@ function HistoryDetails(props) {
|
||||
const {
|
||||
indexer,
|
||||
releaseGroup,
|
||||
customFormatScore,
|
||||
nzbInfoUrl,
|
||||
downloadClient,
|
||||
downloadClientName,
|
||||
downloadId,
|
||||
age,
|
||||
ageHours,
|
||||
@@ -74,6 +77,8 @@ function HistoryDetails(props) {
|
||||
publishedDate
|
||||
} = data;
|
||||
|
||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
@@ -100,7 +105,16 @@ function HistoryDetails(props) {
|
||||
}
|
||||
|
||||
{
|
||||
!!nzbInfoUrl &&
|
||||
customFormatScore && customFormatScore !== '0' ?
|
||||
<DescriptionListItem
|
||||
title="Custom Format Score"
|
||||
data={formatPreferredWordScore(customFormatScore)}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
nzbInfoUrl ?
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
Info URL
|
||||
@@ -109,15 +123,17 @@ function HistoryDetails(props) {
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!downloadClient &&
|
||||
downloadClientNameInfo ?
|
||||
<DescriptionListItem
|
||||
title={translate('DownloadClient')}
|
||||
data={downloadClient}
|
||||
/>
|
||||
data={downloadClientNameInfo}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -173,6 +189,7 @@ function HistoryDetails(props) {
|
||||
|
||||
if (eventType === 'bookFileImported') {
|
||||
const {
|
||||
customFormatScore,
|
||||
droppedPath,
|
||||
importedPath
|
||||
} = data;
|
||||
@@ -195,12 +212,22 @@ function HistoryDetails(props) {
|
||||
}
|
||||
|
||||
{
|
||||
!!importedPath &&
|
||||
importedPath ?
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('ImportedTo')}
|
||||
data={importedPath}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
customFormatScore && customFormatScore !== '0' ?
|
||||
<DescriptionListItem
|
||||
title="Custom Format Score"
|
||||
data={formatPreferredWordScore(customFormatScore)}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
@@ -208,20 +235,21 @@ function HistoryDetails(props) {
|
||||
|
||||
if (eventType === 'bookFileDeleted') {
|
||||
const {
|
||||
reason
|
||||
reason,
|
||||
customFormatScore
|
||||
} = data;
|
||||
|
||||
let reasonMessage = '';
|
||||
|
||||
switch (reason) {
|
||||
case 'Manual':
|
||||
reasonMessage = 'File was deleted by via UI';
|
||||
reasonMessage = translate('FileWasDeletedByViaUI');
|
||||
break;
|
||||
case 'MissingFromDisk':
|
||||
reasonMessage = 'Readarr was unable to find the file on disk so it was removed';
|
||||
reasonMessage = translate('MissingFromDisk');
|
||||
break;
|
||||
case 'Upgrade':
|
||||
reasonMessage = 'File was deleted to import an upgrade';
|
||||
reasonMessage = translate('FileWasDeletedByUpgrade');
|
||||
break;
|
||||
default:
|
||||
reasonMessage = '';
|
||||
@@ -238,6 +266,15 @@ function HistoryDetails(props) {
|
||||
title={translate('Reason')}
|
||||
data={reasonMessage}
|
||||
/>
|
||||
|
||||
{
|
||||
customFormatScore && customFormatScore !== '0' ?
|
||||
<DescriptionListItem
|
||||
title="Custom Format Score"
|
||||
data={formatPreferredWordScore(customFormatScore)}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,38 +60,38 @@ class HistoryConnector extends Component {
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchHistory();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoHistoryPreviousPage();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoHistoryNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoHistoryLastPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoHistoryPage({ page });
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setHistorySort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setHistoryFilter({ selectedFilterKey });
|
||||
}
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setHistoryTableOption(payload);
|
||||
@@ -99,7 +99,7 @@ class HistoryConnector extends Component {
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -23,8 +23,6 @@ function getIconName(eventType) {
|
||||
return icons.RETAG;
|
||||
case 'bookImportIncomplete':
|
||||
return icons.DOWNLOADED;
|
||||
case 'downloadImported':
|
||||
return icons.DOWNLOADED;
|
||||
case 'downloadIgnored':
|
||||
return icons.IGNORE;
|
||||
default:
|
||||
@@ -61,8 +59,6 @@ function getTooltip(eventType, data) {
|
||||
return 'Book file tags updated';
|
||||
case 'bookImportIncomplete':
|
||||
return 'Files downloaded but not all could be imported';
|
||||
case 'downloadImported':
|
||||
return 'Download completed and successfully imported';
|
||||
case 'downloadIgnored':
|
||||
return 'Book Download Ignored';
|
||||
default:
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.customFormatScore {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.releaseGroup {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
import BookFormats from 'Book/BookFormats';
|
||||
import BookQuality from 'Book/BookQuality';
|
||||
import BookTitleLink from 'Book/BookTitleLink';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
@@ -8,6 +9,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import styles from './HistoryRow.css';
|
||||
@@ -40,11 +42,11 @@ class HistoryRow extends Component {
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -54,6 +56,7 @@ class HistoryRow extends Component {
|
||||
author,
|
||||
book,
|
||||
quality,
|
||||
customFormats,
|
||||
qualityCutoffNotMet,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
@@ -127,6 +130,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<BookFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
@@ -158,6 +171,17 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormatScore') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.customFormatScore}
|
||||
>
|
||||
{formatPreferredWordScore(data.customFormatScore)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'releaseGroup') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -169,6 +193,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -209,6 +243,7 @@ HistoryRow.propTypes = {
|
||||
author: PropTypes.object.isRequired,
|
||||
book: PropTypes.object,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
|
||||
@@ -49,7 +49,7 @@ class HistoryRowConnector extends Component {
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.props.markAsFailed({ id: this.props.id });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
.torrent {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
border-color: $torrentColor;
|
||||
background-color: $torrentColor;
|
||||
border-color: var(--torrentColor);
|
||||
background-color: var(--torrentColor);
|
||||
}
|
||||
|
||||
.usenet {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
border-color: $usenetColor;
|
||||
background-color: $usenetColor;
|
||||
border-color: var(--usenetColor);
|
||||
background-color: var(--usenetColor);
|
||||
}
|
||||
|
||||
@@ -75,13 +75,23 @@ class Queue extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextState = {};
|
||||
|
||||
if (prevProps.items !== items) {
|
||||
nextState.items = items;
|
||||
}
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
});
|
||||
|
||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||
this.setState({ isPendingSelected });
|
||||
nextState.isPendingSelected = isPendingSelected;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(nextState)) {
|
||||
this.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,45 +100,45 @@ class Queue extends Component {
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
||||
this._shouldBlockRefresh = isOpen;
|
||||
}
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onGrabSelectedPress = () => {
|
||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
||||
this._shouldBlockRefresh = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -216,26 +226,29 @@ class Queue extends Component {
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isRefreshing && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
isRefreshing && !isAllPopulated ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isRefreshing && hasError &&
|
||||
!isRefreshing && hasError ?
|
||||
<div>
|
||||
Failed to load Queue
|
||||
</div>
|
||||
{translate('FailedToLoadQueue')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length &&
|
||||
isAllPopulated && !hasError && !items.length ?
|
||||
<div>
|
||||
Queue is empty
|
||||
</div>
|
||||
{translate('QueueIsEmpty')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !!items.length &&
|
||||
isAllPopulated && !hasError && !!items.length ?
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
@@ -270,7 +283,8 @@ class Queue extends Component {
|
||||
isFetching={isRefreshing}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
@@ -284,6 +298,17 @@ class Queue extends Component {
|
||||
return !!(item && item.authorId && item.bookId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
@@ -79,34 +79,34 @@ class QueueConnector extends Component {
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchQueue();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoQueuePreviousPage();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoQueueNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoQueueLastPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoQueuePage({ page });
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
@@ -114,21 +114,21 @@ class QueueConnector extends Component {
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onGrabSelectedPress = (ids) => {
|
||||
this.props.grabQueueItems({ ids });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -42,7 +42,7 @@ class QueueOptions extends Component {
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
import BookFormats from 'Book/BookFormats';
|
||||
import BookQuality from 'Book/BookQuality';
|
||||
import BookTitleLink from 'Book/BookTitleLink';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -41,37 +42,37 @@ class QueueRow extends Component {
|
||||
|
||||
onRemoveQueueItemPress = () => {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blacklist, skipredownload) => {
|
||||
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => {
|
||||
const {
|
||||
onRemoveQueueItemPress,
|
||||
onQueueRowModalOpenOrClose
|
||||
} = this.props;
|
||||
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
onRemoveQueueItemPress(blacklist, skipredownload);
|
||||
onRemoveQueueItemPress(blocklist, skipredownload);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(true);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -89,6 +90,7 @@ class QueueRow extends Component {
|
||||
author,
|
||||
book,
|
||||
quality,
|
||||
customFormats,
|
||||
protocol,
|
||||
indexer,
|
||||
outputPath,
|
||||
@@ -210,6 +212,16 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<BookFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'protocol') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
@@ -357,6 +369,7 @@ class QueueRow extends Component {
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!author}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
@@ -378,6 +391,7 @@ QueueRow.propTypes = {
|
||||
author: PropTypes.object,
|
||||
book: PropTypes.object,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
|
||||
@@ -41,11 +41,11 @@ class QueueRowConnector extends Component {
|
||||
|
||||
onGrabPress = () => {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -22,7 +22,7 @@ class RemoveQueueItemModal extends Component {
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false,
|
||||
blocklist: false,
|
||||
skipredownload: false
|
||||
};
|
||||
}
|
||||
@@ -33,37 +33,37 @@ class RemoveQueueItemModal extends Component {
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false,
|
||||
blocklist: false,
|
||||
skipredownload: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
};
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: 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
|
||||
@@ -72,10 +72,11 @@ class RemoveQueueItemModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist, skipredownload } = this.state;
|
||||
const { remove, blocklist, skipredownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -95,37 +96,41 @@ class RemoveQueueItemModal extends Component {
|
||||
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('RemoveFromDownloadClient')}
|
||||
{translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('BlacklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText={translate('BlacklistHelpText')}
|
||||
onChange={this.onBlacklistChange}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blacklist &&
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
@@ -164,6 +169,7 @@ 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
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false,
|
||||
blocklist: false,
|
||||
skipredownload: false
|
||||
};
|
||||
}
|
||||
@@ -31,40 +31,40 @@ class RemoveQueueItemsModal extends Component {
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false,
|
||||
skipredownload: false
|
||||
});
|
||||
}
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipredownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: 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
|
||||
@@ -73,10 +73,11 @@ class RemoveQueueItemsModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist, skipredownload } = this.state;
|
||||
const { remove, blocklist, skipredownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -96,37 +97,41 @@ class RemoveQueueItemsModal extends Component {
|
||||
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
|
||||
</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>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Blacklist Release{selectedCount > 1 ? 's' : ''}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText={translate('BlacklistHelpText')}
|
||||
onChange={this.onBlacklistChange}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blacklist &&
|
||||
blocklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
@@ -165,6 +170,7 @@ 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
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function AuthorMonitorNewItemsOptionsPopoverContent() {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllBooks')}
|
||||
data={translate('DataNewAllBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('NewBooks')}
|
||||
data={translate('DataNewBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data={translate('DataNewNone')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthorMonitorNewItemsOptionsPopoverContent;
|
||||
@@ -1,46 +1,52 @@
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function AuthorMonitoringOptionsPopoverContent() {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllBooks')}
|
||||
data="Monitor all books"
|
||||
/>
|
||||
<>
|
||||
<Alert>
|
||||
{translate('MonitoringOptionsHelpText')}
|
||||
</Alert>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllBooks')}
|
||||
data={translate('DataAllBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('FutureBooks')}
|
||||
data="Monitor books that have not released yet"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('FutureBooks')}
|
||||
data={translate('DataFutureBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('MissingBooks')}
|
||||
data="Monitor books that do not have files or have not released yet"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('MissingBooks')}
|
||||
data={translate('DataMissingBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('ExistingBooks')}
|
||||
data="Monitor books that have files or have not released yet"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('ExistingBooks')}
|
||||
data={translate('DataExistingBooks')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('FirstBook')}
|
||||
data="Monitor the first book. All other books will be ignored"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('FirstBook')}
|
||||
data={translate('DataFirstBook')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('LatestBook')}
|
||||
data="Monitor the latest book and future books"
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('LatestBook')}
|
||||
data={translate('DataLatestBook')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data="No books will be monitored"
|
||||
/>
|
||||
</DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data={translate('DataNone')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,19 @@ import React from 'react';
|
||||
import DocumentTitle from 'react-document-title';
|
||||
import { Provider } from 'react-redux';
|
||||
import PageConnector from 'Components/Page/PageConnector';
|
||||
import ApplyTheme from './ApplyTheme';
|
||||
import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title="Readarr">
|
||||
<DocumentTitle title={window.Readarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
|
||||
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||
import AuthorDetailsPageConnector from 'Author/Details/AuthorDetailsPageConnector';
|
||||
import AuthorEditorConnector from 'Author/Editor/AuthorEditorConnector';
|
||||
import AuthorIndexConnector from 'Author/Index/AuthorIndexConnector';
|
||||
import BookDetailsPageConnector from 'Book/Details/BookDetailsPageConnector';
|
||||
import BookIndexConnector from 'Book/Index/BookIndexConnector';
|
||||
@@ -14,6 +13,7 @@ import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import AddNewItemConnector from 'Search/AddNewItemConnector';
|
||||
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
|
||||
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
@@ -23,7 +23,7 @@ import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementCo
|
||||
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||
import Profiles from 'Settings/Profiles/Profiles';
|
||||
import Quality from 'Settings/Quality/Quality';
|
||||
import QualityConnector from 'Settings/Quality/QualityConnector';
|
||||
import Settings from 'Settings/Settings';
|
||||
import TagSettings from 'Settings/Tags/TagSettings';
|
||||
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
||||
@@ -82,11 +82,6 @@ function AppRoutes(props) {
|
||||
component={AddNewItemConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/authoreditor"
|
||||
component={AuthorEditorConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact={true}
|
||||
path="/shelf"
|
||||
@@ -138,8 +133,8 @@ function AppRoutes(props) {
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/activity/blacklist"
|
||||
component={BlacklistConnector}
|
||||
path="/activity/blocklist"
|
||||
component={BlocklistConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
@@ -178,7 +173,12 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/settings/quality"
|
||||
component={Quality}
|
||||
component={QualityConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/customformats"
|
||||
component={CustomFormatSettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -11,9 +11,47 @@ import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AppUpdatedModalContent.css';
|
||||
|
||||
function mergeUpdates(items, version, prevVersion) {
|
||||
let installedIndex = items.findIndex((u) => u.version === version);
|
||||
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
|
||||
|
||||
if (installedIndex === -1) {
|
||||
installedIndex = 0;
|
||||
}
|
||||
|
||||
if (installedPreviouslyIndex === -1) {
|
||||
installedPreviouslyIndex = items.length;
|
||||
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
||||
installedPreviouslyIndex += 1;
|
||||
}
|
||||
|
||||
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
||||
|
||||
if (!appliedUpdates.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const appliedChanges = { new: [], fixed: [] };
|
||||
appliedUpdates.forEach((u) => {
|
||||
if (u.changes) {
|
||||
appliedChanges.new.push(... u.changes.new);
|
||||
appliedChanges.fixed.push(... u.changes.fixed);
|
||||
}
|
||||
});
|
||||
|
||||
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
|
||||
|
||||
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
||||
mergedUpdate.changes = null;
|
||||
}
|
||||
|
||||
return mergedUpdate;
|
||||
}
|
||||
|
||||
function AppUpdatedModalContent(props) {
|
||||
const {
|
||||
version,
|
||||
prevVersion,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
@@ -21,7 +59,7 @@ function AppUpdatedModalContent(props) {
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
const update = items[0];
|
||||
const update = mergeUpdates(items, version, prevVersion);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -39,7 +77,9 @@ function AppUpdatedModalContent(props) {
|
||||
<div>
|
||||
{
|
||||
!update.changes &&
|
||||
<div className={styles.maintenance}>Maintenance release</div>
|
||||
<div className={styles.maintenance}>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -89,6 +129,7 @@ function AppUpdatedModalContent(props) {
|
||||
|
||||
AppUpdatedModalContent.propTypes = {
|
||||
version: PropTypes.string.isRequired,
|
||||
prevVersion: PropTypes.string,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -8,8 +8,9 @@ import AppUpdatedModalContent from './AppUpdatedModalContent';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app.version,
|
||||
(state) => state.app.prevVersion,
|
||||
(state) => state.system.updates,
|
||||
(version, updates) => {
|
||||
(version, prevVersion, updates) => {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
@@ -18,6 +19,7 @@ function createMapStateToProps() {
|
||||
|
||||
return {
|
||||
version,
|
||||
prevVersion,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
|
||||
49
frontend/src/App/ApplyTheme.js
Normal file
49
frontend/src/App/ApplyTheme.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui.item.theme || window.Readarr.theme,
|
||||
(
|
||||
theme
|
||||
) => {
|
||||
return {
|
||||
theme
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ theme, children }) {
|
||||
// Update the CSS Variables
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||
|
||||
// Loop through each array key and set the CSS Variables
|
||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||
// Based on our snippet from MDN
|
||||
document.documentElement.style.setProperty(
|
||||
`--${cssVariableKey}`,
|
||||
arrayOfVariableValues[index]
|
||||
);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables(theme);
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
ApplyTheme.propTypes = {
|
||||
theme: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(ApplyTheme);
|
||||
@@ -99,7 +99,7 @@ class AuthorImage extends Component {
|
||||
if (this.props.onError) {
|
||||
this.props.onError();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onLoad = () => {
|
||||
this.setState({
|
||||
@@ -110,7 +110,7 @@ class AuthorImage extends Component {
|
||||
if (this.props.onLoad) {
|
||||
this.props.onLoad();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
|
||||
.deleteFilesMessage {
|
||||
margin-top: 20px;
|
||||
color: $dangerColor;
|
||||
color: var(--dangerColor);
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@ class DeleteAuthorModalContent extends Component {
|
||||
|
||||
onDeleteFilesChange = ({ value }) => {
|
||||
this.setState({ deleteFiles: value });
|
||||
}
|
||||
};
|
||||
|
||||
onAddImportListExclusionChange = ({ value }) => {
|
||||
this.setState({ addImportListExclusion: value });
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorConfirmed = () => {
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
@@ -46,7 +46,7 @@ class DeleteAuthorModalContent extends Component {
|
||||
this.setState({ deleteFiles: false });
|
||||
this.setState({ addImportListExclusion: false });
|
||||
this.props.onDeletePress(deleteFiles, addImportListExclusion);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -32,7 +32,7 @@ class DeleteAuthorModalContentConnector extends Component {
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.metadataMessage {
|
||||
color: $helpTextColor;
|
||||
color: var(--helpTextColor);
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
@@ -22,7 +22,7 @@
|
||||
.tabList {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: 1px solid $lightGray;
|
||||
border-bottom: 1px solid var(--lightGray);
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
.selectedTab {
|
||||
border-bottom: 4px solid $linkColor;
|
||||
border-bottom: 4px solid var(--linkColor);
|
||||
}
|
||||
|
||||
.tabContent {
|
||||
@@ -63,7 +63,7 @@
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
color: var(--iconButtonHoverLightColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
|
||||
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
@@ -22,6 +23,7 @@ import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||
@@ -53,66 +55,113 @@ class AuthorDetails extends Component {
|
||||
isDeleteAuthorModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isMonitorOptionsModalOpen: false,
|
||||
isEditorActive: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {},
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
selectedTabIndex: 0
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setSelectedState = (items) => {
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((item) => {
|
||||
const isItemSelected = selectedState[item.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[item.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[item.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
};
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOrganizePress = () => {
|
||||
this.setState({ isOrganizeModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onOrganizeModalClose = () => {
|
||||
this.setState({ isOrganizeModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onRetagPress = () => {
|
||||
this.setState({ isRetagModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onRetagModalClose = () => {
|
||||
this.setState({ isRetagModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onEditAuthorPress = () => {
|
||||
this.setState({ isEditAuthorModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onEditAuthorModalClose = () => {
|
||||
this.setState({ isEditAuthorModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorPress = () => {
|
||||
this.setState({
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorModalClose = () => {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onMonitorOptionsPress = () => {
|
||||
this.setState({ isMonitorOptionsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onMonitorOptionsClose = () => {
|
||||
this.setState({ isMonitorOptionsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onBookEditorTogglePress = () => {
|
||||
this.setState({ isEditorActive: !this.state.isEditorActive });
|
||||
};
|
||||
|
||||
onExpandAllPress = () => {
|
||||
const {
|
||||
@@ -121,7 +170,7 @@ class AuthorDetails extends Component {
|
||||
} = this.state;
|
||||
|
||||
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
|
||||
}
|
||||
};
|
||||
|
||||
onExpandPress = (bookId, isExpanded) => {
|
||||
this.setState((state) => {
|
||||
@@ -135,11 +184,32 @@ class AuthorDetails extends Component {
|
||||
|
||||
return getExpandedState(newState);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectAllPress = () => {
|
||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||
};
|
||||
|
||||
onSelectedChange = (items, id, value, shiftKey = false) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onSaveSelected = (changes) => {
|
||||
this.props.onSaveSelected({
|
||||
bookIds: this.getSelectedIds(),
|
||||
...changes
|
||||
});
|
||||
};
|
||||
|
||||
onTabSelect = (index, lastIndex) => {
|
||||
this.setState({ selectedTabIndex: index });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -165,6 +235,10 @@ class AuthorDetails extends Component {
|
||||
nextAuthor,
|
||||
onRefreshPress,
|
||||
onSearchPress,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
statistics
|
||||
} = this.props;
|
||||
|
||||
@@ -175,6 +249,9 @@ class AuthorDetails extends Component {
|
||||
isDeleteAuthorModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isMonitorOptionsModalOpen,
|
||||
isEditorActive,
|
||||
allSelected,
|
||||
selectedState,
|
||||
allExpanded,
|
||||
allCollapsed,
|
||||
expandedState,
|
||||
@@ -189,12 +266,14 @@ class AuthorDetails extends Component {
|
||||
expandIcon = icons.EXPAND;
|
||||
}
|
||||
|
||||
const selectedBookIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={authorName}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RefreshScan')}
|
||||
label={translate('RefreshAndScan')}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
title={translate('RefreshInformationAndScanDisk')}
|
||||
@@ -252,6 +331,33 @@ class AuthorDetails extends Component {
|
||||
iconName={icons.DELETE}
|
||||
onPress={this.onDeleteAuthorPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={translate('BookList')}
|
||||
iconName={icons.AUTHOR_CONTINUING}
|
||||
onPress={this.onBookEditorTogglePress}
|
||||
/> :
|
||||
<PageToolbarButton
|
||||
label={translate('BookEditor')}
|
||||
iconName={icons.EDIT}
|
||||
onPress={this.onBookEditorTogglePress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={icons.CHECK_SQUARE}
|
||||
onPress={this.onSelectAllPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
@@ -377,7 +483,11 @@ class AuthorDetails extends Component {
|
||||
<AuthorDetailsSeasonConnector
|
||||
authorId={id}
|
||||
isExpanded={true}
|
||||
selectedState={selectedState}
|
||||
onExpandPress={this.onExpandPress}
|
||||
setSelectedState={this.setSelectedState}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
isEditorActive={isEditorActive}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -422,7 +532,6 @@ class AuthorDetails extends Component {
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.metadataMessage}>
|
||||
@@ -474,6 +583,19 @@ class AuthorDetails extends Component {
|
||||
onModalClose={this.onMonitorOptionsClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isEditorActive &&
|
||||
<BookEditorFooter
|
||||
bookIds={selectedBookIds}
|
||||
selectedCount={selectedBookIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
}
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -493,7 +615,6 @@ AuthorDetails.propTypes = {
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
@@ -510,13 +631,17 @@ AuthorDetails.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AuthorDetails.defaultProps = {
|
||||
statistics: {},
|
||||
tags: [],
|
||||
isSaving: false
|
||||
tags: []
|
||||
};
|
||||
|
||||
export default AuthorDetails;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { toggleAuthorMonitored } from 'Store/Actions/authorActions';
|
||||
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
|
||||
import { saveBookEditor } from 'Store/Actions/bookIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
@@ -21,7 +22,8 @@ import AuthorDetails from './AuthorDetails';
|
||||
|
||||
const selectBooks = createSelector(
|
||||
(state) => state.books,
|
||||
(books) => {
|
||||
(state) => state.bookIndex,
|
||||
(books, index) => {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
@@ -29,6 +31,13 @@ const selectBooks = createSelector(
|
||||
error
|
||||
} = books;
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = index;
|
||||
|
||||
const hasBooks = !!items.length;
|
||||
const hasMonitoredBooks = items.some((e) => e.monitored);
|
||||
|
||||
@@ -37,7 +46,11 @@ const selectBooks = createSelector(
|
||||
isBooksPopulated: isPopulated,
|
||||
booksError: error,
|
||||
hasBooks,
|
||||
hasMonitoredBooks
|
||||
hasMonitoredBooks,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -109,7 +122,11 @@ function createMapStateToProps() {
|
||||
isBooksPopulated,
|
||||
booksError,
|
||||
hasBooks,
|
||||
hasMonitoredBooks
|
||||
hasMonitoredBooks,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = books;
|
||||
|
||||
const {
|
||||
@@ -169,6 +186,10 @@ function createMapStateToProps() {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
booksError,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
seriesError,
|
||||
bookFilesError,
|
||||
hasBooks,
|
||||
@@ -187,6 +208,7 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
fetchSeries,
|
||||
clearSeries,
|
||||
saveBookEditor,
|
||||
fetchBookFiles,
|
||||
clearBookFiles,
|
||||
toggleAuthorMonitored,
|
||||
@@ -248,7 +270,7 @@ class AuthorDetailsConnector extends Component {
|
||||
this.props.fetchSeries({ authorId });
|
||||
this.props.fetchBookFiles({ authorId });
|
||||
this.props.fetchQueueDetails({ authorId });
|
||||
}
|
||||
};
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.cancelFetchReleases();
|
||||
@@ -256,7 +278,7 @@ class AuthorDetailsConnector extends Component {
|
||||
this.props.clearBookFiles();
|
||||
this.props.clearQueueDetails();
|
||||
this.props.clearReleases();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
@@ -266,21 +288,25 @@ class AuthorDetailsConnector extends Component {
|
||||
authorId: this.props.id,
|
||||
monitored
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.REFRESH_AUTHOR,
|
||||
authorId: this.props.id
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSearchPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.AUTHOR_SEARCH,
|
||||
authorId: this.props.id
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.saveBookEditor(payload);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -292,6 +318,7 @@ class AuthorDetailsConnector extends Component {
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onSearchPress={this.onSearchPress}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -307,6 +334,7 @@ AuthorDetailsConnector.propTypes = {
|
||||
isRenamingAuthor: PropTypes.bool.isRequired,
|
||||
fetchSeries: PropTypes.func.isRequired,
|
||||
clearSeries: PropTypes.func.isRequired,
|
||||
saveBookEditor: PropTypes.func.isRequired,
|
||||
fetchBookFiles: PropTypes.func.isRequired,
|
||||
clearBookFiles: PropTypes.func.isRequired,
|
||||
toggleAuthorMonitored: PropTypes.func.isRequired,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $black;
|
||||
background: var(--black);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
padding: 30px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $white;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.poster {
|
||||
@@ -69,7 +69,7 @@
|
||||
width: 40px;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
color: var(--iconButtonHoverLightColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
color: var(--iconButtonHoverLightColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,11 +52,11 @@ class AuthorDetailsHeader extends Component {
|
||||
|
||||
onOverviewMeasure = ({ height }) => {
|
||||
this.setState({ overviewHeight: height });
|
||||
}
|
||||
};
|
||||
|
||||
onTitleMeasure = ({ width }) => {
|
||||
this.setState({ titleWidth: width });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -48,7 +48,7 @@ class AuthorDetailsHeaderConnector extends Component {
|
||||
authorId: this.props.authorId,
|
||||
monitored
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.bookType {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid $borderColor;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
background-color: var(--cardBackgroundColor);
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
.books {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid $borderColor;
|
||||
border-top: 1px solid var(--borderColor);
|
||||
}
|
||||
|
||||
.collapseButtonContainer {
|
||||
@@ -86,10 +86,10 @@
|
||||
justify-content: center;
|
||||
padding: 10px 15px;
|
||||
width: 100%;
|
||||
border-top: 1px solid $borderColor;
|
||||
border-top: 1px solid var(--borderColor);
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
background-color: var(--collapseButtonBackgroundColor);
|
||||
}
|
||||
|
||||
.collapseButtonIcon {
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import getToggledRange from 'Utilities/Table/getToggledRange';
|
||||
import BookRowConnector from './BookRowConnector';
|
||||
import styles from './AuthorDetailsSeason.css';
|
||||
@@ -21,6 +22,26 @@ class AuthorDetailsSeason extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.setSelectedState(this.props.items);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
setSelectedState
|
||||
} = this.props;
|
||||
|
||||
if (sortKey !== prevProps.sortKey ||
|
||||
sortDirection !== prevProps.sortDirection ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
setSelectedState(items);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -40,7 +61,16 @@ class AuthorDetailsSeason extends Component {
|
||||
this.setState({ lastToggledBook: bookId });
|
||||
|
||||
this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
const {
|
||||
onSelectedChange,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
return onSelectedChange(items, id, value, shiftKey);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -48,20 +78,27 @@ class AuthorDetailsSeason extends Component {
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isEditorActive,
|
||||
columns,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
onSortPress,
|
||||
onTableOptionChange
|
||||
onTableOptionChange,
|
||||
selectedState
|
||||
} = this.props;
|
||||
|
||||
let titleColumns = columns;
|
||||
if (!isEditorActive) {
|
||||
titleColumns = columns.filter((x) => x.name !== 'select');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.bookType}
|
||||
>
|
||||
<div className={styles.books}>
|
||||
<Table
|
||||
columns={columns}
|
||||
columns={titleColumns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
@@ -76,6 +113,9 @@ class AuthorDetailsSeason extends Component {
|
||||
columns={columns}
|
||||
{...item}
|
||||
onMonitorBookPress={this.onMonitorBookPress}
|
||||
isEditorActive={isEditorActive}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -92,9 +132,13 @@ AuthorDetailsSeason.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onExpandPress: PropTypes.func.isRequired,
|
||||
setSelectedState: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onMonitorBookPress: PropTypes.func.isRequired,
|
||||
uiSettings: PropTypes.object.isRequired
|
||||
|
||||
@@ -66,18 +66,18 @@ class AuthorDetailsSeasonConnector extends Component {
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBooksTableOption(payload);
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setAuthorDetailsSort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onMonitorBookPress = (bookIds, monitored) => {
|
||||
this.props.toggleBooksMonitored({
|
||||
bookIds,
|
||||
monitored
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.bookType {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid $borderColor;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
background-color: var(--cardBackgroundColor);
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
.books {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid $borderColor;
|
||||
border-top: 1px solid var(--borderColor);
|
||||
}
|
||||
|
||||
.collapseButtonContainer {
|
||||
@@ -86,10 +86,10 @@
|
||||
justify-content: center;
|
||||
padding: 10px 15px;
|
||||
width: 100%;
|
||||
border-top: 1px solid $borderColor;
|
||||
border-top: 1px solid var(--borderColor);
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
background-color: var(--cardBackgroundColor);
|
||||
}
|
||||
|
||||
.collapseButtonIcon {
|
||||
|
||||
@@ -73,7 +73,7 @@ class AuthorDetailsSeries extends Component {
|
||||
} = this.props;
|
||||
|
||||
this.props.onExpandPress(id, !isExpanded);
|
||||
}
|
||||
};
|
||||
|
||||
onMonitorBookPress = (bookId, monitored, { shiftKey }) => {
|
||||
const lastToggled = this.state.lastToggledBook;
|
||||
@@ -91,13 +91,13 @@ class AuthorDetailsSeries extends Component {
|
||||
this.setState({ lastToggledBook: bookId });
|
||||
|
||||
this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
|
||||
}
|
||||
};
|
||||
|
||||
onMonitorSeriesPress = (monitored, { shiftKey }) => {
|
||||
const bookIds = this.props.items.map((book) => book.id);
|
||||
|
||||
this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -81,18 +81,18 @@ class AuthorDetailsSeasonConnector extends Component {
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setSeriesTableOption(payload);
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.dispatchSetSeriesSort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onMonitorBookPress = (bookIds, monitored) => {
|
||||
this.props.toggleBooksMonitored({
|
||||
bookIds,
|
||||
monitored
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -6,6 +6,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import StarRating from 'Components/StarRating';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import BookStatus from './BookStatus';
|
||||
import styles from './BookRow.css';
|
||||
@@ -29,23 +30,23 @@ class BookRow extends Component {
|
||||
|
||||
onManualSearchPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onEditBookPress = () => {
|
||||
this.setState({ isEditBookModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onEditBookModalClose = () => {
|
||||
this.setState({ isEditBookModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onMonitorBookPress = (monitored, options) => {
|
||||
this.props.onMonitorBookPress(this.props.id, monitored, options);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -65,6 +66,9 @@ class BookRow extends Component {
|
||||
authorMonitored,
|
||||
titleSlug,
|
||||
bookFiles,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
@@ -84,6 +88,18 @@ class BookRow extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEditorActive && name === 'select') {
|
||||
return (
|
||||
<TableSelectCell
|
||||
key={name}
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
isDisabled={false}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'monitored') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -220,6 +236,9 @@ BookRow.propTypes = {
|
||||
isSaving: PropTypes.bool,
|
||||
authorMonitored: PropTypes.bool.isRequired,
|
||||
bookFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMonitorBookPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ class EditAuthorModalConnector extends Component {
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'author' });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent';
|
||||
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
|
||||
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
@@ -47,13 +48,13 @@ class EditAuthorModalContent extends Component {
|
||||
|
||||
onSavePress(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMoveAuthorPress = () => {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
|
||||
this.props.onSavePress(true);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -73,6 +74,7 @@ class EditAuthorModalContent extends Component {
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
path,
|
||||
@@ -101,6 +103,31 @@ class EditAuthorModalContent extends Component {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MonitorNewItems')}
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MonitorNewItems')}
|
||||
body={<AuthorMonitorNewItemsOptionsPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
|
||||
name="monitorNewItems"
|
||||
helpText={translate('MonitorNewItemsHelpText')}
|
||||
{...monitorNewItems}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('QualityProfile')}
|
||||
|
||||
@@ -39,6 +39,7 @@ function createMapStateToProps() {
|
||||
|
||||
const authorSettings = _.pick(author, [
|
||||
'monitored',
|
||||
'monitorNewItems',
|
||||
'qualityProfileId',
|
||||
'metadataProfileId',
|
||||
'path',
|
||||
@@ -82,14 +83,14 @@ class EditAuthorModalContentConnector extends Component {
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetAuthorValue({ name, value });
|
||||
}
|
||||
};
|
||||
|
||||
onSavePress = (moveFiles) => {
|
||||
this.props.dispatchSaveAuthor({
|
||||
id: this.props.authorId,
|
||||
moveFiles
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -30,11 +30,11 @@ class RetagAuthorModalContent extends Component {
|
||||
|
||||
onCheckInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
onRetagAuthorPress = () => {
|
||||
this.props.onRetagAuthorPress(this.state.updateCovers, this.state.embedMetadata);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -45,7 +45,7 @@ class RetagAuthorModalContentConnector extends Component {
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import NoAuthor from 'Author/NoAuthor';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import RetagAuthorModal from './AudioTags/RetagAuthorModal';
|
||||
import AuthorEditorFilterModalConnector from './AuthorEditorFilterModalConnector';
|
||||
import AuthorEditorFooter from './AuthorEditorFooter';
|
||||
import AuthorEditorRowConnector from './AuthorEditorRowConnector';
|
||||
import OrganizeAuthorModal from './Organize/OrganizeAuthorModal';
|
||||
|
||||
class AuthorEditor extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isOrganizingAuthorModalOpen: false,
|
||||
isRetaggingAuthorModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = this.props;
|
||||
|
||||
const hasFinishedDeleting = prevProps.isDeleting &&
|
||||
!isDeleting &&
|
||||
!deleteError;
|
||||
|
||||
if (hasFinishedDeleting) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSelected = (changes) => {
|
||||
this.props.onSaveSelected({
|
||||
authorIds: this.getSelectedIds(),
|
||||
...changes
|
||||
});
|
||||
}
|
||||
|
||||
onOrganizeAuthorPress = () => {
|
||||
this.setState({ isOrganizingAuthorModalOpen: true });
|
||||
}
|
||||
|
||||
onOrganizeAuthorModalClose = (organized) => {
|
||||
this.setState({ isOrganizingAuthorModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
onRetagAuthorPress = () => {
|
||||
this.setState({ isRetaggingAuthorModalOpen: true });
|
||||
}
|
||||
|
||||
onRetagAuthorModalClose = (organized) => {
|
||||
this.setState({ isRetaggingAuthorModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalItems,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
onTableOptionChange,
|
||||
onSortPress,
|
||||
onFilterSelect
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const selectedAuthorIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={translate('AuthorEditor')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection />
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={AuthorEditorFilterModalConnector}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>{getErrorMessage(error, 'Failed to load author from API')}</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSortPress={onSortPress}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<AuthorEditorRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
columns={columns}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoAuthor totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<AuthorEditorFooter
|
||||
authorIds={selectedAuthorIds}
|
||||
selectedCount={selectedAuthorIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
isOrganizingAuthor={isOrganizingAuthor}
|
||||
isRetaggingAuthor={isRetaggingAuthor}
|
||||
columns={columns}
|
||||
showMetadataProfile={columns.find((column) => column.name === 'metadataProfileId').isVisible}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
|
||||
onRetagAuthorPress={this.onRetagAuthorPress}
|
||||
/>
|
||||
|
||||
<OrganizeAuthorModal
|
||||
isOpen={this.state.isOrganizingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onOrganizeAuthorModalClose}
|
||||
/>
|
||||
|
||||
<RetagAuthorModal
|
||||
isOpen={this.state.isRetaggingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onRetagAuthorModalClose}
|
||||
/>
|
||||
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthorEditor.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingAuthor: PropTypes.bool.isRequired,
|
||||
isRetaggingAuthor: PropTypes.bool.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthorEditor;
|
||||
@@ -1,97 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort, setAuthorEditorTableOption } from 'Store/Actions/authorEditorActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import AuthorEditor from './AuthorEditor';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('authors', 'authorEditor'),
|
||||
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
|
||||
(author, isOrganizingAuthor, isRetaggingAuthor) => {
|
||||
return {
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
...author
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetAuthorEditorSort: setAuthorEditorSort,
|
||||
dispatchSetAuthorEditorFilter: setAuthorEditorFilter,
|
||||
dispatchSetAuthorEditorTableOption: setAuthorEditorTableOption,
|
||||
dispatchSaveAuthorEditor: saveAuthorEditor,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class AuthorEditorConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.dispatchSetAuthorEditorSort({ sortKey });
|
||||
}
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.dispatchSetAuthorEditorFilter({ selectedFilterKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.dispatchSetAuthorEditorTableOption(payload);
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveAuthorEditor(payload);
|
||||
}
|
||||
|
||||
onMoveSelected = (payload) => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.MOVE_AUTHOR,
|
||||
...payload
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AuthorEditor
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthorEditorConnector.propTypes = {
|
||||
dispatchSetAuthorEditorSort: PropTypes.func.isRequired,
|
||||
dispatchSetAuthorEditorFilter: PropTypes.func.isRequired,
|
||||
dispatchSetAuthorEditorTableOption: PropTypes.func.isRequired,
|
||||
dispatchSaveAuthorEditor: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthorEditorConnector);
|
||||
@@ -1,24 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setAuthorEditorFilter } from 'Store/Actions/authorEditorActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.authors.items,
|
||||
(state) => state.authorEditor.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'authorEditor'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setAuthorEditorFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||
@@ -1,11 +1,23 @@
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dropdownContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -24,12 +36,14 @@
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.deleteSelectedButton {
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
margin-bottom: 10px;
|
||||
margin-left: 50px;
|
||||
height: 35px;
|
||||
}
|
||||
@@ -48,6 +62,10 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.dropdownContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -61,6 +79,7 @@
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: block;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
|
||||
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
|
||||
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
|
||||
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
@@ -26,6 +27,7 @@ class AuthorEditorFooter extends Component {
|
||||
|
||||
this.state = {
|
||||
monitored: NO_CHANGE,
|
||||
monitorNewItems: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
metadataProfileId: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
@@ -46,6 +48,7 @@ class AuthorEditorFooter extends Component {
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
monitored: NO_CHANGE,
|
||||
monitorNewItems: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
metadataProfileId: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
@@ -77,7 +80,7 @@ class AuthorEditorFooter extends Component {
|
||||
default:
|
||||
this.props.onSaveSelected({ [name]: value });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onApplyTagsPress = (tags, applyTags) => {
|
||||
this.setState({
|
||||
@@ -89,23 +92,23 @@ class AuthorEditorFooter extends Component {
|
||||
tags,
|
||||
applyTags
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteSelectedPress = () => {
|
||||
this.setState({ isDeleteAuthorModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorModalClose = () => {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onTagsPress = () => {
|
||||
this.setState({ isTagsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onTagsModalClose = () => {
|
||||
this.setState({ isTagsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onSaveRootFolderPress = () => {
|
||||
this.setState({
|
||||
@@ -114,7 +117,7 @@ class AuthorEditorFooter extends Component {
|
||||
});
|
||||
|
||||
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
|
||||
}
|
||||
};
|
||||
|
||||
onMoveAuthorPress = () => {
|
||||
this.setState({
|
||||
@@ -126,7 +129,7 @@ class AuthorEditorFooter extends Component {
|
||||
rootFolderPath: this.state.destinationRootFolder,
|
||||
moveFiles: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -139,13 +142,13 @@ class AuthorEditorFooter extends Component {
|
||||
isDeleting,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
columns,
|
||||
onOrganizeAuthorPress,
|
||||
onRetagAuthorPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
rootFolderPath,
|
||||
@@ -164,112 +167,99 @@ class AuthorEditorFooter extends Component {
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MonitorAuthor')}
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.dropdownContainer}>
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MonitorAuthor')}
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
<SelectInput
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MonitorNewItems')}
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
<MonitorNewItemsSelectInput
|
||||
name="monitorNewItems"
|
||||
value={monitorNewItems}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('QualityProfile')}
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('QualityProfile')}
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
if (name === 'metadataProfileId') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MetadataProfile')}
|
||||
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
|
||||
/>
|
||||
<div
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MetadataProfile')}
|
||||
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<MetadataProfileSelectInputConnector
|
||||
name="metadataProfileId"
|
||||
value={metadataProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<MetadataProfileSelectInputConnector
|
||||
name="metadataProfileId"
|
||||
value={metadataProfileId}
|
||||
includeNoChange={true}
|
||||
includeNone={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('RootFolder')}
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
<div
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('RootFolder')}
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
|
||||
isSaving={false}
|
||||
/>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
|
||||
isSaving={false}
|
||||
/>
|
||||
<div className={styles.buttons}>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<div>
|
||||
<SpinnerButton
|
||||
className={styles.organizeSelectedButton}
|
||||
kind={kinds.WARNING}
|
||||
@@ -277,7 +267,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={onOrganizeAuthorPress}
|
||||
>
|
||||
Rename Files
|
||||
{translate('RenameFiles')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -287,7 +277,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={onRetagAuthorPress}
|
||||
>
|
||||
Write Metadata Tags
|
||||
{translate('WriteMetadataTags')}
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -296,19 +286,20 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={this.onTagsPress}
|
||||
>
|
||||
Set Readarr Tags
|
||||
{translate('SetReadarrTags')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.deleteSelectedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedCount || isDeleting}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
Delete
|
||||
</SpinnerButton>
|
||||
<SpinnerButton
|
||||
className={styles.deleteSelectedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedCount || isDeleting}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -348,7 +339,6 @@ AuthorEditorFooter.propTypes = {
|
||||
isOrganizingAuthor: PropTypes.bool.isRequired,
|
||||
isRetaggingAuthor: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired,
|
||||
onOrganizeAuthorPress: PropTypes.func.isRequired,
|
||||
onRetagAuthorPress: PropTypes.func.isRequired
|
||||
|
||||
@@ -16,7 +16,7 @@ class AuthorEditorRow extends Component {
|
||||
onBookFolderChange = () => {
|
||||
// Mock handler to satisfy `onChange` being required for `CheckInput`.
|
||||
//
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
|
||||
.path {
|
||||
margin-left: 5px;
|
||||
color: $dangerColor;
|
||||
color: var(--dangerColor);
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ class DeleteAuthorModalContent extends Component {
|
||||
|
||||
onDeleteFilesChange = ({ value }) => {
|
||||
this.setState({ deleteFiles: value });
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorConfirmed = () => {
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
|
||||
this.setState({ deleteFiles: false });
|
||||
this.props.onDeleteSelectedPress(deleteFiles);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { bulkDeleteAuthor } from 'Store/Actions/authorEditorActions';
|
||||
import { bulkDeleteAuthor } from 'Store/Actions/authorIndexActions';
|
||||
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
|
||||
import DeleteAuthorModalContent from './DeleteAuthorModalContent';
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class OrganizeAuthorModalContentConnector extends Component {
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -34,7 +34,7 @@ class TagsModalContent extends Component {
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
onApplyTagsPress = () => {
|
||||
const {
|
||||
@@ -43,7 +43,7 @@ class TagsModalContent extends Component {
|
||||
} = this.state;
|
||||
|
||||
this.props.onApplyTagsPress(tags, applyTags);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -54,7 +54,7 @@ class AuthorHistoryContentConnector extends Component {
|
||||
authorId,
|
||||
bookId
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -55,16 +55,16 @@ class AuthorHistoryRow extends Component {
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onConfirmMarkAsFailed = () => {
|
||||
this.props.onMarkAsFailedPress(this.props.id);
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onMarkAsFailedModalClose = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import RetagAuthorModal from 'Author/Editor/AudioTags/RetagAuthorModal';
|
||||
import AuthorEditorFooter from 'Author/Editor/AuthorEditorFooter';
|
||||
import OrganizeAuthorModal from 'Author/Editor/Organize/OrganizeAuthorModal';
|
||||
import NoAuthor from 'Author/NoAuthor';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -15,6 +18,9 @@ import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import AuthorIndexFooterConnector from './AuthorIndexFooterConnector';
|
||||
import AuthorIndexFilterMenu from './Menus/AuthorIndexFilterMenu';
|
||||
import AuthorIndexSortMenu from './Menus/AuthorIndexSortMenu';
|
||||
@@ -52,12 +58,20 @@ class AuthorIndex extends Component {
|
||||
jumpBarItems: { order: [] },
|
||||
jumpToCharacter: null,
|
||||
isPosterOptionsModalOpen: false,
|
||||
isOverviewOptionsModalOpen: false
|
||||
isOverviewOptionsModalOpen: false,
|
||||
isEditorActive: false,
|
||||
isOrganizingAuthorModalOpen: false,
|
||||
isRetaggingAuthorModalOpen: false,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -72,6 +86,7 @@ class AuthorIndex extends Component {
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
if (this.state.jumpToCharacter != null) {
|
||||
@@ -84,6 +99,48 @@ class AuthorIndex extends Component {
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
};
|
||||
|
||||
getSelectedIds = () => {
|
||||
if (this.state.allUnselected) {
|
||||
return [];
|
||||
}
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
setSelectedState() {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((author) => {
|
||||
const isItemSelected = selectedState[author.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[author.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[author.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
}
|
||||
|
||||
setJumpBarItems() {
|
||||
@@ -135,23 +192,85 @@ class AuthorIndex extends Component {
|
||||
|
||||
onPosterOptionsPress = () => {
|
||||
this.setState({ isPosterOptionsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onPosterOptionsModalClose = () => {
|
||||
this.setState({ isPosterOptionsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onOverviewOptionsPress = () => {
|
||||
this.setState({ isOverviewOptionsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onOverviewOptionsModalClose = () => {
|
||||
this.setState({ isOverviewOptionsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onEditorTogglePress = () => {
|
||||
if (this.state.isEditorActive) {
|
||||
this.setState({ isEditorActive: false });
|
||||
} else {
|
||||
const newState = selectAll(this.state.selectedState, false);
|
||||
newState.isEditorActive = true;
|
||||
this.setState(newState);
|
||||
}
|
||||
};
|
||||
|
||||
onJumpBarItemPress = (jumpToCharacter) => {
|
||||
this.setState({ jumpToCharacter });
|
||||
}
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectAllPress = () => {
|
||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onSaveSelected = (changes) => {
|
||||
this.props.onSaveSelected({
|
||||
authorIds: this.getSelectedIds(),
|
||||
...changes
|
||||
});
|
||||
};
|
||||
|
||||
onOrganizeAuthorPress = () => {
|
||||
this.setState({ isOrganizingAuthorModalOpen: true });
|
||||
};
|
||||
|
||||
onOrganizeAuthorModalClose = (organized) => {
|
||||
this.setState({ isOrganizingAuthorModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
};
|
||||
|
||||
onRetagAuthorPress = () => {
|
||||
this.setState({ isRetaggingAuthorModalOpen: true });
|
||||
};
|
||||
|
||||
onRetagAuthorModalClose = (organized) => {
|
||||
this.setState({ isRetaggingAuthorModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
};
|
||||
|
||||
onRefreshAuthorPress = () => {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const refreshIds = this.state.isEditorActive && selectedIds.length > 0 ? selectedIds : [];
|
||||
|
||||
this.props.onRefreshAuthorPress(refreshIds);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -172,11 +291,16 @@ class AuthorIndex extends Component {
|
||||
view,
|
||||
isRefreshingAuthor,
|
||||
isRssSyncExecuting,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
onFilterSelect,
|
||||
onViewSelect,
|
||||
onRefreshAuthorPress,
|
||||
onRssSyncPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -186,23 +310,31 @@ class AuthorIndex extends Component {
|
||||
jumpBarItems,
|
||||
jumpToCharacter,
|
||||
isPosterOptionsModalOpen,
|
||||
isOverviewOptionsModalOpen
|
||||
isOverviewOptionsModalOpen,
|
||||
isEditorActive,
|
||||
selectedState,
|
||||
allSelected,
|
||||
allUnselected
|
||||
} = this.state;
|
||||
|
||||
const selectedAuthorIds = this.getSelectedIds();
|
||||
|
||||
const ViewComponent = getViewComponent(view);
|
||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||
const hasNoAuthor = !totalItems;
|
||||
|
||||
const refreshLabel = isEditorActive && selectedAuthorIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll');
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('UpdateAll')}
|
||||
label={refreshLabel}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isRefreshingAuthor}
|
||||
onPress={onRefreshAuthorPress}
|
||||
onPress={this.onRefreshAuthorPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
@@ -213,6 +345,35 @@ class AuthorIndex extends Component {
|
||||
onPress={onRssSyncPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={translate('AuthorIndex')}
|
||||
iconName={icons.AUTHOR_CONTINUING}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onEditorTogglePress}
|
||||
/> :
|
||||
<PageToolbarButton
|
||||
label={translate('AuthorEditor')}
|
||||
iconName={icons.EDIT}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onEditorTogglePress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={icons.CHECK_SQUARE}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onSelectAllPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
@@ -310,6 +471,12 @@ class AuthorIndex extends Component {
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
jumpToCharacter={jumpToCharacter}
|
||||
isEditorActive={isEditorActive}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
selectedState={selectedState}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
@@ -332,6 +499,24 @@ class AuthorIndex extends Component {
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
isLoaded && isEditorActive &&
|
||||
<AuthorEditorFooter
|
||||
authorIds={selectedAuthorIds}
|
||||
selectedCount={selectedAuthorIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
isOrganizingAuthor={isOrganizingAuthor}
|
||||
isRetaggingAuthor={isRetaggingAuthor}
|
||||
showMetadataProfile={true}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
|
||||
onRetagAuthorPress={this.onRetagAuthorPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<AuthorIndexPosterOptionsModal
|
||||
isOpen={isPosterOptionsModalOpen}
|
||||
onModalClose={this.onPosterOptionsModalClose}
|
||||
@@ -340,8 +525,20 @@ class AuthorIndex extends Component {
|
||||
<AuthorIndexOverviewOptionsModal
|
||||
isOpen={isOverviewOptionsModalOpen}
|
||||
onModalClose={this.onOverviewOptionsModalClose}
|
||||
|
||||
/>
|
||||
|
||||
<OrganizeAuthorModal
|
||||
isOpen={this.state.isOrganizingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onOrganizeAuthorModalClose}
|
||||
/>
|
||||
|
||||
<RetagAuthorModal
|
||||
isOpen={this.state.isRetaggingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onRetagAuthorModalClose}
|
||||
/>
|
||||
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -361,14 +558,21 @@ AuthorIndex.propTypes = {
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
view: PropTypes.string.isRequired,
|
||||
isRefreshingAuthor: PropTypes.bool.isRequired,
|
||||
isOrganizingAuthor: PropTypes.bool.isRequired,
|
||||
isRetaggingAuthor: PropTypes.bool.isRequired,
|
||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onViewSelect: PropTypes.func.isRequired,
|
||||
onRefreshAuthorPress: PropTypes.func.isRequired,
|
||||
onRssSyncPress: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthorIndex;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
|
||||
import { saveAuthorEditor, setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
|
||||
@@ -18,16 +18,22 @@ function createMapStateToProps() {
|
||||
createAuthorClientSideCollectionItemsSelector('authorIndex'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
author,
|
||||
isRefreshingAuthor,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isRssSyncExecuting,
|
||||
dimensionsState
|
||||
) => {
|
||||
return {
|
||||
...author,
|
||||
isRefreshingAuthor,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isRssSyncExecuting,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
@@ -53,9 +59,14 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatch(setAuthorView({ view }));
|
||||
},
|
||||
|
||||
onRefreshAuthorPress() {
|
||||
dispatchSaveAuthorEditor(payload) {
|
||||
dispatch(saveAuthorEditor(payload));
|
||||
},
|
||||
|
||||
onRefreshAuthorPress(items) {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.REFRESH_AUTHOR
|
||||
name: commandNames.BULK_REFRESH_AUTHOR,
|
||||
authorIds: items
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -74,11 +85,15 @@ class AuthorIndexConnector extends Component {
|
||||
|
||||
onViewSelect = (view) => {
|
||||
this.props.dispatchSetAuthorView(view);
|
||||
}
|
||||
};
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveAuthorEditor(payload);
|
||||
};
|
||||
|
||||
onScroll = ({ scrollTop }) => {
|
||||
scrollPositions.authorIndex = scrollTop;
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -89,6 +104,7 @@ class AuthorIndexConnector extends Component {
|
||||
{...this.props}
|
||||
onViewSelect={this.onViewSelect}
|
||||
onScroll={this.onScroll}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -97,7 +113,8 @@ class AuthorIndexConnector extends Component {
|
||||
AuthorIndexConnector.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
view: PropTypes.string.isRequired,
|
||||
dispatchSetAuthorView: PropTypes.func.isRequired
|
||||
dispatchSetAuthorView: PropTypes.func.isRequired,
|
||||
dispatchSaveAuthorEditor: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withScrollPosition(
|
||||
|
||||
@@ -21,32 +21,32 @@
|
||||
.continuing {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: $primaryColor;
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.ended {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: $successColor;
|
||||
background-color: var(--successColor);
|
||||
}
|
||||
|
||||
.missingMonitored {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: $dangerColor;
|
||||
background-color: var(--dangerColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, color($dangerColor shade(5%)), color($dangerColor shade(5%)) 5px, color($dangerColor shade(15%)) 5px, color($dangerColor shade(15%)) 10px);
|
||||
background: repeating-linear-gradient(90deg, color(#f05050 shade(5%)), color(#f05050 shade(5%)) 5px, color(#f05050 shade(15%)) 5px, color(#f05050 shade(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.missingUnmonitored {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: $warningColor;
|
||||
background-color: var(--warningColor);
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(45deg, $warningColor, $warningColor 5px, color($warningColor tint(15%)) 5px, color($warningColor tint(15%)) 10px);
|
||||
background: repeating-linear-gradient(45deg, #ffa500, #ffa500 5px, color(#ffa500 tint(15%)) 5px, color(#ffa500 tint(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,14 +98,14 @@ class AuthorIndexItemConnector extends Component {
|
||||
name: commandNames.REFRESH_AUTHOR,
|
||||
authorId: this.props.id
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSearchPress = () => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.AUTHOR_SEARCH,
|
||||
authorId: this.props.id
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -3,7 +3,7 @@ $hoverScale: 1.05;
|
||||
.container {
|
||||
&:hover {
|
||||
.content {
|
||||
background-color: $tableRowHoverBackgroundColor;
|
||||
background-color: var(--tableRowHoverBackgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,13 @@ $hoverScale: 1.05;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.editorSelect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.posterContainer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -28,10 +35,10 @@ $hoverScale: 1.05;
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
display: block;
|
||||
color: $defaultColor;
|
||||
color: var(--defaultColor);
|
||||
|
||||
&:hover {
|
||||
color: $defaultColor;
|
||||
color: var(--defaultColor);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
@@ -45,8 +52,8 @@ $hoverScale: 1.05;
|
||||
height: 0;
|
||||
border-width: 0 25px 25px 0;
|
||||
border-style: solid;
|
||||
border-color: transparent $dangerColor transparent transparent;
|
||||
color: $white;
|
||||
border-color: transparent var(--dangerColor) transparent transparent;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.info {
|
||||
|
||||
@@ -5,6 +5,7 @@ import AuthorPoster from 'Author/AuthorPoster';
|
||||
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
@@ -50,22 +51,31 @@ class AuthorIndexOverview extends Component {
|
||||
|
||||
onEditAuthorPress = () => {
|
||||
this.setState({ isEditAuthorModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onEditAuthorModalClose = () => {
|
||||
this.setState({ isEditAuthorModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorPress = () => {
|
||||
this.setState({
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorModalClose = () => {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -97,6 +107,8 @@ class AuthorIndexOverview extends Component {
|
||||
isSearchingAuthor,
|
||||
onRefreshAuthorPress,
|
||||
onSearchPress,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -127,6 +139,18 @@ class AuthorIndexOverview extends Component {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
{
|
||||
isEditorActive &&
|
||||
<div className={styles.editorSelect}>
|
||||
<CheckInput
|
||||
className={styles.checkInput}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
@@ -270,7 +294,10 @@ AuthorIndexOverview.propTypes = {
|
||||
isRefreshingAuthor: PropTypes.bool.isRequired,
|
||||
isSearchingAuthor: PropTypes.bool.isRequired,
|
||||
onRefreshAuthorPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AuthorIndexOverview.defaultProps = {
|
||||
|
||||
@@ -73,7 +73,9 @@ class AuthorIndexOverviews extends Component {
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter,
|
||||
scrollTop
|
||||
scrollTop,
|
||||
isEditorActive,
|
||||
selectedState
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -91,6 +93,8 @@ class AuthorIndexOverviews extends Component {
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
||||
prevProps.isEditorActive !== isEditorActive ||
|
||||
prevProps.selectedState !== selectedState ||
|
||||
prevProps.overviewOptions.showTitle !== overviewOptions.showTitle)) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
@@ -119,7 +123,7 @@ class AuthorIndexOverviews extends Component {
|
||||
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
}
|
||||
};
|
||||
|
||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||
const {
|
||||
@@ -137,7 +141,7 @@ class AuthorIndexOverviews extends Component {
|
||||
posterHeight,
|
||||
rowHeight
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
cellRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
@@ -148,7 +152,10 @@ class AuthorIndexOverviews extends Component {
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
isSmallScreen
|
||||
isSmallScreen,
|
||||
selectedState,
|
||||
isEditorActive,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -184,17 +191,20 @@ class AuthorIndexOverviews extends Component {
|
||||
authorId={author.id}
|
||||
qualityProfileId={author.qualityProfileId}
|
||||
metadataProfileId={author.metadataProfileId}
|
||||
isSelected={selectedState[author.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isEditorActive={isEditorActive}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.calculateGrid(width, this.props.isSmallScreen);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -264,7 +274,10 @@ AuthorIndexOverviews.propTypes = {
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthorIndexOverviews;
|
||||
|
||||
@@ -122,7 +122,7 @@ class AuthorIndexOverviewOptionsModalContent extends Component {
|
||||
}, () => {
|
||||
this.props.onChangeOverviewOption({ [name]: value });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -5,7 +5,7 @@ $hoverScale: 1.05;
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 12px $black;
|
||||
box-shadow: 0 0 12px var(--black);
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
.controls {
|
||||
@@ -32,7 +32,7 @@ $hoverScale: 1.05;
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 70px;
|
||||
background-color: $defaultColor;
|
||||
background-color: var(--defaultColor);
|
||||
}
|
||||
|
||||
.overlayTitle {
|
||||
@@ -45,13 +45,13 @@ $hoverScale: 1.05;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $offWhite;
|
||||
color: var(--offWhite);
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.nextAiring {
|
||||
background-color: #fafbfc;
|
||||
background-color: var(--seriesBackgroundColor);
|
||||
text-align: center;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
@@ -59,8 +59,7 @@ $hoverScale: 1.05;
|
||||
.title {
|
||||
@add-mixin truncate;
|
||||
|
||||
background-color: $defaultColor;
|
||||
color: $white;
|
||||
background-color: var(--seriesBackgroundColor);
|
||||
text-align: center;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
@@ -74,8 +73,15 @@ $hoverScale: 1.05;
|
||||
height: 0;
|
||||
border-width: 0 25px 25px 0;
|
||||
border-style: solid;
|
||||
border-color: transparent $dangerColor transparent transparent;
|
||||
color: $white;
|
||||
border-color: transparent var(--dangerColor) transparent transparent;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.editorSelect {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.controls {
|
||||
@@ -84,8 +90,8 @@ $hoverScale: 1.05;
|
||||
left: 10px;
|
||||
z-index: 3;
|
||||
border-radius: 4px;
|
||||
background-color: $themeLightColor;
|
||||
color: $white;
|
||||
background-color: var(--readarrRed);
|
||||
color: var(--white);
|
||||
font-size: $smallFontSize;
|
||||
opacity: 0;
|
||||
transition: opacity 0;
|
||||
|
||||
@@ -4,6 +4,7 @@ import AuthorPoster from 'Author/AuthorPoster';
|
||||
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
@@ -34,34 +35,43 @@ class AuthorIndexPoster extends Component {
|
||||
|
||||
onEditAuthorPress = () => {
|
||||
this.setState({ isEditAuthorModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onEditAuthorModalClose = () => {
|
||||
this.setState({ isEditAuthorModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorPress = () => {
|
||||
this.setState({
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorModalClose = () => {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onPosterLoad = () => {
|
||||
if (this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onPosterLoadError = () => {
|
||||
if (!this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -84,6 +94,7 @@ class AuthorIndexPoster extends Component {
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
showSearchAction,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
@@ -92,6 +103,9 @@ class AuthorIndexPoster extends Component {
|
||||
isSearchingAuthor,
|
||||
onRefreshAuthorPress,
|
||||
onSearchPress,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -120,6 +134,18 @@ class AuthorIndexPoster extends Component {
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
{
|
||||
isEditorActive &&
|
||||
<div className={styles.editorSelect}>
|
||||
<CheckInput
|
||||
className={styles.checkInput}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Label className={styles.controls}>
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
@@ -234,6 +260,7 @@ class AuthorIndexPoster extends Component {
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
qualityProfile={qualityProfile}
|
||||
showQualityProfile={showQualityProfile}
|
||||
metadataProfile={metadataProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
@@ -275,6 +302,7 @@ AuthorIndexPoster.propTypes = {
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
metadataProfile: PropTypes.object.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
@@ -282,7 +310,10 @@ AuthorIndexPoster.propTypes = {
|
||||
isRefreshingAuthor: PropTypes.bool.isRequired,
|
||||
isSearchingAuthor: PropTypes.bool.isRequired,
|
||||
onRefreshAuthorPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AuthorIndexPoster.defaultProps = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.info {
|
||||
background-color: #fafbfc;
|
||||
background-color: var(--seriesBackgroundColor);
|
||||
text-align: center;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ function AuthorIndexPosterInfo(props) {
|
||||
const {
|
||||
qualityProfile,
|
||||
showQualityProfile,
|
||||
previousAiring,
|
||||
metadataProfile,
|
||||
added,
|
||||
nextBook,
|
||||
lastBook,
|
||||
bookCount,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
@@ -27,20 +29,10 @@ function AuthorIndexPosterInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'previousAiring' && previousAiring) {
|
||||
if (sortKey === 'metadataProfileId') {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{
|
||||
getRelativeDate(
|
||||
previousAiring,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)
|
||||
}
|
||||
{metadataProfile.name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -63,6 +55,42 @@ function AuthorIndexPosterInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'nextBook' && nextBook) {
|
||||
const date = getRelativeDate(
|
||||
nextBook.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: false
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{`Next Book ${date}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'lastBook' && lastBook) {
|
||||
const date = getRelativeDate(
|
||||
lastBook.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: false
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{`Last Book ${date}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'bookCount') {
|
||||
let books = '1 book';
|
||||
|
||||
@@ -101,8 +129,10 @@ function AuthorIndexPosterInfo(props) {
|
||||
AuthorIndexPosterInfo.propTypes = {
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
previousAiring: PropTypes.string,
|
||||
metadataProfile: PropTypes.object.isRequired,
|
||||
added: PropTypes.string,
|
||||
nextBook: PropTypes.object,
|
||||
lastBook: PropTypes.object,
|
||||
bookCount: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
sizeOnDisk: PropTypes.number,
|
||||
|
||||
@@ -116,7 +116,9 @@ class AuthorIndexPosters extends Component {
|
||||
posterOptions,
|
||||
jumpToCharacter,
|
||||
isSmallScreen,
|
||||
scrollTop
|
||||
isEditorActive,
|
||||
scrollTop,
|
||||
selectedState
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -138,6 +140,8 @@ class AuthorIndexPosters extends Component {
|
||||
prevState.columnCount !== columnCount ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)) ||
|
||||
prevProps.isEditorActive !== isEditorActive ||
|
||||
prevProps.selectedState !== selectedState ||
|
||||
prevProps.posterOptions.showTitle !== posterOptions.showTitle) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
@@ -167,7 +171,7 @@ class AuthorIndexPosters extends Component {
|
||||
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
}
|
||||
};
|
||||
|
||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||
const {
|
||||
@@ -189,7 +193,7 @@ class AuthorIndexPosters extends Component {
|
||||
posterHeight,
|
||||
rowHeight
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
|
||||
const {
|
||||
@@ -198,7 +202,10 @@ class AuthorIndexPosters extends Component {
|
||||
posterOptions,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
timeFormat,
|
||||
selectedState,
|
||||
isEditorActive,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -246,17 +253,20 @@ class AuthorIndexPosters extends Component {
|
||||
authorId={author.id}
|
||||
qualityProfileId={author.qualityProfileId}
|
||||
metadataProfileId={author.metadataProfileId}
|
||||
isSelected={selectedState[author.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isEditorActive={isEditorActive}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.calculateGrid(width, this.props.isSmallScreen);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -328,7 +338,10 @@ AuthorIndexPosters.propTypes = {
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthorIndexPosters;
|
||||
|
||||
@@ -93,7 +93,7 @@ class AuthorIndexPosterOptionsModalContent extends Component {
|
||||
}, () => {
|
||||
this.props.onChangePosterOption({ [name]: value });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
border-radius: 0;
|
||||
background-color: #5b5b5b;
|
||||
color: $white;
|
||||
color: var(--white);
|
||||
transition: width 200ms ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,22 +27,22 @@ class AuthorIndexActionsCell extends Component {
|
||||
|
||||
onEditAuthorPress = () => {
|
||||
this.setState({ isEditAuthorModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onEditAuthorModalClose = () => {
|
||||
this.setState({ isEditAuthorModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorPress = () => {
|
||||
this.setState({
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteAuthorModalClose = () => {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import AuthorIndexTableOptionsConnector from './AuthorIndexTableOptionsConnector';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
@@ -15,6 +16,10 @@ function AuthorIndexHeader(props) {
|
||||
showBanners,
|
||||
columns,
|
||||
onTableOptionChange,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
isEditorActive,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -33,6 +38,21 @@ function AuthorIndexHeader(props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'select') {
|
||||
if (isEditorActive) {
|
||||
return (
|
||||
<VirtualTableSelectAllHeaderCell
|
||||
key={name}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
@@ -80,6 +100,10 @@ function AuthorIndexHeader(props) {
|
||||
AuthorIndexHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
showBanners: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 70px;
|
||||
background-color: $defaultColor;
|
||||
background-color: var(--defaultColor);
|
||||
}
|
||||
|
||||
.bannerImage {
|
||||
@@ -49,7 +49,7 @@
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $offWhite;
|
||||
color: var(--offWhite);
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
@@ -92,7 +92,7 @@
|
||||
}
|
||||
|
||||
.ratings {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 0 80px;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user