mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-20 16:44:18 -04:00
Compare commits
300 Commits
v3.0.6.126
...
v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2639c069bc | ||
|
|
d185783987 | ||
|
|
e4c0e80e3e | ||
|
|
46bc711558 | ||
|
|
e35e24a4c2 | ||
|
|
7eb61fafa4 | ||
|
|
ab578566be | ||
|
|
35edef91c6 | ||
|
|
78dc2a7e13 | ||
|
|
5d976ac657 | ||
|
|
d5fff15f32 | ||
|
|
7c98c2397a | ||
|
|
ad6081aec6 | ||
|
|
1558929484 | ||
|
|
4a2f120bc1 | ||
|
|
6c0f22a11e | ||
|
|
41a821352e | ||
|
|
0991cfe27e | ||
|
|
97925feed9 | ||
|
|
93fc9abae9 | ||
|
|
2ea7b477cb | ||
|
|
c9df12e6bc | ||
|
|
b542dd0ddd | ||
|
|
c1e5b7f642 | ||
|
|
ccb88919b9 | ||
|
|
f9b2c2d843 | ||
|
|
d48950ec3c | ||
|
|
6a7d84f134 | ||
|
|
a81a80a00f | ||
|
|
c00cbb9a5a | ||
|
|
0d739cd26d | ||
|
|
d01e6d32de | ||
|
|
704cf7aebe | ||
|
|
52d95fa632 | ||
|
|
a71cc1081e | ||
|
|
edf1167a37 | ||
|
|
8f2c4fe4d1 | ||
|
|
cc9fc1e3c3 | ||
|
|
9fb29f42c4 | ||
|
|
9a1a320110 | ||
|
|
f6664b8b42 | ||
|
|
82646db70d | ||
|
|
97e40dc00a | ||
|
|
ae0e23fc8e | ||
|
|
893a6744ac | ||
|
|
fa4b80b86f | ||
|
|
18f7bcd212 | ||
|
|
458c5cd0b3 | ||
|
|
c93f63cd20 | ||
|
|
bc5a43bd92 | ||
|
|
a6a68b4cae | ||
|
|
9183c6b846 | ||
|
|
d73ad3e27a | ||
|
|
bd70fa5410 | ||
|
|
481345226a | ||
|
|
365c6a7741 | ||
|
|
8fa6e5ec6d | ||
|
|
d376ae2f9f | ||
|
|
103d2751ee | ||
|
|
bdd5865876 | ||
|
|
f678775e5c | ||
|
|
5a08d5dc24 | ||
|
|
bba4a5636e | ||
|
|
8d83b1d8d6 | ||
|
|
3be5d6c258 | ||
|
|
40ecdbc12d | ||
|
|
6e271e9272 | ||
|
|
5c5b012ded | ||
|
|
be1acfc2f9 | ||
|
|
ebb48a19cc | ||
|
|
fa9136c4d1 | ||
|
|
e7ca98489e | ||
|
|
a3fd3c5e67 | ||
|
|
cc09f85212 | ||
|
|
581fb2cb3d | ||
|
|
d899225509 | ||
|
|
06464d720c | ||
|
|
d21e9753bc | ||
|
|
07f0db477a | ||
|
|
e280897bc7 | ||
|
|
bb02fc4668 | ||
|
|
e3aa92d09a | ||
|
|
d02d1bbdfe | ||
|
|
5df6f13f36 | ||
|
|
fc2023d67c | ||
|
|
2417c4afb2 | ||
|
|
0521fc5681 | ||
|
|
77412f2376 | ||
|
|
4bfcd0de1d | ||
|
|
c10677dfe7 | ||
|
|
56b3acddc9 | ||
|
|
b24bea415b | ||
|
|
a8cb7784f2 | ||
|
|
f1d07f74ee | ||
|
|
acdf02d569 | ||
|
|
66af08a830 | ||
|
|
61e68b02ed | ||
|
|
80d36a06c8 | ||
|
|
2a45b615ae | ||
|
|
f6b08f697b | ||
|
|
9e82014454 | ||
|
|
341e8023af | ||
|
|
7a0090c7a2 | ||
|
|
66be23a7c4 | ||
|
|
590f306e5f | ||
|
|
715711e6d7 | ||
|
|
9e1b799fb7 | ||
|
|
8946b401cf | ||
|
|
87f03e1f38 | ||
|
|
d18751eff2 | ||
|
|
d71c50b634 | ||
|
|
78aeda1a2c | ||
|
|
c7427f8df8 | ||
|
|
79436149eb | ||
|
|
210768d7d6 | ||
|
|
b3d90d903a | ||
|
|
1bf87bf873 | ||
|
|
69ccb96a36 | ||
|
|
a36397452d | ||
|
|
853f4d1e29 | ||
|
|
52c6bc5549 | ||
|
|
5a9df521ad | ||
|
|
6debc77408 | ||
|
|
1ee40215e7 | ||
|
|
ccc378fd0c | ||
|
|
71dba904a1 | ||
|
|
cee17483d9 | ||
|
|
05b1581b7d | ||
|
|
36395decc7 | ||
|
|
113bb6ad4d | ||
|
|
e6210aede6 | ||
|
|
6de8bdf331 | ||
|
|
c67718d81e | ||
|
|
faa510eb09 | ||
|
|
ac9a98e498 | ||
|
|
13aaa20f1b | ||
|
|
a27984c032 | ||
|
|
9afcec8b1f | ||
|
|
f4dbda1318 | ||
|
|
f67e11d477 | ||
|
|
d4d4bf8784 | ||
|
|
6106362f6c | ||
|
|
86658b05ca | ||
|
|
041689e904 | ||
|
|
0cb8d93069 | ||
|
|
6131a99497 | ||
|
|
4be626a44c | ||
|
|
9889ab7b48 | ||
|
|
d284c29b6f | ||
|
|
87c65932f0 | ||
|
|
5c8f2518ba | ||
|
|
d7c10a4d4d | ||
|
|
f2c7e235af | ||
|
|
e3be3ef91e | ||
|
|
ec866082d4 | ||
|
|
60b4e14522 | ||
|
|
385a756a7e | ||
|
|
de528fff42 | ||
|
|
b90e25f652 | ||
|
|
d80565f6a9 | ||
|
|
245a033ab3 | ||
|
|
8621ecfec1 | ||
|
|
52d8f87c66 | ||
|
|
b869ebeac2 | ||
|
|
3f66eeba4d | ||
|
|
d5b91f81ef | ||
|
|
39192a6622 | ||
|
|
f948a59f6f | ||
|
|
2d67247234 | ||
|
|
cfa93c0a92 | ||
|
|
96a8991ba3 | ||
|
|
e2b16adec6 | ||
|
|
201004113e | ||
|
|
3a8bd451a9 | ||
|
|
aa463e0af1 | ||
|
|
81ff4791ac | ||
|
|
ec62884649 | ||
|
|
0ac0a6223a | ||
|
|
7b694ea71d | ||
|
|
7f079ac8f2 | ||
|
|
1603512ad6 | ||
|
|
75fc550a3f | ||
|
|
d51cd4bbe7 | ||
|
|
70456410a7 | ||
|
|
9e5d173900 | ||
|
|
f08f5cecdc | ||
|
|
226d94b050 | ||
|
|
2d0541c03b | ||
|
|
ae328c1d84 | ||
|
|
61633ab074 | ||
|
|
d600e2e3fb | ||
|
|
7b7da9c1b2 | ||
|
|
ded3f59d2f | ||
|
|
2601a68cd4 | ||
|
|
1d188d32b6 | ||
|
|
73429a0823 | ||
|
|
f26540cdc7 | ||
|
|
2bf1ce1763 | ||
|
|
b184e62fa7 | ||
|
|
86fa6036d0 | ||
|
|
5267e15c17 | ||
|
|
a30ec0eb52 | ||
|
|
dd3b57da27 | ||
|
|
3f9f8b3e2d | ||
|
|
d11c691a73 | ||
|
|
44a160fa06 | ||
|
|
272f8e6136 | ||
|
|
6378e7afef | ||
|
|
b70ef368db | ||
|
|
19510c4932 | ||
|
|
4e28c7d190 | ||
|
|
51d45247d0 | ||
|
|
85a143a1b6 | ||
|
|
f58a2389bd | ||
|
|
a97e83ea4d | ||
|
|
a83ed3bcce | ||
|
|
30aa5f9070 | ||
|
|
4bba820e5a | ||
|
|
cd30175308 | ||
|
|
2e2733a2e1 | ||
|
|
553aa22b29 | ||
|
|
08359e264c | ||
|
|
979219f7df | ||
|
|
bb89fb0867 | ||
|
|
747a4164e2 | ||
|
|
eb4a9f624e | ||
|
|
4bdb7fe795 | ||
|
|
0abd52d6be | ||
|
|
af654e245c | ||
|
|
deed85d2f9 | ||
|
|
89b1d58b86 | ||
|
|
f4c71fc56f | ||
|
|
7e175bf95e | ||
|
|
99843d2876 | ||
|
|
7c74d19515 | ||
|
|
076ad5fe6d | ||
|
|
519a5ca75c | ||
|
|
07c95f06d3 | ||
|
|
ad9f242b5c | ||
|
|
ada01a1116 | ||
|
|
f6fbd3cfee | ||
|
|
5923b4ae0d | ||
|
|
0d99c87d87 | ||
|
|
9111799f46 | ||
|
|
943a3d80c4 | ||
|
|
d9e9b72a89 | ||
|
|
5bf7228658 | ||
|
|
8ad5e5dd13 | ||
|
|
c0a961bb94 | ||
|
|
6994ca720a | ||
|
|
1d8b711eda | ||
|
|
77fdebc366 | ||
|
|
dd3899806b | ||
|
|
574f05e296 | ||
|
|
5c4687e0d9 | ||
|
|
e19d4cf85b | ||
|
|
6b84da614b | ||
|
|
31833253dd | ||
|
|
dbd140d4ec | ||
|
|
e9a49941c9 | ||
|
|
0fe436b952 | ||
|
|
22f044844c | ||
|
|
20306a38e1 | ||
|
|
0d03dba6ea | ||
|
|
1c6863dd27 | ||
|
|
3f60e28c42 | ||
|
|
ead1371846 | ||
|
|
2f6409226a | ||
|
|
3fb5f65f08 | ||
|
|
38feeefea3 | ||
|
|
a7a3c546e5 | ||
|
|
f107ea5678 | ||
|
|
59409a7e72 | ||
|
|
dc7f46027a | ||
|
|
2031da05f6 | ||
|
|
92b9f46399 | ||
|
|
0a30735f34 | ||
|
|
021fd4afa7 | ||
|
|
57e3bd8b4d | ||
|
|
3bbec2ff5d | ||
|
|
155dbd4dd5 | ||
|
|
4bf3ab1511 | ||
|
|
6fd31613c2 | ||
|
|
d4cd4a9549 | ||
|
|
6596d0b4da | ||
|
|
dca2cfcecd | ||
|
|
076c293942 | ||
|
|
94417402d8 | ||
|
|
4659a8366d | ||
|
|
c3d54b312e | ||
|
|
14b551b027 | ||
|
|
43cd103248 | ||
|
|
bd4624c0ab | ||
|
|
b9539cc1f7 | ||
|
|
fc8bbf29d1 | ||
|
|
98e5442f24 | ||
|
|
c30ce3580a | ||
|
|
2ddf131e1a | ||
|
|
2f366bc3b7 | ||
|
|
49e90463e5 |
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,37 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug!
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug! -->
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen.-->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem.-->
|
||||
|
||||
**Platform Information (please complete the following information):**
|
||||
- OS: <!-- [e.g. Windows 10 2004 / Ubuntu 20.04] -->
|
||||
- Docker: <!-- [Yes/No] -->
|
||||
- .net Framework (Windows) or mono (macOS/Linux) (System -> Status): <!--[e.g. Mono 5.8, Mono 6.2, .net 4.5] -->
|
||||
- Browser and Version (Only needed for UI issues): <!--[e.g. chrome 86.0.4240.198] -->
|
||||
- Sonarr Version: <!--[e.g. 2.0.0.5344 , 3.0.4.1077]-->
|
||||
- Sonarr Branch: <!--[e.g. master, develop , phantom-develop]-->
|
||||
|
||||
**Trace Logs**
|
||||
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
|
||||
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
|
||||
<!-- Trace logs are named Sonarr.trace.txt or Sonarr.trace.#.txt and will contain "trace" in them-->
|
||||
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: Bug Report
|
||||
description: 'Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug!'
|
||||
labels: ['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.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: A concise description of what you're experiencing.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps To Reproduce
|
||||
description: Steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
examples:
|
||||
- **OS**: Ubuntu 20.04
|
||||
- **Sonarr**: Sonarr 3.0.6.1265
|
||||
- **Docker Install**: Yes
|
||||
- **Using Reverse Proxy**: No
|
||||
- **Browser**: Firefox 90 (If UI related)
|
||||
value: |
|
||||
- OS:
|
||||
- Sonarr:
|
||||
- Docker Install:
|
||||
- Using Reverse Proxy:
|
||||
- Browser:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What branch are you running?
|
||||
options:
|
||||
- Main
|
||||
- Develop
|
||||
- Other (This issue will be closed)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Trace Logs?
|
||||
description: |
|
||||
Trace Logs (https://wiki.servarr.com/sonarr/troubleshooting#logging-and-log-files)
|
||||
***Generally speaking, all bug reports must have trace logs provided.***
|
||||
*** Info Logs are not trace logs. If the logs do not say trace and are not from a file like `*.trace.*.txt` they are not trace logs.***
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? Screenshots? 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.
|
||||
validations:
|
||||
required: true
|
||||
required: true
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -10,5 +10,5 @@ contact_links:
|
||||
url: https://forums.sonarr.tv/
|
||||
about: Discuss and search through support topics.
|
||||
- name: Support via IRC
|
||||
url: http://webchat.freenode.net/?channels=#sonarr
|
||||
url: https://web.libera.chat/?channels=#sonarr
|
||||
about: Chat with users and devs on support and setup related topics.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for Sonarr
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
||||
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
38
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Feature Request
|
||||
description: 'Suggest an idea for Sonarr'
|
||||
labels: ['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.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe
|
||||
description: A clear and concise description of what the problem is.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: |
|
||||
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: true
|
||||
6
.github/SUPPORT.md
vendored
6
.github/SUPPORT.md
vendored
@@ -1,7 +1,7 @@
|
||||
## Support
|
||||
|
||||
There are a number of frequently asked questions that have been answered in our [FAQ](https://wiki.servarr.com/Sonarr_FAQ)
|
||||
There are a number of frequently asked questions that have been answered in our [FAQ](https://wiki.servarr.com/sonarr/faq)
|
||||
|
||||
The [wiki](https://wiki.servarr.com/Sonarr) contains other information and guides
|
||||
The [wiki](https://wiki.servarr.com/sonarr) contains other information and guides
|
||||
|
||||
Please use one of the support channels: [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord ](https://discord.gg/M6BvZn5), or [IRC ](http://webchat.freenode.net/?channels=#sonarr)for support/questions.
|
||||
Please use one of the support channels: [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord ](https://discord.gg/M6BvZn5), or [IRC](https://web.libera.chat/?channels=#sonarr)for support/questions.
|
||||
@@ -3,7 +3,7 @@
|
||||
We're always looking for people to help make Sonarr even better, there are a number of ways to contribute.
|
||||
|
||||
## Documentation ##
|
||||
Setup guides, [FAQ](https://wiki.servarr.com/Sonarr_FAQ), the more information we have on the [wiki](https://wiki.servarr.com/Sonarr) the better.
|
||||
Setup guides, [FAQ](https://wiki.servarr.com/sonarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/sonarr) the better.
|
||||
|
||||
## Development ##
|
||||
|
||||
@@ -17,26 +17,26 @@ Setup guides, [FAQ](https://wiki.servarr.com/Sonarr_FAQ), the more information w
|
||||
### Getting started ###
|
||||
|
||||
1. Fork Sonarr
|
||||
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)
|
||||
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
|
||||
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.
|
||||
4. Start webpack to monitor your dev environment for any frontend changes that need post processing using `yarn start` command.
|
||||
5. Build the project in Visual Studio, Setting startup project to `Sonarr.Console` and framework to `x86`
|
||||
6. Debug the project in Visual Studio
|
||||
7. Open http://localhost:8989
|
||||
|
||||
### Contributing Code ###
|
||||
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/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 Sonarr's develop (currently phantom-develop) branch, don't merge
|
||||
- Rebase from Sonarr'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 our [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord](https://discord.gg/Ex7FmFK), or [IRC](http://webchat.freenode.net/?channels=#sonarr) if you have any questions
|
||||
- Reach out to us on our [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord](https://discord.gg/Ex7FmFK), or [IRC](https://web.libera.chat/?channels=#sonarr) 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 should be the default for VS 2019 and WebStorm
|
||||
|
||||
### Pull Requesting ###
|
||||
- Only make pull requests to develop (currently phantom-develop), never master, if you make a PR to master we'll comment on it and close it
|
||||
- Only make pull requests to develop (currently `develop`), never `main`, 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)
|
||||
|
||||
19
README.md
19
README.md
@@ -5,7 +5,7 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
|
||||
## Getting Started
|
||||
|
||||
- [Download/Installation](https://sonarr.tv/#downloads-v3)
|
||||
- [FAQ](https://wiki.servarr.com/Sonarr_FAQ)
|
||||
- [FAQ](https://wiki.servarr.com/sonarr/faq)
|
||||
- [Wiki](https://wiki.servarr.com/Sonarr)
|
||||
- [(WIP) API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
|
||||
- [Donate](https://sonarr.tv/donate)
|
||||
@@ -16,9 +16,9 @@ Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
- [Forums](https://forums.sonarr.tv/)
|
||||
- [Discord](https://discord.gg/M6BvZn5)
|
||||
- [GitHub - Bugs and Feature Requests Only](https://github.com/Sonarr/Sonarr/issues)
|
||||
- [IRC ](http://webchat.freenode.net/?channels=#sonarr)
|
||||
- [IRC](https://web.libera.chat/?channels=#sonarr)
|
||||
- [Reddit](https://www.reddit.com/r/sonarr)
|
||||
- [Wiki](https://wiki.servarr.com/Sonarr)
|
||||
- [Wiki](https://wiki.servarr.com/sonarr)
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
### Development
|
||||
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||
|
||||
<a href="https://github.com/Sonarr/Sonarr/graphs/contributors"><img src="https://opencollective.com/Sonarr/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
### Supporters
|
||||
@@ -49,17 +50,17 @@ This project exists thanks to all the people who contribute. [Contribute](CONTRI
|
||||
This project would not be possible without the support of our users and software providers.
|
||||
[**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
|
||||
|
||||
#### Mega Sponsors
|
||||
|
||||
[](https://opencollective.com/sonarr/contribute/mega-sponsor-21443/checkout)
|
||||
|
||||
#### Sponsors
|
||||
|
||||
[](https://opencollective.com/sonarr/contribute/sponsor-21443/checkout)
|
||||
|
||||
#### Flexible Sponsors
|
||||
|
||||
[](https://opencollective.com/sonarr/contribute/flexible-sponsor-21457/checkout)
|
||||
[](https://opencollective.com/sonarr/contribute/sponsor-21457/checkout)
|
||||
|
||||
#### Backers
|
||||
|
||||
[](https://opencollective.com/sonarr/contribute/backer-21442/checkout)
|
||||
[](https://opencollective.com/sonarr/contribute/backer-21442/checkout)
|
||||
|
||||
#### JetBrains
|
||||
|
||||
|
||||
56
build.sh
56
build.sh
@@ -12,8 +12,11 @@ sourceFolder='./src'
|
||||
slnFile=$sourceFolder/Sonarr.sln
|
||||
updateSubFolder=Sonarr.Update
|
||||
|
||||
sqlitePackageDir="$HOME/.nuget/packages/system.data.sqlite.core.servarr/1.0.115.5-18"
|
||||
|
||||
nuget='tools/nuget/nuget.exe';
|
||||
vswhere='tools/vswhere/vswhere.exe';
|
||||
macho='tools/macho/MachOConverter.exe';
|
||||
|
||||
. ./version.sh
|
||||
|
||||
@@ -135,6 +138,9 @@ Build()
|
||||
|
||||
CleanFolder $outputFolder false
|
||||
|
||||
echo "Removing Sonarr.Update/sqlite3.dll"
|
||||
rm $outputFolder/Sonarr.Update/sqlite3.dll
|
||||
|
||||
echo "Removing Mono.Posix.dll"
|
||||
rm $outputFolder/Mono.Posix.dll
|
||||
|
||||
@@ -234,6 +240,7 @@ PackageMono()
|
||||
|
||||
echo "Removing native windows binaries Sqlite, MediaInfo"
|
||||
rm -f $outputFolderLinux/sqlite3.*
|
||||
rm -f $outputFolderLinux/Sonarr.Update/sqlite3.*
|
||||
rm -f $outputFolderLinux/MediaInfo.*
|
||||
|
||||
PatchMono $outputFolderLinux
|
||||
@@ -278,17 +285,22 @@ PackageMacOS()
|
||||
chmod +x $outputFolderMacOS/Sonarr
|
||||
|
||||
echo "Adding Sonarr.Update Launcher"
|
||||
cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOS/Sonarr.Update/
|
||||
mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak
|
||||
mv $outputFolderMacOS/Sonarr.Update/Launcher $outputFolderMacOS/Sonarr.Update/Sonarr.Update
|
||||
mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe
|
||||
CheckExitCode cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOS/Sonarr.Update/
|
||||
CheckExitCode mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak
|
||||
CheckExitCode mv $outputFolderMacOS/Sonarr.Update/Launcher $outputFolderMacOS/Sonarr.Update/Sonarr.Update
|
||||
CheckExitCode mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe
|
||||
chmod +x $outputFolderMacOS/Sonarr.Update/Sonarr.Update
|
||||
|
||||
echo "Adding sqlite dylibs"
|
||||
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOS
|
||||
echo "Adding sqlite dylib"
|
||||
CheckExitCode cp "$sqlitePackageDir/runtimes/osx-x64/native/net46"/*.config $outputFolderMacOS/
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
CheckExitCode $macho merge $outputFolderMacOS "$sqlitePackageDir/runtimes/osx-x64/native/net46"/*.dylib "$sqlitePackageDir/runtimes/osx-arm64/native/net46"/*.dylib
|
||||
else
|
||||
CheckExitCode mono $macho merge $outputFolderMacOS "$sqlitePackageDir/runtimes/osx-x64/native/net46"/*.dylib "$sqlitePackageDir/runtimes/osx-arm64/native/net46"/*.dylib
|
||||
fi
|
||||
|
||||
echo "Adding MediaInfo dylib"
|
||||
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOS
|
||||
CheckExitCode cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOS/
|
||||
|
||||
ProgressEnd 'Creating MacOS Package'
|
||||
}
|
||||
@@ -297,29 +309,37 @@ PackageMacOSApp()
|
||||
{
|
||||
ProgressStart 'Creating macOS App Package'
|
||||
|
||||
outputFolderMacOSAppBase=$outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
||||
outputFolderMacOSAppBin=$outputFolderMacOSAppBase/bin
|
||||
|
||||
rm -rf $outputFolderMacOSApp
|
||||
mkdir $outputFolderMacOSApp
|
||||
cp -r ./distribution/osx/Sonarr.app $outputFolderMacOSApp
|
||||
mkdir -p $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
||||
mkdir -p $outputFolderMacOSAppBase
|
||||
|
||||
echo "Adding Sonarr Launcher"
|
||||
cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/
|
||||
mv $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Launcher $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr
|
||||
chmod +x $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr
|
||||
CheckExitCode cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOSAppBase/
|
||||
CheckExitCode mv $outputFolderMacOSAppBase/Launcher $outputFolderMacOSAppBase/Sonarr
|
||||
chmod +x $outputFolderMacOSAppBase/Sonarr
|
||||
|
||||
echo "Copying Binaries"
|
||||
mkdir -p $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin
|
||||
cp -r $outputFolderLinux/* $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
|
||||
mkdir -p $outputFolderMacOSAppBin
|
||||
CheckExitCode cp -r $outputFolderLinux/* $outputFolderMacOSAppBin
|
||||
|
||||
echo "Adding sqlite dylib"
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
CheckExitCode $macho merge $outputFolderMacOSAppBin "$sqlitePackageDir/runtimes/osx-x64/native/net46" "$sqlitePackageDir/runtimes/osx-arm64/native/net46"
|
||||
else
|
||||
CheckExitCode mono $macho merge $outputFolderMacOSAppBin "$sqlitePackageDir/runtimes/osx-x64/native/net46" "$sqlitePackageDir/runtimes/osx-arm64/native/net46"
|
||||
fi
|
||||
|
||||
echo "Adding sqlite dylibs"
|
||||
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
|
||||
|
||||
echo "Adding MediaInfo dylib"
|
||||
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
|
||||
CheckExitCode cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOSAppBin/
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/Sonarr.Update
|
||||
echo "# Do Not Edit\nPackageVersion=${BUILD_NUMBER}\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=${BUILD_NUMBER}\nUpdateMethod=$PackageUpdater\nBranch=${Branch:-master}" > $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/package_info
|
||||
rm -r $outputFolderMacOSAppBin/Sonarr.Update
|
||||
echo "# Do Not Edit\nPackageVersion=${BUILD_NUMBER}\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=${BUILD_NUMBER}\nUpdateMethod=$PackageUpdater\nBranch=${Branch:-master}" > $outputFolderMacOSAppBase/package_info
|
||||
|
||||
ProgressEnd 'Creating macOS App Package'
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import BlacklistRowConnector from './BlacklistRowConnector';
|
||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
||||
|
||||
class Blacklist extends Component {
|
||||
class Blocklist extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -100,8 +100,8 @@ class Blacklist extends Component {
|
||||
columns,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlacklistExecuting,
|
||||
onClearBlacklistPress,
|
||||
isClearingBlocklistExecuting,
|
||||
onClearBlocklistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -115,7 +115,7 @@ class Blacklist extends Component {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title="Blacklist">
|
||||
<PageContent title="Blocklist">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
@@ -129,8 +129,8 @@ class Blacklist extends Component {
|
||||
<PageToolbarButton
|
||||
label="Clear"
|
||||
iconName={icons.CLEAR}
|
||||
isSpinning={isClearingBlacklistExecuting}
|
||||
onPress={onClearBlacklistPress}
|
||||
isSpinning={isClearingBlocklistExecuting}
|
||||
onPress={onClearBlocklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
@@ -155,13 +155,13 @@ class Blacklist extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load blacklist</div>
|
||||
<div>Unable to load blocklist</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
No history blacklist
|
||||
No history blocklist
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ class Blacklist extends Component {
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlacklistRowConnector
|
||||
<BlocklistRowConnector
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
@@ -206,7 +206,7 @@ class Blacklist extends Component {
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Remove Selected"
|
||||
message={'Are you sure you want to remove the selected items from the blacklist?'}
|
||||
message={'Are you sure you want to remove the selected items from the blocklist?'}
|
||||
confirmLabel="Remove Selected"
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
@@ -216,7 +216,7 @@ class Blacklist extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
Blacklist.propTypes = {
|
||||
Blocklist.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
@@ -224,9 +224,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;
|
||||
@@ -5,30 +5,30 @@ import { createSelector } from 'reselect';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Blacklist from './Blacklist';
|
||||
import Blocklist from './Blocklist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blacklist,
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
|
||||
(blacklist, isClearingBlacklistExecuting) => {
|
||||
(state) => state.blocklist,
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||
(blocklist, isClearingBlocklistExecuting) => {
|
||||
return {
|
||||
isClearingBlacklistExecuting,
|
||||
...blacklist
|
||||
isClearingBlocklistExecuting,
|
||||
...blocklist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...blacklistActions,
|
||||
...blocklistActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class BlacklistConnector extends Component {
|
||||
class BlocklistConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -36,27 +36,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);
|
||||
}
|
||||
|
||||
@@ -64,56 +64,56 @@ 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 });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlacklistTableOption(payload);
|
||||
this.props.setBlocklistTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ class BlacklistConnector extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Blacklist
|
||||
<Blocklist
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
@@ -131,30 +131,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)
|
||||
);
|
||||
@@ -9,7 +9,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
||||
class BlacklistDetailsModal extends Component {
|
||||
class BlocklistDetailsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -77,7 +77,7 @@ class BlacklistDetailsModal extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
BlacklistDetailsModal.propTypes = {
|
||||
BlocklistDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
@@ -86,4 +86,4 @@ BlacklistDetailsModal.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlacklistDetailsModal;
|
||||
export default BlocklistDetailsModal;
|
||||
@@ -9,10 +9,10 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||
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
|
||||
@@ -152,7 +152,7 @@ class BlacklistRow extends Component {
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title="Remove from blacklist"
|
||||
title="Remove from blocklist"
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRemovePress}
|
||||
@@ -165,7 +165,7 @@ class BlacklistRow extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
<BlacklistDetailsModal
|
||||
<BlocklistDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
@@ -179,7 +179,7 @@ class BlacklistRow extends Component {
|
||||
|
||||
}
|
||||
|
||||
BlacklistRow.propTypes = {
|
||||
BlocklistRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
series: PropTypes.object.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
@@ -195,4 +195,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 createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||
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);
|
||||
@@ -24,8 +24,10 @@ function HistoryDetails(props) {
|
||||
indexer,
|
||||
releaseGroup,
|
||||
preferredWordScore,
|
||||
seriesMatchType,
|
||||
nzbInfoUrl,
|
||||
downloadClient,
|
||||
downloadClientName,
|
||||
downloadId,
|
||||
age,
|
||||
ageHours,
|
||||
@@ -33,6 +35,8 @@ function HistoryDetails(props) {
|
||||
publishedDate
|
||||
} = data;
|
||||
|
||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
@@ -69,6 +73,16 @@ function HistoryDetails(props) {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
seriesMatchType ?
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Series Match Type"
|
||||
data={seriesMatchType}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
nzbInfoUrl ?
|
||||
<span>
|
||||
@@ -84,10 +98,10 @@ function HistoryDetails(props) {
|
||||
}
|
||||
|
||||
{
|
||||
downloadClient ?
|
||||
downloadClientNameInfo ?
|
||||
<DescriptionListItem
|
||||
title="Download Client"
|
||||
data={downloadClient}
|
||||
data={downloadClientNameInfo}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
@@ -207,7 +221,7 @@ function HistoryDetails(props) {
|
||||
reasonMessage = 'File was deleted by via UI';
|
||||
break;
|
||||
case 'MissingFromDisk':
|
||||
reasonMessage = 'Sonarr was unable to find the file on disk so it was removed';
|
||||
reasonMessage = 'Sonarr was unable to find the file on disk so the file was unlinked from the episode in the database';
|
||||
break;
|
||||
case 'Upgrade':
|
||||
reasonMessage = 'File was deleted to import an upgrade';
|
||||
|
||||
@@ -217,6 +217,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -279,6 +279,17 @@ class Queue extends Component {
|
||||
return !!(item && item.seriesId && item.episodeId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,7 @@ import QueueStatusCell from './QueueStatusCell';
|
||||
import TimeleftCell from './TimeleftCell';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import styles from './QueueRow.css';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
|
||||
class QueueRow extends Component {
|
||||
|
||||
@@ -41,14 +42,14 @@ class QueueRow extends Component {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blacklist) => {
|
||||
onRemoveQueueItemModalConfirmed = (blocklist) => {
|
||||
const {
|
||||
onRemoveQueueItemPress,
|
||||
onQueueRowModalOpenOrClose
|
||||
} = this.props;
|
||||
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
onRemoveQueueItemPress(blacklist);
|
||||
onRemoveQueueItemPress(blocklist);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
@@ -280,6 +281,12 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell key={name}>{formatBytes(size)}</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'outputPath') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
@@ -370,6 +377,7 @@ class QueueRow extends Component {
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!series}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ class RemoveQueueItemModal extends Component {
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
blocklist: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class RemoveQueueItemModal extends Component {
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
blocklist: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ class RemoveQueueItemModal extends Component {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
}
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
@@ -65,10 +65,11 @@ class RemoveQueueItemModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist } = this.state;
|
||||
const { remove, blocklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -88,28 +89,32 @@ class RemoveQueueItemModal extends Component {
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
<FormLabel>Add Release To Blocklist</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText="Starts a search for this episode again and prevents this release from being grabbed again"
|
||||
onChange={this.onBlacklistChange}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -137,6 +142,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
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
blocklist: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
blocklist: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ class RemoveQueueItemsModal extends Component {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
}
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
@@ -66,10 +66,11 @@ class RemoveQueueItemsModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist } = this.state;
|
||||
const { remove, blocklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -89,30 +90,34 @@ class RemoveQueueItemsModal extends Component {
|
||||
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Blacklist Release{selectedCount > 1 ? 's' : ''}
|
||||
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText="Prevents Sonarr from automatically grabbing this episode again"
|
||||
onChange={this.onBlacklistChange}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -140,6 +145,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
|
||||
};
|
||||
|
||||
@@ -154,7 +154,7 @@ class AddNewSeries extends Component {
|
||||
<div className={styles.noResults}>Couldn't find any results for '{term}'</div>
|
||||
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
|
||||
<div>
|
||||
<Link to="https://wiki.servarr.com/Sonarr_FAQ#Why_cant_I_add_a_new_series_when_I_know_the_TVDB_ID">
|
||||
<Link to="https://wiki.servarr.com/sonarr/faq#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
|
||||
Why can't I find my show?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.series {
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
@@ -6,3 +7,19 @@
|
||||
background-color: $menuItemHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
.series {
|
||||
flex: 1 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tvdbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin-left: auto;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.tvdbLinkIcon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@@ -1,33 +1,28 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||
import styles from './ImportSeriesSearchResult.css';
|
||||
|
||||
class ImportSeriesSearchResult extends Component {
|
||||
function ImportSeriesSearchResult(props) {
|
||||
const {
|
||||
tvdbId,
|
||||
title,
|
||||
year,
|
||||
network,
|
||||
isExistingSeries,
|
||||
onPress
|
||||
} = props;
|
||||
|
||||
//
|
||||
// Listeners
|
||||
const onPressCallback = useCallback(() => onPress(tvdbId), [tvdbId, onPress]);
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.tvdbId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
year,
|
||||
network,
|
||||
isExistingSeries
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Link
|
||||
className={styles.series}
|
||||
onPress={this.onPress}
|
||||
onPress={onPressCallback}
|
||||
>
|
||||
<ImportSeriesTitle
|
||||
title={title}
|
||||
@@ -36,8 +31,19 @@ class ImportSeriesSearchResult extends Component {
|
||||
isExistingSeries={isExistingSeries}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.tvdbLink}
|
||||
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||
>
|
||||
<Icon
|
||||
className={styles.tvdbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={16}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImportSeriesSearchResult.propTypes = {
|
||||
|
||||
@@ -20,24 +20,27 @@ function ImportSeriesTitle(props) {
|
||||
|
||||
{
|
||||
!title.contains(year) &&
|
||||
year > 0 &&
|
||||
year > 0 ?
|
||||
<span className={styles.year}>
|
||||
({year})
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!network &&
|
||||
<Label>{network}</Label>
|
||||
network ?
|
||||
<Label>{network}</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isExistingSeries &&
|
||||
isExistingSeries ?
|
||||
<Label
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
Existing
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,3 +30,9 @@
|
||||
.importButtonIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.addErrorAlert {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -47,21 +48,27 @@ class ImportSeriesSelectFolder extends Component {
|
||||
isWindows,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
isSaving,
|
||||
error,
|
||||
saveError,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const hasRootFolders = items.length > 0;
|
||||
|
||||
return (
|
||||
<PageContent title="Import Series">
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
isFetching && !isPopulated ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load root folders</div>
|
||||
!isFetching && error ?
|
||||
<div>Unable to load root folders</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -87,7 +94,7 @@ class ImportSeriesSelectFolder extends Component {
|
||||
</div>
|
||||
|
||||
{
|
||||
items.length > 0 ?
|
||||
hasRootFolders ?
|
||||
<div className={styles.recentFolders}>
|
||||
<FieldSet legend="Root Folders">
|
||||
<RootFolders
|
||||
@@ -97,35 +104,51 @@ class ImportSeriesSelectFolder extends Component {
|
||||
items={items}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={this.onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.importButtonIcon}
|
||||
name={icons.DRIVE}
|
||||
/>
|
||||
Choose another folder
|
||||
</Button>
|
||||
</div> :
|
||||
|
||||
<div className={styles.startImport}>
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={this.onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.importButtonIcon}
|
||||
name={icons.DRIVE}
|
||||
/>
|
||||
Start Import
|
||||
</Button>
|
||||
</div>
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isSaving && saveError ?
|
||||
<Alert
|
||||
className={styles.addErrorAlert}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
Unable to add root folder
|
||||
|
||||
<ul>
|
||||
{
|
||||
saveError.responseJSON.map((e, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{e.errorMessage}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className={hasRootFolders ? undefined : styles.startImport}>
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={this.onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.importButtonIcon}
|
||||
name={icons.DRIVE}
|
||||
/>
|
||||
{
|
||||
hasRootFolders ?
|
||||
'Choose another folder' :
|
||||
'Start Import'
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<FileBrowserModal
|
||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
||||
name="rootFolderPath"
|
||||
@@ -145,7 +168,9 @@ ImportSeriesSelectFolder.propTypes = {
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
saveError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title="Sonarr">
|
||||
<DocumentTitle title={window.Sonarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
|
||||
@@ -13,13 +13,13 @@ import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnecto
|
||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
|
||||
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
import Settings from 'Settings/Settings';
|
||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||
import Profiles from 'Settings/Profiles/Profiles';
|
||||
import Quality from 'Settings/Quality/Quality';
|
||||
import QualityConnector from 'Settings/Quality/QualityConnector';
|
||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
@@ -118,8 +118,8 @@ function AppRoutes(props) {
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/activity/blacklist"
|
||||
component={BlacklistConnector}
|
||||
path="/activity/blocklist"
|
||||
component={BlocklistConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
@@ -158,7 +158,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/settings/quality"
|
||||
component={Quality}
|
||||
component={QualityConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
@@ -27,7 +27,7 @@ function ConnectionLostModal(props) {
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
Sonarr has lost it's connection to the backend and will need to be reloaded to restore functionality.
|
||||
Sonarr has lost its connection to the backend and will need to be reloaded to restore functionality.
|
||||
</div>
|
||||
|
||||
<div className={styles.automatic}>
|
||||
|
||||
@@ -50,6 +50,7 @@ class AgendaEvent extends Component {
|
||||
absoluteEpisodeNumber,
|
||||
airDateUtc,
|
||||
monitored,
|
||||
unverifiedSceneNumbering,
|
||||
hasFile,
|
||||
grabbed,
|
||||
queueItem,
|
||||
@@ -70,7 +71,7 @@ class AgendaEvent extends Component {
|
||||
const statusStyle = getStatusStyle(hasFile, downloading, startTime, endTime, isMonitored);
|
||||
const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
|
||||
const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
|
||||
const seasonStatistics = season.statistics || {};
|
||||
const seasonStatistics = season?.statistics || {};
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -131,6 +132,16 @@ class AgendaEvent extends Component {
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
unverifiedSceneNumbering && !missingAbsoluteNumber ?
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.WARNING}
|
||||
title="Scene number hasn't been verified yet"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!queueItem &&
|
||||
<span className={styles.statusIcon}>
|
||||
@@ -237,6 +248,7 @@ AgendaEvent.propTypes = {
|
||||
absoluteEpisodeNumber: PropTypes.number,
|
||||
airDateUtc: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
unverifiedSceneNumbering: PropTypes.bool,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
grabbed: PropTypes.bool,
|
||||
queueItem: PropTypes.object,
|
||||
|
||||
@@ -55,6 +55,7 @@ class CalendarEvent extends Component {
|
||||
absoluteEpisodeNumber,
|
||||
airDateUtc,
|
||||
monitored,
|
||||
unverifiedSceneNumbering,
|
||||
hasFile,
|
||||
grabbed,
|
||||
queueItem,
|
||||
@@ -78,7 +79,7 @@ class CalendarEvent extends Component {
|
||||
const statusStyle = getStatusStyle(hasFile, isDownloading, startTime, endTime, isMonitored);
|
||||
const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
|
||||
const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
|
||||
const seasonStatistics = season.statistics || {};
|
||||
const seasonStatistics = season?.statistics || {};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -108,6 +109,16 @@ class CalendarEvent extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
unverifiedSceneNumbering && !missingAbsoluteNumber ?
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.WARNING}
|
||||
title="Scene number hasn't been verified yet"
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
queueItem ?
|
||||
<span className={styles.statusIcon}>
|
||||
@@ -244,6 +255,7 @@ CalendarEvent.propTypes = {
|
||||
absoluteEpisodeNumber: PropTypes.number,
|
||||
airDateUtc: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
unverifiedSceneNumbering: PropTypes.bool,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
grabbed: PropTypes.bool,
|
||||
queueItem: PropTypes.object,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const APPLICATION_UPDATE = 'ApplicationUpdate';
|
||||
export const BACKUP = 'Backup';
|
||||
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
|
||||
export const CLEAR_BLACKLIST = 'ClearBlacklist';
|
||||
export const CLEAR_BLOCKLIST = 'ClearBlocklist';
|
||||
export const CLEAR_LOGS = 'ClearLog';
|
||||
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
|
||||
export const DELETE_LOG_FILES = 'DeleteLogFiles';
|
||||
@@ -15,6 +15,7 @@ export const REFRESH_SERIES = 'RefreshSeries';
|
||||
export const RENAME_FILES = 'RenameFiles';
|
||||
export const RENAME_SERIES = 'RenameSeries';
|
||||
export const RESET_API_KEY = 'ResetApiKey';
|
||||
export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
|
||||
export const RSS_SYNC = 'RssSync';
|
||||
export const SEASON_SEARCH = 'SeasonSearch';
|
||||
export const SERIES_SEARCH = 'SeriesSearch';
|
||||
|
||||
@@ -128,7 +128,7 @@ class FileBrowserModalContent extends Component {
|
||||
className={styles.mappedDrivesWarning}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/Sonarr_FAQ#Why_cant_Sonarr_see_my_files_on_a_remote_server">FAQ</Link> for more information.
|
||||
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server">FAQ</Link> for more information.
|
||||
</Alert>
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ class DateFilterBuilderRowValue extends Component {
|
||||
<TextInput
|
||||
name={NAME}
|
||||
value={filterValue}
|
||||
type="date"
|
||||
placeholder="yyyy-mm-dd"
|
||||
onChange={this.onValueChange}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.downloadClients,
|
||||
(state, { includeAny }) => includeAny,
|
||||
(state, { protocol }) => protocol,
|
||||
(downloadClients, includeAny, protocolFilter) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = downloadClients;
|
||||
|
||||
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
||||
|
||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||
return {
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchDownloadClients: fetchDownloadClients
|
||||
};
|
||||
|
||||
class DownloadClientSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchDownloadClients();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value: parseInt(value) });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadClientSelectInputConnector.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeAny: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dispatchFetchDownloadClients: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DownloadClientSelectInputConnector.defaultProps = {
|
||||
includeAny: false,
|
||||
protocol: 'torrent'
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);
|
||||
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import classNames from 'classnames';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { icons, sizes, scrollDirections } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
|
||||
@@ -6,6 +6,7 @@ import AutoCompleteInput from './AutoCompleteInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
import DeviceInputConnector from './DeviceInputConnector';
|
||||
import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
|
||||
import NumberInput from './NumberInput';
|
||||
@@ -68,6 +69,9 @@ function getComponent(type) {
|
||||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||
return DownloadClientSelectInputConnector;
|
||||
|
||||
case inputTypes.ROOT_FOLDER_SELECT:
|
||||
return RootFolderSelectInputConnector;
|
||||
|
||||
|
||||
@@ -8,8 +8,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
.keyInputWrapper {
|
||||
flex: 6 0 0;
|
||||
}
|
||||
|
||||
.valueInputWrapper {
|
||||
flex: 1 0 0;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
|
||||
@@ -63,7 +63,7 @@ class KeyValueListInputItem extends Component {
|
||||
|
||||
return (
|
||||
<div className={styles.itemContainer}>
|
||||
<div className={styles.inputWrapper}>
|
||||
<div className={styles.keyInputWrapper}>
|
||||
<TextInput
|
||||
className={styles.keyInput}
|
||||
name="key"
|
||||
@@ -75,7 +75,7 @@ class KeyValueListInputItem extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputWrapper}>
|
||||
<div className={styles.valueInputWrapper}>
|
||||
<TextInput
|
||||
className={styles.valueInput}
|
||||
name="value"
|
||||
|
||||
@@ -2,9 +2,18 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.link {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.linkWithEdit {
|
||||
max-width: calc(100% - 9px - 4px - 2px);
|
||||
}
|
||||
|
||||
.editContainer {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
@@ -15,5 +24,11 @@
|
||||
.editButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
width: auto;
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MiddleTruncate from 'react-middle-truncate';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||
import Label from 'Components/Label';
|
||||
@@ -48,20 +49,26 @@ class TagInputTag extends Component {
|
||||
kind,
|
||||
canEdit
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.tag}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Label
|
||||
className={styles.label}
|
||||
kind={kind}
|
||||
>
|
||||
<Link
|
||||
className={canEdit ? styles.linkWithEdit : styles.link}
|
||||
tabIndex={-1}
|
||||
onPress={this.onDelete}
|
||||
>
|
||||
|
||||
{tag.name}
|
||||
<MiddleTruncate
|
||||
text={tag.name}
|
||||
start={10}
|
||||
end={10}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{
|
||||
|
||||
@@ -46,13 +46,13 @@ class TextTagInputConnector extends Component {
|
||||
// to oddities with restrictions (as an example).
|
||||
|
||||
const newValue = [...valueArray];
|
||||
const newTags = split(tag.name);
|
||||
const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
|
||||
|
||||
newTags.forEach((newTag) => {
|
||||
newValue.push(newTag.trim());
|
||||
});
|
||||
|
||||
onChange({ name, value: newValue.join(',') });
|
||||
onChange({ name, value: newValue });
|
||||
}
|
||||
|
||||
onTagDelete = ({ index }) => {
|
||||
@@ -67,7 +67,7 @@ class TextTagInputConnector extends Component {
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: newValue.join(',')
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,3 +2,7 @@
|
||||
margin-right: 5px;
|
||||
color: $themeRed;
|
||||
}
|
||||
|
||||
.rating {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import styles from './HeartRating.css';
|
||||
|
||||
function HeartRating({ rating, iconSize }) {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.rating}>
|
||||
<Icon
|
||||
className={styles.heart}
|
||||
name={icons.HEART}
|
||||
|
||||
@@ -57,6 +57,7 @@ class FilterMenu extends Component {
|
||||
>
|
||||
<ButtonComponent
|
||||
iconName={icons.FILTER}
|
||||
showIndicator={selectedFilterKey !== 'all'}
|
||||
text="Filter"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
.menuButton {
|
||||
composes: menuButton from '~./MenuButton.css';
|
||||
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.indicatorContainer {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import styles from './PageMenuButton.css';
|
||||
|
||||
function PageMenuButton(props) {
|
||||
const {
|
||||
iconName,
|
||||
showIndicator,
|
||||
text,
|
||||
...otherProps
|
||||
} = props;
|
||||
@@ -21,6 +24,22 @@ function PageMenuButton(props) {
|
||||
size={18}
|
||||
/>
|
||||
|
||||
{
|
||||
showIndicator ?
|
||||
<span
|
||||
className={classNames(
|
||||
styles.indicatorContainer,
|
||||
'fa-layers fa-fw'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CIRCLE}
|
||||
size={9}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className={styles.label}>
|
||||
{text}
|
||||
</div>
|
||||
@@ -30,7 +49,12 @@ function PageMenuButton(props) {
|
||||
|
||||
PageMenuButton.propTypes = {
|
||||
iconName: PropTypes.object.isRequired,
|
||||
showIndicator: PropTypes.bool.isRequired,
|
||||
text: PropTypes.string
|
||||
};
|
||||
|
||||
PageMenuButton.defaultProps = {
|
||||
showIndicator: false
|
||||
};
|
||||
|
||||
export default PageMenuButton;
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.indicatorContainer {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.labelContainer {
|
||||
composes: labelContainer from '~Components/Page/Toolbar/PageToolbarButton.css';
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import styles from './ToolbarMenuButton.css';
|
||||
|
||||
function ToolbarMenuButton(props) {
|
||||
const {
|
||||
iconName,
|
||||
showIndicator,
|
||||
text,
|
||||
...otherProps
|
||||
} = props;
|
||||
@@ -22,6 +25,22 @@ function ToolbarMenuButton(props) {
|
||||
size={21}
|
||||
/>
|
||||
|
||||
{
|
||||
showIndicator ?
|
||||
<span
|
||||
className={classNames(
|
||||
styles.indicatorContainer,
|
||||
'fa-layers fa-fw'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CIRCLE}
|
||||
size={9}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{text}
|
||||
@@ -34,7 +53,12 @@ function ToolbarMenuButton(props) {
|
||||
|
||||
ToolbarMenuButton.propTypes = {
|
||||
iconName: PropTypes.object.isRequired,
|
||||
showIndicator: PropTypes.bool.isRequired,
|
||||
text: PropTypes.string
|
||||
};
|
||||
|
||||
ToolbarMenuButton.defaultProps = {
|
||||
showIndicator: false
|
||||
};
|
||||
|
||||
export default ToolbarMenuButton;
|
||||
|
||||
@@ -5,7 +5,7 @@ import FocusLock from 'react-focus-lock';
|
||||
import classNames from 'classnames';
|
||||
import elementClass from 'element-class';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import { isIOS } from 'Utilities/mobile';
|
||||
import { isIOS } from 'Utilities/browser';
|
||||
import { setScrollLock } from 'Utilities/scrollLock';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
|
||||
@@ -14,7 +14,7 @@ function PageContent(props) {
|
||||
|
||||
return (
|
||||
<ErrorBoundary errorComponent={PageContentError}>
|
||||
<DocumentTitle title={title ? `${title} - Sonarr` : 'Sonarr'}>
|
||||
<DocumentTitle title={title ? `${title} - ${window.Sonarr.instanceName}` : window.Sonarr.instanceName}>
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isMobile, isFirefox } from 'Utilities/browser';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||
@@ -15,7 +15,8 @@ class PageContentBody extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._isMobile = isMobileUtil();
|
||||
this._isMobile = isMobile();
|
||||
this._isSmallScreenFirefox = isFirefox && window.innerWidth < 768;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -41,7 +42,9 @@ class PageContentBody extends Component {
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
||||
const ScrollerComponent = this._isMobile || this._isSmallScreenFirefox ?
|
||||
Scroller :
|
||||
OverlayScroller;
|
||||
|
||||
return (
|
||||
<ScrollerComponent
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||
.contentFooter {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ class PageJumpBar extends Component {
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ height }) => {
|
||||
this.setState({ height });
|
||||
if (height > 0) {
|
||||
this.setState({ height });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -64,8 +64,8 @@ const links = [
|
||||
to: '/activity/history'
|
||||
},
|
||||
{
|
||||
title: 'Blacklist',
|
||||
to: '/activity/blacklist'
|
||||
title: 'Blocklist',
|
||||
to: '/activity/blocklist'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import { fetchHealth } from 'Store/Actions/systemActions';
|
||||
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchTags, fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
import { fetchQualityDefinitions } from 'Store/Actions/settingsActions';
|
||||
|
||||
function getState(status) {
|
||||
switch (status) {
|
||||
@@ -70,6 +71,7 @@ const mapDispatchToProps = {
|
||||
dispatchUpdateItem: updateItem,
|
||||
dispatchRemoveItem: removeItem,
|
||||
dispatchFetchHealth: fetchHealth,
|
||||
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
|
||||
dispatchFetchQueue: fetchQueue,
|
||||
dispatchFetchQueueDetails: fetchQueueDetails,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
@@ -221,6 +223,10 @@ class SignalRConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleQualitydefinition = () => {
|
||||
this.props.dispatchFetchQualityDefinitions();
|
||||
}
|
||||
|
||||
handleQueue = () => {
|
||||
if (this.props.isQueuePopulated) {
|
||||
this.props.dispatchFetchQueue();
|
||||
@@ -377,6 +383,7 @@ SignalRConnector.propTypes = {
|
||||
dispatchUpdateItem: PropTypes.func.isRequired,
|
||||
dispatchRemoveItem: PropTypes.func.isRequired,
|
||||
dispatchFetchHealth: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
|
||||
dispatchFetchQueue: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
|
||||
@@ -7,6 +7,8 @@ import { WindowScroller, Grid } from 'react-virtualized';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import styles from './VirtualTable.css';
|
||||
|
||||
const ROW_HEIGHT = 38;
|
||||
|
||||
function overscanIndicesGetter(options) {
|
||||
const {
|
||||
cellCount,
|
||||
@@ -37,7 +39,8 @@ class VirtualTable extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0
|
||||
width: 0,
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
@@ -45,17 +48,32 @@ class VirtualTable extends Component {
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items
|
||||
items,
|
||||
scrollIndex,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width
|
||||
width,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== undefined && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: scrollIndex,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -85,9 +103,8 @@ class VirtualTable extends Component {
|
||||
scroller,
|
||||
header,
|
||||
headerHeight,
|
||||
rowRenderer,
|
||||
rowHeight,
|
||||
scrollIndex,
|
||||
rowRenderer,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -117,11 +134,6 @@ class VirtualTable extends Component {
|
||||
if (!height) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const finalScrollTop = scrollIndex == null ?
|
||||
scrollTop :
|
||||
scrollIndex * rowHeight;
|
||||
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
@@ -134,6 +146,7 @@ class VirtualTable extends Component {
|
||||
{header}
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
{...otherProps}
|
||||
ref={this.setGridRef}
|
||||
autoContainerWidth={true}
|
||||
autoHeight={true}
|
||||
@@ -145,7 +158,7 @@ class VirtualTable extends Component {
|
||||
rowCount={items.length}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
scrollTop={finalScrollTop}
|
||||
scrollTop={scrollTop}
|
||||
onScroll={onChildScroll}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={rowRenderer}
|
||||
@@ -155,7 +168,6 @@ class VirtualTable extends Component {
|
||||
className={styles.tableBodyContainer}
|
||||
style={gridStyle}
|
||||
containerStyle={containerStyle}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
</Scroller>
|
||||
@@ -173,6 +185,7 @@ VirtualTable.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
scrollIndex: PropTypes.number,
|
||||
scrollTop: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
header: PropTypes.node.isRequired,
|
||||
headerHeight: PropTypes.number.isRequired,
|
||||
@@ -182,7 +195,8 @@ VirtualTable.propTypes = {
|
||||
|
||||
VirtualTable.defaultProps = {
|
||||
className: styles.tableContainer,
|
||||
headerHeight: 38
|
||||
headerHeight: 38,
|
||||
rowHeight: ROW_HEIGHT
|
||||
};
|
||||
|
||||
export default VirtualTable;
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import classNames from 'classnames';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Portal from 'Components/Portal';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
|
||||
@@ -8,7 +8,7 @@ function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||
history
|
||||
} = props;
|
||||
|
||||
const scrollTop = history.action === 'POP' ?
|
||||
const scrollTop = history.action === 'POP' || (history.location.state && history.location.state.restoreScrollPosition) ?
|
||||
scrollPositions[scrollPositionKey] :
|
||||
0;
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EpisodeFileEditorModalContentConnector from './EpisodeFileEditorModalContentConnector';
|
||||
|
||||
function EpisodeFileEditorModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{
|
||||
isOpen &&
|
||||
<EpisodeFileEditorModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EpisodeFileEditorModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EpisodeFileEditorModal;
|
||||
@@ -1,8 +0,0 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.selectInput {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
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 { kinds } from 'Helpers/Props';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import SeasonNumber from 'Season/SeasonNumber';
|
||||
import EpisodeFileEditorRow from './EpisodeFileEditorRow';
|
||||
import styles from './EpisodeFileEditorModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'episodeNumber',
|
||||
label: 'Episode',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: 'Relative Path',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'airDateUtc',
|
||||
label: 'Air Date',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: 'Language',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class EpisodeFileEditorModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmDeleteModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState((state) => {
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
const selectedIds = getSelectedIds(this.state.selectedState);
|
||||
|
||||
return selectedIds.reduce((acc, id) => {
|
||||
const matchingItem = this.props.items.find((item) => item.id === id);
|
||||
|
||||
if (matchingItem && !acc.includes(matchingItem.episodeFileId)) {
|
||||
acc.push(matchingItem.episodeFileId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
//
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
onDeletePress = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmDelete = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
this.props.onDeletePress(this.getSelectedIds());
|
||||
}
|
||||
|
||||
onConfirmDeleteModalClose = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
}
|
||||
|
||||
onLanguageChange = ({ value }) => {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
if (!selectedIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onLanguageChange(selectedIds, parseInt(value));
|
||||
}
|
||||
|
||||
onQualityChange = ({ value }) => {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
if (!selectedIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onQualityChange(selectedIds, parseInt(value));
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
seasonNumber,
|
||||
isDeleting,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
languages,
|
||||
qualities,
|
||||
seriesType,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmDeleteModalOpen
|
||||
} = this.state;
|
||||
|
||||
const languageOptions = _.reduceRight(languages, (acc, language) => {
|
||||
acc.push({
|
||||
key: language.id,
|
||||
value: language.name
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, [{ key: 'selectLanguage', value: 'Select Language', disabled: true }]);
|
||||
|
||||
const qualityOptions = _.reduceRight(qualities, (acc, quality) => {
|
||||
acc.push({
|
||||
key: quality.id,
|
||||
value: quality.name
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]);
|
||||
|
||||
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manage Episodes {seasonNumber != null && <SeasonNumber seasonNumber={seasonNumber} />}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching && !isPopulated ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<div>{error}</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !items.length ?
|
||||
<div>
|
||||
No episode files to manage.
|
||||
</div>:
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && items.length ?
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<EpisodeFileEditorRow
|
||||
key={item.id}
|
||||
seriesType={seriesType}
|
||||
isSelected={selectedState[item.id]}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<div className={styles.actions}>
|
||||
<SpinnerButton
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onPress={this.onDeletePress}
|
||||
>
|
||||
Delete
|
||||
</SpinnerButton>
|
||||
|
||||
<div className={styles.selectInput}>
|
||||
<SelectInput
|
||||
name="language"
|
||||
value="selectLanguage"
|
||||
values={languageOptions}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onChange={this.onLanguageChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.selectInput}>
|
||||
<SelectInput
|
||||
name="quality"
|
||||
value="selectQuality"
|
||||
values={qualityOptions}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onChange={this.onQualityChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Selected Episode Files"
|
||||
message={'Are you sure you want to delete the selected episode files?'}
|
||||
confirmLabel="Delete"
|
||||
onConfirm={this.onConfirmDelete}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeFileEditorModalContent.propTypes = {
|
||||
seasonNumber: PropTypes.number,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
seriesType: PropTypes.string.isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired,
|
||||
onLanguageChange: PropTypes.func.isRequired,
|
||||
onQualityChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EpisodeFileEditorModalContent;
|
||||
@@ -1,174 +0,0 @@
|
||||
/* eslint max-params: 0 */
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import getQualities from 'Utilities/Quality/getQualities';
|
||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||
import { deleteEpisodeFiles, updateEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||
import { fetchLanguageProfileSchema, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import EpisodeFileEditorModalContent from './EpisodeFileEditorModalContent';
|
||||
|
||||
function createSchemaSelector() {
|
||||
return createSelector(
|
||||
(state) => state.settings.languageProfiles,
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(languageProfiles, qualityProfiles) => {
|
||||
const languages = _.map(languageProfiles.schema.languages, 'language');
|
||||
const qualities = getQualities(qualityProfiles.schema.items);
|
||||
|
||||
let error = null;
|
||||
|
||||
if (languageProfiles.schemaError) {
|
||||
error = 'Unable to load languages';
|
||||
} else if (qualityProfiles.schemaError) {
|
||||
error = 'Unable to load qualities';
|
||||
}
|
||||
|
||||
return {
|
||||
isFetching: languageProfiles.isSchemaFetching || qualityProfiles.isSchemaFetching,
|
||||
isPopulated: languageProfiles.isSchemaPopulated && qualityProfiles.isSchemaPopulated,
|
||||
error,
|
||||
languages,
|
||||
qualities
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { seasonNumber }) => seasonNumber,
|
||||
(state) => state.episodes,
|
||||
(state) => state.episodeFiles,
|
||||
createSchemaSelector(),
|
||||
createSeriesSelector(),
|
||||
(
|
||||
seasonNumber,
|
||||
episodes,
|
||||
episodeFiles,
|
||||
schema,
|
||||
series
|
||||
) => {
|
||||
const filtered = _.filter(episodes.items, (episode) => {
|
||||
if (seasonNumber >= 0 && episode.seasonNumber !== seasonNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!episode.episodeFileId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return _.some(episodeFiles.items, { id: episode.episodeFileId });
|
||||
});
|
||||
|
||||
const sorted = _.orderBy(filtered, ['seasonNumber', 'episodeNumber'], ['desc', 'desc']);
|
||||
|
||||
const items = _.map(sorted, (episode) => {
|
||||
const episodeFile = _.find(episodeFiles.items, { id: episode.episodeFileId });
|
||||
|
||||
return {
|
||||
relativePath: episodeFile.relativePath,
|
||||
language: episodeFile.language,
|
||||
quality: episodeFile.quality,
|
||||
languageCutoffNotMet: episodeFile.languageCutoffNotMet,
|
||||
qualityCutoffNotMet: episodeFile.qualityCutoffNotMet,
|
||||
...episode
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
...schema,
|
||||
items,
|
||||
seriesType: series.seriesType,
|
||||
isDeleting: episodeFiles.isDeleting,
|
||||
isSaving: episodeFiles.isSaving
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchLanguageProfileSchema(name, path) {
|
||||
dispatch(fetchLanguageProfileSchema());
|
||||
},
|
||||
|
||||
dispatchFetchQualityProfileSchema(name, path) {
|
||||
dispatch(fetchQualityProfileSchema());
|
||||
},
|
||||
|
||||
dispatchUpdateEpisodeFiles(updateProps) {
|
||||
dispatch(updateEpisodeFiles(updateProps));
|
||||
},
|
||||
|
||||
onDeletePress(episodeFileIds) {
|
||||
dispatch(deleteEpisodeFiles({ episodeFileIds }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EpisodeFileEditorModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchLanguageProfileSchema();
|
||||
this.props.dispatchFetchQualityProfileSchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onLanguageChange = (episodeFileIds, languageId) => {
|
||||
const language = _.find(this.props.languages, { id: languageId });
|
||||
|
||||
this.props.dispatchUpdateEpisodeFiles({ episodeFileIds, language });
|
||||
}
|
||||
|
||||
onQualityChange = (episodeFileIds, qualityId) => {
|
||||
const quality = {
|
||||
quality: _.find(this.props.qualities, { id: qualityId }),
|
||||
revision: {
|
||||
version: 1,
|
||||
real: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.props.dispatchUpdateEpisodeFiles({ episodeFileIds, quality });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchLanguageProfileSchema,
|
||||
dispatchFetchQualityProfileSchema,
|
||||
dispatchUpdateEpisodeFiles,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EpisodeFileEditorModalContent
|
||||
{...otherProps}
|
||||
onLanguageChange={this.onLanguageChange}
|
||||
onQualityChange={this.onQualityChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeFileEditorModalContentConnector.propTypes = {
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
seasonNumber: PropTypes.number,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
dispatchUpdateEpisodeFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeFileEditorModalContentConnector);
|
||||
@@ -1,3 +0,0 @@
|
||||
.absoluteEpisodeNumber {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import styles from './EpisodeFileEditorRow';
|
||||
|
||||
function EpisodeFileEditorRow(props) {
|
||||
const {
|
||||
id,
|
||||
seriesType,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
absoluteEpisodeNumber,
|
||||
relativePath,
|
||||
airDateUtc,
|
||||
language,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
languageCutoffNotMet,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<TableRowCell>
|
||||
{seasonNumber}x{padNumber(episodeNumber, 2)}
|
||||
|
||||
{
|
||||
seriesType === 'anime' && !!absoluteEpisodeNumber &&
|
||||
<span className={styles.absoluteEpisodeNumber}>
|
||||
({absoluteEpisodeNumber})
|
||||
</span>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{relativePath}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={airDateUtc}
|
||||
/>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeLanguage
|
||||
language={language}
|
||||
isCutoffNotMet={languageCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
EpisodeFileEditorRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
seriesType: PropTypes.string.isRequired,
|
||||
seasonNumber: PropTypes.number.isRequired,
|
||||
episodeNumber: PropTypes.number.isRequired,
|
||||
absoluteEpisodeNumber: PropTypes.number,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
airDateUtc: PropTypes.string.isRequired,
|
||||
language: PropTypes.object.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
languageCutoffNotMet: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EpisodeFileEditorRow;
|
||||
@@ -1,13 +1,39 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as mediaInfoTypes from './mediaInfoTypes';
|
||||
|
||||
function formatLanguages(languages) {
|
||||
if (!languages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const splitLanguages = _.uniq(languages.split(' / '));
|
||||
|
||||
if (splitLanguages.length > 3) {
|
||||
return (
|
||||
<span title={splitLanguages.join(', ')}>
|
||||
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{splitLanguages.join(', ')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function MediaInfo(props) {
|
||||
const {
|
||||
type,
|
||||
audioChannels,
|
||||
audioCodec,
|
||||
videoCodec
|
||||
audioLanguages,
|
||||
subtitles,
|
||||
videoCodec,
|
||||
videoDynamicRangeType
|
||||
} = props;
|
||||
|
||||
if (type === mediaInfoTypes.AUDIO) {
|
||||
@@ -31,6 +57,14 @@ function MediaInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.AUDIO_LANGUAGES) {
|
||||
return formatLanguages(audioLanguages);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.SUBTITLES) {
|
||||
return formatLanguages(subtitles);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.VIDEO) {
|
||||
return (
|
||||
<span>
|
||||
@@ -39,6 +73,14 @@ function MediaInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
|
||||
return (
|
||||
<span>
|
||||
{videoDynamicRangeType}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -46,7 +88,10 @@ MediaInfo.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
audioChannels: PropTypes.number,
|
||||
audioCodec: PropTypes.string,
|
||||
videoCodec: PropTypes.string
|
||||
audioLanguages: PropTypes.string,
|
||||
subtitles: PropTypes.string,
|
||||
videoCodec: PropTypes.string,
|
||||
videoDynamicRangeType: PropTypes.string
|
||||
};
|
||||
|
||||
export default MediaInfo;
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export const AUDIO = 'audio';
|
||||
export const AUDIO_LANGUAGES = 'audioLanguages';
|
||||
export const SUBTITLES = 'subtitles';
|
||||
export const VIDEO = 'video';
|
||||
export const VIDEO_DYNAMIC_RANGE_TYPE = 'videoDynamicRangeType';
|
||||
|
||||
@@ -11,6 +11,7 @@ export const PATH = 'path';
|
||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
|
||||
export const INDEXER_SELECT = 'indexerSelect';
|
||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const SELECT = 'select';
|
||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||
@@ -35,6 +36,7 @@ export const all = [
|
||||
QUALITY_PROFILE_SELECT,
|
||||
LANGUAGE_PROFILE_SELECT,
|
||||
INDEXER_SELECT,
|
||||
DOWNLOAD_CLIENT_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
SELECT,
|
||||
DYNAMIC_SELECT,
|
||||
|
||||
@@ -96,6 +96,7 @@ class SelectEpisodeModalContent extends Component {
|
||||
isAnime,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
modalTitle,
|
||||
onSortPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@@ -121,7 +122,7 @@ class SelectEpisodeModalContent extends Component {
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
<div className={styles.header}>
|
||||
Manual Import - Select Episode(s)
|
||||
{modalTitle} - Select Episode(s)
|
||||
</div>
|
||||
|
||||
</ModalHeader>
|
||||
@@ -235,6 +236,7 @@ SelectEpisodeModalContent.propTypes = {
|
||||
isAnime: PropTypes.bool.isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
modalTitle: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onEpisodesSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
||||
@@ -67,6 +67,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
||||
const {
|
||||
recentFolders,
|
||||
onRemoveRecentFolderPress,
|
||||
modalTitle,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
@@ -75,7 +76,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Folder
|
||||
{modalTitle} - Select Folder
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -159,6 +160,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
||||
|
||||
InteractiveImportSelectFolderModalContent.propTypes = {
|
||||
recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onQuickImportPress: PropTypes.func.isRequired,
|
||||
onInteractiveImportPress: PropTypes.func.isRequired,
|
||||
onRemoveRecentFolderPress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.importMode,
|
||||
.bulkSelect {
|
||||
composes: select from '~Components/Form/SelectInput.css';
|
||||
|
||||
@@ -7,6 +7,7 @@ import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
@@ -14,6 +15,7 @@ import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SelectedMenuItem from 'Components/Menu/SelectedMenuItem';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
@@ -25,6 +27,7 @@ import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import InteractiveImportRow from './InteractiveImportRow';
|
||||
import styles from './InteractiveImportModalContent.css';
|
||||
|
||||
@@ -51,6 +54,11 @@ const columns = [
|
||||
label: 'Episode(s)',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'releaseGroup',
|
||||
label: 'Release Group',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
@@ -75,6 +83,7 @@ const columns = [
|
||||
name: icons.DANGER,
|
||||
kind: kinds.DANGER
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
@@ -85,6 +94,7 @@ const filterExistingFilesOptions = {
|
||||
};
|
||||
|
||||
const importModeOptions = [
|
||||
{ key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true },
|
||||
{ key: 'move', value: 'Move Files' },
|
||||
{ key: 'copy', value: 'Hardlink/Copy Files' }
|
||||
];
|
||||
@@ -93,8 +103,9 @@ const SELECT = 'select';
|
||||
const SERIES = 'series';
|
||||
const SEASON = 'season';
|
||||
const EPISODE = 'episode';
|
||||
const LANGUAGE = 'language';
|
||||
const RELEASE_GROUP = 'releaseGroup';
|
||||
const QUALITY = 'quality';
|
||||
const LANGUAGE = 'language';
|
||||
|
||||
class InteractiveImportModalContent extends Component {
|
||||
|
||||
@@ -104,16 +115,37 @@ class InteractiveImportModalContent extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const instanceColumns = _.cloneDeep(columns);
|
||||
|
||||
if (!props.showSeries) {
|
||||
instanceColumns.find((c) => c.name === 'series').isVisible = false;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
invalidRowsSelected: [],
|
||||
selectModalOpen: null
|
||||
withoutEpisodeFileIdRowsSelected: [],
|
||||
selectModalOpen: null,
|
||||
columns: instanceColumns,
|
||||
isConfirmDeleteModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isDeleting,
|
||||
deleteError,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
if (!isDeleting && prevProps.isDeleting && !deleteError) {
|
||||
onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
@@ -128,9 +160,14 @@ class InteractiveImportModalContent extends Component {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
onSelectedChange = ({ id, value, hasEpisodeFileId, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
return {
|
||||
...toggleSelected(state, this.props.items, id, value, shiftKey),
|
||||
withoutEpisodeFileIdRowsSelected: hasEpisodeFileId || !value ?
|
||||
_.without(state.withoutEpisodeFileIdRowsSelected, id) :
|
||||
[...state.withoutEpisodeFileIdRowsSelected, id]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -148,6 +185,19 @@ class InteractiveImportModalContent extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteSelectedPress = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmDelete = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
this.props.onDeleteSelectedPress(this.getSelectedIds());
|
||||
}
|
||||
|
||||
onConfirmDeleteModalClose = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
}
|
||||
|
||||
onImportSelectedPress = () => {
|
||||
const {
|
||||
downloadId,
|
||||
@@ -185,7 +235,9 @@ class InteractiveImportModalContent extends Component {
|
||||
const {
|
||||
downloadId,
|
||||
allowSeriesChange,
|
||||
autoSelectRow,
|
||||
showFilterExistingFiles,
|
||||
showDelete,
|
||||
showImportMode,
|
||||
filterExistingFiles,
|
||||
title,
|
||||
@@ -198,6 +250,8 @@ class InteractiveImportModalContent extends Component {
|
||||
sortDirection,
|
||||
importMode,
|
||||
interactiveImportErrorMessage,
|
||||
isDeleting,
|
||||
modalTitle,
|
||||
onSortPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@@ -207,7 +261,9 @@ class InteractiveImportModalContent extends Component {
|
||||
allUnselected,
|
||||
selectedState,
|
||||
invalidRowsSelected,
|
||||
selectModalOpen
|
||||
withoutEpisodeFileIdRowsSelected,
|
||||
selectModalOpen,
|
||||
isConfirmDeleteModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
@@ -230,8 +286,9 @@ class InteractiveImportModalContent extends Component {
|
||||
{ key: SELECT, value: 'Select...', disabled: true },
|
||||
{ key: SEASON, value: 'Select Season' },
|
||||
{ key: EPISODE, value: 'Select Episode(s)' },
|
||||
{ key: LANGUAGE, value: 'Select Language' },
|
||||
{ key: QUALITY, value: 'Select Quality' }
|
||||
{ key: QUALITY, value: 'Select Quality' },
|
||||
{ key: RELEASE_GROUP, value: 'Select Release Group' },
|
||||
{ key: LANGUAGE, value: 'Select Language' }
|
||||
];
|
||||
|
||||
if (allowSeriesChange) {
|
||||
@@ -244,7 +301,7 @@ class InteractiveImportModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - {title || folder}
|
||||
{modalTitle} - {title || folder}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
@@ -299,7 +356,7 @@ class InteractiveImportModalContent extends Component {
|
||||
{
|
||||
isPopulated && !!items.length && !isFetching && !isFetching &&
|
||||
<Table
|
||||
columns={columns}
|
||||
columns={this.state.columns}
|
||||
horizontalScroll={true}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
@@ -318,6 +375,9 @@ class InteractiveImportModalContent extends Component {
|
||||
isSelected={selectedState[item.id]}
|
||||
{...item}
|
||||
allowSeriesChange={allowSeriesChange}
|
||||
autoSelectRow={autoSelectRow}
|
||||
columns={this.state.columns}
|
||||
modalTitle={modalTitle}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onValidRowChange={this.onValidRowChange}
|
||||
/>
|
||||
@@ -336,6 +396,20 @@ class InteractiveImportModalContent extends Component {
|
||||
|
||||
<ModalFooter className={styles.footer}>
|
||||
<div className={styles.leftButtons}>
|
||||
{
|
||||
showDelete ?
|
||||
<SpinnerButton
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedIds.length || !!withoutEpisodeFileIdRowsSelected.length}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
Delete
|
||||
</SpinnerButton> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!downloadId && showImportMode ?
|
||||
<SelectInput
|
||||
@@ -381,6 +455,7 @@ class InteractiveImportModalContent extends Component {
|
||||
<SelectSeriesModal
|
||||
isOpen={selectModalOpen === SERIES}
|
||||
ids={selectedIds}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -388,6 +463,7 @@ class InteractiveImportModalContent extends Component {
|
||||
isOpen={selectModalOpen === SEASON}
|
||||
ids={selectedIds}
|
||||
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -396,6 +472,15 @@ class InteractiveImportModalContent extends Component {
|
||||
ids={orderedSelectedIds}
|
||||
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
||||
seasonNumber={selectedItem && selectedItem.seasonNumber}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectReleaseGroupModal
|
||||
isOpen={selectModalOpen === RELEASE_GROUP}
|
||||
ids={selectedIds}
|
||||
releaseGroup=""
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -403,6 +488,7 @@ class InteractiveImportModalContent extends Component {
|
||||
isOpen={selectModalOpen === LANGUAGE}
|
||||
ids={selectedIds}
|
||||
languageId={0}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
@@ -412,8 +498,19 @@ class InteractiveImportModalContent extends Component {
|
||||
qualityId={0}
|
||||
proper={false}
|
||||
real={false}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Selected Episode Files"
|
||||
message={'Are you sure you want to delete the selected episode files?'}
|
||||
confirmLabel="Delete"
|
||||
onConfirm={this.onConfirmDelete}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
@@ -421,7 +518,10 @@ class InteractiveImportModalContent extends Component {
|
||||
|
||||
InteractiveImportModalContent.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
showSeries: PropTypes.bool.isRequired,
|
||||
allowSeriesChange: PropTypes.bool.isRequired,
|
||||
autoSelectRow: PropTypes.bool.isRequired,
|
||||
showDelete: PropTypes.bool.isRequired,
|
||||
showImportMode: PropTypes.bool.isRequired,
|
||||
showFilterExistingFiles: PropTypes.bool.isRequired,
|
||||
filterExistingFiles: PropTypes.bool.isRequired,
|
||||
@@ -435,16 +535,23 @@ InteractiveImportModalContent.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
interactiveImportErrorMessage: PropTypes.string,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterExistingFilesChange: PropTypes.func.isRequired,
|
||||
onImportModeChange: PropTypes.func.isRequired,
|
||||
onDeleteSelectedPress: PropTypes.func.isRequired,
|
||||
onImportSelectedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportModalContent.defaultProps = {
|
||||
showSeries: true,
|
||||
allowSeriesChange: true,
|
||||
autoSelectRow: true,
|
||||
showFilterExistingFiles: false,
|
||||
showDelete: false,
|
||||
showImportMode: true,
|
||||
importMode: 'move'
|
||||
};
|
||||
|
||||
@@ -1,19 +1,49 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { updateEpisodeFiles, deleteEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import InteractiveImportModalContent from './InteractiveImportModalContent';
|
||||
|
||||
function isSameEpisodeFile(file, originalFile) {
|
||||
const {
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes
|
||||
} = file;
|
||||
|
||||
if (!originalFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!originalFile.series || series.id !== originalFile.series.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seasonNumber !== originalFile.seasonNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !hasDifferentItems(originalFile.episodes, episodes);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('interactiveImport'),
|
||||
(interactiveImport) => {
|
||||
return interactiveImport;
|
||||
(state) => state.episodeFiles.isDeleting,
|
||||
(state) => state.episodeFiles.deleteError,
|
||||
(interactiveImport, isDeleting, deleteError) => {
|
||||
return {
|
||||
...interactiveImport,
|
||||
isDeleting,
|
||||
deleteError
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -23,6 +53,8 @@ const mapDispatchToProps = {
|
||||
dispatchSetInteractiveImportSort: setInteractiveImportSort,
|
||||
dispatchSetInteractiveImportMode: setInteractiveImportMode,
|
||||
dispatchClearInteractiveImport: clearInteractiveImport,
|
||||
dispatchUpdateEpisodeFiles: updateEpisodeFiles,
|
||||
dispatchDeleteEpisodeFiles: deleteEpisodeFiles,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
@@ -44,16 +76,34 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
const {
|
||||
downloadId,
|
||||
seriesId,
|
||||
folder
|
||||
seasonNumber,
|
||||
folder,
|
||||
initialSortKey,
|
||||
initialSortDirection,
|
||||
dispatchSetInteractiveImportSort,
|
||||
dispatchFetchInteractiveImportItems
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
filterExistingFiles
|
||||
} = this.state;
|
||||
|
||||
this.props.dispatchFetchInteractiveImportItems({
|
||||
if (initialSortKey) {
|
||||
const sortProps = {
|
||||
sortKey: initialSortKey
|
||||
};
|
||||
|
||||
if (initialSortDirection) {
|
||||
sortProps.sortDirection = initialSortDirection;
|
||||
}
|
||||
|
||||
dispatchSetInteractiveImportSort(sortProps);
|
||||
}
|
||||
|
||||
dispatchFetchInteractiveImportItems({
|
||||
downloadId,
|
||||
seriesId,
|
||||
seasonNumber,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
});
|
||||
@@ -99,10 +149,41 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
this.props.dispatchSetInteractiveImportMode({ importMode });
|
||||
}
|
||||
|
||||
onDeleteSelectedPress = (selected) => {
|
||||
const {
|
||||
items,
|
||||
dispatchDeleteEpisodeFiles
|
||||
} = this.props;
|
||||
|
||||
const episodeFileIds = items.reduce((acc, item) => {
|
||||
if (selected.indexOf(item.id) > -1 && item.episodeFileId) {
|
||||
acc.push(item.episodeFileId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
dispatchDeleteEpisodeFiles({ episodeFileIds });
|
||||
}
|
||||
|
||||
onImportSelectedPress = (selected, importMode) => {
|
||||
const {
|
||||
items,
|
||||
originalItems,
|
||||
dispatchUpdateEpisodeFiles,
|
||||
dispatchExecuteCommand,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const existingFiles = [];
|
||||
const files = [];
|
||||
|
||||
_.forEach(this.props.items, (item) => {
|
||||
if (importMode === 'chooseImportMode') {
|
||||
this.setState({ interactiveImportErrorMessage: 'An import mode must be selected' });
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
const isSelected = selected.indexOf(item.id) > -1;
|
||||
|
||||
if (isSelected) {
|
||||
@@ -110,33 +191,50 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
releaseGroup,
|
||||
quality,
|
||||
language
|
||||
language,
|
||||
episodeFileId
|
||||
} = item;
|
||||
|
||||
if (!series) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' });
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(seasonNumber)) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' });
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!episodes || !episodes.length) {
|
||||
this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' });
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!quality) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Quality must be chosen for each selected file' });
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!language) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Language must be chosen for each selected file' });
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (episodeFileId) {
|
||||
const originalItem = originalItems.find((i) => i.id === item.id);
|
||||
|
||||
if (isSameEpisodeFile(item, originalItem)) {
|
||||
existingFiles.push({
|
||||
id: episodeFileId,
|
||||
releaseGroup,
|
||||
quality,
|
||||
language
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
files.push({
|
||||
@@ -144,24 +242,38 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
folderName: item.folderName,
|
||||
seriesId: series.id,
|
||||
episodeIds: episodes.map((e) => e.id),
|
||||
releaseGroup,
|
||||
quality,
|
||||
language,
|
||||
downloadId: this.props.downloadId
|
||||
downloadId: this.props.downloadId,
|
||||
episodeFileId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!files.length) {
|
||||
return;
|
||||
let shouldClose = false;
|
||||
|
||||
if (existingFiles.length) {
|
||||
dispatchUpdateEpisodeFiles({
|
||||
files: existingFiles
|
||||
});
|
||||
|
||||
shouldClose = true;
|
||||
}
|
||||
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.INTERACTIVE_IMPORT,
|
||||
files,
|
||||
importMode
|
||||
});
|
||||
if (files.length) {
|
||||
dispatchExecuteCommand({
|
||||
name: commandNames.INTERACTIVE_IMPORT,
|
||||
files,
|
||||
importMode
|
||||
});
|
||||
|
||||
this.props.onModalClose();
|
||||
shouldClose = true;
|
||||
}
|
||||
|
||||
if (shouldClose) {
|
||||
onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -181,6 +293,7 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterExistingFilesChange={this.onFilterExistingFilesChange}
|
||||
onImportModeChange={this.onImportModeChange}
|
||||
onDeleteSelectedPress={this.onDeleteSelectedPress}
|
||||
onImportSelectedPress={this.onImportSelectedPress}
|
||||
/>
|
||||
);
|
||||
@@ -190,13 +303,19 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
InteractiveImportModalContentConnector.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
seriesId: PropTypes.number,
|
||||
seasonNumber: PropTypes.number,
|
||||
folder: PropTypes.string,
|
||||
filterExistingFiles: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
initialSortKey: PropTypes.string,
|
||||
initialSortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
originalItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchFetchInteractiveImportItems: PropTypes.func.isRequired,
|
||||
dispatchSetInteractiveImportSort: PropTypes.func.isRequired,
|
||||
dispatchSetInteractiveImportMode: PropTypes.func.isRequired,
|
||||
dispatchClearInteractiveImport: PropTypes.func.isRequired,
|
||||
dispatchUpdateEpisodeFiles: PropTypes.func.isRequired,
|
||||
dispatchDeleteEpisodeFiles: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||
import styles from './InteractiveImportRow.css';
|
||||
@@ -32,6 +33,7 @@ class InteractiveImportRow extends Component {
|
||||
isSelectSeriesModalOpen: false,
|
||||
isSelectSeasonModalOpen: false,
|
||||
isSelectEpisodeModalOpen: false,
|
||||
isSelectReleaseGroupModalOpen: false,
|
||||
isSelectQualityModalOpen: false,
|
||||
isSelectLanguageModalOpen: false
|
||||
};
|
||||
@@ -39,23 +41,35 @@ class InteractiveImportRow extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
allowSeriesChange,
|
||||
id,
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
quality,
|
||||
language
|
||||
language,
|
||||
episodeFileId,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
allowSeriesChange &&
|
||||
series &&
|
||||
seasonNumber != null &&
|
||||
episodes.length &&
|
||||
quality &&
|
||||
language
|
||||
) {
|
||||
this.props.onSelectedChange({ id, value: true });
|
||||
this.props.onSelectedChange({
|
||||
id,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
value: true
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isSeriesColumnVisible: columns.find((c) => c.name === 'series').isVisible
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -102,17 +116,34 @@ class InteractiveImportRow extends Component {
|
||||
selectRowAfterChange = (value) => {
|
||||
const {
|
||||
id,
|
||||
episodeFileId,
|
||||
isSelected
|
||||
} = this.props;
|
||||
|
||||
if (!isSelected && value === true) {
|
||||
this.props.onSelectedChange({ id, value });
|
||||
this.props.onSelectedChange({
|
||||
id,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectedChange = (result) => {
|
||||
const {
|
||||
episodeFileId,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({
|
||||
...result,
|
||||
hasEpisodeFileId: !!episodeFileId
|
||||
});
|
||||
}
|
||||
|
||||
onSelectSeriesPress = () => {
|
||||
this.setState({ isSelectSeriesModalOpen: true });
|
||||
}
|
||||
@@ -125,6 +156,10 @@ class InteractiveImportRow extends Component {
|
||||
this.setState({ isSelectEpisodeModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectReleaseGroupPress = () => {
|
||||
this.setState({ isSelectReleaseGroupModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectQualityPress = () => {
|
||||
this.setState({ isSelectQualityModalOpen: true });
|
||||
}
|
||||
@@ -148,6 +183,11 @@ class InteractiveImportRow extends Component {
|
||||
this.selectRowAfterChange(changed);
|
||||
}
|
||||
|
||||
onSelectReleaseGroupModalClose = (changed) => {
|
||||
this.setState({ isSelectReleaseGroupModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
}
|
||||
|
||||
onSelectQualityModalClose = (changed) => {
|
||||
this.setState({ isSelectQualityModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
@@ -171,17 +211,19 @@ class InteractiveImportRow extends Component {
|
||||
episodes,
|
||||
quality,
|
||||
language,
|
||||
releaseGroup,
|
||||
size,
|
||||
rejections,
|
||||
isReprocessing,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
modalTitle
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isSelectSeriesModalOpen,
|
||||
isSelectSeasonModalOpen,
|
||||
isSelectEpisodeModalOpen,
|
||||
isSelectReleaseGroupModalOpen,
|
||||
isSelectQualityModalOpen,
|
||||
isSelectLanguageModalOpen
|
||||
} = this.state;
|
||||
@@ -193,7 +235,13 @@ class InteractiveImportRow extends Component {
|
||||
return (
|
||||
<div key={episode.id}>
|
||||
{episode.episodeNumber}
|
||||
{isAnime ? ` (${episode.absoluteEpisodeNumber})` : ''}
|
||||
|
||||
{
|
||||
isAnime && episode.absoluteEpisodeNumber != null ?
|
||||
` (${episode.absoluteEpisodeNumber})` :
|
||||
''
|
||||
}
|
||||
|
||||
{` - ${episode.title}`}
|
||||
</div>
|
||||
);
|
||||
@@ -202,6 +250,7 @@ class InteractiveImportRow extends Component {
|
||||
const showSeriesPlaceholder = isSelected && !series;
|
||||
const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber) && !isReprocessing;
|
||||
const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length;
|
||||
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
|
||||
const showQualityPlaceholder = isSelected && !quality;
|
||||
const showLanguagePlaceholder = isSelected && !language;
|
||||
|
||||
@@ -210,7 +259,7 @@ class InteractiveImportRow extends Component {
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
|
||||
<TableRowCell
|
||||
@@ -220,15 +269,19 @@ class InteractiveImportRow extends Component {
|
||||
{relativePath}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!allowSeriesChange}
|
||||
title={allowSeriesChange ? 'Click to change series' : undefined}
|
||||
onPress={this.onSelectSeriesPress}
|
||||
>
|
||||
{
|
||||
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
|
||||
}
|
||||
</TableRowCellButton>
|
||||
{
|
||||
this.state.isSeriesColumnVisible ?
|
||||
<TableRowCellButton
|
||||
isDisabled={!allowSeriesChange}
|
||||
title={allowSeriesChange ? 'Click to change series' : undefined}
|
||||
onPress={this.onSelectSeriesPress}
|
||||
>
|
||||
{
|
||||
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
|
||||
}
|
||||
</TableRowCellButton> :
|
||||
null
|
||||
}
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!series}
|
||||
@@ -246,7 +299,6 @@ class InteractiveImportRow extends Component {
|
||||
|
||||
/> : null
|
||||
}
|
||||
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
@@ -259,6 +311,17 @@ class InteractiveImportRow extends Component {
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
title="Click to change release group"
|
||||
onPress={this.onSelectReleaseGroupPress}
|
||||
>
|
||||
{
|
||||
showReleaseGroupPlaceholder ?
|
||||
<InteractiveImportRowCellPlaceholder /> :
|
||||
releaseGroup
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
className={styles.quality}
|
||||
title="Click to change quality"
|
||||
@@ -334,6 +397,7 @@ class InteractiveImportRow extends Component {
|
||||
<SelectSeriesModal
|
||||
isOpen={isSelectSeriesModalOpen}
|
||||
ids={[id]}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectSeriesModalClose}
|
||||
/>
|
||||
|
||||
@@ -341,6 +405,7 @@ class InteractiveImportRow extends Component {
|
||||
isOpen={isSelectSeasonModalOpen}
|
||||
ids={[id]}
|
||||
seriesId={series && series.id}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectSeasonModalClose}
|
||||
/>
|
||||
|
||||
@@ -351,15 +416,25 @@ class InteractiveImportRow extends Component {
|
||||
isAnime={isAnime}
|
||||
seasonNumber={seasonNumber}
|
||||
relativePath={relativePath}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectEpisodeModalClose}
|
||||
/>
|
||||
|
||||
<SelectReleaseGroupModal
|
||||
isOpen={isSelectReleaseGroupModalOpen}
|
||||
ids={[id]}
|
||||
releaseGroup={releaseGroup ?? ''}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectReleaseGroupModalClose}
|
||||
/>
|
||||
|
||||
<SelectQualityModal
|
||||
isOpen={isSelectQualityModalOpen}
|
||||
ids={[id]}
|
||||
qualityId={quality ? quality.quality.id : 0}
|
||||
proper={quality ? quality.revision.version > 1 : false}
|
||||
real={quality ? quality.revision.real > 0 : false}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectQualityModalClose}
|
||||
/>
|
||||
|
||||
@@ -367,6 +442,7 @@ class InteractiveImportRow extends Component {
|
||||
isOpen={isSelectLanguageModalOpen}
|
||||
ids={[id]}
|
||||
languageId={language ? language.id : 0}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectLanguageModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
@@ -382,12 +458,16 @@ InteractiveImportRow.propTypes = {
|
||||
series: PropTypes.object,
|
||||
seasonNumber: PropTypes.number,
|
||||
episodes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
releaseGroup: PropTypes.string,
|
||||
quality: PropTypes.object,
|
||||
language: PropTypes.object,
|
||||
size: PropTypes.number.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
episodeFileId: PropTypes.number,
|
||||
isReprocessing: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onValidRowChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -75,7 +75,12 @@ InteractiveImportModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
folder: PropTypes.string,
|
||||
downloadId: PropTypes.string,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportModal.defaultProps = {
|
||||
modalTitle: 'Manual Import'
|
||||
};
|
||||
|
||||
export default InteractiveImportModal;
|
||||
|
||||
@@ -19,6 +19,7 @@ function SelectLanguageModalContent(props) {
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
modalTitle,
|
||||
onModalClose,
|
||||
onLanguageSelect
|
||||
} = props;
|
||||
@@ -33,7 +34,7 @@ function SelectLanguageModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Language
|
||||
{modalTitle} - Select Language
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -80,6 +81,7 @@ SelectLanguageModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onLanguageSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -61,6 +61,7 @@ class SelectQualityModalContent extends Component {
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
modalTitle,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
@@ -80,7 +81,7 @@ class SelectQualityModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Quality
|
||||
{modalTitle} - Select Quality
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -159,6 +160,7 @@ SelectQualityModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onQualitySelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectReleaseGroupModalContentConnector from './SelectReleaseGroupModalContentConnector';
|
||||
|
||||
class SelectReleaseGroupModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectReleaseGroupModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectReleaseGroupModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectReleaseGroupModal;
|
||||
@@ -0,0 +1,7 @@
|
||||
.modalBody {
|
||||
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './SelectReleaseGroupModalContent.css';
|
||||
|
||||
class SelectReleaseGroupModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
releaseGroup
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
releaseGroup
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onReleaseGroupChange = ({ value }) => {
|
||||
this.setState({ releaseGroup: value });
|
||||
}
|
||||
|
||||
onReleaseGroupSelect = () => {
|
||||
this.props.onReleaseGroupSelect(this.state);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
modalTitle,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
releaseGroup
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{modalTitle} - Set Release Group
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Release Group</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="releaseGroup"
|
||||
value={releaseGroup}
|
||||
autoFocus={true}
|
||||
onChange={this.onReleaseGroupChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
onPress={this.onReleaseGroupSelect}
|
||||
>
|
||||
Set Release Group
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectReleaseGroupModalContent.propTypes = {
|
||||
releaseGroup: PropTypes.string.isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onReleaseGroupSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectReleaseGroupModalContent;
|
||||
@@ -0,0 +1,54 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { updateInteractiveImportItems, reprocessInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||
import SelectReleaseGroupModalContent from './SelectReleaseGroupModalContent';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
|
||||
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
|
||||
};
|
||||
|
||||
class SelectReleaseGroupModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onReleaseGroupSelect = ({ releaseGroup }) => {
|
||||
const {
|
||||
ids,
|
||||
dispatchUpdateInteractiveImportItems,
|
||||
dispatchReprocessInteractiveImportItems
|
||||
} = this.props;
|
||||
|
||||
dispatchUpdateInteractiveImportItems({
|
||||
ids,
|
||||
releaseGroup
|
||||
});
|
||||
|
||||
dispatchReprocessInteractiveImportItems({ ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectReleaseGroupModalContent
|
||||
{...this.props}
|
||||
onReleaseGroupSelect={this.onReleaseGroupSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectReleaseGroupModalContentConnector.propTypes = {
|
||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
|
||||
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(SelectReleaseGroupModalContentConnector);
|
||||
@@ -15,6 +15,7 @@ class SelectSeasonModalContent extends Component {
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
modalTitle,
|
||||
onSeasonSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@@ -22,7 +23,7 @@ class SelectSeasonModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Season
|
||||
{modalTitle} - Select Season
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -51,6 +52,7 @@ class SelectSeasonModalContent extends Component {
|
||||
|
||||
SelectSeasonModalContent.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSeasonSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ class SelectSeriesModalContent extends Component {
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
modalTitle,
|
||||
onSeriesSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
@@ -47,7 +48,7 @@ class SelectSeriesModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Series
|
||||
{modalTitle} - Select Series
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
@@ -96,6 +97,7 @@ class SelectSeriesModalContent extends Component {
|
||||
|
||||
SelectSeriesModalContent.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSeriesSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -175,7 +175,7 @@ function InteractiveSearch(props) {
|
||||
items.map((item) => {
|
||||
return (
|
||||
<InteractiveSearchRow
|
||||
key={item.guid}
|
||||
key={`${item.indexerId}-${item.guid}`}
|
||||
{...item}
|
||||
searchPayload={searchPayload}
|
||||
longDateFormat={longDateFormat}
|
||||
|
||||
@@ -35,12 +35,12 @@ function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return 'Added to downloaded queue';
|
||||
return 'Added to download queue';
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return 'Add to downloaded queue';
|
||||
return 'Add to download queue';
|
||||
}
|
||||
|
||||
class InteractiveSearchRow extends Component {
|
||||
|
||||
@@ -192,7 +192,7 @@ OrganizePreviewModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
seasonNumber: PropTypes.string.isRequired,
|
||||
seasonNumber: PropTypes.number,
|
||||
path: PropTypes.string.isRequired,
|
||||
renameEpisodes: PropTypes.bool,
|
||||
episodeFormat: PropTypes.string,
|
||||
|
||||
@@ -36,3 +36,17 @@
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.audioLanguages,
|
||||
.videoDynamicRangeType,
|
||||
.subtitles {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 165px;
|
||||
}
|
||||
|
||||
.releaseGroup {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ class EpisodeRow extends Component {
|
||||
episodeFilePath,
|
||||
episodeFileRelativePath,
|
||||
episodeFileSize,
|
||||
releaseGroup,
|
||||
alternateTitles,
|
||||
columns
|
||||
} = this.props;
|
||||
@@ -195,6 +196,34 @@ class EpisodeRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'audioLanguages') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.audioLanguages}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
type={mediaInfoTypes.AUDIO_LANGUAGES}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'subtitleLanguages') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.subtitles}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
type={mediaInfoTypes.SUBTITLES}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'videoCodec') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -209,6 +238,20 @@ class EpisodeRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'videoDynamicRangeType') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.videoDynamicRangeType}
|
||||
>
|
||||
<MediaInfoConnector
|
||||
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -220,6 +263,17 @@ class EpisodeRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'releaseGroup') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.releaseGroup}
|
||||
>
|
||||
{releaseGroup}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -274,6 +328,7 @@ EpisodeRow.propTypes = {
|
||||
episodeFilePath: PropTypes.string,
|
||||
episodeFileRelativePath: PropTypes.string,
|
||||
episodeFileSize: PropTypes.number,
|
||||
releaseGroup: PropTypes.string,
|
||||
mediaInfo: PropTypes.object,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -17,6 +17,7 @@ function createMapStateToProps() {
|
||||
episodeFilePath: episodeFile ? episodeFile.path : null,
|
||||
episodeFileRelativePath: episodeFile ? episodeFile.relativePath : null,
|
||||
episodeFileSize: episodeFile ? episodeFile.size : null,
|
||||
releaseGroup: episodeFile ? episodeFile.releaseGroup : null,
|
||||
alternateTitles: series.alternateTitles
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import TextTruncate from 'react-text-truncate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -22,7 +22,6 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import EpisodeFileEditorModal from 'EpisodeFile/Editor/EpisodeFileEditorModal';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
@@ -32,6 +31,7 @@ import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
|
||||
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
|
||||
import SeriesAlternateTitles from './SeriesAlternateTitles';
|
||||
import SeriesDetailsSeasonConnector from './SeriesDetailsSeasonConnector';
|
||||
import SeriesGenres from './SeriesGenres';
|
||||
import SeriesTagsConnector from './SeriesTagsConnector';
|
||||
import SeriesDetailsLinks from './SeriesDetailsLinks';
|
||||
import MonitoringOptionsModal from 'Series/MonitoringOptions/MonitoringOptionsModal';
|
||||
@@ -57,6 +57,11 @@ function getExpandedState(newState) {
|
||||
};
|
||||
}
|
||||
|
||||
function getDateYear(date) {
|
||||
const dateDate = new Date(date);
|
||||
return dateDate.getFullYear();
|
||||
}
|
||||
|
||||
class SeriesDetails extends Component {
|
||||
|
||||
//
|
||||
@@ -71,7 +76,6 @@ class SeriesDetails extends Component {
|
||||
isEditSeriesModalOpen: false,
|
||||
isDeleteSeriesModalOpen: false,
|
||||
isSeriesHistoryModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isMonitorOptionsModalOpen: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
@@ -99,14 +103,6 @@ class SeriesDetails extends Component {
|
||||
this.setState({ isManageEpisodesOpen: false });
|
||||
}
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
}
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
|
||||
onEditSeriesPress = () => {
|
||||
this.setState({ isEditSeriesModalOpen: true });
|
||||
}
|
||||
@@ -191,7 +187,10 @@ class SeriesDetails extends Component {
|
||||
images,
|
||||
seasons,
|
||||
alternateTitles,
|
||||
genres,
|
||||
tags,
|
||||
year,
|
||||
previousAiring,
|
||||
isSaving,
|
||||
isRefreshing,
|
||||
isSearching,
|
||||
@@ -220,7 +219,6 @@ class SeriesDetails extends Component {
|
||||
isEditSeriesModalOpen,
|
||||
isDeleteSeriesModalOpen,
|
||||
isSeriesHistoryModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isMonitorOptionsModalOpen,
|
||||
allExpanded,
|
||||
allCollapsed,
|
||||
@@ -229,6 +227,7 @@ class SeriesDetails extends Component {
|
||||
} = this.state;
|
||||
|
||||
const statusDetails = getSeriesStatusDetails(status);
|
||||
const runningYears = statusDetails.title === 'Ended' ? `${year}-${getDateYear(previousAiring)}` : `${year}-`;
|
||||
|
||||
let episodeFilesCountMessage = 'No episode files';
|
||||
|
||||
@@ -280,7 +279,6 @@ class SeriesDetails extends Component {
|
||||
<PageToolbarButton
|
||||
label="Manage Episodes"
|
||||
iconName={icons.EPISODE_FILE}
|
||||
isDisabled={!hasEpisodeFiles}
|
||||
onPress={this.onManageEpisodesPress}
|
||||
/>
|
||||
|
||||
@@ -291,12 +289,6 @@ class SeriesDetails extends Component {
|
||||
onPress={this.onSeriesHistoryPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Manual File Import"
|
||||
iconName={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
@@ -414,6 +406,12 @@ class SeriesDetails extends Component {
|
||||
rating={ratings.value}
|
||||
iconSize={20}
|
||||
/>
|
||||
|
||||
<SeriesGenres genres={genres} />
|
||||
|
||||
<span>
|
||||
{runningYears}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -640,9 +638,19 @@ class SeriesDetails extends Component {
|
||||
onModalClose={this.onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<EpisodeFileEditorModal
|
||||
<InteractiveImportModal
|
||||
isOpen={isManageEpisodesOpen}
|
||||
seriesId={id}
|
||||
title={title}
|
||||
folder={path}
|
||||
initialSortKey="relativePath"
|
||||
initialSortDirection={sortDirections.DESCENDING}
|
||||
showSeries={false}
|
||||
allowSeriesChange={false}
|
||||
autoSelectRow={false}
|
||||
showDelete={true}
|
||||
showImportMode={false}
|
||||
modalTitle={'Manage Episodes'}
|
||||
onModalClose={this.onManageEpisodesModalClose}
|
||||
/>
|
||||
|
||||
@@ -665,16 +673,6 @@ class SeriesDetails extends Component {
|
||||
onModalClose={this.onDeleteSeriesModalClose}
|
||||
/>
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
seriesId={id}
|
||||
folder={path}
|
||||
allowSeriesChange={false}
|
||||
showFilterExistingFiles={true}
|
||||
showImportMode={false}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<MonitoringOptionsModal
|
||||
isOpen={isMonitorOptionsModalOpen}
|
||||
seriesId={id}
|
||||
@@ -705,7 +703,10 @@ SeriesDetails.propTypes = {
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
seasons: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
previousAiring: PropTypes.string,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -5,7 +5,7 @@ import isAfter from 'Utilities/Date/isAfter';
|
||||
import isBefore from 'Utilities/Date/isBefore';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import getToggledRange from 'Utilities/Table/getToggledRange';
|
||||
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Label from 'Components/Label';
|
||||
@@ -20,7 +20,7 @@ import MenuItem from 'Components/Menu/MenuItem';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeFileEditorModal from 'EpisodeFile/Editor/EpisodeFileEditorModal';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
|
||||
import SeasonInteractiveSearchModalConnector from 'Series/Search/SeasonInteractiveSearchModalConnector';
|
||||
@@ -204,6 +204,7 @@ class SeriesDetailsSeason extends Component {
|
||||
render() {
|
||||
const {
|
||||
seriesId,
|
||||
path,
|
||||
monitored,
|
||||
seasonNumber,
|
||||
items,
|
||||
@@ -234,6 +235,8 @@ class SeriesDetailsSeason extends Component {
|
||||
isInteractiveSearchModalOpen
|
||||
} = this.state;
|
||||
|
||||
const title = seasonNumber === 0 ? 'Specials' : `Season ${seasonNumber}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.season}
|
||||
@@ -248,15 +251,9 @@ class SeriesDetailsSeason extends Component {
|
||||
onPress={onMonitorSeasonPress}
|
||||
/>
|
||||
|
||||
{
|
||||
seasonNumber === 0 ?
|
||||
<span className={styles.seasonNumber}>
|
||||
Specials
|
||||
</span> :
|
||||
<span className={styles.seasonNumber}>
|
||||
Season {seasonNumber}
|
||||
</span>
|
||||
}
|
||||
<span className={styles.seasonNumber}>
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<Popover
|
||||
className={styles.episodeCountTooltip}
|
||||
@@ -486,10 +483,19 @@ class SeriesDetailsSeason extends Component {
|
||||
onModalClose={this.onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<EpisodeFileEditorModal
|
||||
<InteractiveImportModal
|
||||
isOpen={isManageEpisodesOpen}
|
||||
seriesId={seriesId}
|
||||
seasonNumber={seasonNumber}
|
||||
title={title}
|
||||
folder={path}
|
||||
initialSortKey="relativePath"
|
||||
initialSortDirection={sortDirections.DESCENDING}
|
||||
showSeries={false}
|
||||
allowSeriesChange={false}
|
||||
autoSelectRow={false}
|
||||
showDelete={true}
|
||||
showImportMode={false}
|
||||
onModalClose={this.onManageEpisodesModalClose}
|
||||
/>
|
||||
|
||||
@@ -513,6 +519,7 @@ class SeriesDetailsSeason extends Component {
|
||||
|
||||
SeriesDetailsSeason.propTypes = {
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
seasonNumber: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -34,6 +34,7 @@ function createMapStateToProps() {
|
||||
columns: episodes.columns,
|
||||
isSearching,
|
||||
seriesMonitored: series.monitored,
|
||||
path: series.path,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
|
||||
3
frontend/src/Series/Details/SeriesGenres.css
Normal file
3
frontend/src/Series/Details/SeriesGenres.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.genres {
|
||||
margin-right: 15px;
|
||||
}
|
||||
53
frontend/src/Series/Details/SeriesGenres.js
Normal file
53
frontend/src/Series/Details/SeriesGenres.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import Label from 'Components/Label';
|
||||
import styles from './SeriesGenres.css';
|
||||
|
||||
function SeriesGenres({ genres }) {
|
||||
const [firstGenre, ...otherGenres] = genres;
|
||||
|
||||
if (otherGenres.length) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={
|
||||
<span className={styles.genres}>
|
||||
{firstGenre}
|
||||
</span>
|
||||
}
|
||||
tooltip={
|
||||
<div>
|
||||
{
|
||||
otherGenres.map((tag) => {
|
||||
return (
|
||||
<Label
|
||||
key={tag}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{tag}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.genres}>
|
||||
{firstGenre}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
SeriesGenres.propTypes = {
|
||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
};
|
||||
|
||||
export default SeriesGenres;
|
||||
@@ -270,7 +270,6 @@ SeriesIndexOverview.propTypes = {
|
||||
};
|
||||
|
||||
SeriesIndexOverview.defaultProps = {
|
||||
overview: '',
|
||||
statistics: {
|
||||
seasonCount: 0,
|
||||
episodeCount: 0,
|
||||
|
||||
@@ -60,7 +60,8 @@ class SeriesIndexOverviews extends Component {
|
||||
columnCount: 1,
|
||||
posterWidth: 162,
|
||||
posterHeight: 238,
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
@@ -71,12 +72,15 @@ class SeriesIndexOverviews extends Component {
|
||||
items,
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter,
|
||||
scrollTop,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
rowHeight
|
||||
rowHeight,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
@@ -95,6 +99,23 @@ class SeriesIndexOverviews extends Component {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (this._grid && index != null) {
|
||||
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: index,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -188,7 +209,6 @@ class SeriesIndexOverviews extends Component {
|
||||
const {
|
||||
scroller,
|
||||
items,
|
||||
jumpToCharacter,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
@@ -210,24 +230,6 @@ class SeriesIndexOverviews extends Component {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
let finalScrollTop = scrollTop;
|
||||
|
||||
if (jumpToCharacter != null) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (index != null) {
|
||||
if (index > 0) {
|
||||
// Adjust 5px upwards so there is a gap between the bottom
|
||||
// of the toolbar and top of the poster.
|
||||
|
||||
finalScrollTop = rowHeight * index - 5;
|
||||
} else {
|
||||
finalScrollTop = 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
@@ -241,7 +243,7 @@ class SeriesIndexOverviews extends Component {
|
||||
rowHeight={rowHeight}
|
||||
width={width}
|
||||
onScroll={onChildScroll}
|
||||
scrollTop={finalScrollTop}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
scrollToAlignment={'start'}
|
||||
@@ -261,6 +263,7 @@ SeriesIndexOverviews.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
overviewOptions: PropTypes.object.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -101,7 +101,8 @@ class SeriesIndexPosters extends Component {
|
||||
columnCount: 1,
|
||||
posterWidth: 162,
|
||||
posterHeight: 238,
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._isInitialized = false;
|
||||
@@ -114,6 +115,8 @@ class SeriesIndexPosters extends Component {
|
||||
items,
|
||||
sortKey,
|
||||
posterOptions,
|
||||
jumpToCharacter,
|
||||
scrollTop,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
@@ -121,7 +124,8 @@ class SeriesIndexPosters extends Component {
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
rowHeight
|
||||
rowHeight,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
@@ -138,6 +142,24 @@ class SeriesIndexPosters extends Component {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (this._grid && index != null) {
|
||||
const row = Math.floor(index / columnCount);
|
||||
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: row,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -242,7 +264,6 @@ class SeriesIndexPosters extends Component {
|
||||
const {
|
||||
scroller,
|
||||
items,
|
||||
jumpToCharacter,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
@@ -268,18 +289,6 @@ class SeriesIndexPosters extends Component {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
let finalScrollTop = scrollTop;
|
||||
|
||||
if (jumpToCharacter != null) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (index != null) {
|
||||
const row = Math.floor(index / columnCount);
|
||||
|
||||
finalScrollTop = rowHeight * row;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
@@ -293,7 +302,7 @@ class SeriesIndexPosters extends Component {
|
||||
rowHeight={rowHeight}
|
||||
width={width}
|
||||
onScroll={onChildScroll}
|
||||
scrollTop={finalScrollTop}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
scrollToAlignment={'start'}
|
||||
@@ -314,6 +323,7 @@ SeriesIndexPosters.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
posterOptions: PropTypes.object.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.releaseGroups,
|
||||
.nextAiring,
|
||||
.previousAiring,
|
||||
.added,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user