mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
275 Commits
v4.1.0.609
...
sonarr-pul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
676dcbae0e | ||
|
|
5a7a9db7ed | ||
|
|
182cda47b0 | ||
|
|
294d95fae4 | ||
|
|
0e3f871e0e | ||
|
|
b0f5f02edc | ||
|
|
2afe6af5a6 | ||
|
|
e2eaf91aa7 | ||
|
|
0e1c2c3c50 | ||
|
|
69cf2e89a6 | ||
|
|
9830230589 | ||
|
|
b1f0b2c216 | ||
|
|
7c6858ecfb | ||
|
|
ee32d42c94 | ||
|
|
3390df4085 | ||
|
|
01bc5f6fc8 | ||
|
|
2d867a6cb6 | ||
|
|
411f8866ec | ||
|
|
5316382113 | ||
|
|
8fe81b428a | ||
|
|
43a2a2d335 | ||
|
|
5c8b58c30d | ||
|
|
131a223bb9 | ||
|
|
dfaab639bf | ||
|
|
c7be63d48f | ||
|
|
2958faf4a8 | ||
|
|
4280df8b61 | ||
|
|
1f91be6407 | ||
|
|
eb43a3c2d0 | ||
|
|
20c7e84676 | ||
|
|
691a8955fe | ||
|
|
53a9c849cb | ||
|
|
856a55a9c9 | ||
|
|
43cd536746 | ||
|
|
4a205d8041 | ||
|
|
2525ac2d1a | ||
|
|
e5ceb20a83 | ||
|
|
0f6b11f55d | ||
|
|
500bc3a571 | ||
|
|
e6567d0365 | ||
|
|
dbca393772 | ||
|
|
9662495fa2 | ||
|
|
76f0c54b3c | ||
|
|
d7ff92115c | ||
|
|
54a49d6878 | ||
|
|
a8362511f9 | ||
|
|
b9f2b3e06f | ||
|
|
d995bc5a7e | ||
|
|
8886162bba | ||
|
|
eb9eb4ec64 | ||
|
|
f910a8fde7 | ||
|
|
f6904608a7 | ||
|
|
0c79548fc4 | ||
|
|
362e664ce6 | ||
|
|
c2cbfb274a | ||
|
|
9b3770a018 | ||
|
|
9db6289693 | ||
|
|
8a63f6ae37 | ||
|
|
069b18e5e3 | ||
|
|
f05333db51 | ||
|
|
f50e8f631e | ||
|
|
b9886cd11c | ||
|
|
9f3eecb2a9 | ||
|
|
52c24a4333 | ||
|
|
9bc31b46fa | ||
|
|
f4d8e113c1 | ||
|
|
1e1a4240d1 | ||
|
|
8fb53df4af | ||
|
|
f6dd600d2b | ||
|
|
40a15d59e0 | ||
|
|
c7baa66de2 | ||
|
|
2be70f5001 | ||
|
|
da857701f6 | ||
|
|
828d7eb1f3 | ||
|
|
b3a056edf9 | ||
|
|
98437c3cac | ||
|
|
c5616c5ba1 | ||
|
|
61979bff7a | ||
|
|
90d0d8bec8 | ||
|
|
2d814ecd20 | ||
|
|
6542119402 | ||
|
|
b9185574f3 | ||
|
|
99e0d42b71 | ||
|
|
d01fa5e6a4 | ||
|
|
2ce9d099e1 | ||
|
|
12829580e5 | ||
|
|
dadd796737 | ||
|
|
a3f508b8d4 | ||
|
|
1ab3df03a3 | ||
|
|
5558e10711 | ||
|
|
573405bae7 | ||
|
|
43d77308f9 | ||
|
|
b3c3f7ddae | ||
|
|
dd5bc41eda | ||
|
|
c8ab4f8c68 | ||
|
|
a4ddae0ccc | ||
|
|
d730161800 | ||
|
|
66c1af0555 | ||
|
|
dca00db317 | ||
|
|
812e5ac5a3 | ||
|
|
d01bae92bf | ||
|
|
1a6bf51741 | ||
|
|
f3e7843150 | ||
|
|
886b9b1c05 | ||
|
|
d8891ee4ea | ||
|
|
192dd9c137 | ||
|
|
b549fddf95 | ||
|
|
c1f538ed97 | ||
|
|
e72f8097fb | ||
|
|
3eec088306 | ||
|
|
ad097dd1a2 | ||
|
|
b4b38a5318 | ||
|
|
b0717a0803 | ||
|
|
4d1d08d345 | ||
|
|
e689817508 | ||
|
|
3b191caf16 | ||
|
|
cc6ca0b067 | ||
|
|
57cb63fb18 | ||
|
|
20f709d22a | ||
|
|
5d8775ac96 | ||
|
|
4890972e16 | ||
|
|
40dc808f61 | ||
|
|
97077e09d2 | ||
|
|
9ba7027d00 | ||
|
|
9903e70925 | ||
|
|
3a6f3666f5 | ||
|
|
915c66be50 | ||
|
|
70b22e483a | ||
|
|
cad1191da5 | ||
|
|
43910af127 | ||
|
|
f01c477b81 | ||
|
|
0054318658 | ||
|
|
03a3f4522a | ||
|
|
3d3562dcda | ||
|
|
7a079c5e0c | ||
|
|
4d70798f2f | ||
|
|
d55864f869 | ||
|
|
3c41c84fb0 | ||
|
|
eae9a6d6e0 | ||
|
|
867f8f5835 | ||
|
|
0c81387cfb | ||
|
|
c5fb5200de | ||
|
|
cc306fcd36 | ||
|
|
2bb7984961 | ||
|
|
21e605452a | ||
|
|
476f5b5bfd | ||
|
|
b6920cfe82 | ||
|
|
e89b98d0f6 | ||
|
|
1db690ad39 | ||
|
|
d5c524719b | ||
|
|
ced6586860 | ||
|
|
8b3019821a | ||
|
|
16ed68d5de | ||
|
|
098a893083 | ||
|
|
548e3400b5 | ||
|
|
5c31e3f1a2 | ||
|
|
7404793dcf | ||
|
|
d8af17ce3d | ||
|
|
44c912f02d | ||
|
|
b104368e23 | ||
|
|
aa0104b6bc | ||
|
|
69fcd8ec94 | ||
|
|
a59928c66a | ||
|
|
1cb7ae11a2 | ||
|
|
ca519047dd | ||
|
|
f15a6abde0 | ||
|
|
2aacebc938 | ||
|
|
120e9b673e | ||
|
|
0a77a13fa8 | ||
|
|
383f9647c3 | ||
|
|
7f7c672b93 | ||
|
|
2690ad8fe1 | ||
|
|
801204b6de | ||
|
|
cb9514abaf | ||
|
|
fd22cb44f6 | ||
|
|
2d68716376 | ||
|
|
b97e76c8b8 | ||
|
|
bfad4a8cd1 | ||
|
|
61f05710f5 | ||
|
|
a8ecefd91f | ||
|
|
e3468daba0 | ||
|
|
f2a7d0d520 | ||
|
|
43257f0726 | ||
|
|
6c2bf860fe | ||
|
|
3a1d848e59 | ||
|
|
f6590e71d2 | ||
|
|
586dd737fd | ||
|
|
fa84dda38c | ||
|
|
4a233ce915 | ||
|
|
ffdd9a1708 | ||
|
|
f890aadffa | ||
|
|
a8695959f1 | ||
|
|
696bb845a5 | ||
|
|
301a6904c0 | ||
|
|
8b4621db61 | ||
|
|
822b597f26 | ||
|
|
737a0176d4 | ||
|
|
5dc541c69e | ||
|
|
a9627771e6 | ||
|
|
5facab0744 | ||
|
|
63dde3eb89 | ||
|
|
144f564076 | ||
|
|
a6205c1ad4 | ||
|
|
64f27bca4f | ||
|
|
8a84975954 | ||
|
|
e923b2fc6c | ||
|
|
a4136150d0 | ||
|
|
1f1f3cdaa2 | ||
|
|
bd85936a62 | ||
|
|
93b2395228 | ||
|
|
14cccd3a23 | ||
|
|
006c9289de | ||
|
|
79cd2b2346 | ||
|
|
f80272a659 | ||
|
|
a158e008e9 | ||
|
|
c798987379 | ||
|
|
097b46c49f | ||
|
|
ddeb3a7840 | ||
|
|
50018d0325 | ||
|
|
a37fc0dc1f | ||
|
|
39ad315e73 | ||
|
|
0559996566 | ||
|
|
e7d4429fe4 | ||
|
|
6c494e9a92 | ||
|
|
62faa1aad8 | ||
|
|
907950e309 | ||
|
|
1caced614e | ||
|
|
5824ba963b | ||
|
|
7f2d5d8d10 | ||
|
|
81bffe243a | ||
|
|
2d6fde282a | ||
|
|
3125b038d5 | ||
|
|
89e25a6241 | ||
|
|
4db6688fe0 | ||
|
|
4ac1aeaf06 | ||
|
|
e2ae743ee1 | ||
|
|
9ad316a6f5 | ||
|
|
b643d2e23d | ||
|
|
6a03eddda9 | ||
|
|
1576bf1f17 | ||
|
|
6325b70e27 | ||
|
|
24206ad0a3 | ||
|
|
2fc7cbff89 | ||
|
|
55ef505d74 | ||
|
|
cabdad6306 | ||
|
|
8d4b2dd21b | ||
|
|
ad04031c99 | ||
|
|
e9a5f87e45 | ||
|
|
bc6ac0cd4b | ||
|
|
c2328e4b79 | ||
|
|
36119facf0 | ||
|
|
a1fa1ddf5d | ||
|
|
ba770dce73 | ||
|
|
d58f0806f6 | ||
|
|
a95f8fa873 | ||
|
|
d1a9cf98cc | ||
|
|
be29fc6adb | ||
|
|
c1085e965b | ||
|
|
dc5c997e9b | ||
|
|
040acbf65a | ||
|
|
d089d036e6 | ||
|
|
46732c7d73 | ||
|
|
8fd3254745 | ||
|
|
9b21408a03 | ||
|
|
bd53092f0c | ||
|
|
80b1aa9a2c | ||
|
|
df863a08a1 | ||
|
|
8a0c318540 | ||
|
|
4f9d067361 | ||
|
|
6739310ba8 | ||
|
|
a45b91abe8 | ||
|
|
1ad2dc54b3 | ||
|
|
7f0b708cb9 | ||
|
|
c8cdf03077 | ||
|
|
c8afe578f3 |
@@ -19,10 +19,10 @@ indent_size = 4
|
|||||||
dotnet_sort_system_directives_first = true
|
dotnet_sort_system_directives_first = true
|
||||||
|
|
||||||
# Avoid "this." and "Me." if not necessary
|
# Avoid "this." and "Me." if not necessary
|
||||||
dotnet_style_qualification_for_field = false:refactoring
|
dotnet_style_qualification_for_field = false:warning
|
||||||
dotnet_style_qualification_for_property = false:refactoring
|
dotnet_style_qualification_for_property = false:warning
|
||||||
dotnet_style_qualification_for_method = false:refactoring
|
dotnet_style_qualification_for_method = false:warning
|
||||||
dotnet_style_qualification_for_event = false:refactoring
|
dotnet_style_qualification_for_event = false:warning
|
||||||
|
|
||||||
# Indentation preferences
|
# Indentation preferences
|
||||||
csharp_indent_block_contents = true
|
csharp_indent_block_contents = true
|
||||||
@@ -32,10 +32,6 @@ csharp_indent_case_contents_when_block = true
|
|||||||
csharp_indent_switch_labels = true
|
csharp_indent_switch_labels = true
|
||||||
csharp_indent_labels = flush_left
|
csharp_indent_labels = flush_left
|
||||||
|
|
||||||
dotnet_style_qualification_for_field = false:suggestion
|
|
||||||
dotnet_style_qualification_for_property = false:suggestion
|
|
||||||
dotnet_style_qualification_for_method = false:suggestion
|
|
||||||
dotnet_style_qualification_for_event = false:suggestion
|
|
||||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"paths": [
|
"paths": [
|
||||||
"frontend/src/**/*.js",
|
"frontend/src/**/*.js"
|
||||||
"src/NzbDrone.Core/Localization/Core/*.json"
|
|
||||||
],
|
],
|
||||||
"ignored": [
|
"ignored": [
|
||||||
"**/node_modules/**/*"
|
"**/node_modules/**/*"
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
github: radarr
|
||||||
patreon: # Replace with a single Patreon username
|
patreon: # Replace with a single Patreon username
|
||||||
open_collective: radarr
|
open_collective: radarr
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
|||||||
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: Is there an existing issue for this?
|
||||||
description: Please search to see if an issue already exists for the bug you encountered.
|
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues
|
- label: I have searched the existing open and closed issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
@@ -42,12 +42,14 @@ body:
|
|||||||
- **Docker Install**: Yes
|
- **Docker Install**: Yes
|
||||||
- **Using Reverse Proxy**: No
|
- **Using Reverse Proxy**: No
|
||||||
- **Browser**: Firefox 90 (If UI related)
|
- **Browser**: Firefox 90 (If UI related)
|
||||||
|
- **Database**: Sqlite 3.36.0
|
||||||
value: |
|
value: |
|
||||||
- OS:
|
- OS:
|
||||||
- Radarr:
|
- Radarr:
|
||||||
- Docker Install:
|
- Docker Install:
|
||||||
- Using Reverse Proxy:
|
- Using Reverse Proxy:
|
||||||
- Browser:
|
- Browser:
|
||||||
|
- Database:
|
||||||
render: markdown
|
render: markdown
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -5,9 +5,9 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Is there an existing issue for this?
|
label: Is there an existing issue for this?
|
||||||
description: Please search to see if an issue already exists for the feature you are requesting.
|
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues
|
- label: I have searched the existing open and closed issues
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
4
.github/workflows/azuresync.yml
vendored
4
.github/workflows/azuresync.yml
vendored
@@ -7,8 +7,12 @@ on:
|
|||||||
|
|
||||||
concurrency: azuresync-${{ github.event.issue.number }}
|
concurrency: azuresync-${{ github.event.issue.number }}
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
alert:
|
alert:
|
||||||
|
permissions:
|
||||||
|
issues: write # to update issue body
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: danhellem/github-actions-issue-to-work-item@master
|
- uses: danhellem/github-actions-issue-to-work-item@master
|
||||||
|
|||||||
5
.github/workflows/lock.yml
vendored
5
.github/workflows/lock.yml
vendored
@@ -5,8 +5,13 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * *'
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
lock:
|
lock:
|
||||||
|
permissions:
|
||||||
|
issues: write # to lock issues (dessant/lock-threads)
|
||||||
|
pull-requests: write # to lock PRs (dessant/lock-threads)
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v2
|
- uses: dessant/lock-threads@v2
|
||||||
|
|||||||
4
.github/workflows/support.yml
vendored
4
.github/workflows/support.yml
vendored
@@ -4,8 +4,12 @@ on:
|
|||||||
issues:
|
issues:
|
||||||
types: [labeled, unlabeled, reopened]
|
types: [labeled, unlabeled, reopened]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
support:
|
support:
|
||||||
|
permissions:
|
||||||
|
issues: write # to modify issues
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/support-requests@v2
|
- uses: dessant/support-requests@v2
|
||||||
|
|||||||
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
<development@radarr.video>.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
@@ -7,16 +7,20 @@ variables:
|
|||||||
outputFolder: './_output'
|
outputFolder: './_output'
|
||||||
artifactsFolder: './_artifacts'
|
artifactsFolder: './_artifacts'
|
||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
majorVersion: '4.1.0'
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
|
majorVersion: '4.3.1'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.201'
|
dotnetVersion: '6.0.400'
|
||||||
nodeVersion: '16.X'
|
nodeVersion: '16.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
windowsImage: 'windows-2022'
|
||||||
|
linuxImage: 'ubuntu-20.04'
|
||||||
|
macImage: 'macOS-11'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
@@ -40,7 +44,7 @@ stages:
|
|||||||
- job:
|
- job:
|
||||||
displayName: Build Variables
|
displayName: Build Variables
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
steps:
|
steps:
|
||||||
# Set the build name properly. The 'name' property won't recursively expand so hack here:
|
# Set the build name properly. The 'name' property won't recursively expand so hack here:
|
||||||
- bash: echo "##vso[build.updatebuildnumber]$RADARRVERSION"
|
- bash: echo "##vso[build.updatebuildnumber]$RADARRVERSION"
|
||||||
@@ -66,15 +70,15 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
Linux:
|
Linux:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
imageName: 'ubuntu-18.04'
|
imageName: ${{ variables.linuxImage }}
|
||||||
enableAnalysis: 'true'
|
enableAnalysis: 'true'
|
||||||
Mac:
|
Mac:
|
||||||
osName: 'Mac'
|
osName: 'Mac'
|
||||||
imageName: 'macos-10.15'
|
imageName: ${{ variables.macImage }}
|
||||||
enableAnalysis: 'false'
|
enableAnalysis: 'false'
|
||||||
Windows:
|
Windows:
|
||||||
osName: 'Windows'
|
osName: 'Windows'
|
||||||
imageName: 'windows-2019'
|
imageName: ${{ variables.windowsImage }}
|
||||||
enableAnalysis: 'false'
|
enableAnalysis: 'false'
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
@@ -93,15 +97,14 @@ stages:
|
|||||||
- bash: |
|
- bash: |
|
||||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||||
echo $BUNDLEDVERSIONS
|
echo $BUNDLEDVERSIONS
|
||||||
grep osx-x64 $BUNDLEDVERSIONS
|
|
||||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||||
echo "BSD already enabled"
|
echo "Extra platforms already enabled"
|
||||||
else
|
else
|
||||||
echo "Enabling BSD support"
|
echo "Enabling extra platform support"
|
||||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
|
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||||
fi
|
fi
|
||||||
displayName: Enable FreeBSD Support
|
displayName: Enable Extra Platform Support
|
||||||
- bash: ./build.sh --backend --enable-bsd
|
- bash: ./build.sh --backend --enable-extra-platforms
|
||||||
displayName: Build Radarr Backend
|
displayName: Build Radarr Backend
|
||||||
- bash: |
|
- bash: |
|
||||||
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||||
@@ -115,24 +118,28 @@ stages:
|
|||||||
displayName: Publish Backend
|
displayName: Publish Backend
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||||
artifact: WindowsCoreTests
|
artifact: win-x64-tests
|
||||||
displayName: Publish Windows Test Package
|
displayName: Publish win-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||||
artifact: LinuxCoreTests
|
artifact: linux-x64-tests
|
||||||
displayName: Publish Linux Test Package
|
displayName: Publish linux-x64 Test Package
|
||||||
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
|
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||||
|
artifact: linux-x86-tests
|
||||||
|
displayName: Publish linux-x86 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||||
artifact: LinuxMuslCoreTests
|
artifact: linux-musl-x64-tests
|
||||||
displayName: Publish Linux Musl Test Package
|
displayName: Publish linux-musl-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||||
artifact: FreebsdCoreTests
|
artifact: freebsd-x64-tests
|
||||||
displayName: Publish FreeBSD Test Package
|
displayName: Publish freebsd-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||||
artifact: MacCoreTests
|
artifact: osx-x64-tests
|
||||||
displayName: Publish MacOS Test Package
|
displayName: Publish osx-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
|
|
||||||
- stage: Build_Frontend
|
- stage: Build_Frontend
|
||||||
@@ -144,13 +151,13 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
Linux:
|
Linux:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
imageName: 'ubuntu-18.04'
|
imageName: ${{ variables.linuxImage }}
|
||||||
Mac:
|
Mac:
|
||||||
osName: 'Mac'
|
osName: 'Mac'
|
||||||
imageName: 'macos-10.15'
|
imageName: ${{ variables.macImage }}
|
||||||
Windows:
|
Windows:
|
||||||
osName: 'Windows'
|
osName: 'Windows'
|
||||||
imageName: 'windows-2019'
|
imageName: ${{ variables.windowsImage }}
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
@@ -166,7 +173,6 @@ stages:
|
|||||||
key: 'yarn | "$(osName)" | yarn.lock'
|
key: 'yarn | "$(osName)" | yarn.lock'
|
||||||
restoreKeys: |
|
restoreKeys: |
|
||||||
yarn | "$(osName)"
|
yarn | "$(osName)"
|
||||||
yarn
|
|
||||||
path: $(yarnCacheFolder)
|
path: $(yarnCacheFolder)
|
||||||
displayName: Cache Yarn packages
|
displayName: Cache Yarn packages
|
||||||
- bash: ./build.sh --frontend
|
- bash: ./build.sh --frontend
|
||||||
@@ -187,7 +193,7 @@ stages:
|
|||||||
- job: Windows_Installer
|
- job: Windows_Installer
|
||||||
displayName: Create Installer
|
displayName: Create Installer
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'windows-2019'
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self
|
- checkout: self
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -220,7 +226,7 @@ stages:
|
|||||||
- job: Other_Packages
|
- job: Other_Packages
|
||||||
displayName: Create Standard Packages
|
displayName: Create Standard Packages
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self
|
- checkout: self
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
@@ -236,7 +242,7 @@ stages:
|
|||||||
artifactName: WindowsFrontend
|
artifactName: WindowsFrontend
|
||||||
targetPath: _output
|
targetPath: _output
|
||||||
displayName: Fetch Frontend
|
displayName: Fetch Frontend
|
||||||
- bash: ./build.sh --packages --enable-bsd
|
- bash: ./build.sh --packages --enable-extra-platforms
|
||||||
displayName: Create Packages
|
displayName: Create Packages
|
||||||
- bash: |
|
- bash: |
|
||||||
find . -name "ffprobe" -exec chmod a+x {} \;
|
find . -name "ffprobe" -exec chmod a+x {} \;
|
||||||
@@ -244,28 +250,28 @@ stages:
|
|||||||
find . -name "Radarr.Update" -exec chmod a+x {} \;
|
find . -name "Radarr.Update" -exec chmod a+x {} \;
|
||||||
displayName: Set executable bits
|
displayName: Set executable bits
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create Windows Core zip
|
displayName: Create win-x64 zip
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create Windows x86 Core zip
|
displayName: Create win-x86 zip
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create MacOS x64 Core app
|
displayName: Create osx-x64 app
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create MacOS x64 Core tar
|
displayName: Create osx-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -273,14 +279,14 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create MacOS arm64 Core app
|
displayName: Create osx-arm64 app
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create MacOS arm64 Core tar
|
displayName: Create osx-arm64 tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -288,7 +294,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create Linux Core tar
|
displayName: Create linux-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -296,7 +302,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create Linux Musl Core tar
|
displayName: Create linux-musl-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-x64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-x64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -304,7 +310,15 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create ARM32 Linux Core tar
|
displayName: Create linux-x86 tar
|
||||||
|
inputs:
|
||||||
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x86.tar.gz'
|
||||||
|
archiveType: 'tar'
|
||||||
|
tarCompression: 'gz'
|
||||||
|
includeRootFolder: false
|
||||||
|
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
|
||||||
|
- task: ArchiveFiles@2
|
||||||
|
displayName: Create linux-arm tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -312,7 +326,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create ARM32 Linux Musl Core tar
|
displayName: Create linux-musl-arm tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -320,7 +334,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create ARM64 Linux Core tar
|
displayName: Create linux-arm64 tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -328,7 +342,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create ARM64 Linux Musl Core tar
|
displayName: Create linux-musl-arm64 tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -336,7 +350,7 @@ stages:
|
|||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create FreeBSD Core Core tar
|
displayName: Create freebsd-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).freebsd-core-x64.tar.gz'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).freebsd-core-x64.tar.gz'
|
||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
@@ -381,7 +395,7 @@ stages:
|
|||||||
jobs:
|
jobs:
|
||||||
- job: Prepare
|
- job: Prepare
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: none
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
@@ -403,22 +417,22 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
MacCore:
|
MacCore:
|
||||||
osName: 'Mac'
|
osName: 'Mac'
|
||||||
testName: 'MacCore'
|
testName: 'osx-x64'
|
||||||
poolName: 'Azure Pipelines'
|
poolName: 'Azure Pipelines'
|
||||||
imageName: 'macos-10.15'
|
imageName: ${{ variables.macImage }}
|
||||||
WindowsCore:
|
WindowsCore:
|
||||||
osName: 'Windows'
|
osName: 'Windows'
|
||||||
testName: 'WindowsCore'
|
testName: 'win-x64'
|
||||||
poolName: 'Azure Pipelines'
|
poolName: 'Azure Pipelines'
|
||||||
imageName: 'windows-2019'
|
imageName: ${{ variables.windowsImage }}
|
||||||
LinuxCore:
|
LinuxCore:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
testName: 'LinuxCore'
|
testName: 'linux-x64'
|
||||||
poolName: 'Azure Pipelines'
|
poolName: 'Azure Pipelines'
|
||||||
imageName: 'ubuntu-18.04'
|
imageName: ${{ variables.linuxImage }}
|
||||||
FreebsdCore:
|
FreebsdCore:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
testName: 'FreebsdCore'
|
testName: 'freebsd-x64'
|
||||||
poolName: 'FreeBSD'
|
poolName: 'FreeBSD'
|
||||||
imageName:
|
imageName:
|
||||||
|
|
||||||
@@ -437,7 +451,7 @@ stages:
|
|||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: '$(testName)Tests'
|
artifactName: '$(testName)-tests'
|
||||||
targetPath: $(testsFolder)
|
targetPath: $(testsFolder)
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
@@ -471,11 +485,15 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
alpine:
|
alpine:
|
||||||
testName: 'Musl Net Core'
|
testName: 'Musl Net Core'
|
||||||
artifactName: LinuxMuslCoreTests
|
artifactName: linux-musl-x64-tests
|
||||||
containerImage: ghcr.io/servarr/testimages:alpine
|
containerImage: ghcr.io/servarr/testimages:alpine
|
||||||
|
linux-x86:
|
||||||
|
testName: 'linux-x86'
|
||||||
|
artifactName: linux-x86-tests
|
||||||
|
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
container: $[ variables['containerImage'] ]
|
container: $[ variables['containerImage'] ]
|
||||||
|
|
||||||
@@ -483,9 +501,15 @@ stages:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .NET'
|
||||||
inputs:
|
inputs:
|
||||||
version: $(dotnetVersion)
|
version: $(dotnetVersion)
|
||||||
|
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||||
|
- bash: |
|
||||||
|
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||||
|
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||||
|
displayName: 'Install .NET'
|
||||||
|
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||||
- checkout: none
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
@@ -511,6 +535,62 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
|
||||||
|
- job: Unit_LinuxCore_Postgres
|
||||||
|
displayName: Unit Native LinuxCore with Postgres Database
|
||||||
|
dependsOn: Prepare
|
||||||
|
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||||
|
variables:
|
||||||
|
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||||
|
artifactName: linux-x64-tests
|
||||||
|
Radarr__Postgres__Host: 'localhost'
|
||||||
|
Radarr__Postgres__Port: '5432'
|
||||||
|
Radarr__Postgres__User: 'radarr'
|
||||||
|
Radarr__Postgres__Password: 'radarr'
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: UseDotNet@2
|
||||||
|
displayName: 'Install .net core'
|
||||||
|
inputs:
|
||||||
|
version: $(dotnetVersion)
|
||||||
|
- checkout: none
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Test Artifact
|
||||||
|
inputs:
|
||||||
|
buildType: 'current'
|
||||||
|
artifactName: $(artifactName)
|
||||||
|
targetPath: $(testsFolder)
|
||||||
|
- bash: |
|
||||||
|
chmod a+x _tests/ffprobe
|
||||||
|
displayName: Make ffprobe Executable
|
||||||
|
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
|
||||||
|
displayName: Make Test Dummy Executable
|
||||||
|
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||||
|
- bash: |
|
||||||
|
docker run -d --name=postgres14 \
|
||||||
|
-e POSTGRES_PASSWORD=radarr \
|
||||||
|
-e POSTGRES_USER=radarr \
|
||||||
|
-p 5432:5432/tcp \
|
||||||
|
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||||
|
postgres:14
|
||||||
|
displayName: Start postgres
|
||||||
|
- bash: |
|
||||||
|
chmod a+x ${TESTSFOLDER}/test.sh
|
||||||
|
ls -lR ${TESTSFOLDER}
|
||||||
|
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||||
|
displayName: Run Tests
|
||||||
|
- task: PublishTestResults@2
|
||||||
|
displayName: Publish Test Results
|
||||||
|
inputs:
|
||||||
|
testResultsFormat: 'NUnit'
|
||||||
|
testResultsFiles: '**/TestResult.xml'
|
||||||
|
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||||
|
failTaskOnFailedTests: true
|
||||||
|
|
||||||
- stage: Integration
|
- stage: Integration
|
||||||
displayName: Integration
|
displayName: Integration
|
||||||
@@ -519,7 +599,7 @@ stages:
|
|||||||
jobs:
|
jobs:
|
||||||
- job: Prepare
|
- job: Prepare
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: none
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
@@ -538,18 +618,18 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
MacCore:
|
MacCore:
|
||||||
osName: 'Mac'
|
osName: 'Mac'
|
||||||
testName: 'MacCore'
|
testName: 'osx-x64'
|
||||||
imageName: 'macos-10.15'
|
imageName: ${{ variables.macImage }}
|
||||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||||
WindowsCore:
|
WindowsCore:
|
||||||
osName: 'Windows'
|
osName: 'Windows'
|
||||||
testName: 'WindowsCore'
|
testName: 'win-x64'
|
||||||
imageName: 'windows-2019'
|
imageName: ${{ variables.windowsImage }}
|
||||||
pattern: 'Radarr.*.windows-core-x64.zip'
|
pattern: 'Radarr.*.windows-core-x64.zip'
|
||||||
LinuxCore:
|
LinuxCore:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
testName: 'LinuxCore'
|
testName: 'linux-x64'
|
||||||
imageName: 'ubuntu-18.04'
|
imageName: ${{ variables.linuxImage }}
|
||||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
@@ -565,7 +645,7 @@ stages:
|
|||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: '$(testName)Tests'
|
artifactName: '$(testName)-tests'
|
||||||
targetPath: $(testsFolder)
|
targetPath: $(testsFolder)
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Build Artifact
|
displayName: Download Build Artifact
|
||||||
@@ -595,6 +675,68 @@ stages:
|
|||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
|
- job: Integration_LinuxCore_Postgres
|
||||||
|
displayName: Integration Native LinuxCore with Postgres Database
|
||||||
|
dependsOn: Prepare
|
||||||
|
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||||
|
variables:
|
||||||
|
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||||
|
Radarr__Postgres__Host: 'localhost'
|
||||||
|
Radarr__Postgres__Port: '5432'
|
||||||
|
Radarr__Postgres__User: 'radarr'
|
||||||
|
Radarr__Postgres__Password: 'radarr'
|
||||||
|
|
||||||
|
pool:
|
||||||
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- task: UseDotNet@2
|
||||||
|
displayName: 'Install .net core'
|
||||||
|
inputs:
|
||||||
|
version: $(dotnetVersion)
|
||||||
|
- checkout: none
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Test Artifact
|
||||||
|
inputs:
|
||||||
|
buildType: 'current'
|
||||||
|
artifactName: 'linux-x64-tests'
|
||||||
|
targetPath: $(testsFolder)
|
||||||
|
- task: DownloadPipelineArtifact@2
|
||||||
|
displayName: Download Build Artifact
|
||||||
|
inputs:
|
||||||
|
buildType: 'current'
|
||||||
|
artifactName: Packages
|
||||||
|
itemPattern: '**/$(pattern)'
|
||||||
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
|
- task: ExtractFiles@1
|
||||||
|
inputs:
|
||||||
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
|
displayName: Extract Package
|
||||||
|
- bash: |
|
||||||
|
mkdir -p ./bin/
|
||||||
|
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
|
||||||
|
displayName: Move Package Contents
|
||||||
|
- bash: |
|
||||||
|
docker run -d --name=postgres14 \
|
||||||
|
-e POSTGRES_PASSWORD=radarr \
|
||||||
|
-e POSTGRES_USER=radarr \
|
||||||
|
-p 5432:5432/tcp \
|
||||||
|
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||||
|
postgres:14
|
||||||
|
displayName: Start postgres
|
||||||
|
- bash: |
|
||||||
|
chmod a+x ${TESTSFOLDER}/test.sh
|
||||||
|
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||||
|
displayName: Run Integration Tests
|
||||||
|
- task: PublishTestResults@2
|
||||||
|
inputs:
|
||||||
|
testResultsFormat: 'NUnit'
|
||||||
|
testResultsFiles: '**/TestResult.xml'
|
||||||
|
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||||
|
failTaskOnFailedTests: true
|
||||||
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_FreeBSD
|
- job: Integration_FreeBSD
|
||||||
displayName: Integration Native FreeBSD
|
displayName: Integration Native FreeBSD
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
@@ -612,14 +754,14 @@ stages:
|
|||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: 'FreebsdCoreTests'
|
artifactName: 'freebsd-x64-tests'
|
||||||
targetPath: $(testsFolder)
|
targetPath: $(testsFolder)
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Build Artifact
|
displayName: Download Build Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: Packages
|
artifactName: Packages
|
||||||
itemPattern: '/$(pattern)'
|
itemPattern: '**/$(pattern)'
|
||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- bash: |
|
- bash: |
|
||||||
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
|
||||||
@@ -648,12 +790,17 @@ stages:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
alpine:
|
alpine:
|
||||||
testName: 'Musl Net Core'
|
testName: 'linux-musl-x64'
|
||||||
artifactName: LinuxMuslCoreTests
|
artifactName: linux-musl-x64-tests
|
||||||
containerImage: ghcr.io/servarr/testimages:alpine
|
containerImage: ghcr.io/servarr/testimages:alpine
|
||||||
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz'
|
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz'
|
||||||
|
linux-x86:
|
||||||
|
testName: 'linux-x86'
|
||||||
|
artifactName: linux-x86-tests
|
||||||
|
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||||
|
pattern: 'Radarr.*.linux-core-x86.tar.gz'
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
container: $[ variables['containerImage'] ]
|
container: $[ variables['containerImage'] ]
|
||||||
|
|
||||||
@@ -661,9 +808,15 @@ stages:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .NET'
|
||||||
inputs:
|
inputs:
|
||||||
version: $(dotnetVersion)
|
version: $(dotnetVersion)
|
||||||
|
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||||
|
- bash: |
|
||||||
|
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||||
|
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||||
|
displayName: 'Install .NET'
|
||||||
|
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||||
- checkout: none
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
@@ -709,17 +862,20 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
Linux:
|
Linux:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
imageName: 'ubuntu-18.04'
|
artifactName: 'linux-x64'
|
||||||
|
imageName: ${{ variables.linuxImage }}
|
||||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||||
failBuild: true
|
failBuild: true
|
||||||
Mac:
|
Mac:
|
||||||
osName: 'Mac'
|
osName: 'Mac'
|
||||||
imageName: 'macos-10.15'
|
artifactName: 'osx-x64'
|
||||||
|
imageName: ${{ variables.macImage }}
|
||||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||||
failBuild: true
|
failBuild: true
|
||||||
Windows:
|
Windows:
|
||||||
osName: 'Windows'
|
osName: 'Windows'
|
||||||
imageName: 'windows-2019'
|
artifactName: 'win-x64'
|
||||||
|
imageName: ${{ variables.windowsImage }}
|
||||||
pattern: 'Radarr.*.windows-core-x64.zip'
|
pattern: 'Radarr.*.windows-core-x64.zip'
|
||||||
failBuild: true
|
failBuild: true
|
||||||
|
|
||||||
@@ -736,7 +892,7 @@ stages:
|
|||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
inputs:
|
inputs:
|
||||||
buildType: 'current'
|
buildType: 'current'
|
||||||
artifactName: '$(osName)CoreTests'
|
artifactName: '$(artifactName)-tests'
|
||||||
targetPath: $(testsFolder)
|
targetPath: $(testsFolder)
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Build Artifact
|
displayName: Download Build Artifact
|
||||||
@@ -785,7 +941,7 @@ stages:
|
|||||||
jobs:
|
jobs:
|
||||||
- job: Prepare
|
- job: Prepare
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: none
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
@@ -802,10 +958,10 @@ stages:
|
|||||||
matrix:
|
matrix:
|
||||||
Linux:
|
Linux:
|
||||||
osName: 'Linux'
|
osName: 'Linux'
|
||||||
imageName: 'ubuntu-18.04'
|
imageName: ${{ variables.linuxImage }}
|
||||||
Windows:
|
Windows:
|
||||||
osName: 'Windows'
|
osName: 'Windows'
|
||||||
imageName: 'windows-2019'
|
imageName: ${{ variables.windowsImage }}
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
@@ -821,7 +977,6 @@ stages:
|
|||||||
key: 'yarn | "$(osName)" | yarn.lock'
|
key: 'yarn | "$(osName)" | yarn.lock'
|
||||||
restoreKeys: |
|
restoreKeys: |
|
||||||
yarn | "$(osName)"
|
yarn | "$(osName)"
|
||||||
yarn
|
|
||||||
path: $(yarnCacheFolder)
|
path: $(yarnCacheFolder)
|
||||||
displayName: Cache Yarn packages
|
displayName: Cache Yarn packages
|
||||||
- bash: ./build.sh --lint
|
- bash: ./build.sh --lint
|
||||||
@@ -834,7 +989,7 @@ stages:
|
|||||||
displayName: Frontend
|
displayName: Frontend
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
pool:
|
pool:
|
||||||
vmImage: windows-2019
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@1
|
- task: SonarCloudPrepare@1
|
||||||
@@ -862,7 +1017,7 @@ stages:
|
|||||||
)
|
)
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: windows-2019
|
vmImage: ${{ variables.windowsImage }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
@@ -915,7 +1070,7 @@ stages:
|
|||||||
EnableAnalyzers: 'false'
|
EnableAnalyzers: 'false'
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: windows-2019
|
vmImage: ${{ variables.windowsImage }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
@@ -972,7 +1127,7 @@ stages:
|
|||||||
- job:
|
- job:
|
||||||
displayName: Discord Notification
|
displayName: Discord Notification
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-18.04'
|
vmImage: ${{ variables.linuxImage }}
|
||||||
steps:
|
steps:
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
continueOnError: true
|
continueOnError: true
|
||||||
@@ -988,4 +1143,5 @@ stages:
|
|||||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||||
DISCORDCHANNELID: $(discordChannelId)
|
DISCORDCHANNELID: $(discordChannelId)
|
||||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||||
|
DISCORDTHREADID: $(discordThreadId)
|
||||||
|
|
||||||
|
|||||||
44
build.sh
44
build.sh
@@ -25,14 +25,22 @@ UpdateVersionNumber()
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
EnableBsdSupport()
|
EnableExtraPlatformsInSDK()
|
||||||
{
|
{
|
||||||
#todo enable sdk with
|
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||||
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||||
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||||
|
echo "Extra platforms already enabled"
|
||||||
|
else
|
||||||
|
echo "Enabling extra platform support"
|
||||||
|
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
EnableExtraPlatforms()
|
||||||
|
{
|
||||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
|
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +300,8 @@ if [ $# -eq 0 ]; then
|
|||||||
PACKAGES=YES
|
PACKAGES=YES
|
||||||
INSTALLER=NO
|
INSTALLER=NO
|
||||||
LINT=YES
|
LINT=YES
|
||||||
ENABLE_BSD=NO
|
ENABLE_EXTRA_PLATFORMS=NO
|
||||||
|
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]
|
while [[ $# -gt 0 ]]
|
||||||
@@ -304,8 +313,12 @@ case $key in
|
|||||||
BACKEND=YES
|
BACKEND=YES
|
||||||
shift # past argument
|
shift # past argument
|
||||||
;;
|
;;
|
||||||
--enable-bsd)
|
--enable-bsd|--enable-extra-platforms)
|
||||||
ENABLE_BSD=YES
|
ENABLE_EXTRA_PLATFORMS=YES
|
||||||
|
shift # past argument
|
||||||
|
;;
|
||||||
|
--enable-extra-platforms-in-sdk)
|
||||||
|
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
|
||||||
shift # past argument
|
shift # past argument
|
||||||
;;
|
;;
|
||||||
-r|--runtime)
|
-r|--runtime)
|
||||||
@@ -349,12 +362,17 @@ esac
|
|||||||
done
|
done
|
||||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||||
|
|
||||||
|
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
|
||||||
|
then
|
||||||
|
EnableExtraPlatformsInSDK
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$BACKEND" = "YES" ];
|
if [ "$BACKEND" = "YES" ];
|
||||||
then
|
then
|
||||||
UpdateVersionNumber
|
UpdateVersionNumber
|
||||||
if [ "$ENABLE_BSD" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
EnableBsdSupport
|
EnableExtraPlatforms
|
||||||
fi
|
fi
|
||||||
Build
|
Build
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
@@ -364,9 +382,10 @@ then
|
|||||||
PackageTests "net6.0" "linux-x64"
|
PackageTests "net6.0" "linux-x64"
|
||||||
PackageTests "net6.0" "linux-musl-x64"
|
PackageTests "net6.0" "linux-musl-x64"
|
||||||
PackageTests "net6.0" "osx-x64"
|
PackageTests "net6.0" "osx-x64"
|
||||||
if [ "$ENABLE_BSD" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "freebsd-x64"
|
PackageTests "net6.0" "freebsd-x64"
|
||||||
|
PackageTests "net6.0" "linux-x86"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
PackageTests "$FRAMEWORK" "$RID"
|
PackageTests "$FRAMEWORK" "$RID"
|
||||||
@@ -405,9 +424,10 @@ then
|
|||||||
Package "net6.0" "linux-musl-arm"
|
Package "net6.0" "linux-musl-arm"
|
||||||
Package "net6.0" "osx-x64"
|
Package "net6.0" "osx-x64"
|
||||||
Package "net6.0" "osx-arm64"
|
Package "net6.0" "osx-arm64"
|
||||||
if [ "$ENABLE_BSD" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
Package "net6.0" "freebsd-x64"
|
Package "net6.0" "freebsd-x64"
|
||||||
|
Package "net6.0" "linux-x86"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
Package "$FRAMEWORK" "$RID"
|
Package "$FRAMEWORK" "$RID"
|
||||||
|
|||||||
@@ -223,7 +223,6 @@ module.exports = (env) => {
|
|||||||
{
|
{
|
||||||
loader: 'url-loader',
|
loader: 'url-loader',
|
||||||
options: {
|
options: {
|
||||||
limit: 10240,
|
|
||||||
mimetype: 'application/font-woff',
|
mimetype: 'application/font-woff',
|
||||||
emitFile: false,
|
emitFile: false,
|
||||||
name: 'Content/Fonts/[name].[ext]'
|
name: 'Content/Fonts/[name].[ext]'
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customFormatScore {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
.releaseGroup {
|
.releaseGroup {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import MovieFormats from 'Movie/MovieFormats';
|
|||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||||
import styles from './HistoryRow.css';
|
import styles from './HistoryRow.css';
|
||||||
@@ -168,6 +169,17 @@ class HistoryRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.customFormatScore}
|
||||||
|
>
|
||||||
|
{formatCustomFormatScore(data.customFormatScore)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'releaseGroup') {
|
if (name === 'releaseGroup') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
@@ -179,6 +191,16 @@ class HistoryRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
>
|
||||||
|
{sourceTitle}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'details') {
|
if (name === 'details') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import HeartRating from 'Components/HeartRating';
|
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
@@ -190,7 +190,7 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label size={sizes.LARGE}>
|
<Label size={sizes.LARGE}>
|
||||||
<HeartRating
|
<TmdbRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
iconSize={13}
|
iconSize={13}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.movie {
|
.container {
|
||||||
|
display: flex;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -6,3 +7,19 @@
|
|||||||
background-color: $menuItemHoverBackgroundColor;
|
background-color: $menuItemHoverBackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.movie {
|
||||||
|
flex: 1 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdbLink {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
margin-left: auto;
|
||||||
|
color: $textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tmdbLinkIcon {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
import ImportMovieTitle from './ImportMovieTitle';
|
import ImportMovieTitle from './ImportMovieTitle';
|
||||||
import styles from './ImportMovieSearchResult.css';
|
import styles from './ImportMovieSearchResult.css';
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ class ImportMovieSearchResult extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
tmdbId,
|
||||||
title,
|
title,
|
||||||
year,
|
year,
|
||||||
studio,
|
studio,
|
||||||
@@ -25,17 +28,30 @@ class ImportMovieSearchResult extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<div className={styles.container}>
|
||||||
className={styles.movie}
|
<Link
|
||||||
onPress={this.onPress}
|
className={styles.movie}
|
||||||
>
|
onPress={this.onPress}
|
||||||
<ImportMovieTitle
|
>
|
||||||
title={title}
|
<ImportMovieTitle
|
||||||
year={year}
|
title={title}
|
||||||
network={studio}
|
year={year}
|
||||||
isExistingMovie={isExistingMovie}
|
network={studio}
|
||||||
/>
|
isExistingMovie={isExistingMovie}
|
||||||
</Link>
|
/>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={styles.tmdbLink}
|
||||||
|
to={`https://www.themoviedb.org/movie/${tmdbId}`}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={styles.tmdbLinkIcon}
|
||||||
|
name={icons.EXTERNAL_LINK}
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
|||||||
|
|
||||||
function App({ store, history }) {
|
function App({ store, history }) {
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title="Radarr">
|
<DocumentTitle title={window.Radarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import QueueConnector from 'Activity/Queue/QueueConnector';
|
|||||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||||
|
import CollectionConnector from 'Collection/CollectionConnector';
|
||||||
import NotFound from 'Components/NotFound';
|
import NotFound from 'Components/NotFound';
|
||||||
import Switch from 'Components/Router/Switch';
|
import Switch from 'Components/Router/Switch';
|
||||||
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
||||||
@@ -72,6 +73,11 @@ function AppRoutes(props) {
|
|||||||
component={AddNewMovieConnector}
|
component={AddNewMovieConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/collections"
|
||||||
|
component={CollectionConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/add/import"
|
path="/add/import"
|
||||||
component={ImportMovies}
|
component={ImportMovies}
|
||||||
|
|||||||
31
frontend/src/Collection/AddNewCollectionMovieModal.js
Normal file
31
frontend/src/Collection/AddNewCollectionMovieModal.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AddNewCollectionMovieModalContentConnector from './AddNewCollectionMovieModalContentConnector';
|
||||||
|
|
||||||
|
function AddNewCollectionMovieModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AddNewCollectionMovieModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewCollectionMovieModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddNewCollectionMovieModal;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: $disabledColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
flex: 0 0 170px;
|
||||||
|
margin-right: 20px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingMovieLabelContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingMovieLabel {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingMovieContainer {
|
||||||
|
composes: container from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
flex: 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchForMissingMovieInput {
|
||||||
|
composes: input from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter {
|
||||||
|
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
@add-mixin truncate;
|
||||||
|
composes: button from '~Components/Link/SpinnerButton.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.modalFooter {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
204
frontend/src/Collection/AddNewCollectionMovieModalContent.js
Normal file
204
frontend/src/Collection/AddNewCollectionMovieModalContent.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AddNewCollectionMovieModalContent.css';
|
||||||
|
|
||||||
|
class AddNewCollectionMovieModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onQualityProfileIdChange = ({ value }) => {
|
||||||
|
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMoviePress = () => {
|
||||||
|
this.props.onAddMoviePress();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
overview,
|
||||||
|
images,
|
||||||
|
isAdding,
|
||||||
|
folder,
|
||||||
|
tags,
|
||||||
|
isSmallScreen,
|
||||||
|
isWindows,
|
||||||
|
onModalClose,
|
||||||
|
onInputChange,
|
||||||
|
rootFolderPath,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
searchForMovie
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{
|
||||||
|
!title.contains(year) && !!year &&
|
||||||
|
<span className={styles.year}>({year})</span>
|
||||||
|
}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.overview}>
|
||||||
|
{overview}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
valueOptions={{
|
||||||
|
movieFolder: folder,
|
||||||
|
isWindows
|
||||||
|
}}
|
||||||
|
selectedValueOptions={{
|
||||||
|
movieFolder: folder,
|
||||||
|
isWindows
|
||||||
|
}}
|
||||||
|
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...rootFolderPath}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('Monitor')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MOVIE_MONITORED_SELECT}
|
||||||
|
name="monitor"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...monitor}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
name="minimumAvailability"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...minimumAvailability}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
onChange={this.onQualityProfileIdChange}
|
||||||
|
{...qualityProfileId}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...tags}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<label className={styles.searchForMissingMovieLabelContainer}>
|
||||||
|
<span className={styles.searchForMissingMovieLabel}>
|
||||||
|
{translate('StartSearchForMissingMovie')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.searchForMissingMovieContainer}
|
||||||
|
className={styles.searchForMissingMovieInput}
|
||||||
|
name="searchForMovie"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...searchForMovie}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.addButton}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
isSpinning={isAdding}
|
||||||
|
onPress={this.onAddMoviePress}
|
||||||
|
>
|
||||||
|
{translate('AddMovie')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewCollectionMovieModalContent.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
overview: PropTypes.string,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
addError: PropTypes.object,
|
||||||
|
rootFolderPath: PropTypes.object,
|
||||||
|
monitor: PropTypes.object.isRequired,
|
||||||
|
qualityProfileId: PropTypes.object,
|
||||||
|
minimumAvailability: PropTypes.object.isRequired,
|
||||||
|
searchForMovie: PropTypes.object.isRequired,
|
||||||
|
folder: PropTypes.string.isRequired,
|
||||||
|
tags: PropTypes.object.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
isWindows: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onAddMoviePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddNewCollectionMovieModalContent;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { addMovie, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import AddNewMovieModalContent from './AddNewCollectionMovieModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections,
|
||||||
|
createCollectionSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
createSystemStatusSelector(),
|
||||||
|
(discoverMovieState, collection, dimensions, systemStatus) => {
|
||||||
|
const {
|
||||||
|
isAdding,
|
||||||
|
addError,
|
||||||
|
pendingChanges
|
||||||
|
} = discoverMovieState;
|
||||||
|
|
||||||
|
const collectionDefaults = {
|
||||||
|
rootFolderPath: collection.rootFolderPath,
|
||||||
|
monitor: 'movieOnly',
|
||||||
|
qualityProfileId: collection.qualityProfileId,
|
||||||
|
minimumAvailability: collection.minimumAvailability,
|
||||||
|
searchForMovie: collection.searchOnAdd,
|
||||||
|
tags: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
settings,
|
||||||
|
validationErrors,
|
||||||
|
validationWarnings
|
||||||
|
} = selectSettings(collectionDefaults, pendingChanges, addError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAdding,
|
||||||
|
addError,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
validationErrors,
|
||||||
|
validationWarnings,
|
||||||
|
isWindows: systemStatus.isWindows,
|
||||||
|
...settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
addMovie,
|
||||||
|
setMovieCollectionValue
|
||||||
|
};
|
||||||
|
|
||||||
|
class AddNewCollectionMovieModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setMovieCollectionValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMoviePress = () => {
|
||||||
|
const {
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
rootFolderPath,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
searchForMovie,
|
||||||
|
tags
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.addMovie({
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
rootFolderPath: rootFolderPath.value,
|
||||||
|
monitor: monitor.value,
|
||||||
|
qualityProfileId: qualityProfileId.value,
|
||||||
|
minimumAvailability: minimumAvailability.value,
|
||||||
|
searchForMovie: searchForMovie.value,
|
||||||
|
tags: tags.value
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.onModalClose(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AddNewMovieModalContent
|
||||||
|
{...this.props}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onAddMoviePress={this.onAddMoviePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddNewCollectionMovieModalContentConnector.propTypes = {
|
||||||
|
tmdbId: PropTypes.number.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
rootFolderPath: PropTypes.object,
|
||||||
|
monitor: PropTypes.object.isRequired,
|
||||||
|
qualityProfileId: PropTypes.object,
|
||||||
|
minimumAvailability: PropTypes.object.isRequired,
|
||||||
|
searchForMovie: PropTypes.object.isRequired,
|
||||||
|
tags: PropTypes.object.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
addMovie: PropTypes.func.isRequired,
|
||||||
|
setMovieCollectionValue: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewCollectionMovieModalContentConnector);
|
||||||
403
frontend/src/Collection/Collection.js
Normal file
403
frontend/src/Collection/Collection.js
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||||
|
import styles from 'Movie/Index/MovieIndex.css';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import CollectionFooter from './CollectionFooter';
|
||||||
|
import CollectionFilterMenu from './Menus/CollectionFilterMenu';
|
||||||
|
import CollectionSortMenu from './Menus/CollectionSortMenu';
|
||||||
|
import NoCollection from './NoCollection';
|
||||||
|
import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector';
|
||||||
|
import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal';
|
||||||
|
|
||||||
|
function getViewComponent(view) {
|
||||||
|
return CollectionOverviewsConnector;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Collection extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
scroller: null,
|
||||||
|
jumpBarItems: { order: [] },
|
||||||
|
jumpToCharacter: null,
|
||||||
|
isPosterOptionsModalOpen: false,
|
||||||
|
isOverviewOptionsModalOpen: false,
|
||||||
|
isConfirmSearchModalOpen: false,
|
||||||
|
searchType: null,
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setJumpBarItems();
|
||||||
|
this.setSelectedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (sortKey !== prevProps.sortKey ||
|
||||||
|
sortDirection !== prevProps.sortDirection ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||||
|
) {
|
||||||
|
this.setJumpBarItems();
|
||||||
|
this.setSelectedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.jumpToCharacter != null) {
|
||||||
|
this.setState({ jumpToCharacter: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setScrollerRef = (ref) => {
|
||||||
|
this.setState({ scroller: ref });
|
||||||
|
};
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
if (this.state.allUnselected) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
setSelectedState() {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedState
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const newSelectedState = {};
|
||||||
|
|
||||||
|
items.forEach((collection) => {
|
||||||
|
const isItemSelected = selectedState[collection.id];
|
||||||
|
|
||||||
|
if (isItemSelected) {
|
||||||
|
newSelectedState[collection.id] = isItemSelected;
|
||||||
|
} else {
|
||||||
|
newSelectedState[collection.id] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||||
|
const newStateCount = Object.keys(newSelectedState).length;
|
||||||
|
let isAllSelected = false;
|
||||||
|
let isAllUnselected = false;
|
||||||
|
|
||||||
|
if (selectedCount === 0) {
|
||||||
|
isAllUnselected = true;
|
||||||
|
} else if (selectedCount === newStateCount) {
|
||||||
|
isAllSelected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||||
|
}
|
||||||
|
|
||||||
|
setJumpBarItems() {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
sortDirection
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// Reset if not sorting by sortTitle
|
||||||
|
if (sortKey !== 'sortTitle') {
|
||||||
|
this.setState({ jumpBarItems: { order: [] } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characters = _.reduce(items, (acc, item) => {
|
||||||
|
let char = item.sortTitle.charAt(0);
|
||||||
|
|
||||||
|
if (!isNaN(char)) {
|
||||||
|
char = '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char in acc) {
|
||||||
|
acc[char] = acc[char] + 1;
|
||||||
|
} else {
|
||||||
|
acc[char] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const order = Object.keys(characters).sort();
|
||||||
|
|
||||||
|
// Reverse if sorting descending
|
||||||
|
if (sortDirection === sortDirections.DESCENDING) {
|
||||||
|
order.reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const jumpBarItems = {
|
||||||
|
characters,
|
||||||
|
order
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setState({ jumpBarItems });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onOverviewOptionsPress = () => {
|
||||||
|
this.setState({ isOverviewOptionsModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onOverviewOptionsModalClose = () => {
|
||||||
|
this.setState({ isOverviewOptionsModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onJumpBarItemPress = (jumpToCharacter) => {
|
||||||
|
this.setState({ jumpToCharacter });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAllPress = () => {
|
||||||
|
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||||
|
};
|
||||||
|
|
||||||
|
onRefreshMovieCollectionsPress = () => {
|
||||||
|
this.props.onRefreshMovieCollectionsPress();
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey, 'id');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdateSelectedPress = (changes) => {
|
||||||
|
this.props.onUpdateSelectedPress({
|
||||||
|
collectionIds: this.getSelectedIds(),
|
||||||
|
...changes
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
totalItems,
|
||||||
|
items,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
view,
|
||||||
|
onSortSelect,
|
||||||
|
onFilterSelect,
|
||||||
|
onScroll,
|
||||||
|
isRefreshingCollections,
|
||||||
|
isSaving,
|
||||||
|
isAdding,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
scroller,
|
||||||
|
jumpBarItems,
|
||||||
|
jumpToCharacter,
|
||||||
|
isOverviewOptionsModalOpen,
|
||||||
|
selectedState,
|
||||||
|
allSelected,
|
||||||
|
allUnselected
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const selectedMovieIds = this.getSelectedIds();
|
||||||
|
|
||||||
|
const ViewComponent = getViewComponent(view);
|
||||||
|
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||||
|
const hasNoCollection = !totalItems;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RefreshCollections')}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isRefreshingCollections}
|
||||||
|
isDisabled={hasNoCollection}
|
||||||
|
onPress={this.onRefreshMovieCollectionsPress}
|
||||||
|
/>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||||
|
iconName={icons.CHECK_SQUARE}
|
||||||
|
isDisabled={hasNoCollection}
|
||||||
|
onPress={this.onSelectAllPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection
|
||||||
|
alignContent={align.RIGHT}
|
||||||
|
collapseButtons={false}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
view === 'overview' ?
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.OVERVIEW}
|
||||||
|
onPress={this.onOverviewOptionsPress}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
(view === 'posters' || view === 'overview') &&
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
}
|
||||||
|
|
||||||
|
<CollectionSortMenu
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
isDisabled={hasNoCollection}
|
||||||
|
onSortSelect={onSortSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CollectionFilterMenu
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
isDisabled={hasNoCollection}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<div className={styles.pageContentBodyWrapper}>
|
||||||
|
<PageContentBody
|
||||||
|
registerScroller={this.setScrollerRef}
|
||||||
|
className={styles.contentBody}
|
||||||
|
innerClassName={styles[`${view}InnerContentBody`]}
|
||||||
|
onScroll={onScroll}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isFetching && !isPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>
|
||||||
|
{translate('UnableToLoadCollections')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoaded &&
|
||||||
|
<div className={styles.contentBodyContainer}>
|
||||||
|
<ViewComponent
|
||||||
|
scroller={scroller}
|
||||||
|
items={items}
|
||||||
|
filters={filters}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
jumpToCharacter={jumpToCharacter}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
selectedState={selectedState}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!error && isPopulated && !items.length &&
|
||||||
|
<NoCollection totalItems={totalItems} />
|
||||||
|
}
|
||||||
|
</PageContentBody>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoaded && !!jumpBarItems.order.length &&
|
||||||
|
<PageJumpBar
|
||||||
|
items={jumpBarItems}
|
||||||
|
onItemPress={this.onJumpBarItemPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
isLoaded &&
|
||||||
|
<CollectionFooter
|
||||||
|
selectedIds={selectedMovieIds}
|
||||||
|
isSaving={isSaving}
|
||||||
|
isAdding={isAdding}
|
||||||
|
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<CollectionOverviewOptionsModal
|
||||||
|
isOpen={isOverviewOptionsModalOpen}
|
||||||
|
onModalClose={this.onOverviewOptionsModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
totalItems: PropTypes.number.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
|
view: PropTypes.string.isRequired,
|
||||||
|
isRefreshingCollections: PropTypes.bool.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
onSortSelect: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
onScroll: PropTypes.func.isRequired,
|
||||||
|
onUpdateSelectedPress: PropTypes.func.isRequired,
|
||||||
|
onRefreshMovieCollectionsPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Collection;
|
||||||
108
frontend/src/Collection/CollectionConnector.js
Normal file
108
frontend/src/Collection/CollectionConnector.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
|
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import Collection from './Collection';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createCollectionClientSideCollectionItemsSelector('movieCollections'),
|
||||||
|
createCommandExecutingSelector(commandNames.REFRESH_COLLECTIONS),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(
|
||||||
|
collections,
|
||||||
|
isRefreshingCollections,
|
||||||
|
dimensionsState
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
...collections,
|
||||||
|
isRefreshingCollections,
|
||||||
|
isSmallScreen: dimensionsState.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
dispatchFetchRootFolders() {
|
||||||
|
dispatch(fetchRootFolders());
|
||||||
|
},
|
||||||
|
onUpdateSelectedPress(payload) {
|
||||||
|
dispatch(saveMovieCollections(payload));
|
||||||
|
},
|
||||||
|
onSortSelect(sortKey) {
|
||||||
|
dispatch(setMovieCollectionsSort({ sortKey }));
|
||||||
|
},
|
||||||
|
onFilterSelect(selectedFilterKey) {
|
||||||
|
dispatch(setMovieCollectionsFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
onRefreshMovieCollectionsPress() {
|
||||||
|
dispatch(executeCommand({
|
||||||
|
name: commandNames.REFRESH_COLLECTIONS
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectionConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
registerPagePopulator(this.repopulate);
|
||||||
|
this.props.dispatchFetchRootFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onScroll = ({ scrollTop }) => {
|
||||||
|
scrollPositions.movieCollections = scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdateSelectedPress = (payload) => {
|
||||||
|
this.props.onUpdateSelectedPress(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Collection
|
||||||
|
{...this.props}
|
||||||
|
onViewSelect={this.onViewSelect}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionConnector.propTypes = {
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
view: PropTypes.string.isRequired,
|
||||||
|
onUpdateSelectedPress: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchRootFolders: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withScrollPosition(
|
||||||
|
connect(createMapStateToProps, createMapDispatchToProps)(CollectionConnector),
|
||||||
|
'movieCollections'
|
||||||
|
);
|
||||||
24
frontend/src/Collection/CollectionFilterModalConnector.js
Normal file
24
frontend/src/Collection/CollectionFilterModalConnector.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setMovieCollectionsFilter } from 'Store/Actions/movieCollectionActions';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections.items,
|
||||||
|
(state) => state.movieCollections.filterBuilderProps,
|
||||||
|
(sectionItems, filterBuilderProps) => {
|
||||||
|
return {
|
||||||
|
sectionItems,
|
||||||
|
filterBuilderProps,
|
||||||
|
customFilterType: 'movieCollections'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchSetFilter: setMovieCollectionsFilter
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||||
56
frontend/src/Collection/CollectionFooter.css
Normal file
56
frontend/src/Collection/CollectionFooter.css
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
.inputContainer {
|
||||||
|
margin-right: 20px;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainerContent {
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addSelectedButton {
|
||||||
|
composes: button from '~Components/Link/SpinnerButton.css';
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excludeSelectedButton {
|
||||||
|
composes: button from '~Components/Link/SpinnerButton.css';
|
||||||
|
|
||||||
|
margin-left: 25px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.inputContainer {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainerContent {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedMovieLabel {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
236
frontend/src/Collection/CollectionFooter.js
Normal file
236
frontend/src/Collection/CollectionFooter.js
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
|
||||||
|
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
|
||||||
|
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||||
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import CollectionFooterLabel from './CollectionFooterLabel';
|
||||||
|
import styles from './CollectionFooter.css';
|
||||||
|
|
||||||
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
|
class CollectionFooter extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
monitor: NO_CHANGE,
|
||||||
|
monitored: NO_CHANGE,
|
||||||
|
qualityProfileId: NO_CHANGE,
|
||||||
|
minimumAvailability: NO_CHANGE,
|
||||||
|
rootFolderPath: NO_CHANGE,
|
||||||
|
destinationRootFolder: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
isSaving,
|
||||||
|
saveError
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const newState = {};
|
||||||
|
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||||
|
this.setState({
|
||||||
|
monitored: NO_CHANGE,
|
||||||
|
monitor: NO_CHANGE,
|
||||||
|
qualityProfileId: NO_CHANGE,
|
||||||
|
rootFolderPath: NO_CHANGE,
|
||||||
|
minimumAvailability: NO_CHANGE
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isEmpty(newState)) {
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.setState({ [name]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onUpdateSelectedPress = () => {
|
||||||
|
const {
|
||||||
|
monitor,
|
||||||
|
monitored,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
rootFolderPath
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const changes = {};
|
||||||
|
|
||||||
|
if (monitored !== NO_CHANGE) {
|
||||||
|
changes.monitored = monitored === 'monitored';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (monitor !== NO_CHANGE) {
|
||||||
|
changes.monitor = monitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qualityProfileId !== NO_CHANGE) {
|
||||||
|
changes.qualityProfileId = qualityProfileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minimumAvailability !== NO_CHANGE) {
|
||||||
|
changes.minimumAvailability = minimumAvailability;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootFolderPath !== NO_CHANGE) {
|
||||||
|
changes.rootFolderPath = rootFolderPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onUpdateSelectedPress(changes);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
selectedIds,
|
||||||
|
isSaving
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitored,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
rootFolderPath
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const monitoredOptions = [
|
||||||
|
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||||
|
{ key: 'monitored', value: translate('Monitored') },
|
||||||
|
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContentFooter>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('MonitorCollection')}
|
||||||
|
isSaving={isSaving}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
name="monitored"
|
||||||
|
value={monitored}
|
||||||
|
values={monitoredOptions}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('MonitorMovies')}
|
||||||
|
isSaving={isSaving}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
name="monitor"
|
||||||
|
value={monitor}
|
||||||
|
values={monitoredOptions}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('QualityProfile')}
|
||||||
|
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QualityProfileSelectInputConnector
|
||||||
|
name="qualityProfileId"
|
||||||
|
value={qualityProfileId}
|
||||||
|
includeNoChange={true}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('MinimumAvailability')}
|
||||||
|
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AvailabilitySelectInput
|
||||||
|
name="minimumAvailability"
|
||||||
|
value={minimumAvailability}
|
||||||
|
includeNoChange={true}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('RootFolder')}
|
||||||
|
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RootFolderSelectInputConnector
|
||||||
|
name="rootFolderPath"
|
||||||
|
value={rootFolderPath}
|
||||||
|
includeNoChange={true}
|
||||||
|
isDisabled={!selectedCount}
|
||||||
|
selectedValueOptions={{ includeFreeSpace: false }}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<div className={styles.buttonContainerContent}>
|
||||||
|
<CollectionFooterLabel
|
||||||
|
label={translate('CollectionsSelectedInterp', [selectedCount])}
|
||||||
|
isSaving={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<div>
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.addSelectedButton}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
isDisabled={!selectedCount || isSaving}
|
||||||
|
onPress={this.onUpdateSelectedPress}
|
||||||
|
>
|
||||||
|
{translate('UpdateSelected')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageContentFooter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionFooter.propTypes = {
|
||||||
|
selectedIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
isAdding: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
onUpdateSelectedPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionFooter;
|
||||||
8
frontend/src/Collection/CollectionFooterLabel.css
Normal file
8
frontend/src/Collection/CollectionFooterLabel.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.label {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.savingIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
40
frontend/src/Collection/CollectionFooterLabel.js
Normal file
40
frontend/src/Collection/CollectionFooterLabel.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import styles from './CollectionFooterLabel.css';
|
||||||
|
|
||||||
|
function CollectionFooterLabel(props) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
label,
|
||||||
|
isSaving
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
{label}
|
||||||
|
|
||||||
|
{
|
||||||
|
isSaving &&
|
||||||
|
<SpinnerIcon
|
||||||
|
className={styles.savingIcon}
|
||||||
|
name={icons.SPINNER}
|
||||||
|
isSpinning={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionFooterLabel.propTypes = {
|
||||||
|
className: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
CollectionFooterLabel.defaultProps = {
|
||||||
|
className: styles.label
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionFooterLabel;
|
||||||
75
frontend/src/Collection/CollectionItemConnector.js
Normal file
75
frontend/src/Collection/CollectionItemConnector.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
|
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createCollectionSelector(),
|
||||||
|
createAllMoviesSelector(),
|
||||||
|
(
|
||||||
|
collection,
|
||||||
|
allMovies
|
||||||
|
) => {
|
||||||
|
// If a movie is deleted this selector may fire before the parent
|
||||||
|
// selecors, which will result in an undefined movie, if that happens
|
||||||
|
// we want to return early here and again in the render function to avoid
|
||||||
|
// trying to show a movie that has no information available.
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let allGenres = [];
|
||||||
|
let libraryMovies = 0;
|
||||||
|
|
||||||
|
collection.movies.forEach((movie) => {
|
||||||
|
allGenres = allGenres.concat(movie.genres);
|
||||||
|
|
||||||
|
if (allMovies.find((libraryMovie) => libraryMovie.tmdbId === movie.tmdbId)) {
|
||||||
|
libraryMovies++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...collection,
|
||||||
|
genres: Array.from(new Set(allGenres)).slice(0, 3),
|
||||||
|
missingMovies: collection.movies.length - libraryMovies
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectionItemConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
component: ItemComponent,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemComponent
|
||||||
|
{...otherProps}
|
||||||
|
id={id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionItemConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
component: PropTypes.elementType.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(CollectionItemConnector);
|
||||||
25
frontend/src/Collection/Edit/EditCollectionModal.js
Normal file
25
frontend/src/Collection/Edit/EditCollectionModal.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import EditCollectionModalContentConnector from './EditCollectionModalContentConnector';
|
||||||
|
|
||||||
|
function EditCollectionModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<EditCollectionModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCollectionModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditCollectionModal;
|
||||||
39
frontend/src/Collection/Edit/EditCollectionModalConnector.js
Normal file
39
frontend/src/Collection/Edit/EditCollectionModalConnector.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditCollectionModal from './EditCollectionModal';
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
clearPendingChanges
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditCollectionModalConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.props.clearPendingChanges({ section: 'movieCollections' });
|
||||||
|
this.props.onModalClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditCollectionModal
|
||||||
|
{...this.props}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCollectionModalConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(undefined, mapDispatchToProps)(EditCollectionModalConnector);
|
||||||
17
frontend/src/Collection/Edit/EditCollectionModalContent.css
Normal file
17
frontend/src/Collection/Edit/EditCollectionModalContent.css
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
flex: 0 0 170px;
|
||||||
|
margin-right: 20px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
178
frontend/src/Collection/Edit/EditCollectionModalContent.js
Normal file
178
frontend/src/Collection/Edit/EditCollectionModalContent.js
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './EditCollectionModalContent.css';
|
||||||
|
|
||||||
|
class EditCollectionModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
const {
|
||||||
|
onSavePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onSavePress(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
images,
|
||||||
|
overview,
|
||||||
|
item,
|
||||||
|
isSaving,
|
||||||
|
onInputChange,
|
||||||
|
onModalClose,
|
||||||
|
isSmallScreen,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitored,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
// Id,
|
||||||
|
rootFolderPath,
|
||||||
|
searchOnAdd
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('Edit')} - {title}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.overview}>
|
||||||
|
{overview}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="monitored"
|
||||||
|
helpText={translate('MonitoredCollectionHelpText')}
|
||||||
|
{...monitored}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
name="minimumAvailability"
|
||||||
|
{...minimumAvailability}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
{...qualityProfileId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
{...rootFolderPath}
|
||||||
|
includeMissingValue={true}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="searchOnAdd"
|
||||||
|
helpText={translate('SearchOnAddCollectionHelpText')}
|
||||||
|
{...searchOnAdd}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
{translate('Cancel')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
onPress={this.onSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCollectionModalContent.propTypes = {
|
||||||
|
collectionId: PropTypes.number.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
overview: PropTypes.string.isRequired,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
isPathChanging: PropTypes.bool.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditCollectionModalContent;
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { saveMovieCollection, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import EditCollectionModalContent from './EditCollectionModalContent';
|
||||||
|
|
||||||
|
function createIsPathChangingSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections.pendingChanges,
|
||||||
|
createCollectionSelector(),
|
||||||
|
(pendingChanges, collection) => {
|
||||||
|
const rootFolderPath = pendingChanges.rootFolderPath;
|
||||||
|
|
||||||
|
if (rootFolderPath == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection.rootFolderPath !== rootFolderPath;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections,
|
||||||
|
createCollectionSelector(),
|
||||||
|
createIsPathChangingSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(moviesState, collection, isPathChanging, dimensions) => {
|
||||||
|
const {
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
pendingChanges
|
||||||
|
} = moviesState;
|
||||||
|
|
||||||
|
const movieSettings = {
|
||||||
|
monitored: collection.monitored,
|
||||||
|
qualityProfileId: collection.qualityProfileId,
|
||||||
|
minimumAvailability: collection.minimumAvailability,
|
||||||
|
rootFolderPath: collection.rootFolderPath,
|
||||||
|
searchOnAdd: collection.searchOnAdd
|
||||||
|
};
|
||||||
|
|
||||||
|
const settings = selectSettings(movieSettings, pendingChanges, saveError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: collection.title,
|
||||||
|
images: collection.images,
|
||||||
|
overview: collection.overview,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isPathChanging,
|
||||||
|
originalPath: collection.path,
|
||||||
|
item: settings.settings,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
...settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchSetMovieCollectionValue: setMovieCollectionValue,
|
||||||
|
dispatchSaveMovieCollection: saveMovieCollection
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditCollectionModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.dispatchSetMovieCollectionValue({ name, value });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.dispatchSaveMovieCollection({
|
||||||
|
id: this.props.collectionId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditCollectionModalContent
|
||||||
|
{...this.props}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
onMoveMoviePress={this.onMoveMoviePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCollectionModalContentConnector.propTypes = {
|
||||||
|
collectionId: PropTypes.number,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
dispatchSetMovieCollectionValue: PropTypes.func.isRequired,
|
||||||
|
dispatchSaveMovieCollection: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(EditCollectionModalContentConnector);
|
||||||
41
frontend/src/Collection/Menus/CollectionFilterMenu.js
Normal file
41
frontend/src/Collection/Menus/CollectionFilterMenu.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import CollectionFilterModalConnector from 'Collection/CollectionFilterModalConnector';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import { align } from 'Helpers/Props';
|
||||||
|
|
||||||
|
function CollectionFilterMenu(props) {
|
||||||
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
isDisabled,
|
||||||
|
onFilterSelect
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={CollectionFilterModalConnector}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionFilterMenu.propTypes = {
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
CollectionFilterMenu.defaultProps = {
|
||||||
|
showCustomFilters: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionFilterMenu;
|
||||||
43
frontend/src/Collection/Menus/CollectionSortMenu.js
Normal file
43
frontend/src/Collection/Menus/CollectionSortMenu.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
|
import SortMenu from 'Components/Menu/SortMenu';
|
||||||
|
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||||
|
import { align, sortDirections } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function CollectionSortMenu(props) {
|
||||||
|
const {
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
isDisabled,
|
||||||
|
onSortSelect
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortMenu
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
>
|
||||||
|
<MenuContent>
|
||||||
|
<SortMenuItem
|
||||||
|
name="sortTitle"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('Title')}
|
||||||
|
</SortMenuItem>
|
||||||
|
</MenuContent>
|
||||||
|
</SortMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionSortMenu.propTypes = {
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
onSortSelect: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionSortMenu;
|
||||||
11
frontend/src/Collection/NoCollection.css
Normal file
11
frontend/src/Collection/NoCollection.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.message {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
52
frontend/src/Collection/NoCollection.js
Normal file
52
frontend/src/Collection/NoCollection.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './NoCollection.css';
|
||||||
|
|
||||||
|
function NoCollection(props) {
|
||||||
|
const { totalItems } = props;
|
||||||
|
|
||||||
|
if (totalItems > 0) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.message}>
|
||||||
|
{translate('AllCollectionsHiddenDueToFilter')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.message}>
|
||||||
|
{translate('NoCollections')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
to="/add/import"
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
>
|
||||||
|
{translate('ImportExistingMovies')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Button
|
||||||
|
to="/add/new"
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
>
|
||||||
|
{translate('AddNewMovie')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NoCollection.propTypes = {
|
||||||
|
totalItems: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoCollection;
|
||||||
117
frontend/src/Collection/Overview/CollectionMovie.css
Normal file
117
frontend/src/Collection/Overview/CollectionMovie.css
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 0 10px $black;
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 100ms linear 100ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayTitle {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 100ms linear 100ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.posterContainer {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
background-color: $defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayTitle {
|
||||||
|
padding: 5px;
|
||||||
|
color: $offWhite;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 15px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
background-color: #fafbfc;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #707070;
|
||||||
|
color: $white;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
composes: button from '~Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $radarrYellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.container {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorSelect {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.externalLinks {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
background-color: $defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitorToggleButton {
|
||||||
|
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||||
|
|
||||||
|
width: 25px;
|
||||||
|
color: $white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $iconButtonHoverLightColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
191
frontend/src/Collection/Overview/CollectionMovie.js
Normal file
191
frontend/src/Collection/Overview/CollectionMovie.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
|
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal';
|
||||||
|
import styles from './CollectionMovie.css';
|
||||||
|
|
||||||
|
class CollectionMovie extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hasPosterError: false,
|
||||||
|
isEditMovieModalOpen: false,
|
||||||
|
isNewAddMovieModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditMoviePress = () => {
|
||||||
|
this.setState({ isEditMovieModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditMovieModalClose = () => {
|
||||||
|
this.setState({ isEditMovieModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMoviePress = () => {
|
||||||
|
this.setState({ isNewAddMovieModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMovieModalClose = () => {
|
||||||
|
this.setState({ isNewAddMovieModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onPosterLoad = () => {
|
||||||
|
if (this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onPosterLoadError = () => {
|
||||||
|
if (!this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
overview,
|
||||||
|
year,
|
||||||
|
tmdbId,
|
||||||
|
images,
|
||||||
|
monitored,
|
||||||
|
hasFile,
|
||||||
|
folder,
|
||||||
|
isAvailable,
|
||||||
|
isExistingMovie,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
detailedProgressBar,
|
||||||
|
onMonitorTogglePress,
|
||||||
|
collectionId
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEditMovieModalOpen,
|
||||||
|
isNewAddMovieModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const linkProps = id ? { to: `/movie/${tmdbId}` } : { onPress: this.onAddMoviePress };
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
{
|
||||||
|
isExistingMovie &&
|
||||||
|
<div className={styles.editorSelect}>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={20}
|
||||||
|
onPress={onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
style={elementStyle}
|
||||||
|
{...linkProps}
|
||||||
|
>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
onError={this.onPosterLoadError}
|
||||||
|
onLoad={this.onPosterLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.overlayTitle}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<div className={styles.overlayStatus}>
|
||||||
|
<MovieIndexProgressBar
|
||||||
|
monitored={monitored}
|
||||||
|
hasFile={hasFile}
|
||||||
|
status={status}
|
||||||
|
bottomRadius={true}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
detailedProgressBar={detailedProgressBar}
|
||||||
|
isAvailable={isAvailable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddNewCollectionMovieModal
|
||||||
|
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
|
||||||
|
tmdbId={tmdbId}
|
||||||
|
title={title}
|
||||||
|
year={year}
|
||||||
|
overview={overview}
|
||||||
|
images={images}
|
||||||
|
folder={folder}
|
||||||
|
onModalClose={this.onAddMovieModalClose}
|
||||||
|
collectionId={collectionId}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditMovieModalConnector
|
||||||
|
isOpen={isEditMovieModalOpen}
|
||||||
|
movieId={id}
|
||||||
|
onModalClose={this.onEditMovieModalClose}
|
||||||
|
onDeleteMoviePress={this.onDeleteMoviePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionMovie.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
overview: PropTypes.string.isRequired,
|
||||||
|
monitored: PropTypes.bool,
|
||||||
|
collectionId: PropTypes.number.isRequired,
|
||||||
|
hasFile: PropTypes.bool,
|
||||||
|
folder: PropTypes.string,
|
||||||
|
isAvailable: PropTypes.bool,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
posterWidth: PropTypes.number.isRequired,
|
||||||
|
posterHeight: PropTypes.number.isRequired,
|
||||||
|
detailedProgressBar: PropTypes.bool.isRequired,
|
||||||
|
isExistingMovie: PropTypes.bool,
|
||||||
|
tmdbId: PropTypes.number.isRequired,
|
||||||
|
imdbId: PropTypes.string,
|
||||||
|
youTubeTrailerId: PropTypes.string,
|
||||||
|
onMonitorTogglePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionMovie;
|
||||||
59
frontend/src/Collection/Overview/CollectionMovieConnector.js
Normal file
59
frontend/src/Collection/Overview/CollectionMovieConnector.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||||
|
import createCollectionExistingMovieSelector from 'Store/Selectors/createCollectionExistingMovieSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import CollectionMovie from './CollectionMovie';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createDimensionsSelector(),
|
||||||
|
createCollectionExistingMovieSelector(),
|
||||||
|
(dimensions, existingMovie) => {
|
||||||
|
return {
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
isExistingMovie: !!existingMovie,
|
||||||
|
...existingMovie
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleMovieMonitored
|
||||||
|
};
|
||||||
|
|
||||||
|
class CollectionMovieConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMonitorTogglePress = (monitored) => {
|
||||||
|
this.props.toggleMovieMonitored({
|
||||||
|
movieId: this.props.id,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CollectionMovie
|
||||||
|
{...this.props}
|
||||||
|
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionMovieConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
monitored: PropTypes.bool,
|
||||||
|
toggleMovieMonitored: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionMovieConnector);
|
||||||
50
frontend/src/Collection/Overview/CollectionMovieLabel.css
Normal file
50
frontend/src/Collection/Overview/CollectionMovieLabel.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
.movie {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 2px 4px;
|
||||||
|
border: 1px solid $borderColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #eee;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movieTitle {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movieStatus {
|
||||||
|
padding: 0 4px;
|
||||||
|
border-left: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
background-color: $white;
|
||||||
|
color: $defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
border-color: $primaryColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
border-color: $dangerColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
border-color: $successColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purple {
|
||||||
|
border-color: $purple;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
border-color: $warningColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
border-color: $infoColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue {
|
||||||
|
border-color: $queueColor;
|
||||||
|
}
|
||||||
84
frontend/src/Collection/Overview/CollectionMovieLabel.js
Normal file
84
frontend/src/Collection/Overview/CollectionMovieLabel.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './CollectionMovieLabel.css';
|
||||||
|
|
||||||
|
class CollectionMovieLabel extends Component {
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
monitored,
|
||||||
|
isAvailable,
|
||||||
|
hasFile,
|
||||||
|
onMonitorTogglePress,
|
||||||
|
isSaving
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.movie}>
|
||||||
|
<div className={styles.movieTitle}>
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<MonitorToggleButton
|
||||||
|
monitored={monitored}
|
||||||
|
isSaving={isSaving}
|
||||||
|
onPress={onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
title
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.movieStatus,
|
||||||
|
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
hasFile ? translate('Downloaded') : translate('Missing')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionMovieLabel.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
status: PropTypes.string,
|
||||||
|
isAvailable: PropTypes.bool,
|
||||||
|
monitored: PropTypes.bool,
|
||||||
|
hasFile: PropTypes.bool,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
movieFile: PropTypes.object,
|
||||||
|
movieFileId: PropTypes.number,
|
||||||
|
onMonitorTogglePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
CollectionMovieLabel.defaultProps = {
|
||||||
|
isSaving: false,
|
||||||
|
statistics: {
|
||||||
|
episodeFileCount: 0,
|
||||||
|
totalEpisodeCount: 0,
|
||||||
|
percentOfEpisodes: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionMovieLabel;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||||
|
import createCollectionExistingMovieSelector from 'Store/Selectors/createCollectionExistingMovieSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import CollectionMovieLabel from './CollectionMovieLabel';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createDimensionsSelector(),
|
||||||
|
createCollectionExistingMovieSelector(),
|
||||||
|
(dimensions, existingMovie) => {
|
||||||
|
return {
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
isExistingMovie: !!existingMovie,
|
||||||
|
...existingMovie
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleMovieMonitored
|
||||||
|
};
|
||||||
|
|
||||||
|
class CollectionMovieLabelConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMonitorTogglePress = (monitored) => {
|
||||||
|
this.props.toggleMovieMonitored({
|
||||||
|
movieId: this.props.id,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CollectionMovieLabel
|
||||||
|
{...this.props}
|
||||||
|
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionMovieLabelConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
monitored: PropTypes.bool,
|
||||||
|
toggleMovieMonitored: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionMovieLabelConnector);
|
||||||
137
frontend/src/Collection/Overview/CollectionOverview.css
Normal file
137
frontend/src/Collection/Overview/CollectionOverview.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorSelect {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleMonitoredContainer {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderContainer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moviesContainer {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie {
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 1px;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.defaults {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsLabel {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
margin: 5px 10px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path,
|
||||||
|
.status,
|
||||||
|
.genres,
|
||||||
|
.qualityProfileName {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitorToggleButton {
|
||||||
|
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||||
|
|
||||||
|
width: 25px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $iconButtonHoverLightColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.navigationButtons {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggleMonitoredContainer {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitorToggleButton {
|
||||||
|
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||||
|
|
||||||
|
width: 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $iconButtonHoverLightColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
353
frontend/src/Collection/Overview/CollectionOverview.js
Normal file
353
frontend/src/Collection/Overview/CollectionOverview.js
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import { Navigation } from 'swiper';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import EditCollectionModalConnector from 'Collection/Edit/EditCollectionModalConnector';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import { icons, sizes } from 'Helpers/Props';
|
||||||
|
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import fonts from 'Styles/Variables/fonts';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import CollectionMovieConnector from './CollectionMovieConnector';
|
||||||
|
import CollectionMovieLabelConnector from './CollectionMovieLabelConnector';
|
||||||
|
import styles from './CollectionOverview.css';
|
||||||
|
|
||||||
|
// Import Swiper styles
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||||
|
const lineHeight = parseFloat(fonts.lineHeight);
|
||||||
|
|
||||||
|
// Hardcoded height beased on line-height of 32 + bottom margin of 10. 19 + 5 for List Row
|
||||||
|
// Less side-effecty than using react-measure.
|
||||||
|
const titleRowHeight = 100;
|
||||||
|
|
||||||
|
function getContentHeight(rowHeight, isSmallScreen) {
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
|
||||||
|
return rowHeight - padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectionOverview extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditCollectionModalOpen: false,
|
||||||
|
isNewAddMovieModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setSliderPrevRef = (ref) => {
|
||||||
|
this._swiperPrevRef = ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
setSliderNextRef = (ref) => {
|
||||||
|
this._swiperNextRef = ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onPress = () => {
|
||||||
|
this.setState({ isNewAddMovieModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditCollectionPress = () => {
|
||||||
|
this.setState({ isEditCollectionModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditCollectionModalClose = () => {
|
||||||
|
this.setState({ isEditCollectionModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onAddMovieModalClose = () => {
|
||||||
|
this.setState({ isNewAddMovieModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = ({ value, shiftKey }) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
onSelectedChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onSelectedChange({ id, value, shiftKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
monitored,
|
||||||
|
qualityProfileId,
|
||||||
|
rootFolderPath,
|
||||||
|
genres,
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
movies,
|
||||||
|
overview,
|
||||||
|
missingMovies,
|
||||||
|
posterHeight,
|
||||||
|
posterWidth,
|
||||||
|
rowHeight,
|
||||||
|
isSmallScreen,
|
||||||
|
isSelected,
|
||||||
|
onMonitorTogglePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
showDetails,
|
||||||
|
showOverview,
|
||||||
|
showPosters,
|
||||||
|
detailedProgressBar
|
||||||
|
} = this.props.overviewOptions;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEditCollectionModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||||
|
const overviewHeight = contentHeight - titleRowHeight - posterHeight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.editorSelect}>
|
||||||
|
<CheckInput
|
||||||
|
className={styles.checkInput}
|
||||||
|
name={id.toString()}
|
||||||
|
value={isSelected}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.info} style={{ maxHeight: contentHeight }}>
|
||||||
|
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<div className={styles.titleContainer}>
|
||||||
|
<div className={styles.toggleMonitoredContainer}>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={isSmallScreen ? 20 : 25}
|
||||||
|
onPress={onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
title={translate('EditCollection')}
|
||||||
|
onPress={this.onEditCollectionPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
showPosters &&
|
||||||
|
<div className={styles.navigationButtons}>
|
||||||
|
<span ref={this.setSliderPrevRef}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.ARROW_LEFT}
|
||||||
|
title={translate('ScrollMovies')}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span ref={this.setSliderNextRef}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.ARROW_RIGHT}
|
||||||
|
title={translate('ScrollMovies')}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
showDetails &&
|
||||||
|
<div className={styles.defaults}>
|
||||||
|
<Label
|
||||||
|
className={styles.detailsLabel}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.DRIVE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.status}>
|
||||||
|
{`${missingMovies} missing movie(s)`}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<Label
|
||||||
|
className={styles.detailsLabel}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.PROFILE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.qualityProfileName}>
|
||||||
|
{
|
||||||
|
<QualityProfileNameConnector
|
||||||
|
qualityProfileId={qualityProfileId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<Label
|
||||||
|
className={styles.detailsLabel}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.FOLDER}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.path}>
|
||||||
|
{rootFolderPath}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<Label
|
||||||
|
className={styles.detailsLabel}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.PROFILE}
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
|
<span className={styles.genres}>
|
||||||
|
{genres.join(', ')}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showOverview &&
|
||||||
|
<div className={styles.details}>
|
||||||
|
<div className={styles.overview}>
|
||||||
|
<TextTruncate
|
||||||
|
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||||
|
text={overview}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showPosters ?
|
||||||
|
<div className={styles.sliderContainer}>
|
||||||
|
<Swiper
|
||||||
|
slidesPerView='auto'
|
||||||
|
spaceBetween={10}
|
||||||
|
slidesPerGroup={3}
|
||||||
|
loop={false}
|
||||||
|
loopFillGroupWithBlank={true}
|
||||||
|
className="mySwiper"
|
||||||
|
modules={[Navigation]}
|
||||||
|
onInit={(swiper) => {
|
||||||
|
swiper.params.navigation.prevEl = this._swiperPrevRef;
|
||||||
|
swiper.params.navigation.nextEl = this._swiperNextRef;
|
||||||
|
swiper.navigation.init();
|
||||||
|
swiper.navigation.update();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{movies.map((movie) => (
|
||||||
|
<SwiperSlide key={movie.tmdbId} style={{ width: posterWidth }}>
|
||||||
|
<CollectionMovieConnector
|
||||||
|
key={movie.tmdbId}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
posterHeight={posterHeight}
|
||||||
|
detailedProgressBar={detailedProgressBar}
|
||||||
|
collectionId={id}
|
||||||
|
{...movie}
|
||||||
|
/>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div> :
|
||||||
|
<div className={styles.labelsContainer}>
|
||||||
|
{movies.map((movie) => (
|
||||||
|
<CollectionMovieLabelConnector
|
||||||
|
key={movie.tmdbId}
|
||||||
|
collectionId={id}
|
||||||
|
{...movie}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditCollectionModalConnector
|
||||||
|
isOpen={isEditCollectionModalOpen}
|
||||||
|
collectionId={id}
|
||||||
|
onModalClose={this.onEditCollectionModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverview.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
qualityProfileId: PropTypes.number.isRequired,
|
||||||
|
minimumAvailability: PropTypes.string.isRequired,
|
||||||
|
searchOnAdd: PropTypes.bool.isRequired,
|
||||||
|
rootFolderPath: PropTypes.string.isRequired,
|
||||||
|
tmdbId: PropTypes.number.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
overview: PropTypes.string.isRequired,
|
||||||
|
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
missingMovies: PropTypes.number.isRequired,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
rowHeight: PropTypes.number.isRequired,
|
||||||
|
posterHeight: PropTypes.number.isRequired,
|
||||||
|
posterWidth: PropTypes.number.isRequired,
|
||||||
|
overviewOptions: PropTypes.object.isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionOverview;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import CollectionOverview from './CollectionOverview';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(dimensions) => {
|
||||||
|
return {
|
||||||
|
isSmallScreen: dimensions.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleCollectionMonitored
|
||||||
|
};
|
||||||
|
|
||||||
|
class CollectionOverviewConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMonitorTogglePress = (monitored) => {
|
||||||
|
this.props.toggleCollectionMonitored({
|
||||||
|
collectionId: this.props.collectionId,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CollectionOverview
|
||||||
|
{...this.props}
|
||||||
|
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverviewConnector.propTypes = {
|
||||||
|
collectionId: PropTypes.number.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
toggleCollectionMonitored: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionOverviewConnector);
|
||||||
15
frontend/src/Collection/Overview/CollectionOverviews.css
Normal file
15
frontend/src/Collection/Overview/CollectionOverviews.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.grid {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
&:hover {
|
||||||
|
.content {
|
||||||
|
background-color: $tableRowHoverBackgroundColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.externalLinks {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
272
frontend/src/Collection/Overview/CollectionOverviews.js
Normal file
272
frontend/src/Collection/Overview/CollectionOverviews.js
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
import CollectionItemConnector from 'Collection/CollectionItemConnector';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||||
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
|
import CollectionOverviewConnector from './CollectionOverviewConnector';
|
||||||
|
import styles from './CollectionOverviews.css';
|
||||||
|
|
||||||
|
// Poster container dimensions
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
|
||||||
|
function calculatePosterWidth(posterSize, isSmallScreen) {
|
||||||
|
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
|
||||||
|
|
||||||
|
if (posterSize === 'large') {
|
||||||
|
return maxiumPosterWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (posterSize === 'medium') {
|
||||||
|
return Math.floor(maxiumPosterWidth * 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(maxiumPosterWidth * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
||||||
|
|
||||||
|
const heights = [
|
||||||
|
overviewOptions.showPosters ? posterHeight : 75,
|
||||||
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||||
|
];
|
||||||
|
|
||||||
|
return heights.reduce((acc, height) => acc + height + 80, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculatePosterHeight(posterWidth) {
|
||||||
|
return Math.ceil((250 / 170) * posterWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CollectionOverviews extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
width: 0,
|
||||||
|
columnCount: 1,
|
||||||
|
posterWidth: 162,
|
||||||
|
posterHeight: 238,
|
||||||
|
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
|
||||||
|
};
|
||||||
|
|
||||||
|
this._grid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
overviewOptions,
|
||||||
|
jumpToCharacter,
|
||||||
|
scrollTop,
|
||||||
|
isSmallScreen
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
rowHeight,
|
||||||
|
scrollRestored
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (prevProps.sortKey !== sortKey ||
|
||||||
|
prevProps.overviewOptions !== overviewOptions) {
|
||||||
|
this.calculateGrid(this.state.width, isSmallScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._grid &&
|
||||||
|
(prevState.width !== width ||
|
||||||
|
prevState.rowHeight !== rowHeight ||
|
||||||
|
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
||||||
|
prevProps.overviewOptions !== overviewOptions)) {
|
||||||
|
// 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setGridRef = (ref) => {
|
||||||
|
this._grid = ref;
|
||||||
|
};
|
||||||
|
|
||||||
|
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||||
|
const {
|
||||||
|
sortKey,
|
||||||
|
overviewOptions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
|
||||||
|
const posterHeight = calculatePosterHeight(posterWidth);
|
||||||
|
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width,
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
rowHeight
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
cellRenderer = ({ key, rowIndex, style }) => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
overviewOptions,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
isSmallScreen,
|
||||||
|
selectedState,
|
||||||
|
onSelectedChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const collection = items[rowIndex];
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<CollectionItemConnector
|
||||||
|
key={collection.id}
|
||||||
|
component={CollectionOverviewConnector}
|
||||||
|
sortKey={sortKey}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
posterHeight={posterHeight}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
overviewOptions={overviewOptions}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
longDateFormat={longDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
collectionId={collection.id}
|
||||||
|
isSelected={selectedState[collection.id]}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = ({ width }) => {
|
||||||
|
this.calculateGrid(width, this.props.isSmallScreen);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isSmallScreen,
|
||||||
|
scroller,
|
||||||
|
items,
|
||||||
|
selectedState
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Measure
|
||||||
|
whitelist={['width']}
|
||||||
|
onMeasure={this.onMeasure}
|
||||||
|
>
|
||||||
|
<WindowScroller
|
||||||
|
scrollElement={isSmallScreen ? undefined : scroller}
|
||||||
|
>
|
||||||
|
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||||
|
if (!height) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={registerChild}>
|
||||||
|
<Grid
|
||||||
|
ref={this.setGridRef}
|
||||||
|
className={styles.grid}
|
||||||
|
autoHeight={true}
|
||||||
|
height={height}
|
||||||
|
columnCount={1}
|
||||||
|
columnWidth={width}
|
||||||
|
rowCount={items.length}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
width={width}
|
||||||
|
onScroll={onChildScroll}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
overscanRowCount={2}
|
||||||
|
cellRenderer={this.cellRenderer}
|
||||||
|
selectedState={selectedState}
|
||||||
|
scrollToAlignment={'start'}
|
||||||
|
isScrollingOptout={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</WindowScroller>
|
||||||
|
</Measure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverviews.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
overviewOptions: PropTypes.object.isRequired,
|
||||||
|
jumpToCharacter: PropTypes.string,
|
||||||
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
selectedState: PropTypes.object.isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionOverviews;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import CollectionOverviews from './CollectionOverviews';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections.overviewOptions,
|
||||||
|
createUISettingsSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(overviewOptions, uiSettings, dimensions) => {
|
||||||
|
return {
|
||||||
|
overviewOptions,
|
||||||
|
showRelativeDates: uiSettings.showRelativeDates,
|
||||||
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
|
longDateFormat: uiSettings.longDateFormat,
|
||||||
|
timeFormat: uiSettings.timeFormat,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(CollectionOverviews);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import CollectionOverviewOptionsModalContentConnector from './CollectionOverviewOptionsModalContentConnector';
|
||||||
|
|
||||||
|
function CollectionOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<CollectionOverviewOptionsModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverviewOptionsModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionOverviewOptionsModal;
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
const posterSizeOptions = [
|
||||||
|
{ key: 'small', value: translate('Small') },
|
||||||
|
{ key: 'medium', value: translate('Medium') },
|
||||||
|
{ key: 'large', value: translate('Large') }
|
||||||
|
];
|
||||||
|
|
||||||
|
class CollectionOverviewOptionsModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
detailedProgressBar: props.detailedProgressBar,
|
||||||
|
size: props.size,
|
||||||
|
showDetails: props.showDetails,
|
||||||
|
showOverview: props.showOverview,
|
||||||
|
showPosters: props.showPosters
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
size,
|
||||||
|
showDetails,
|
||||||
|
showOverview,
|
||||||
|
showPosters
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const state = {};
|
||||||
|
|
||||||
|
if (detailedProgressBar !== prevProps.detailedProgressBar) {
|
||||||
|
state.detailedProgressBar = detailedProgressBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size !== prevProps.size) {
|
||||||
|
state.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDetails !== prevProps.showDetails) {
|
||||||
|
state.showDetails = showDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showOverview !== prevProps.showOverview) {
|
||||||
|
state.showOverview = showOverview;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPosters !== prevProps.showPosters) {
|
||||||
|
state.showPosters = showPosters;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isEmpty(state)) {
|
||||||
|
this.setState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onChangeOverviewOption = ({ name, value }) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => {
|
||||||
|
this.props.onChangeOverviewOption({ [name]: value });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onChangeOption = ({ name, value }) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => {
|
||||||
|
this.props.onChangeOption({
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
size,
|
||||||
|
detailedProgressBar,
|
||||||
|
showDetails,
|
||||||
|
showPosters,
|
||||||
|
showOverview
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('CollectionOptions')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="size"
|
||||||
|
value={size}
|
||||||
|
values={posterSizeOptions}
|
||||||
|
onChange={this.onChangeOverviewOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="detailedProgressBar"
|
||||||
|
value={detailedProgressBar}
|
||||||
|
helpText={translate('DetailedProgressBarHelpText')}
|
||||||
|
onChange={this.onChangeOverviewOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowCollectionDetails')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showDetails"
|
||||||
|
value={showDetails}
|
||||||
|
helpText={translate('CollectionShowDetailsHelpText')}
|
||||||
|
onChange={this.onChangeOverviewOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowOverview')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showOverview"
|
||||||
|
value={showOverview}
|
||||||
|
helpText={translate('CollectionShowOverviewsHelpText')}
|
||||||
|
onChange={this.onChangeOverviewOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowPosters')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showPosters"
|
||||||
|
value={showPosters}
|
||||||
|
helpText={translate('CollectionShowPostersHelpText')}
|
||||||
|
onChange={this.onChangeOverviewOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
{translate('Close')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionOverviewOptionsModalContent.propTypes = {
|
||||||
|
detailedProgressBar: PropTypes.bool.isRequired,
|
||||||
|
size: PropTypes.string.isRequired,
|
||||||
|
showDetails: PropTypes.bool.isRequired,
|
||||||
|
showOverview: PropTypes.bool.isRequired,
|
||||||
|
showPosters: PropTypes.bool.isRequired,
|
||||||
|
onChangeOverviewOption: PropTypes.func.isRequired,
|
||||||
|
onChangeOption: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CollectionOverviewOptionsModalContent;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setMovieCollectionsOption, setMovieCollectionsOverviewOption } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import CollectionOverviewOptionsModalContent from './CollectionOverviewOptionsModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.movieCollections,
|
||||||
|
(movieCollections) => {
|
||||||
|
return {
|
||||||
|
...movieCollections.options,
|
||||||
|
...movieCollections.overviewOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onChangeOverviewOption(payload) {
|
||||||
|
dispatch(setMovieCollectionsOverviewOption(payload));
|
||||||
|
},
|
||||||
|
onChangeOption(payload) {
|
||||||
|
dispatch(setMovieCollectionsOption(payload));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(CollectionOverviewOptionsModalContent);
|
||||||
@@ -10,6 +10,7 @@ export const DOWNLOADED_MOVIES_SCAN = 'DownloadedMoviesScan';
|
|||||||
export const INTERACTIVE_IMPORT = 'ManualImport';
|
export const INTERACTIVE_IMPORT = 'ManualImport';
|
||||||
export const MISSING_MOVIES_SEARCH = 'MissingMoviesSearch';
|
export const MISSING_MOVIES_SEARCH = 'MissingMoviesSearch';
|
||||||
export const MOVE_MOVIE = 'MoveMovie';
|
export const MOVE_MOVIE = 'MoveMovie';
|
||||||
|
export const REFRESH_COLLECTIONS = 'RefreshCollections';
|
||||||
export const REFRESH_MOVIE = 'RefreshMovie';
|
export const REFRESH_MOVIE = 'RefreshMovie';
|
||||||
export const RENAME_FILES = 'RenameFiles';
|
export const RENAME_FILES = 'RenameFiles';
|
||||||
export const RENAME_MOVIE = 'RenameMovie';
|
export const RENAME_MOVIE = 'RenameMovie';
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ class DateFilterBuilderRowValue extends Component {
|
|||||||
<TextInput
|
<TextInput
|
||||||
name={NAME}
|
name={NAME}
|
||||||
value={filterValue}
|
value={filterValue}
|
||||||
|
type="date"
|
||||||
placeholder="yyyy-mm-dd"
|
placeholder="yyyy-mm-dd"
|
||||||
onChange={this.onValueChange}
|
onChange={this.onValueChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import Portal from 'Components/Portal';
|
import Portal from 'Components/Portal';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
import { icons, scrollDirections, sizes } from 'Helpers/Props';
|
import { icons, scrollDirections, sizes } from 'Helpers/Props';
|
||||||
|
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
|
||||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import translate from 'Utilities/String/translate';
|
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
const monitorTypesOptions = [
|
|
||||||
{ key: 'true', value: translate('Yes') },
|
|
||||||
{ key: 'false', value: translate('No') }
|
|
||||||
];
|
|
||||||
|
|
||||||
function MovieMonitoredSelectInput(props) {
|
function MovieMonitoredSelectInput(props) {
|
||||||
const values = [...monitorTypesOptions];
|
const values = [...monitorOptions];
|
||||||
|
|
||||||
const {
|
const {
|
||||||
includeNoChange,
|
includeNoChange,
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
.image {
|
|
||||||
align-content: center;
|
|
||||||
margin-right: 5px;
|
|
||||||
vertical-align: -0.125em;
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -22,11 +22,11 @@ class ImdbRating extends PureComponent {
|
|||||||
let ratingString = '0.0';
|
let ratingString = '0.0';
|
||||||
|
|
||||||
if (rating) {
|
if (rating) {
|
||||||
ratingString = `${rating.value}`;
|
ratingString = `${rating.value.toFixed(1)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span title={`${rating.votes} votes`}>
|
<span title={`${rating ? rating.votes : 0} votes`}>
|
||||||
{
|
{
|
||||||
!hideIcon &&
|
!hideIcon &&
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import ReactDOM from 'react-dom';
|
|||||||
import FocusLock from 'react-focus-lock';
|
import FocusLock from 'react-focus-lock';
|
||||||
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||||
import { sizes } from 'Helpers/Props';
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { isIOS } from 'Utilities/browser';
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||||
import { isIOS } from 'Utilities/mobile';
|
|
||||||
import { setScrollLock } from 'Utilities/scrollLock';
|
import { setScrollLock } from 'Utilities/scrollLock';
|
||||||
import ModalError from './ModalError';
|
import ModalError from './ModalError';
|
||||||
import styles from './Modal.css';
|
import styles from './Modal.css';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createSelector } from 'reselect';
|
|||||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
import { fetchMovies } from 'Store/Actions/movieActions';
|
import { fetchMovies } from 'Store/Actions/movieActions';
|
||||||
|
import { fetchMovieCollections } from 'Store/Actions/movieCollectionActions';
|
||||||
import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
@@ -51,6 +52,7 @@ const selectIsPopulated = createSelector(
|
|||||||
(state) => state.settings.indexerFlags.isPopulated,
|
(state) => state.settings.indexerFlags.isPopulated,
|
||||||
(state) => state.settings.importLists.isPopulated,
|
(state) => state.settings.importLists.isPopulated,
|
||||||
(state) => state.system.status.isPopulated,
|
(state) => state.system.status.isPopulated,
|
||||||
|
(state) => state.movieCollections.isPopulated,
|
||||||
(
|
(
|
||||||
customFiltersIsPopulated,
|
customFiltersIsPopulated,
|
||||||
tagsIsPopulated,
|
tagsIsPopulated,
|
||||||
@@ -59,7 +61,8 @@ const selectIsPopulated = createSelector(
|
|||||||
languagesIsPopulated,
|
languagesIsPopulated,
|
||||||
indexerFlagsIsPopulated,
|
indexerFlagsIsPopulated,
|
||||||
importListsIsPopulated,
|
importListsIsPopulated,
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated,
|
||||||
|
movieCollectionsIsPopulated
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
customFiltersIsPopulated &&
|
customFiltersIsPopulated &&
|
||||||
@@ -69,7 +72,8 @@ const selectIsPopulated = createSelector(
|
|||||||
languagesIsPopulated &&
|
languagesIsPopulated &&
|
||||||
indexerFlagsIsPopulated &&
|
indexerFlagsIsPopulated &&
|
||||||
importListsIsPopulated &&
|
importListsIsPopulated &&
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated &&
|
||||||
|
movieCollectionsIsPopulated
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -83,6 +87,7 @@ const selectErrors = createSelector(
|
|||||||
(state) => state.settings.indexerFlags.error,
|
(state) => state.settings.indexerFlags.error,
|
||||||
(state) => state.settings.importLists.error,
|
(state) => state.settings.importLists.error,
|
||||||
(state) => state.system.status.error,
|
(state) => state.system.status.error,
|
||||||
|
(state) => state.movieCollections.error,
|
||||||
(
|
(
|
||||||
customFiltersError,
|
customFiltersError,
|
||||||
tagsError,
|
tagsError,
|
||||||
@@ -91,7 +96,8 @@ const selectErrors = createSelector(
|
|||||||
languagesError,
|
languagesError,
|
||||||
indexerFlagsError,
|
indexerFlagsError,
|
||||||
importListsError,
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError,
|
||||||
|
movieCollectionsError
|
||||||
) => {
|
) => {
|
||||||
const hasError = !!(
|
const hasError = !!(
|
||||||
customFiltersError ||
|
customFiltersError ||
|
||||||
@@ -101,7 +107,8 @@ const selectErrors = createSelector(
|
|||||||
languagesError ||
|
languagesError ||
|
||||||
indexerFlagsError ||
|
indexerFlagsError ||
|
||||||
importListsError ||
|
importListsError ||
|
||||||
systemStatusError
|
systemStatusError ||
|
||||||
|
movieCollectionsError
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -113,7 +120,8 @@ const selectErrors = createSelector(
|
|||||||
languagesError,
|
languagesError,
|
||||||
indexerFlagsError,
|
indexerFlagsError,
|
||||||
importListsError,
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError,
|
||||||
|
movieCollectionsError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -148,6 +156,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatchFetchMovies() {
|
dispatchFetchMovies() {
|
||||||
dispatch(fetchMovies());
|
dispatch(fetchMovies());
|
||||||
},
|
},
|
||||||
|
dispatchFetchMovieCollections() {
|
||||||
|
dispatch(fetchMovieCollections());
|
||||||
|
},
|
||||||
dispatchFetchCustomFilters() {
|
dispatchFetchCustomFilters() {
|
||||||
dispatch(fetchCustomFilters());
|
dispatch(fetchCustomFilters());
|
||||||
},
|
},
|
||||||
@@ -197,6 +208,7 @@ class PageConnector extends Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!this.props.isPopulated) {
|
if (!this.props.isPopulated) {
|
||||||
this.props.dispatchFetchMovies();
|
this.props.dispatchFetchMovies();
|
||||||
|
this.props.dispatchFetchMovieCollections();
|
||||||
this.props.dispatchFetchCustomFilters();
|
this.props.dispatchFetchCustomFilters();
|
||||||
this.props.dispatchFetchTags();
|
this.props.dispatchFetchTags();
|
||||||
this.props.dispatchFetchQualityProfiles();
|
this.props.dispatchFetchQualityProfiles();
|
||||||
@@ -223,6 +235,7 @@ class PageConnector extends Component {
|
|||||||
isPopulated,
|
isPopulated,
|
||||||
hasError,
|
hasError,
|
||||||
dispatchFetchMovies,
|
dispatchFetchMovies,
|
||||||
|
dispatchFetchMovieCollections,
|
||||||
dispatchFetchTags,
|
dispatchFetchTags,
|
||||||
dispatchFetchQualityProfiles,
|
dispatchFetchQualityProfiles,
|
||||||
dispatchFetchLanguages,
|
dispatchFetchLanguages,
|
||||||
@@ -262,6 +275,7 @@ PageConnector.propTypes = {
|
|||||||
hasError: PropTypes.bool.isRequired,
|
hasError: PropTypes.bool.isRequired,
|
||||||
isSidebarVisible: PropTypes.bool.isRequired,
|
isSidebarVisible: PropTypes.bool.isRequired,
|
||||||
dispatchFetchMovies: PropTypes.func.isRequired,
|
dispatchFetchMovies: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchMovieCollections: PropTypes.func.isRequired,
|
||||||
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
||||||
dispatchFetchTags: PropTypes.func.isRequired,
|
dispatchFetchTags: PropTypes.func.isRequired,
|
||||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function PageContent(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary errorComponent={PageContentError}>
|
<ErrorBoundary errorComponent={PageContentError}>
|
||||||
<DocumentTitle title={title ? `${title} - Radarr` : 'Radarr'}>
|
<DocumentTitle title={title ? `${title} - ${window.Radarr.instanceName}` : window.Radarr.instanceName}>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
import { scrollDirections } from 'Helpers/Props';
|
import { scrollDirections } from 'Helpers/Props';
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
import { isFirefox, isMobile } from 'Utilities/browser';
|
||||||
import { isLocked } from 'Utilities/scrollLock';
|
import { isLocked } from 'Utilities/scrollLock';
|
||||||
import styles from './PageContentBody.css';
|
import styles from './PageContentBody.css';
|
||||||
|
|
||||||
@@ -15,7 +15,8 @@ class PageContentBody extends Component {
|
|||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(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
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
const ScrollerComponent = this._isMobile || this._isSmallScreenFirefox ?
|
||||||
|
Scroller :
|
||||||
|
OverlayScroller;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollerComponent
|
<ScrollerComponent
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.jumpBar {
|
.jumpBar {
|
||||||
|
z-index: $pageJumpBarZIndex;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ const links = [
|
|||||||
title: translate('ImportLibrary'),
|
title: translate('ImportLibrary'),
|
||||||
to: '/add/import'
|
to: '/add/import'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: translate('Collections'),
|
||||||
|
to: '/collections'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: translate('Discover'),
|
title: translate('Discover'),
|
||||||
to: '/add/discover'
|
to: '/add/discover'
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
composes: link from '~Components/Link/Link.css';
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
width: $toolbarButtonWidth;
|
min-width: $toolbarButtonWidth;
|
||||||
|
width: min-content;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ class RottenTomatoRating extends PureComponent {
|
|||||||
const rating = ratings.rottenTomatoes;
|
const rating = ratings.rottenTomatoes;
|
||||||
|
|
||||||
let ratingString = '0%';
|
let ratingString = '0%';
|
||||||
|
let ratingImage = rtFresh;
|
||||||
|
|
||||||
if (rating) {
|
if (rating) {
|
||||||
ratingString = `${rating.value}%`;
|
ratingString = `${rating.value}%`;
|
||||||
|
ratingImage = rating.value > 50 ? rtFresh : rtRotten;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,7 +34,7 @@ class RottenTomatoRating extends PureComponent {
|
|||||||
!hideIcon &&
|
!hideIcon &&
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
src={rating.value > 50 ? rtFresh : rtRotten}
|
src={ratingImage}
|
||||||
style={{
|
style={{
|
||||||
height: `${iconSize}px`
|
height: `${iconSize}px`
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -203,6 +203,19 @@ class SignalRConnector extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleCollection = (body) => {
|
||||||
|
const action = body.action;
|
||||||
|
const section = 'movieCollections';
|
||||||
|
|
||||||
|
console.log(body);
|
||||||
|
|
||||||
|
if (action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({ section, ...body.resource });
|
||||||
|
} else if (action === 'deleted') {
|
||||||
|
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleQueue = () => {
|
handleQueue = () => {
|
||||||
if (this.props.isQueuePopulated) {
|
if (this.props.isQueuePopulated) {
|
||||||
this.props.dispatchFetchQueue();
|
this.props.dispatchFetchQueue();
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class TableOptionsModal extends Component {
|
|||||||
<TableOptionsColumnDragSource
|
<TableOptionsColumnDragSource
|
||||||
key={name}
|
key={name}
|
||||||
name={name}
|
name={name}
|
||||||
label={label || columnLabel}
|
label={columnLabel || label}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
isModifiable={true}
|
isModifiable={true}
|
||||||
index={index}
|
index={index}
|
||||||
@@ -210,7 +210,7 @@ class TableOptionsModal extends Component {
|
|||||||
<TableOptionsColumn
|
<TableOptionsColumn
|
||||||
key={name}
|
key={name}
|
||||||
name={name}
|
name={name}
|
||||||
label={label || columnLabel}
|
label={columnLabel || label}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
index={index}
|
index={index}
|
||||||
isModifiable={false}
|
isModifiable={false}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TmdbRating extends PureComponent {
|
|||||||
let ratingString = '0%';
|
let ratingString = '0%';
|
||||||
|
|
||||||
if (rating) {
|
if (rating) {
|
||||||
ratingString = `${rating.value * 10}%`;
|
ratingString = `${(rating.value * 10).toFixed()}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Manager, Popper, Reference } from 'react-popper';
|
|||||||
import Portal from 'Components/Portal';
|
import Portal from 'Components/Portal';
|
||||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
import { isMobile as isMobileUtil } from 'Utilities/browser';
|
||||||
import styles from './Tooltip.css';
|
import styles from './Tooltip.css';
|
||||||
|
|
||||||
let maxWidth = null;
|
let maxWidth = null;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SelectInput from 'Components/Form/SelectInput';
|
|||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import DiscoverMovieFooterLabel from './DiscoverMovieFooterLabel';
|
import DiscoverMovieFooterLabel from './DiscoverMovieFooterLabel';
|
||||||
import ExcludeMovieModal from './Exclusion/ExcludeMovieModal';
|
import ExcludeMovieModal from './Exclusion/ExcludeMovieModal';
|
||||||
@@ -137,11 +138,6 @@ class DiscoverMovieFooter extends Component {
|
|||||||
isExcludeMovieModalOpen
|
isExcludeMovieModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const monitoredOptions = [
|
|
||||||
{ key: true, value: translate('Monitored') },
|
|
||||||
{ key: false, value: translate('Unmonitored') }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentFooter>
|
<PageContentFooter>
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
@@ -153,7 +149,7 @@ class DiscoverMovieFooter extends Component {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
name="monitor"
|
name="monitor"
|
||||||
value={monitor}
|
value={monitor}
|
||||||
values={monitoredOptions}
|
values={monitorOptions}
|
||||||
isDisabled={!selectedCount}
|
isDisabled={!selectedCount}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import HeartRating from 'Components/HeartRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
||||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
@@ -111,7 +111,7 @@ function DiscoverMoviePosterInfo(props) {
|
|||||||
if (sortKey === 'ratings' && ratings) {
|
if (sortKey === 'ratings' && ratings) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<HeartRating
|
<TmdbRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +129,7 @@ DiscoverMoviePosterInfo.propTypes = {
|
|||||||
digitalRelease: PropTypes.string,
|
digitalRelease: PropTypes.string,
|
||||||
physicalRelease: PropTypes.string,
|
physicalRelease: PropTypes.string,
|
||||||
runtime: PropTypes.number,
|
runtime: PropTypes.number,
|
||||||
ratings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
sortKey: PropTypes.string.isRequired,
|
sortKey: PropTypes.string.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import HeartRating from 'Components/HeartRating';
|
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImportListListConnector from 'Components/ImportListListConnector';
|
import ImportListListConnector from 'Components/ImportListListConnector';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
@@ -8,6 +7,7 @@ import Link from 'Components/Link/Link';
|
|||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||||
@@ -164,7 +164,7 @@ class DiscoverMovieRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{collection ? collection.name : null }
|
{collection ? collection.title : null }
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -245,7 +245,7 @@ class DiscoverMovieRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
<HeartRating
|
<TmdbRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
@@ -373,7 +373,7 @@ DiscoverMovieRow.propTypes = {
|
|||||||
digitalRelease: PropTypes.string,
|
digitalRelease: PropTypes.string,
|
||||||
runtime: PropTypes.number,
|
runtime: PropTypes.number,
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
ratings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
certification: PropTypes.string,
|
certification: PropTypes.string,
|
||||||
collection: PropTypes.object,
|
collection: PropTypes.object,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
faArrowCircleLeft as fasArrowCircleLeft,
|
faArrowCircleLeft as fasArrowCircleLeft,
|
||||||
faArrowCircleRight as fasArrowCircleRight,
|
faArrowCircleRight as fasArrowCircleRight,
|
||||||
|
faAsterisk as fasAsterisk,
|
||||||
faBackward as fasBackward,
|
faBackward as fasBackward,
|
||||||
faBan as fasBan,
|
faBan as fasBan,
|
||||||
faBars as fasBars,
|
faBars as fasBars,
|
||||||
@@ -154,6 +155,7 @@ export const FILE = farFile;
|
|||||||
export const FILM = fasFilm;
|
export const FILM = fasFilm;
|
||||||
export const FILTER = fasFilter;
|
export const FILTER = fasFilter;
|
||||||
export const FLAG = fasFlag;
|
export const FLAG = fasFlag;
|
||||||
|
export const FOOTNOTE = fasAsterisk;
|
||||||
export const FOLDER = farFolder;
|
export const FOLDER = farFolder;
|
||||||
export const FOLDER_OPEN = fasFolderOpen;
|
export const FOLDER_OPEN = fasFolderOpen;
|
||||||
export const GENRE = fasTheaterMasks;
|
export const GENRE = fasTheaterMasks;
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const filterExistingFilesOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const importModeOptions = [
|
const importModeOptions = [
|
||||||
|
{ key: 'chooseImportMode', value: translate('ChooseImportMode'), disabled: true },
|
||||||
{ key: 'move', value: translate('MoveFiles') },
|
{ key: 'move', value: translate('MoveFiles') },
|
||||||
{ key: 'copy', value: translate('HardlinkCopyFiles') }
|
{ key: 'copy', value: translate('HardlinkCopyFiles') }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@@ -101,9 +100,18 @@ class InteractiveImportModalContentConnector extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onImportSelectedPress = (selected, importMode) => {
|
onImportSelectedPress = (selected, importMode) => {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const files = [];
|
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;
|
const isSelected = selected.indexOf(item.id) > -1;
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.modalBody {
|
||||||
|
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
@@ -9,8 +9,9 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './SelectReleaseGroupModalContent.css';
|
||||||
|
|
||||||
class SelectReleaseGroupModalContent extends Component {
|
class SelectReleaseGroupModalContent extends Component {
|
||||||
|
|
||||||
@@ -58,7 +59,10 @@ class SelectReleaseGroupModalContent extends Component {
|
|||||||
{translate('ManualImportSetReleaseGroup')}
|
{translate('ManualImportSetReleaseGroup')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody
|
||||||
|
className={styles.modalBody}
|
||||||
|
scrollDirection={scrollDirections.NONE}
|
||||||
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
|
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
|
||||||
@@ -67,6 +71,7 @@ class SelectReleaseGroupModalContent extends Component {
|
|||||||
type={inputTypes.TEXT}
|
type={inputTypes.TEXT}
|
||||||
name="releaseGroup"
|
name="releaseGroup"
|
||||||
value={releaseGroup}
|
value={releaseGroup}
|
||||||
|
autoFocus={true}
|
||||||
onChange={this.onReleaseGroupChange}
|
onChange={this.onReleaseGroupChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|||||||
@@ -7,3 +7,9 @@
|
|||||||
.filteredMessage {
|
.filteredMessage {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blankpad {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,21 +127,21 @@ function InteractiveSearchContent(props) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error &&
|
||||||
<div>
|
<div className={styles.blankpad}>
|
||||||
{translate('UnableToLoadResultsIntSearch')}
|
{translate('UnableToLoadResultsIntSearch')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && isPopulated && !totalReleasesCount &&
|
!isFetching && isPopulated && !totalReleasesCount &&
|
||||||
<div>
|
<div className={styles.blankpad}>
|
||||||
{translate('NoResultsFound')}
|
{translate('NoResultsFound')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!totalReleasesCount && isPopulated && !items.length &&
|
!!totalReleasesCount && isPopulated && !items.length &&
|
||||||
<div>
|
<div className={styles.blankpad}>
|
||||||
{translate('AllResultsHiddenFilter')}
|
{translate('AllResultsHiddenFilter')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ function InteractiveSearchContent(props) {
|
|||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<InteractiveSearchRowConnector
|
<InteractiveSearchRowConnector
|
||||||
key={item.guid}
|
key={`${item.indexerId}-${item.guid}`}
|
||||||
{...item}
|
{...item}
|
||||||
searchPayload={searchPayload}
|
searchPayload={searchPayload}
|
||||||
longDateFormat={longDateFormat}
|
longDateFormat={longDateFormat}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import MovieCollectionConnector from './../MovieCollectionConnector';
|
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
||||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
||||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
||||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||||
@@ -583,10 +583,8 @@ class MovieDetails extends Component {
|
|||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<div className={styles.collection}>
|
<div className={styles.collection}>
|
||||||
<MovieCollectionConnector
|
<MovieCollectionLabelConnector
|
||||||
tmdbId={collection.tmdbId}
|
tmdbId={collection.tmdbId}
|
||||||
name={collection.name}
|
|
||||||
movieId={id}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</InfoLabel>
|
</InfoLabel>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { icons, kinds } from 'Helpers/Props';
|
|||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './MovieHistoryRow.css';
|
import styles from './MovieHistoryRow.css';
|
||||||
|
|
||||||
@@ -104,6 +105,10 @@ class MovieHistoryRow extends Component {
|
|||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{formatCustomFormatScore(data.customFormatScore)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
date={date}
|
date={date}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
@@ -35,6 +36,15 @@ const columns = [
|
|||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormatScore',
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.SCORE,
|
||||||
|
title: 'Custom format score'
|
||||||
|
}),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'date',
|
||||||
label: translate('Date'),
|
label: translate('Date'),
|
||||||
|
|||||||
@@ -102,12 +102,21 @@ function MovieIndexSortMenu(props) {
|
|||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="ratings"
|
name="imdbRating"
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
onPress={onSortSelect}
|
onPress={onSortSelect}
|
||||||
>
|
>
|
||||||
{translate('Ratings')}
|
{translate('ImdbRating')}
|
||||||
|
</SortMenuItem>
|
||||||
|
|
||||||
|
<SortMenuItem
|
||||||
|
name="tmdbRating"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('TmdbRating')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
|
|||||||
@@ -7,6 +7,15 @@
|
|||||||
transition: width 200ms ease;
|
transition: width 200ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progressRadius {
|
||||||
|
composes: container from '~Components/ProgressBar.css';
|
||||||
|
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
background-color: #5b5b5b;
|
||||||
|
color: $white;
|
||||||
|
transition: width 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
.progressBar {
|
.progressBar {
|
||||||
composes: progressBar from '~Components/ProgressBar.css';
|
composes: progressBar from '~Components/ProgressBar.css';
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function MovieIndexProgressBar(props) {
|
|||||||
isAvailable,
|
isAvailable,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
detailedProgressBar,
|
detailedProgressBar,
|
||||||
|
bottomRadius,
|
||||||
queueStatus,
|
queueStatus,
|
||||||
queueState
|
queueState
|
||||||
} = props;
|
} = props;
|
||||||
@@ -40,7 +41,7 @@ function MovieIndexProgressBar(props) {
|
|||||||
return (
|
return (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
className={styles.progressBar}
|
className={styles.progressBar}
|
||||||
containerClassName={styles.progress}
|
containerClassName={bottomRadius ? styles.progressRadius : styles.progress}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
kind={getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds', queueStatusText)}
|
kind={getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds', queueStatusText)}
|
||||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||||
@@ -54,6 +55,7 @@ function MovieIndexProgressBar(props) {
|
|||||||
MovieIndexProgressBar.propTypes = {
|
MovieIndexProgressBar.propTypes = {
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
hasFile: PropTypes.bool.isRequired,
|
hasFile: PropTypes.bool.isRequired,
|
||||||
|
bottomRadius: PropTypes.bool,
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
posterWidth: PropTypes.number.isRequired,
|
posterWidth: PropTypes.number.isRequired,
|
||||||
@@ -62,4 +64,8 @@ MovieIndexProgressBar.propTypes = {
|
|||||||
queueState: PropTypes.string
|
queueState: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MovieIndexProgressBar.defaultProps = {
|
||||||
|
bottomRadius: false
|
||||||
|
};
|
||||||
|
|
||||||
export default MovieIndexProgressBar;
|
export default MovieIndexProgressBar;
|
||||||
|
|||||||
@@ -77,7 +77,9 @@
|
|||||||
flex: 0 0 120px;
|
flex: 0 0 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ratings {
|
.imdbRating,
|
||||||
|
.tmdbRating,
|
||||||
|
.rottenTomatoesRating {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 80px;
|
flex: 0 0 80px;
|
||||||
|
|||||||
@@ -84,7 +84,9 @@
|
|||||||
flex: 0 0 120px;
|
flex: 0 0 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ratings {
|
.imdbRating,
|
||||||
|
.tmdbRating,
|
||||||
|
.rottenTomatoesRating {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 80px;
|
flex: 0 0 80px;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import HeartRating from 'Components/HeartRating';
|
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
@@ -170,7 +172,7 @@ class MovieIndexRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
{collection ? collection.name : null }
|
{collection ? collection.title : null }
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -349,13 +351,39 @@ class MovieIndexRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'ratings') {
|
if (name === 'tmdbRating') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell
|
<VirtualTableRowCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
>
|
>
|
||||||
<HeartRating
|
<TmdbRating
|
||||||
|
ratings={ratings}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'rottenTomatoesRating') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
>
|
||||||
|
<RottenTomatoRating
|
||||||
|
ratings={ratings}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'imdbRating') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
>
|
||||||
|
<ImdbRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class MovieIndexTable extends Component {
|
|||||||
component={MovieIndexRow}
|
component={MovieIndexRow}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
movieId={movie.id}
|
movieId={movie.id}
|
||||||
|
collectionId={movie.collectionId}
|
||||||
qualityProfileId={movie.qualityProfileId}
|
qualityProfileId={movie.qualityProfileId}
|
||||||
isSelected={selectedState[movie.id]}
|
isSelected={selectedState[movie.id]}
|
||||||
onSelectedChange={onSelectedChange}
|
onSelectedChange={onSelectedChange}
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|
||||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
|
||||||
import styles from './MovieCollection.css';
|
|
||||||
|
|
||||||
class MovieCollection extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hasPosterError: false,
|
|
||||||
isEditImportListModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddImportListPress = (monitored) => {
|
|
||||||
if (this.props.collectionList) {
|
|
||||||
this.props.onMonitorTogglePress(monitored);
|
|
||||||
} else {
|
|
||||||
this.props.onMonitorTogglePress(monitored);
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditImportListModalClose = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
collectionList,
|
|
||||||
isSaving
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const monitored = collectionList !== undefined && collectionList.enabled && collectionList.enableAuto;
|
|
||||||
const importListId = collectionList ? collectionList.id : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<MonitorToggleButton
|
|
||||||
className={styles.monitorToggleButton}
|
|
||||||
monitored={monitored}
|
|
||||||
isSaving={isSaving}
|
|
||||||
size={15}
|
|
||||||
onPress={this.onAddImportListPress}
|
|
||||||
/>
|
|
||||||
{name}
|
|
||||||
<EditImportListModalConnector
|
|
||||||
id={importListId}
|
|
||||||
isOpen={this.state.isEditImportListModalOpen}
|
|
||||||
onModalClose={this.onEditImportListModalClose}
|
|
||||||
onDeleteImportListPress={this.onDeleteImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCollection.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
collectionList: PropTypes.object,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
onMonitorTogglePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieCollection;
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { saveImportList, selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
|
|
||||||
import createMovieCollectionListSelector from 'Store/Selectors/createMovieCollectionListSelector';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import MovieCollection from './MovieCollection';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieSelector(),
|
|
||||||
createMovieCollectionListSelector(),
|
|
||||||
(state) => state.settings.importLists,
|
|
||||||
(movie, collectionList, importLists) => {
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability
|
|
||||||
} = movie;
|
|
||||||
|
|
||||||
return {
|
|
||||||
collectionList,
|
|
||||||
monitored,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
isSaving: importLists.isSaving
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
selectImportListSchema,
|
|
||||||
setImportListFieldValue,
|
|
||||||
setImportListValue,
|
|
||||||
saveImportList
|
|
||||||
};
|
|
||||||
|
|
||||||
class MovieCollectionConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMonitorTogglePress = (monitored) => {
|
|
||||||
if (this.props.collectionList) {
|
|
||||||
this.props.setImportListValue({ name: 'enabled', value: monitored });
|
|
||||||
this.props.setImportListValue({ name: 'enableAuto', value: monitored });
|
|
||||||
this.props.saveImportList({ id: this.props.collectionList.id });
|
|
||||||
} else {
|
|
||||||
this.props.selectImportListSchema({ implementation: 'TMDbCollectionImport', presetName: undefined });
|
|
||||||
this.props.setImportListFieldValue({ name: 'collectionId', value: this.props.tmdbId.toString() });
|
|
||||||
this.props.setImportListValue({ name: 'enabled', value: true });
|
|
||||||
this.props.setImportListValue({ name: 'enableAuto', value: true });
|
|
||||||
this.props.setImportListValue({ name: 'name', value: `${this.props.name} - ${this.props.tmdbId}` });
|
|
||||||
this.props.setImportListValue({ name: 'qualityProfileId', value: this.props.qualityProfileId });
|
|
||||||
this.props.setImportListValue({ name: 'monitored', value: this.props.monitored });
|
|
||||||
this.props.setImportListValue({ name: 'minimumAvailability', value: this.props.minimumAvailability });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<MovieCollection
|
|
||||||
{...this.props}
|
|
||||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCollectionConnector.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
movieId: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
collectionList: PropTypes.object,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
qualityProfileId: PropTypes.number.isRequired,
|
|
||||||
minimumAvailability: PropTypes.string.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
selectImportListSchema: PropTypes.func.isRequired,
|
|
||||||
setImportListFieldValue: PropTypes.func.isRequired,
|
|
||||||
setImportListValue: PropTypes.func.isRequired,
|
|
||||||
saveImportList: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionConnector);
|
|
||||||
46
frontend/src/Movie/MovieCollectionLabel.js
Normal file
46
frontend/src/Movie/MovieCollectionLabel.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import styles from './MovieCollectionLabel.css';
|
||||||
|
|
||||||
|
class MovieCollectionLabel extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hasPosterError: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
monitored,
|
||||||
|
onMonitorTogglePress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={15}
|
||||||
|
onPress={onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieCollectionLabel.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
onMonitorTogglePress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieCollectionLabel;
|
||||||
57
frontend/src/Movie/MovieCollectionLabelConnector.js
Normal file
57
frontend/src/Movie/MovieCollectionLabelConnector.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
|
||||||
|
import MovieCollectionLabel from './MovieCollectionLabel';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { tmdbId }) => tmdbId,
|
||||||
|
(state) => state.movieCollections.items,
|
||||||
|
(tmdbId, collections) => {
|
||||||
|
const collection = collections.find((movie) => movie.tmdbId === tmdbId);
|
||||||
|
return {
|
||||||
|
...collection
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleCollectionMonitored
|
||||||
|
};
|
||||||
|
|
||||||
|
class MovieCollectionLabelConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMonitorTogglePress = (monitored) => {
|
||||||
|
this.props.toggleCollectionMonitored({
|
||||||
|
collectionId: this.props.id,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<MovieCollectionLabel
|
||||||
|
{...this.props}
|
||||||
|
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieCollectionLabelConnector.propTypes = {
|
||||||
|
tmdbId: PropTypes.number.isRequired,
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
toggleCollectionMonitored: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionLabelConnector);
|
||||||
@@ -36,3 +36,9 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user