mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-05 13:21:25 -05:00
Compare commits
333 Commits
ending-col
...
update-cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b4a6b1309 | ||
|
|
fcebfe6759 | ||
|
|
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 | ||
|
|
9bb394f420 | ||
|
|
4f512c5cdf | ||
|
|
8ae84222d1 | ||
|
|
7ec0fd1cea | ||
|
|
ad8629fac9 | ||
|
|
32393eabb7 | ||
|
|
6f42dd671f | ||
|
|
966963b53d | ||
|
|
43a74ab32a | ||
|
|
897e4b4d0a | ||
|
|
0dd639ff76 | ||
|
|
64da17a9ab | ||
|
|
dbaabf90a5 | ||
|
|
1aabc4bc5c | ||
|
|
cce4334310 | ||
|
|
8d54b4268b | ||
|
|
2be9b306a3 | ||
|
|
7f5cd9a765 | ||
|
|
d26ca9a7a5 | ||
|
|
933796e214 | ||
|
|
121a347fc9 | ||
|
|
ed060d552f | ||
|
|
51485259ae | ||
|
|
76a3ca37ed | ||
|
|
e5e7292f11 | ||
|
|
397ee20160 | ||
|
|
2914c3c88f | ||
|
|
750cb94692 | ||
|
|
1ffbae9d16 | ||
|
|
a515c5c7a5 | ||
|
|
c58f31d690 | ||
|
|
2041797587 | ||
|
|
b1d8763593 | ||
|
|
ff7f95a0ab | ||
|
|
ccd561eb24 | ||
|
|
3e353a6710 | ||
|
|
544e942fe4 | ||
|
|
8bb3764708 | ||
|
|
356013a84e | ||
|
|
75b2414e29 | ||
|
|
4084ce5c2f | ||
|
|
ca2f12338e | ||
|
|
347fdc1572 | ||
|
|
6c04d0701c | ||
|
|
3f4e5e55a3 | ||
|
|
7310481e6a | ||
|
|
e59a1a64f9 | ||
|
|
5c1ebc72b3 | ||
|
|
a96971beff | ||
|
|
b34f4fde1b | ||
|
|
a9b2635cb0 | ||
|
|
1ed8bd7350 | ||
|
|
f0891e9df7 | ||
|
|
5834afe30b | ||
|
|
c015a3ebf0 | ||
|
|
8fd1f121f4 | ||
|
|
b18cc49a83 | ||
|
|
69b969cfc8 | ||
|
|
bc3e3714b9 | ||
|
|
85dd5f5754 | ||
|
|
7de270b212 | ||
|
|
b1afd7aaaa | ||
|
|
894fafcad7 | ||
|
|
2cf62915b0 | ||
|
|
d3743446da | ||
|
|
b332fa55de | ||
|
|
36da57f87b | ||
|
|
07bb5e416b | ||
|
|
69207ba77b | ||
|
|
ff409d3661 | ||
|
|
dfb8d2ea0f | ||
|
|
603db7c76b | ||
|
|
6fa0cdc9a8 | ||
|
|
c0cbbc7ed4 | ||
|
|
daa9ee30a2 | ||
|
|
86102349c5 | ||
|
|
c4d035f0ad | ||
|
|
95d44f968f | ||
|
|
e7a8f6332c | ||
|
|
b8c92d23f4 | ||
|
|
093e076db0 | ||
|
|
f6f949415c | ||
|
|
ea2576a56c | ||
|
|
595acb696d | ||
|
|
38c9534eac | ||
|
|
9377ef7942 | ||
|
|
c2e5686bcf | ||
|
|
f08807daf6 | ||
|
|
72b3caa72d | ||
|
|
589368781b | ||
|
|
8fd6101121 | ||
|
|
ac9d6cbf0a | ||
|
|
6e0ed36e9f | ||
|
|
fcb65055ef | ||
|
|
90456bbfed | ||
|
|
2a74b7b2e1 | ||
|
|
fc08c39fb8 | ||
|
|
76d65bf990 | ||
|
|
de243991dd | ||
|
|
4d1f251c1f | ||
|
|
ebb1e3131a | ||
|
|
6e502d63c2 | ||
|
|
57e05b70da | ||
|
|
59186adbfc | ||
|
|
bc20e159ba | ||
|
|
39b99341cd | ||
|
|
b626c5bbf0 | ||
|
|
a33b861cec | ||
|
|
3a48f07702 | ||
|
|
1aec0b7ee5 | ||
|
|
3e32161791 | ||
|
|
fda1ad237b | ||
|
|
52b6f39026 | ||
|
|
100fd95dd9 | ||
|
|
d571c7b75a | ||
|
|
8d7f48739b | ||
|
|
c061d7cec8 | ||
|
|
91691205db | ||
|
|
c1e07b30d7 | ||
|
|
78a7770858 | ||
|
|
599f4907f4 | ||
|
|
ec9a7f5c8e | ||
|
|
54c914d48f | ||
|
|
75270d8151 | ||
|
|
7a859f340b | ||
|
|
13e44ce19a | ||
|
|
9e4c94592d | ||
|
|
9d2a59b7fd | ||
|
|
194e0f3d7f | ||
|
|
d1fa92bc6c | ||
|
|
974e44ce48 | ||
|
|
de05be62d7 | ||
|
|
cae5badee0 | ||
|
|
45d8227654 | ||
|
|
7bbd2246c4 | ||
|
|
59fed13442 | ||
|
|
50b273acae | ||
|
|
4278415fd7 | ||
|
|
124b50288d | ||
|
|
3fcc395964 | ||
|
|
0ee9981cba | ||
|
|
2848899206 | ||
|
|
f1a00764cd | ||
|
|
346236764c | ||
|
|
eecd4e4b7d | ||
|
|
2838d8ca29 | ||
|
|
4ebcbc28aa | ||
|
|
2c24f7ca04 | ||
|
|
ec86de78d2 | ||
|
|
4f5f9ff77e | ||
|
|
465bb403a9 | ||
|
|
9e175e28ef | ||
|
|
4d2a311e40 | ||
|
|
b2195148a2 | ||
|
|
2ae7371d73 | ||
|
|
7b03a856c9 | ||
|
|
48d1d47b67 | ||
|
|
906b9bb92a | ||
|
|
6fc14278e6 | ||
|
|
34b269086d | ||
|
|
6c40a27f2e | ||
|
|
be158a09b4 | ||
|
|
eecd746f51 | ||
|
|
5ed034320e | ||
|
|
41dd678dfd | ||
|
|
716eadc551 | ||
|
|
1cb31aa95c | ||
|
|
568dd2fbb2 | ||
|
|
5d091e519e | ||
|
|
faab78c00a | ||
|
|
f1461056ce | ||
|
|
2042ffce62 | ||
|
|
c6ae6f7b1c | ||
|
|
8d7affae68 | ||
|
|
a418111245 | ||
|
|
6359ed5757 | ||
|
|
9a8c1d7d1b | ||
|
|
e89c2ee9f7 | ||
|
|
3d36f88939 | ||
|
|
8e4320a93b | ||
|
|
b095676010 | ||
|
|
41d69d8484 | ||
|
|
073e59e3db | ||
|
|
588a0843a4 | ||
|
|
26cedfd47d | ||
|
|
16789e5b6b | ||
|
|
159edcde94 | ||
|
|
5855773842 | ||
|
|
73f2da72f3 | ||
|
|
7dda481824 | ||
|
|
4d6c3369c6 | ||
|
|
5c7756b575 | ||
|
|
cf8aa09615 | ||
|
|
cdde7d4d8b | ||
|
|
e139708bb2 | ||
|
|
47206dd2bd | ||
|
|
9442666493 | ||
|
|
005ad00caf | ||
|
|
2ae056e727 | ||
|
|
c538424229 | ||
|
|
507e8ec814 | ||
|
|
9d6614b14a | ||
|
|
f9dab9d780 | ||
|
|
18e0656d21 | ||
|
|
759d14cf99 | ||
|
|
1238f60a5e | ||
|
|
2d28828e5e | ||
|
|
9a395b52ac | ||
|
|
e9dffb4819 | ||
|
|
8b93038937 | ||
|
|
c4cf38255e | ||
|
|
1c0621af0a | ||
|
|
dd80a64560 | ||
|
|
beb22844c9 | ||
|
|
399f242f87 | ||
|
|
6befbec381 | ||
|
|
8b8f79d6c3 | ||
|
|
e54d4765dd | ||
|
|
4068cfcabb | ||
|
|
c5b736e422 | ||
|
|
025634cd19 | ||
|
|
9c86c20c00 | ||
|
|
7bf44e2771 | ||
|
|
b18daebc8a | ||
|
|
183d3d0872 | ||
|
|
f1de24ccc8 | ||
|
|
498d9086b5 | ||
|
|
2737937d37 | ||
|
|
3c9e818933 | ||
|
|
a8b563de7b | ||
|
|
823fe2261e | ||
|
|
329d141128 | ||
|
|
d33bed6a36 | ||
|
|
3acc6a3f9d | ||
|
|
8d0a26e284 | ||
|
|
f0f8a4ffaf | ||
|
|
e7ff13085e | ||
|
|
6a8f6dc5f7 | ||
|
|
13c03d9958 | ||
|
|
5ce1829709 | ||
|
|
86da4e87ea | ||
|
|
a219b4a1b8 | ||
|
|
84dd10f032 | ||
|
|
f844cbecaf | ||
|
|
279692f9b1 | ||
|
|
6fcbdc5ba3 |
@@ -19,10 +19,10 @@ indent_size = 4
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:refactoring
|
||||
dotnet_style_qualification_for_property = false:refactoring
|
||||
dotnet_style_qualification_for_method = false:refactoring
|
||||
dotnet_style_qualification_for_event = false:refactoring
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
@@ -32,10 +32,6 @@ csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,4 @@
|
||||
name: Bug Report
|
||||
title: "[BUG]: "
|
||||
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
@@ -64,12 +63,11 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Anything else?
|
||||
label: Trace Logs?
|
||||
description: |
|
||||
Trace Logs (https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files)
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
***Generally speaking, all bug reports must have trace logs provided.***
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,5 +1,4 @@
|
||||
name: Feature Request
|
||||
title: "[FEAT]: "
|
||||
description: 'Suggest an idea for Radarr'
|
||||
labels: ['Type: Feature Request', 'Status: Needs Triage']
|
||||
body:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -187,6 +187,10 @@ packages.config.md5sum
|
||||
**/.idea/**/*.iml
|
||||
**/.idea/**/contentModel.xml
|
||||
**/.idea/**/modules.xml
|
||||
|
||||
# ignore node_modules symlink
|
||||
node_modules
|
||||
node_modules.nosync
|
||||
|
||||
# API doc generation
|
||||
.config/
|
||||
|
||||
43
README.md
43
README.md
@@ -1,19 +1,21 @@
|
||||
# Radarr
|
||||
|
||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/radarr/installation#docker)
|
||||

|
||||
[](#backers)
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
[](#mega-sponsors)
|
||||
|
||||
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
|
||||
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
|
||||
|
||||
## Major Features Include:
|
||||
## Major Features Include
|
||||
|
||||
* Adding new movies with lots of information, such as trailers, ratings, etc.
|
||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
* Can watch for better quality of the movies you have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
||||
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray*
|
||||
* Automatic failed download handling will try another release if one fails
|
||||
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||
* Full integration with SABnzbd and NZBGet
|
||||
@@ -21,53 +23,60 @@ Radarr is a movie collection manager for Usenet and BitTorrent users. It can mon
|
||||
* Automatically importing downloaded movies
|
||||
* Recognizing Special Editions, Director's Cut, etc.
|
||||
* Identifying releases with hardcoded subs
|
||||
* QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported
|
||||
* Full integration with Kodi, Plex (notification, library update)
|
||||
* A beautiful UI
|
||||
* Identifying releases with AKA movie names
|
||||
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
|
||||
* Full integration with Kodi and Plex (notifications, library updates)
|
||||
* Importing Metadata such as trailers or subtitles
|
||||
* Adding metadata such as posters and information for Kodi and others to use
|
||||
* Advanced customization for profiles, such that Radarr will always download the copy you want
|
||||
* A beautiful UI
|
||||
|
||||
## Support
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
[](https://wiki.servarr.com/radarr)
|
||||
[](https://radarr.video/discord)
|
||||
[](https://www.reddit.com/r/Radarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
[](https://github.com/Radarr/Radarr/issues)
|
||||
[](https://wiki.servarr.com/radarr)
|
||||
|
||||
## Contributors & Developers
|
||||
|
||||
[API Documentation](https://radarr.video/docs/api/)
|
||||
|
||||
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||
<a href="https://github.com/Radarr/Radarr/graphs/contributors"><img src="https://opencollective.com/Radarr/contributors.svg?width=890&button=false" /></a>
|
||||
This project exists thanks to all the people who contribute.
|
||||
- [Contribute (GitHub)](CONTRIBUTING.md)
|
||||
- [Contribution (Wiki Article)](https://wiki.servarr.com/radarr/contributing)
|
||||
|
||||
[](https://github.com/Radarr/Radarr/graphs/contributors)
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Radarr#backer)
|
||||
|
||||
<img src="https://opencollective.com/Radarr/backers.svg?width=890"></a>
|
||||
[](https://opencollective.com/Radarr#backer)
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/Radarr#sponsor)
|
||||
|
||||
<img src="https://opencollective.com/Radarr/sponsors.svg?width=890"></a>
|
||||
[](https://opencollective.com/Radarr#sponsor)
|
||||
|
||||
## Mega Sponsors
|
||||
|
||||
<img src="https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890"></a>
|
||||
[](https://opencollective.com/Radarr#mega-sponsor)
|
||||
|
||||
## JetBrains
|
||||
|
||||
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
||||
|
||||
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2021
|
||||
* Copyright 2010-2022
|
||||
|
||||
@@ -7,14 +7,20 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '4.0.0'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '4.2.0'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '5.0.401'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
dotnetVersion: '6.0.300'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
@@ -29,6 +35,7 @@ pr:
|
||||
paths:
|
||||
exclude:
|
||||
- src/NzbDrone.Core/Localization/Core
|
||||
- src/Radarr.Api.*/openapi.json
|
||||
|
||||
stages:
|
||||
- stage: Setup
|
||||
@@ -37,7 +44,7 @@ stages:
|
||||
- job:
|
||||
displayName: Build Variables
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
# Set the build name properly. The 'name' property won't recursively expand so hack here:
|
||||
- bash: echo "##vso[build.updatebuildnumber]$RADARRVERSION"
|
||||
@@ -63,15 +70,15 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
enableAnalysis: 'true'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: ${{ variables.macImage }}
|
||||
enableAnalysis: 'false'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
enableAnalysis: 'false'
|
||||
|
||||
pool:
|
||||
@@ -90,15 +97,14 @@ stages:
|
||||
- bash: |
|
||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||
echo $BUNDLEDVERSIONS
|
||||
grep osx-x64 $BUNDLEDVERSIONS
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "BSD already enabled"
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling BSD support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
displayName: Enable FreeBSD Support
|
||||
- bash: ./build.sh --backend --enable-bsd
|
||||
displayName: Enable Extra Platform Support
|
||||
- bash: ./build.sh --backend --enable-extra-platforms
|
||||
displayName: Build Radarr Backend
|
||||
- bash: |
|
||||
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||
@@ -111,25 +117,29 @@ stages:
|
||||
artifact: '$(osName)Backend'
|
||||
displayName: Publish Backend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/win-x64/publish'
|
||||
artifact: WindowsCoreTests
|
||||
displayName: Publish Windows Test Package
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
artifact: win-x64-tests
|
||||
displayName: Publish win-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/linux-x64/publish'
|
||||
artifact: LinuxCoreTests
|
||||
displayName: Publish Linux Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
artifact: linux-x64-tests
|
||||
displayName: Publish linux-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
|
||||
artifact: LinuxMuslCoreTests
|
||||
displayName: Publish Linux Musl Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||
artifact: linux-x86-tests
|
||||
displayName: Publish linux-x86 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
|
||||
artifact: FreebsdCoreTests
|
||||
displayName: Publish FreeBSD Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
artifact: linux-musl-x64-tests
|
||||
displayName: Publish linux-musl-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
|
||||
artifact: MacCoreTests
|
||||
displayName: Publish MacOS Test Package
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
artifact: freebsd-x64-tests
|
||||
displayName: Publish freebsd-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
artifact: osx-x64-tests
|
||||
displayName: Publish osx-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
|
||||
- stage: Build_Frontend
|
||||
@@ -141,20 +151,20 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: ${{ variables.macImage }}
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -184,7 +194,7 @@ stages:
|
||||
- job: Windows_Installer
|
||||
displayName: Create Installer
|
||||
pool:
|
||||
vmImage: 'windows-2019'
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
@@ -200,16 +210,11 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x86
|
||||
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
- bash: |
|
||||
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x64
|
||||
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
./build.sh --packages --installer
|
||||
cp setup/output/Radarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
cp setup/output/Radarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create Installers
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'WindowsInstaller'
|
||||
displayName: Publish Installer
|
||||
@@ -222,7 +227,7 @@ stages:
|
||||
- job: Other_Packages
|
||||
displayName: Create Standard Packages
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
@@ -238,89 +243,121 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages --enable-bsd
|
||||
- bash: ./build.sh --packages --enable-extra-platforms
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
find . -name "ffprobe" -exec chmod a+x {} \;
|
||||
find . -name "Radarr" -exec chmod a+x {} \;
|
||||
find . -name "Radarr.Update" -exec chmod a+x {} \;
|
||||
displayName: Set executable bits
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows Core zip
|
||||
displayName: Create win-x64 zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows x86 Core zip
|
||||
displayName: Create win-x86 zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS Core app
|
||||
displayName: Create osx-x64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS Core tar
|
||||
displayName: Create osx-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/macos/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Core tar
|
||||
displayName: Create osx-arm64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create osx-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Musl Core tar
|
||||
displayName: Create linux-musl-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Core tar
|
||||
displayName: Create linux-x86 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/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:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Core tar
|
||||
displayName: Create linux-musl-arm tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Musl Core tar
|
||||
displayName: Create linux-musl-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create FreeBSD Core Core tar
|
||||
displayName: Create freebsd-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).freebsd-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'Packages'
|
||||
displayName: Publish Packages
|
||||
@@ -359,7 +396,7 @@ stages:
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
@@ -381,22 +418,22 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
testName: 'osx-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'macos-10.14'
|
||||
imageName: ${{ variables.macImage }}
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
testName: 'win-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
testName: 'linux-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
FreebsdCore:
|
||||
osName: 'Linux'
|
||||
testName: 'FreebsdCore'
|
||||
testName: 'freebsd-x64'
|
||||
poolName: 'FreeBSD'
|
||||
imageName:
|
||||
|
||||
@@ -415,18 +452,15 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: '$(testName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- bash: |
|
||||
wget https://mediaarea.net/repo/deb/repo-mediaarea_1.0-11_all.deb
|
||||
sudo dpkg -i repo-mediaarea_1.0-11_all.deb
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --allow-unauthenticated libmediainfo-dev libmediainfo0v5 mediainfo
|
||||
displayName: Install mediainfo
|
||||
condition: and(succeeded(), eq(variables['testName'], 'LinuxCore'))
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
chmod a+x _tests/ffprobe
|
||||
displayName: Make ffprobe Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
@@ -452,11 +486,15 @@ stages:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
@@ -464,9 +502,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -474,6 +518,9 @@ stages:
|
||||
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'))
|
||||
@@ -489,6 +536,61 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
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: 'ubuntu-18.04'
|
||||
|
||||
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 \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
ls -lR ${TESTSFOLDER}
|
||||
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||
displayName: Run Tests
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration
|
||||
@@ -497,7 +599,7 @@ stages:
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
@@ -516,18 +618,18 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
imageName: 'macos-10.14'
|
||||
testName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
imageName: 'windows-2019'
|
||||
testName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Radarr.*.windows-core-x64.zip'
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
imageName: 'ubuntu-18.04'
|
||||
testName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||
|
||||
pool:
|
||||
@@ -543,7 +645,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: '$(testName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -573,6 +675,67 @@ stages:
|
||||
failTaskOnFailedTests: true
|
||||
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: 'ubuntu-18.04'
|
||||
|
||||
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 \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||
displayName: Run Integration Tests
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_FreeBSD
|
||||
displayName: Integration Native FreeBSD
|
||||
dependsOn: Prepare
|
||||
@@ -590,7 +753,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'FreebsdCoreTests'
|
||||
artifactName: 'freebsd-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -626,12 +789,17 @@ stages:
|
||||
strategy:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
testName: 'linux-musl-x64'
|
||||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
pattern: '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:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
@@ -639,9 +807,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -687,17 +861,20 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
artifactName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||
failBuild: true
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
artifactName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||
failBuild: true
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
artifactName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Radarr.*.windows-core-x64.zip'
|
||||
failBuild: true
|
||||
|
||||
@@ -714,7 +891,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(osName)CoreTests'
|
||||
artifactName: '$(artifactName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -763,7 +940,7 @@ stages:
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
@@ -780,17 +957,17 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -812,7 +989,7 @@ stages:
|
||||
displayName: Frontend
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@1
|
||||
@@ -828,6 +1005,60 @@ stages:
|
||||
cliProjectVersion: '$(radarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@1
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
dependsOn: Prepare
|
||||
condition: |
|
||||
and
|
||||
(
|
||||
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
|
||||
and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
)
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
persistCredentials: true
|
||||
fetchDepth: 1
|
||||
- bash: ./docs.sh Windows
|
||||
displayName: Create openapi.json
|
||||
- bash: |
|
||||
git config --global user.email "development@lidarr.audio"
|
||||
git config --global user.name "Servarr"
|
||||
git checkout -b api-docs
|
||||
git add .
|
||||
git status
|
||||
if git status | grep modified
|
||||
then
|
||||
git commit -am 'Automated API Docs update'
|
||||
git push -f --set-upstream origin api-docs
|
||||
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/radarr/radarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
|
||||
else
|
||||
echo "No changes since last run"
|
||||
fi
|
||||
displayName: Commit API Doc Change
|
||||
continueOnError: true
|
||||
env:
|
||||
GITHUBTOKEN: $(githubToken)
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy openapi.json to: $(Build.ArtifactStagingDirectory)'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)'
|
||||
Contents: |
|
||||
**/*openapi.json
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/api_docs'
|
||||
- publish: $(Build.ArtifactStagingDirectory)/api_docs
|
||||
artifact: 'APIDocs'
|
||||
displayName: Publish API Docs Bundle
|
||||
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
|
||||
|
||||
- job: Analyze_Backend
|
||||
displayName: Backend
|
||||
@@ -839,7 +1070,7 @@ stages:
|
||||
EnableAnalyzers: 'false'
|
||||
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
@@ -865,8 +1096,8 @@ stages:
|
||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||
- bash: |
|
||||
./build.sh --backend -f net5.0 -r win-x64
|
||||
TEST_DIR=_tests/net5.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
./build.sh --backend -f net6.0 -r win-x64
|
||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
@@ -896,7 +1127,7 @@ stages:
|
||||
- job:
|
||||
displayName: Discord Notification
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
continueOnError: true
|
||||
|
||||
137
build.sh
137
build.sh
@@ -25,14 +25,22 @@ UpdateVersionNumber()
|
||||
fi
|
||||
}
|
||||
|
||||
EnableBsdSupport()
|
||||
EnableExtraPlatformsInSDK()
|
||||
{
|
||||
#todo enable sdk with
|
||||
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
}
|
||||
|
||||
EnableExtraPlatforms()
|
||||
{
|
||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -129,7 +137,7 @@ PackageLinux()
|
||||
|
||||
echo "Adding Radarr.Mono to UpdatePackage"
|
||||
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
||||
if [ "$framework" = "net5.0" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
||||
fi
|
||||
@@ -140,12 +148,13 @@ PackageLinux()
|
||||
PackageMacOS()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating MacOS Package for $framework"
|
||||
ProgressStart "Creating MacOS Package for $framework $runtime"
|
||||
|
||||
local folder=$artifactsFolder/macos/$framework/Radarr
|
||||
local folder=$artifactsFolder/$runtime/$framework/Radarr
|
||||
|
||||
PackageFiles "$folder" "$framework" "osx-x64"
|
||||
PackageFiles "$folder" "$framework" "$runtime"
|
||||
|
||||
echo "Removing Service helpers"
|
||||
rm -f $folder/ServiceUninstall.*
|
||||
@@ -156,7 +165,7 @@ PackageMacOS()
|
||||
|
||||
echo "Adding Radarr.Mono to UpdatePackage"
|
||||
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
||||
if [ "$framework" = "net5.0" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
||||
fi
|
||||
@@ -167,10 +176,11 @@ PackageMacOS()
|
||||
PackageMacOSApp()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating macOS App Package for $framework"
|
||||
ProgressStart "Creating macOS App Package for $framework $runtime"
|
||||
|
||||
local folder=$artifactsFolder/macos-app/$framework
|
||||
local folder="$artifactsFolder/$runtime-app/$framework"
|
||||
|
||||
rm -rf $folder
|
||||
mkdir -p $folder
|
||||
@@ -178,7 +188,7 @@ PackageMacOSApp()
|
||||
mkdir -p $folder/Radarr.app/Contents/MacOS
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $artifactsFolder/macos/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
|
||||
cp -r $artifactsFolder/$runtime/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $folder/Radarr.app/Contents/MacOS/Radarr.Update
|
||||
@@ -225,12 +235,38 @@ Package()
|
||||
PackageWindows "$framework" "$runtime"
|
||||
;;
|
||||
osx)
|
||||
PackageMacOS "$framework"
|
||||
PackageMacOSApp "$framework"
|
||||
PackageMacOS "$framework" "$runtime"
|
||||
PackageMacOSApp "$framework" "$runtime"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
BuildInstaller()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
./_inno/ISCC.exe setup/radarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
|
||||
}
|
||||
|
||||
InstallInno()
|
||||
{
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
ProgressEnd "Installed portable Inno Setup"
|
||||
}
|
||||
|
||||
RemoveInno()
|
||||
{
|
||||
rm -rf _inno
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
{
|
||||
local framework="$1"
|
||||
@@ -262,8 +298,10 @@ if [ $# -eq 0 ]; then
|
||||
BACKEND=YES
|
||||
FRONTEND=YES
|
||||
PACKAGES=YES
|
||||
INSTALLER=NO
|
||||
LINT=YES
|
||||
ENABLE_BSD=NO
|
||||
ENABLE_EXTRA_PLATFORMS=NO
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
@@ -275,8 +313,12 @@ case $key in
|
||||
BACKEND=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-bsd)
|
||||
ENABLE_BSD=YES
|
||||
--enable-bsd|--enable-extra-platforms)
|
||||
ENABLE_EXTRA_PLATFORMS=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-extra-platforms-in-sdk)
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
|
||||
shift # past argument
|
||||
;;
|
||||
-r|--runtime)
|
||||
@@ -297,6 +339,10 @@ case $key in
|
||||
PACKAGES=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--installer)
|
||||
INSTALLER=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--lint)
|
||||
LINT=YES
|
||||
shift # past argument
|
||||
@@ -316,24 +362,30 @@ esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
|
||||
then
|
||||
EnableExtraPlatformsInSDK
|
||||
fi
|
||||
|
||||
if [ "$BACKEND" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
EnableBsdSupport
|
||||
EnableExtraPlatforms
|
||||
fi
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
PackageTests "net5.0" "win-x64"
|
||||
PackageTests "net5.0" "win-x86"
|
||||
PackageTests "net5.0" "linux-x64"
|
||||
PackageTests "net5.0" "linux-musl-x64"
|
||||
PackageTests "net5.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
PackageTests "net6.0" "win-x64"
|
||||
PackageTests "net6.0" "win-x86"
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
PackageTests "net5.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
@@ -362,19 +414,30 @@ then
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
Package "net5.0" "win-x64"
|
||||
Package "net5.0" "win-x86"
|
||||
Package "net5.0" "linux-x64"
|
||||
Package "net5.0" "linux-musl-x64"
|
||||
Package "net5.0" "linux-arm64"
|
||||
Package "net5.0" "linux-musl-arm64"
|
||||
Package "net5.0" "linux-arm"
|
||||
Package "net5.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
Package "net6.0" "win-x64"
|
||||
Package "net6.0" "win-x86"
|
||||
Package "net6.0" "linux-x64"
|
||||
Package "net6.0" "linux-musl-x64"
|
||||
Package "net6.0" "linux-arm64"
|
||||
Package "net6.0" "linux-musl-arm64"
|
||||
Package "net6.0" "linux-arm"
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
Package "net5.0" "freebsd-x64"
|
||||
Package "net6.0" "freebsd-x64"
|
||||
Package "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$INSTALLER" = "YES" ];
|
||||
then
|
||||
InstallInno
|
||||
BuildInstaller "net6.0" "win-x64"
|
||||
BuildInstaller "net6.0" "win-x86"
|
||||
RemoveInno
|
||||
fi
|
||||
|
||||
38
docs.sh
Normal file
38
docs.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
PLATFORM=$1
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-x64"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
WHERE="linux-x64"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
WHERE="osx-x64"
|
||||
else
|
||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
outputFolder='_output'
|
||||
testPackageFolder='_tests'
|
||||
|
||||
rm -rf $outputFolder
|
||||
rm -rf $testPackageFolder
|
||||
|
||||
slnFile=src/Radarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
|
||||
|
||||
sleep 45
|
||||
|
||||
kill %1
|
||||
|
||||
exit 0
|
||||
@@ -223,7 +223,7 @@ module.exports = (env) => {
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10240,
|
||||
limit: 24096,
|
||||
mimetype: 'application/font-woff',
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
@@ -233,11 +233,12 @@ module.exports = (env) => {
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
test: /\.(ttf|eot|eot?#iefix|gif|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
limit: 24096,
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
|
||||
@@ -61,33 +61,33 @@ class Blocklist extends Component {
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = () => {
|
||||
this.props.onRemoveSelected(this.getSelectedIds());
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -120,7 +120,7 @@ class Blocklist extends Component {
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Remove Selected"
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={!selectedIds.length}
|
||||
isSpinning={isRemoving}
|
||||
|
||||
@@ -65,37 +65,37 @@ class BlocklistConnector extends Component {
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchBlocklist();
|
||||
}
|
||||
};
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoBlocklistPreviousPage();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoBlocklistNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoBlocklistLastPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoBlocklistPage({ page });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlocklistItems({ ids });
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlocklistSort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlocklistTableOption(payload);
|
||||
@@ -103,11 +103,11 @@ class BlocklistConnector extends Component {
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -32,11 +32,11 @@ class BlocklistRow extends Component {
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -25,6 +25,7 @@ function HistoryDetails(props) {
|
||||
releaseGroup,
|
||||
nzbInfoUrl,
|
||||
downloadClient,
|
||||
downloadClientName,
|
||||
downloadId,
|
||||
age,
|
||||
ageHours,
|
||||
@@ -32,6 +33,8 @@ function HistoryDetails(props) {
|
||||
publishedDate
|
||||
} = data;
|
||||
|
||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
@@ -71,11 +74,12 @@ function HistoryDetails(props) {
|
||||
}
|
||||
|
||||
{
|
||||
!!downloadClient &&
|
||||
downloadClientNameInfo ?
|
||||
<DescriptionListItem
|
||||
title={translate('DownloadClient')}
|
||||
data={downloadClient}
|
||||
/>
|
||||
data={downloadClientNameInfo}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -57,38 +57,38 @@ class HistoryConnector extends Component {
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchHistory();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoHistoryPreviousPage();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoHistoryNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoHistoryLastPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoHistoryPage({ page });
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setHistorySort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setHistoryFilter({ selectedFilterKey });
|
||||
}
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setHistoryTableOption(payload);
|
||||
@@ -96,7 +96,7 @@ class HistoryConnector extends Component {
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -41,11 +41,11 @@ class HistoryRow extends Component {
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -179,6 +179,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -46,7 +46,7 @@ class HistoryRowConnector extends Component {
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.props.markAsFailed({ id: this.props.id });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -90,45 +90,45 @@ class Queue extends Component {
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
||||
this._shouldBlockRefresh = isOpen;
|
||||
}
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onGrabSelectedPress = () => {
|
||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
||||
this._shouldBlockRefresh = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -282,6 +282,17 @@ class Queue extends Component {
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
@@ -77,34 +77,34 @@ class QueueConnector extends Component {
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchQueue();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoQueuePreviousPage();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoQueueNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoQueueLastPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoQueuePage({ page });
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
@@ -112,21 +112,21 @@ class QueueConnector extends Component {
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onGrabSelectedPress = (ids) => {
|
||||
this.props.grabQueueItems({ ids });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -42,7 +42,7 @@ class QueueOptions extends Component {
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -40,7 +40,7 @@ class QueueRow extends Component {
|
||||
|
||||
onRemoveQueueItemPress = () => {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blocklist) => {
|
||||
const {
|
||||
@@ -52,25 +52,25 @@ class QueueRow extends Component {
|
||||
onRemoveQueueItemPress(blocklist);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(true);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -332,6 +332,7 @@ class QueueRow extends Component {
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!movie}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
|
||||
@@ -37,11 +37,11 @@ class QueueRowConnector extends Component {
|
||||
|
||||
onGrabPress = () => {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -34,30 +34,30 @@ class RemoveQueueItemModal extends Component {
|
||||
remove: true,
|
||||
blocklist: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -66,7 +66,8 @@ class RemoveQueueItemModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist } = this.state;
|
||||
@@ -89,18 +90,22 @@ class RemoveQueueItemModal extends Component {
|
||||
{translate('RemoveFromQueueText', [sourceTitle])}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
||||
@@ -137,6 +142,7 @@ RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -30,35 +30,35 @@ class RemoveQueueItemsModal extends Component {
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false
|
||||
});
|
||||
}
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -67,7 +67,8 @@ class RemoveQueueItemsModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist } = this.state;
|
||||
@@ -82,30 +83,34 @@ class RemoveQueueItemsModal extends Component {
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Remove Selected Item{selectedCount > 1 ? 's' : ''}
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', [selectedCount, selectedCount > 1 ? 's' : ''])}
|
||||
{selectedCount > 1 ? translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', selectedCount) : translate('AreYouSureYouWantToRemoveSelectedItemFromQueue')}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Blocklist Release{selectedCount > 1 ? 's' : ''}
|
||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -141,6 +146,7 @@ RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -67,12 +67,12 @@ class AddNewMovie extends Component {
|
||||
this.props.onClearMovieLookup();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onClearMovieLookupPress = () => {
|
||||
this.setState({ term: '' });
|
||||
this.props.onClearMovieLookup();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -161,7 +161,7 @@ class AddNewMovie extends Component {
|
||||
{translate('YouCanAlsoSearch')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to="https://wiki.servarr.com/radarr/faq#why-cant-i-add-a-new-movie-to-radarr">
|
||||
<Link to="https://wiki.servarr.com/radarr/faq#why-can-i-not-add-a-new-movie-to-radarr">
|
||||
{translate('CantFindMovie')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -79,11 +79,11 @@ class AddNewMovieConnector extends Component {
|
||||
this.props.lookupMovie({ term });
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClearMovieLookup = () => {
|
||||
this.props.clearAddMovie();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -22,11 +22,11 @@ class AddNewMovieModalContent extends Component {
|
||||
|
||||
onQualityProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||
}
|
||||
};
|
||||
|
||||
onAddMoviePress = () => {
|
||||
this.props.onAddMoviePress();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -51,7 +51,7 @@ class AddNewMovieModalContentConnector extends Component {
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setAddMovieDefault({ [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
onAddMoviePress = () => {
|
||||
const {
|
||||
@@ -73,7 +73,7 @@ class AddNewMovieModalContentConnector extends Component {
|
||||
searchForMovie: searchForMovie.value,
|
||||
tags: tags.value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -39,15 +39,15 @@ class AddNewMovieSearchResult extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onAddMovieModalClose = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onExternalLinkPress = (event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -86,6 +86,13 @@ class AddNewMovieSearchResult extends Component {
|
||||
} = this.state;
|
||||
|
||||
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
|
||||
const posterWidth = 167;
|
||||
const posterHeight = 250;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.searchResult}>
|
||||
@@ -102,6 +109,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
<div className={styles.posterContainer}>
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
overflow={true}
|
||||
@@ -114,7 +122,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
monitored={monitored}
|
||||
hasFile={hasFile}
|
||||
status={status}
|
||||
posterWidth={167}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={true}
|
||||
queueStatus={queueStatus}
|
||||
queueState={queueState}
|
||||
@@ -183,7 +191,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
<div>
|
||||
<Label size={sizes.LARGE}>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
ratings={ratings}
|
||||
iconSize={13}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
@@ -32,25 +32,25 @@ class ImportMovie extends Component {
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState, { parseIds: false });
|
||||
}
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
// Only select non-dupes
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedStateItem = (id) => {
|
||||
this.setState((state) => {
|
||||
@@ -62,15 +62,15 @@ class ImportMovie extends Component {
|
||||
selectedState
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onInputChange(this.getSelectedIds(), name, value);
|
||||
}
|
||||
};
|
||||
|
||||
onImportPress = () => {
|
||||
this.props.onImportPress(this.getSelectedIds());
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -112,11 +112,11 @@ class ImportMovieConnector extends Component {
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onImportPress = (ids) => {
|
||||
this.props.dispatchImportMovie({ ids });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -83,7 +83,7 @@ class ImportMovieFooter extends Component {
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
this.props.onInputChange({ name, value });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -48,7 +48,7 @@ class ImportMovieRowConnector extends Component {
|
||||
id: this.props.id,
|
||||
[name]: value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -121,7 +121,7 @@ class ImportMovieTable extends Component {
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -11,7 +11,7 @@ class ImportMovieSearchResult extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.tmdbId);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -69,7 +69,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
this.setState({ isOpen: false });
|
||||
this._removeListener();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
if (this.state.isOpen) {
|
||||
@@ -79,7 +79,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
}
|
||||
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
};
|
||||
|
||||
onSearchInputChange = ({ value }) => {
|
||||
if (this._movieLookupTimeout) {
|
||||
@@ -91,17 +91,17 @@ class ImportMovieSelectMovie extends Component {
|
||||
this.props.onSearchInputChange(value);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.onSearchInputChange(this.state.term);
|
||||
}
|
||||
};
|
||||
|
||||
onMovieSelect = (tmdbId) => {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onMovieSelect(tmdbId);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -36,7 +36,7 @@ class ImportMovieSelectMovieConnector extends Component {
|
||||
term,
|
||||
topOfQueue: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMovieSelect = (tmdbId) => {
|
||||
const {
|
||||
@@ -48,7 +48,7 @@ class ImportMovieSelectMovieConnector extends Component {
|
||||
id,
|
||||
selectedMovie: _.find(items, { tmdbId })
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -25,7 +25,7 @@ class ImportMovieRootFolderRowConnector extends Component {
|
||||
|
||||
onDeletePress = () => {
|
||||
this.props.deleteRootFolder({ id: this.props.id });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -55,15 +55,15 @@ class ImportMovieSelectFolder extends Component {
|
||||
|
||||
onAddNewRootFolderPress = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onNewRootFolderSelect = ({ value }) => {
|
||||
this.props.onNewRootFolderSelect(value);
|
||||
}
|
||||
};
|
||||
|
||||
onAddRootFolderModalClose = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -58,11 +58,11 @@ class ImportMovieSelectFolderConnector extends Component {
|
||||
|
||||
onNewRootFolderSelect = (path) => {
|
||||
this.props.addRootFolder({ path });
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteRootFolderPress = (id) => {
|
||||
this.props.deleteRootFolder({ id });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title="Radarr">
|
||||
<DocumentTitle title={window.Radarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
|
||||
@@ -7,6 +7,7 @@ import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
import CollectionConnector from 'Collection/CollectionConnector';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
||||
@@ -72,6 +73,11 @@ function AppRoutes(props) {
|
||||
component={AddNewMovieConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/collections"
|
||||
component={CollectionConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/add/import"
|
||||
component={ImportMovies}
|
||||
|
||||
@@ -89,12 +89,12 @@ function AppUpdatedModalContent(props) {
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('New')}
|
||||
changes={update.changes.new}
|
||||
changes={Array.from(new Set(update.changes.new))}
|
||||
/>
|
||||
|
||||
<UpdateChanges
|
||||
title={translate('Fixed')}
|
||||
changes={update.changes.fixed}
|
||||
changes={Array.from(new Set(update.changes.fixed))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ class AgendaEvent extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -119,43 +119,43 @@ class CalendarConnector extends Component {
|
||||
|
||||
this.props.fetchQueueDetails({ time, view });
|
||||
this.props.fetchCalendar({ time, view });
|
||||
}
|
||||
};
|
||||
|
||||
scheduleUpdate = () => {
|
||||
this.clearUpdateTimeout();
|
||||
|
||||
this.updateTimeoutId = setTimeout(this.updateCalendar, UPDATE_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
clearUpdateTimeout = () => {
|
||||
if (this.updateTimeoutId) {
|
||||
clearTimeout(this.updateTimeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateCalendar = () => {
|
||||
this.props.gotoCalendarToday();
|
||||
this.scheduleUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCalendarViewChange = (view) => {
|
||||
this.props.setCalendarView({ view });
|
||||
}
|
||||
};
|
||||
|
||||
onTodayPress = () => {
|
||||
this.props.gotoCalendarToday();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPress = () => {
|
||||
this.props.gotoCalendarPreviousRange();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPress = () => {
|
||||
this.props.gotoCalendarNextRange();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -44,23 +44,23 @@ class CalendarPage extends Component {
|
||||
const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH)));
|
||||
|
||||
this.props.onDaysCountChange(days);
|
||||
}
|
||||
};
|
||||
|
||||
onGetCalendarLinkPress = () => {
|
||||
this.setState({ isCalendarLinkModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onGetCalendarLinkModalClose = () => {
|
||||
this.setState({ isCalendarLinkModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onOptionsPress = () => {
|
||||
this.setState({ isOptionsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onOptionsModalClose = () => {
|
||||
this.setState({ isOptionsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onSearchMissingPress = () => {
|
||||
const {
|
||||
@@ -69,7 +69,7 @@ class CalendarPage extends Component {
|
||||
} = this.props;
|
||||
|
||||
onSearchMissingPress(missingMovieIds);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -60,20 +60,20 @@ class CalendarDays extends Component {
|
||||
this.setState({ todaysDate: todaysDate.toISOString() });
|
||||
|
||||
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
|
||||
}
|
||||
};
|
||||
|
||||
clearUpdateTimeout = () => {
|
||||
if (this.updateTimeoutId) {
|
||||
clearTimeout(this.updateTimeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEventModalOpenToggle = (isEventModalOpen) => {
|
||||
this.setState({ isEventModalOpen });
|
||||
}
|
||||
};
|
||||
|
||||
onTouchStart = (event) => {
|
||||
const touches = event.touches;
|
||||
@@ -92,7 +92,7 @@ class CalendarDays extends Component {
|
||||
}
|
||||
|
||||
this._touchStart = touchStart;
|
||||
}
|
||||
};
|
||||
|
||||
onTouchEnd = (event) => {
|
||||
const touches = event.changedTouches;
|
||||
@@ -109,17 +109,17 @@ class CalendarDays extends Component {
|
||||
}
|
||||
|
||||
this._touchStart = null;
|
||||
}
|
||||
};
|
||||
|
||||
onTouchCancel = (event) => {
|
||||
this._touchStart = null;
|
||||
}
|
||||
};
|
||||
|
||||
onTouchMove = (event) => {
|
||||
if (!this._touchStart) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -47,13 +47,13 @@ class DaysOfWeek extends Component {
|
||||
});
|
||||
|
||||
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
|
||||
}
|
||||
};
|
||||
|
||||
clearUpdateTimeout = () => {
|
||||
if (this.updateTimeoutId) {
|
||||
clearTimeout(this.updateTimeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -43,15 +43,15 @@ class CalendarEvent extends Component {
|
||||
const link = `/movie/${titleSlug}`;
|
||||
const eventType = [];
|
||||
|
||||
if (moment(date).isSame(moment(inCinemas), 'day')) {
|
||||
if (inCinemas && moment(date).isSame(moment(inCinemas), 'day')) {
|
||||
eventType.push('Cinemas');
|
||||
}
|
||||
|
||||
if (moment(date).isSame(moment(physicalRelease), 'day')) {
|
||||
if (physicalRelease && moment(date).isSame(moment(physicalRelease), 'day')) {
|
||||
eventType.push('Physical');
|
||||
}
|
||||
|
||||
if (moment(date).isSame(moment(digitalRelease), 'day')) {
|
||||
if (digitalRelease && moment(date).isSame(moment(digitalRelease), 'day')) {
|
||||
eventType.push('Digital');
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class CalendarHeader extends Component {
|
||||
this.setState({ view }, () => {
|
||||
this.props.onViewChange(view);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -41,19 +41,19 @@ class CalendarHeaderConnector extends Component {
|
||||
|
||||
onViewChange = (view) => {
|
||||
this.props.setCalendarView({ view });
|
||||
}
|
||||
};
|
||||
|
||||
onTodayPress = () => {
|
||||
this.props.gotoCalendarToday();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPress = () => {
|
||||
this.props.gotoCalendarPreviousRange();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPress = () => {
|
||||
this.props.gotoCalendarNextRange();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -12,7 +12,7 @@ class CalendarHeaderViewButton extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.view);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -69,7 +69,7 @@ class CalendarOptionsModalContent extends Component {
|
||||
} = this.props;
|
||||
|
||||
dispatchSetCalendarOption({ [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
onGlobalInputChange = ({ name, value }) => {
|
||||
const {
|
||||
@@ -81,11 +81,11 @@ class CalendarOptionsModalContent extends Component {
|
||||
this.setState(setting, () => {
|
||||
dispatchSaveUISettings(setting);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onLinkFocus = (event) => {
|
||||
event.target.select();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -84,11 +84,11 @@ class CalendarLinkModalContent extends Component {
|
||||
[name]: value,
|
||||
...urls
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onLinkFocus = (event) => {
|
||||
event.target.select();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
163
frontend/src/Collection/CollectionFooter.js
Normal file
163
frontend/src/Collection/CollectionFooter.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
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,
|
||||
destinationRootFolder: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isSaving,
|
||||
saveError
|
||||
} = this.props;
|
||||
|
||||
const newState = {};
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
monitored: NO_CHANGE,
|
||||
monitor: NO_CHANGE
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isEmpty(newState)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
};
|
||||
|
||||
onUpdateSelectedPress = () => {
|
||||
const {
|
||||
monitor,
|
||||
monitored
|
||||
} = this.state;
|
||||
|
||||
const changes = {};
|
||||
|
||||
if (monitored !== NO_CHANGE) {
|
||||
changes.monitored = monitored === 'monitored';
|
||||
}
|
||||
|
||||
if (monitor !== NO_CHANGE) {
|
||||
changes.monitor = monitor;
|
||||
}
|
||||
|
||||
this.props.onUpdateSelectedPress(changes);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedIds,
|
||||
isSaving
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitor
|
||||
} = 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.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('Folder')}</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
340
frontend/src/Collection/Overview/CollectionOverview.js
Normal file
340
frontend/src/Collection/Overview/CollectionOverview.js
Normal file
@@ -0,0 +1,340 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Slider from 'react-slick';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
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 'slick-carousel/slick/slick.css';
|
||||
import 'slick-carousel/slick/slick-theme.css';
|
||||
|
||||
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
|
||||
|
||||
setSliderRef = (ref) => {
|
||||
this.setState({ slider: 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;
|
||||
|
||||
const sliderSettings = {
|
||||
arrows: false,
|
||||
dots: false,
|
||||
infinite: false,
|
||||
slidesToShow: 1,
|
||||
slidesToScroll: 1,
|
||||
variableWidth: true
|
||||
};
|
||||
|
||||
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}>
|
||||
<IconButton
|
||||
name={icons.ARROW_LEFT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickPrev}
|
||||
size={20}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.ARROW_RIGHT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickNext}
|
||||
size={20}
|
||||
/>
|
||||
</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}>
|
||||
<Slider ref={this.setSliderRef} {...sliderSettings}>
|
||||
{movies.map((movie) => (
|
||||
<div className={styles.movie} key={movie.tmdbId}>
|
||||
<CollectionMovieConnector
|
||||
key={movie.tmdbId}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
collectionId={id}
|
||||
{...movie}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Slider>
|
||||
</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 MISSING_MOVIES_SEARCH = 'MissingMoviesSearch';
|
||||
export const MOVE_MOVIE = 'MoveMovie';
|
||||
export const REFRESH_COLLECTIONS = 'RefreshCollections';
|
||||
export const REFRESH_MOVIE = 'RefreshMovie';
|
||||
export const RENAME_FILES = 'RenameFiles';
|
||||
export const RENAME_MOVIE = 'RenameMovie';
|
||||
|
||||
@@ -16,4 +16,9 @@
|
||||
color: #3a3f51;
|
||||
font-size: 21px;
|
||||
line-height: inherit;
|
||||
|
||||
&.small {
|
||||
color: #909293;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import styles from './FieldSet.css';
|
||||
|
||||
class FieldSet extends Component {
|
||||
@@ -9,13 +11,14 @@ class FieldSet extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
size,
|
||||
legend,
|
||||
children
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<fieldset className={styles.fieldSet}>
|
||||
<legend className={styles.legend}>
|
||||
<legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
|
||||
{legend}
|
||||
</legend>
|
||||
{children}
|
||||
@@ -26,8 +29,13 @@ class FieldSet extends Component {
|
||||
}
|
||||
|
||||
FieldSet.propTypes = {
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
FieldSet.defaultProps = {
|
||||
size: sizes.MEDIUM
|
||||
};
|
||||
|
||||
export default FieldSet;
|
||||
|
||||
@@ -70,18 +70,18 @@ class FileBrowserModalContent extends Component {
|
||||
} else {
|
||||
this._scrollerNode = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPathInputChange = ({ value }) => {
|
||||
this.setState({ currentPath: value });
|
||||
}
|
||||
};
|
||||
|
||||
onRowPress = (path) => {
|
||||
this.props.onFetchPaths(path);
|
||||
}
|
||||
};
|
||||
|
||||
onOkPress = () => {
|
||||
this.props.onChange({
|
||||
@@ -91,7 +91,7 @@ class FileBrowserModalContent extends Component {
|
||||
|
||||
this.props.onClearPaths();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -78,16 +78,16 @@ class FileBrowserModalContentConnector extends Component {
|
||||
allowFoldersWithoutTrailingSlashes: true,
|
||||
includeFiles
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onClearPaths = () => {
|
||||
// this.props.dispatchClearPaths();
|
||||
}
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPaths();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -28,7 +28,7 @@ class FileBrowserRow extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.path);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -103,7 +103,7 @@ class DateFilterBuilderRowValue extends Component {
|
||||
name: NAME,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onTimeChange = ({ value }) => {
|
||||
const {
|
||||
@@ -118,7 +118,7 @@ class DateFilterBuilderRowValue extends Component {
|
||||
value: filterValue.value
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -161,6 +161,7 @@ class DateFilterBuilderRowValue extends Component {
|
||||
<TextInput
|
||||
name={NAME}
|
||||
value={filterValue}
|
||||
type="date"
|
||||
placeholder="yyyy-mm-dd"
|
||||
onChange={this.onValueChange}
|
||||
/>
|
||||
|
||||
@@ -63,7 +63,7 @@ class FilterBuilderModalContent extends Component {
|
||||
|
||||
onLabelChange = ({ value }) => {
|
||||
this.setState({ label: value });
|
||||
}
|
||||
};
|
||||
|
||||
onFilterChange = (index, filter) => {
|
||||
const filters = [...this.state.filters];
|
||||
@@ -72,7 +72,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onAddFilterPress = () => {
|
||||
const filters = [...this.state.filters];
|
||||
@@ -81,7 +81,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveFilterPress = (index) => {
|
||||
const filters = [...this.state.filters];
|
||||
@@ -90,7 +90,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSaveFilterPress = () => {
|
||||
const {
|
||||
@@ -122,7 +122,7 @@ class FilterBuilderModalContent extends Component {
|
||||
label,
|
||||
filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -166,7 +166,9 @@ class FilterBuilderModalContent extends Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.label}>Filters</div>
|
||||
<div className={styles.label}>
|
||||
{translate('Filters')}
|
||||
</div>
|
||||
|
||||
<div className={styles.rows}>
|
||||
{
|
||||
|
||||
@@ -154,7 +154,7 @@ class FilterBuilderRow extends Component {
|
||||
|
||||
this.selectedFilterBuilderProp = selectedFilterBuilderProp;
|
||||
onFilterChange(index, filter);
|
||||
}
|
||||
};
|
||||
|
||||
onFilterChange = ({ name, value }) => {
|
||||
const {
|
||||
@@ -174,7 +174,7 @@ class FilterBuilderRow extends Component {
|
||||
filter[name] = value;
|
||||
|
||||
onFilterChange(index, filter);
|
||||
}
|
||||
};
|
||||
|
||||
onAddPress = () => {
|
||||
const {
|
||||
@@ -183,7 +183,7 @@ class FilterBuilderRow extends Component {
|
||||
} = this.props;
|
||||
|
||||
onAddPress(index);
|
||||
}
|
||||
};
|
||||
|
||||
onRemovePress = () => {
|
||||
const {
|
||||
@@ -192,7 +192,7 @@ class FilterBuilderRow extends Component {
|
||||
} = this.props;
|
||||
|
||||
onRemovePress(index);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -84,7 +84,7 @@ class FilterBuilderRowValue extends Component {
|
||||
name: NAME,
|
||||
value: [...filterValue, value]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onTagDelete = ({ index }) => {
|
||||
const {
|
||||
@@ -98,7 +98,7 @@ class FilterBuilderRowValue extends Component {
|
||||
name: NAME,
|
||||
value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -47,7 +47,7 @@ class IndexerFilterBuilderRowValueConnector extends Component {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchIndexers();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user