1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-10 15:10:57 -04:00

Compare commits

..

47 Commits

Author SHA1 Message Date
Qstick
66aeb19d88 Handle auth options correctly in Security Settings
(cherry picked from commit 0fad20e327503bac767c4df4c893f5e418866831)
2023-04-02 16:37:54 -05:00
Robin Dadswell
455aca85bd New: SSO goes straight to authentication provider 2023-04-02 16:37:54 -05:00
ta264
a29d794cbe Add api endpoint to generate the required login cookie
(cherry picked from commit 4180e2787a1ad5284873de4847f345b2c47df72a)
2023-04-02 16:37:54 -05:00
ta264
bb3123772f New: OIDC and Plex authentication methods
(cherry picked from commit 3ff3de6b90704fba266833115cd9d03ace99aae9)
2023-04-02 16:37:53 -05:00
ta264
46a20e1dcd Add explicit ApiKey requirement for ApiKey auth
(cherry picked from commit 8a3a998243e888e8f27c609f4bace5b42ad7ec50)
2023-04-02 16:37:53 -05:00
Qstick
993144b67a Cleanup Translation Implementation in UI 2023-04-02 16:37:53 -05:00
Mark McDowall
1f209848dc New: Calendar option for full color events
(cherry picked from commit 0210b5c5c1b5c56dce6f4c9f3f56366adba950d3)

Fixup Calendar for Full Color View, Final CSS fixups

Update localization
2023-04-02 16:37:52 -05:00
Qstick
3dafe44fed Bump SQLite to 3.38.5 (1.0.116) 2023-04-02 16:37:52 -05:00
Marty Zalega
767e75ca45 Don't lowercase UrlBase in ConfigFileProvider
UrlBase should honour the case it is given.

(cherry picked from commit e1de523c89f7649e64f520b090bbdb2f56cc4b85)
2023-04-02 16:37:52 -05:00
Qstick
1d4db26f17 New: Rework Movie Details view 2023-04-02 16:37:52 -05:00
Mark McDowall
757cb9a956 New: Migrate user passwords to Pbkdf2
(cherry picked from commit 269e72a2193b584476bec338ef41e6fb2e5cbea6)
2023-04-02 16:37:51 -05:00
Qstick
5dc3726023 New: v4 API (DROP v3 AFTER TESTING PERIOD) 2023-04-02 16:37:50 -05:00
Qstick
5b2f30227b Build Branch [REVERT] 2023-04-02 16:37:49 -05:00
Qstick
ed94eee859 New: Multiple Quality Profiles and Files Per Movie 2023-04-02 16:37:49 -05:00
Qstick
6eb271eee4 New: Rework and Require Authentication
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2023-04-02 15:48:32 -05:00
Qstick
9fe205727c Bump Version to 5 2023-04-02 15:48:28 -05:00
Mark McDowall
4627093616 Fixed: Multiple Downloaded Episodes Scan commands should not run in parallel
(cherry picked from commit b3d1e4f520d14c41aa6a7dff049ee9b9ef48fecb)
2023-04-02 15:28:14 -05:00
Lagicrus
a006984d5e Fixed: Lock rating to 0 decimal places in Discovery (#8279)
* Locks to 1 decimal place instead of infinite
2023-04-02 15:27:36 -05:00
Qstick
7d9183ef12 Fixed: Queue error for language custom format on unknown items 2023-03-29 19:21:35 -05:00
Qstick
d35c6683e9 New: Add indexer option for Discord on grab notifications
Fixes #8242

Co-Authored-By: lodu <48859312+lodu@users.noreply.github.com>
2023-03-29 19:01:11 -05:00
Qstick
ac26bcddd9 Fixed: Page Plex Watchlist results
Fixes #8223
Fixes #8042
2023-03-29 18:44:18 -05:00
Devin Buhl
15bafce8cc Add Overview to CustomScript and Webhook Notifications (#8239)
* Add Overview to customscript
2023-03-29 18:34:29 -05:00
Qstick
2167da87ce Enable all analyzers and enforce code style on build
Fixes #8201
Fixes #8202
Fixes #8203
Fixes #8204
Fixes #8205
Fixes #8207
Fixes #8208
Fixes #8209
Fixes #8210
Fixes #8211
Fixes #8212
Fixes #8213
Fixes #8214
Fixes #8215
Closes #8216
Fixes #8217
Closes #8218
Fixes #8219
Closes #8220
2023-03-29 18:27:30 -05:00
Mark McDowall
926d37a572 Fixed: Permissions after installing on Windows and opening Firewall port
(cherry picked from commit ff2e8ffc372a34d08028db3c49f603cdfb87d832)
2023-03-29 17:43:11 -05:00
Bogdan
42c9e4e3e5 Fixed: Parsing of RoDubbed releases as Romanian 2023-03-24 20:40:01 -04:00
bakerboy448
89b609a221 Fixed: Improve some request failure messaging
(cherry picked from commit e968919e63616e30cc401964bd51db8e9e0e26de)

Fixes #8152
2023-03-24 20:38:48 -04:00
Qstick
dfc9f74116 Fixed broken path tests
Fixes #8132

(cherry picked from commit 5a22afc42bb03c9cdfb0a46d470d084dbdd495d5)
2023-03-12 16:56:29 -05:00
Mark McDowall
189603c756 Fixed: USB drives mounted to folders are treated as different mounts
(cherry picked from commit 75378f7bde90b9d3d9b72404c25c017da2cd147c)
2023-03-12 16:56:29 -05:00
Mark McDowall
a78693a2f7 Fixed: Prevent getting disk space from returning no information when it partially fails
(cherry picked from commit 2c65e4fa41418157d0d27b34c3bab80158cff219)
2023-03-12 16:07:22 -05:00
Servarr
cea0c5033a Automated API Docs update 2023-03-12 16:02:48 -05:00
Derek Antrican
1e3a42bf42 Implemented OnMovieAdded for Discord 2023-03-12 16:00:41 -05:00
Qstick
030744ab7b Fixed: Indexer Flags CF Scores not shown in Search
Fixes #8165
2023-03-12 15:51:39 -05:00
Qstick
17fda02d8c Fixed: Drag with touch on Profiles page 2023-03-12 15:32:58 -05:00
cicomalieran
aabf6b9ff8 Fixed: Processing very long ETA from Transmission
(cherry picked from commit 9800bd6b439257e73e3545e125cd03900a3036bb)
2023-03-12 12:28:33 -05:00
Mark McDowall
7b2fd5140b Switch to eslint for linting
(cherry picked from commit a18c3774661f466727ab46315211aecb43ef1def)
2023-03-08 14:31:03 +00:00
Weblate
b6b10d7c6f Translated using Weblate (Croatian) [skip ci]
Currently translated at 19.7% (229 of 1159 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 100.0% (1159 of 1159 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1159 of 1159 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 100.0% (1159 of 1159 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 98.8% (1146 of 1159 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (1159 of 1159 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1159 of 1159 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 95.5% (1107 of 1159 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 100.0% (1159 of 1159 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 100.0% (1159 of 1159 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 99.9% (1158 of 1159 strings)

Update translation files  [skip ci]

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Spanish) [skip ci]

Currently translated at 98.7% (1145 of 1159 strings)

Translated using Weblate (Croatian) [skip ci]

Currently translated at 16.1% (187 of 1159 strings)

Translated using Weblate (Croatian) [skip ci]

Currently translated at 16.1% (187 of 1159 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 100.0% (1159 of 1159 strings)

Translated using Weblate (Croatian) [skip ci]

Currently translated at 13.2% (153 of 1159 strings)

Translated using Weblate (Croatian) [skip ci]

Currently translated at 13.2% (153 of 1159 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 1.6% (19 of 1159 strings)

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 99.8% (1157 of 1159 strings)

Translated using Weblate (Slovak) [skip ci]

Currently translated at 21.4% (249 of 1159 strings)

Translated using Weblate (Norwegian Bokmål) [skip ci]

Currently translated at 22.1% (257 of 1159 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 98.9% (1147 of 1159 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 90.3% (1047 of 1159 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 99.9% (1158 of 1159 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.9% (1158 of 1159 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 89.9% (1042 of 1159 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 87.8% (1018 of 1159 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 89.9% (1042 of 1159 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 87.5% (1015 of 1159 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 89.9% (1042 of 1159 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 89.7% (1040 of 1159 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 90.7% (1052 of 1159 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 99.9% (1158 of 1159 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 98.8% (1146 of 1159 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 98.8% (1146 of 1159 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 95.5% (1107 of 1159 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 21.8% (253 of 1159 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 89.9% (1042 of 1159 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 89.9% (1042 of 1159 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 99.9% (1158 of 1159 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 92.8% (1076 of 1159 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 99.8% (1157 of 1159 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 99.9% (1158 of 1159 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 89.9% (1042 of 1159 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 90.7% (1052 of 1159 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 94.3% (1094 of 1159 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 98.5% (1142 of 1159 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 99.5% (1154 of 1159 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 99.9% (1158 of 1159 strings)

Added translation using Weblate (Tamil) [skip ci]

Added translation using Weblate (Indonesian) [skip ci]

Added translation using Weblate (Estonian) [skip ci]

Added translation using Weblate (Serbian) [skip ci]

Added translation using Weblate (Croatian) [skip ci]

Added translation using Weblate (Bosnian) [skip ci]

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 1.5% (18 of 1157 strings)

Added translation using Weblate (Spanish (Mexico)) [skip ci]

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Qstick <qstick@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: TheHrle <Hpranjkovic@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: dtalens <databio@gmail.com>
Co-authored-by: fiego14 <alvaross_96@hotmail.com>
Co-authored-by: libsu <libsu@qq.com>
Co-authored-by: pedrom20 <pedrom20@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2023-02-27 19:12:11 -06:00
Qstick
16fcf0b56b New: Additional custom filter predicates for strings
(cherry picked from commit 6082253166b67d59d7907d83c362116d47bcdaeb)
2023-02-27 19:10:58 -06:00
Bakerboy448
c222a1a434 New: Use Best PageSize for Newznab/Torznab
Max of Default or Max and no more than 100
2023-02-12 18:34:22 -06:00
Qstick
c6e91e028b New: Add Additional Languages
Fixes #6257
Fixes #7967
Closes #7592
Reference #7788

Co-Authored-By: dtalens <6631832+dtalens@users.noreply.github.com>
2023-02-12 15:03:45 -06:00
Bakerboy448
fcf5984944 Fixed: Translations 2023-02-12 13:29:46 -06:00
Mark McDowall
fdfe8ca656 New: Return static response to requests while app is starting
(cherry picked from commit 303fc5d786ccf2ad14c8523fc239696c5d37ea53)

Fixes #8079
Closes #8080
2023-02-12 13:29:46 -06:00
Qstick
150a5c1fc6 Bump version to 4.4.2 2023-02-11 13:23:52 -06:00
Matthew Barrington
9ea0957351 New: Add Ireland as a Certification Country (#8085)
Co-authored-by: Matthew Barrington <git@barrington.it>
2023-02-11 12:36:04 -06:00
Weblate
8befa436cc Translated using Weblate (French) [skip ci]
Currently translated at 99.6% (1153 of 1157 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1157 of 1157 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: KevoM <lilmarsu@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-02-11 12:35:19 -06:00
Qstick
53fdb6f07f Delete azuresync.yml 2023-02-11 10:36:40 -06:00
Servarr
0697dbff96 Automated API Docs update 2023-02-07 20:26:31 -06:00
Qstick
34924859aa Fixed: Settings fail to save for some auth setups
(cherry picked from commit a379d0c403449b2623f84aa6851c850971528ff8)
2023-02-07 20:24:48 -06:00
571 changed files with 26854 additions and 2615 deletions

View File

@@ -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:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
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
# Indentation preferences
csharp_indent_block_contents = true
@@ -32,6 +32,10 @@ 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 = _
@@ -64,6 +68,7 @@ dotnet_diagnostic.SA1406.severity = suggestion
dotnet_diagnostic.SA1410.severity = suggestion
dotnet_diagnostic.SA1411.severity = suggestion
dotnet_diagnostic.SA1413.severity = none
dotnet_diagnostic.SA1512.severity = none
dotnet_diagnostic.SA1516.severity = none
dotnet_diagnostic.SA1600.severity = none
dotnet_diagnostic.SA1601.severity = none
@@ -162,6 +167,7 @@ dotnet_diagnostic.CA1309.severity = suggestion
dotnet_diagnostic.CA1310.severity = suggestion
dotnet_diagnostic.CA1401.severity = suggestion
dotnet_diagnostic.CA1416.severity = suggestion
dotnet_diagnostic.CA1419.severity = suggestion
dotnet_diagnostic.CA1507.severity = suggestion
dotnet_diagnostic.CA1508.severity = suggestion
dotnet_diagnostic.CA1707.severity = suggestion
@@ -177,9 +183,6 @@ dotnet_diagnostic.CA1720.severity = suggestion
dotnet_diagnostic.CA1721.severity = suggestion
dotnet_diagnostic.CA1724.severity = suggestion
dotnet_diagnostic.CA1725.severity = suggestion
dotnet_diagnostic.CA1801.severity = suggestion
dotnet_diagnostic.CA1802.severity = suggestion
dotnet_diagnostic.CA1805.severity = suggestion
dotnet_diagnostic.CA1806.severity = suggestion
dotnet_diagnostic.CA1810.severity = suggestion
dotnet_diagnostic.CA1812.severity = suggestion
@@ -191,13 +194,11 @@ dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1822.severity = suggestion
dotnet_diagnostic.CA1823.severity = suggestion
dotnet_diagnostic.CA1824.severity = suggestion
dotnet_diagnostic.CA1848.severity = suggestion
dotnet_diagnostic.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA2008.severity = suggestion
dotnet_diagnostic.CA2009.severity = suggestion
dotnet_diagnostic.CA2010.severity = suggestion
dotnet_diagnostic.CA2011.severity = suggestion
dotnet_diagnostic.CA2012.severity = suggestion
dotnet_diagnostic.CA2013.severity = suggestion
dotnet_diagnostic.CA2100.severity = suggestion
@@ -228,6 +229,9 @@ dotnet_diagnostic.CA2243.severity = suggestion
dotnet_diagnostic.CA2244.severity = suggestion
dotnet_diagnostic.CA2245.severity = suggestion
dotnet_diagnostic.CA2246.severity = suggestion
dotnet_diagnostic.CA2249.severity = suggestion
dotnet_diagnostic.CA2251.severity = suggestion
dotnet_diagnostic.CA2254.severity = suggestion
dotnet_diagnostic.CA3061.severity = suggestion
dotnet_diagnostic.CA3075.severity = suggestion
dotnet_diagnostic.CA3076.severity = suggestion
@@ -255,7 +259,7 @@ dotnet_diagnostic.CA5392.severity = suggestion
dotnet_diagnostic.CA5394.severity = suggestion
dotnet_diagnostic.CA5397.severity = suggestion
dotnet_diagnostic.SYSLIB0006.severity = none
[*.{js,html,js,hbs,less,css}]
charset = utf-8

View File

@@ -1,9 +0,0 @@
{
"paths": [
"frontend/src/**/*.js"
],
"ignored": [
"**/node_modules/**/*"
],
"port": 5004
}

View File

@@ -1,45 +0,0 @@
name: Sync issue to Azure DevOps work item
on:
issues:
types:
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
concurrency: azuresync-${{ github.event.issue.number }}
permissions: {}
jobs:
alert:
permissions:
issues: write # to update issue body
runs-on: ubuntu-latest
steps:
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "Bug"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "User Story"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '4.4.2'
majorVersion: '5.0.0'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -27,6 +27,7 @@ trigger:
include:
- develop
- master
- zeus
pr:
branches:
@@ -1092,7 +1093,7 @@ stages:
projectVersion: '$(radarrVersion)'
extraProperties: |
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
sonar.coverage.exclusions=**/Radarr.Api.V3/**/*
sonar.coverage.exclusions=**/Radarr.Api.V*/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: |

View File

@@ -29,7 +29,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
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 &
dotnet tool run swagger tofile --output ./src/Radarr.Api.V4/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v4 &
sleep 45

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line filenames/match-exported
const loaderUtils = require('loader-utils');
module.exports = function cssVariablesLoader(source) {

View File

@@ -20,10 +20,6 @@ class AddNewMovieModalContent extends Component {
//
// Listeners
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
};
onAddMoviePress = () => {
this.props.onAddMoviePress();
};
@@ -40,7 +36,7 @@ class AddNewMovieModalContent extends Component {
isAdding,
rootFolderPath,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
searchForMovie,
folder,
@@ -130,9 +126,9 @@ class AddNewMovieModalContent extends Component {
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
onChange={this.onQualityProfileIdChange}
{...qualityProfileId}
name="qualityProfileIds"
onChange={onInputChange}
{...qualityProfileIds}
/>
</FormGroup>
@@ -189,7 +185,7 @@ AddNewMovieModalContent.propTypes = {
addError: PropTypes.object,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,

View File

@@ -58,7 +58,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId,
rootFolderPath,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
searchForMovie,
tags
@@ -68,7 +68,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
qualityProfileIds: qualityProfileIds.value,
minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value,
tags: tags.value
@@ -93,7 +93,7 @@ AddNewMovieModalContentConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,

View File

@@ -72,15 +72,19 @@ class AddNewMovieSearchResult extends Component {
colorImpairedMode,
id,
monitored,
hasFile,
isAvailable,
queueStatus,
queueState,
runtime,
movieRuntimeFormat,
certification
certification,
statistics
} = this.props;
const {
movieFileCount
} = statistics;
const {
isNewAddMovieModalOpen
} = this.state;
@@ -120,7 +124,7 @@ class AddNewMovieSearchResult extends Component {
isExistingMovie &&
<MovieIndexProgressBar
monitored={monitored}
hasFile={hasFile}
hasFile={movieFileCount > 0}
status={status}
posterWidth={posterWidth}
detailedProgressBar={true}
@@ -233,7 +237,7 @@ class AddNewMovieSearchResult extends Component {
{
isExistingMovie && isSmallScreen &&
<MovieStatusLabel
hasMovieFiles={hasFile}
hasMovieFiles={movieFileCount > 0}
monitored={monitored}
isAvailable={isAvailable}
id={id}
@@ -290,7 +294,14 @@ AddNewMovieSearchResult.propTypes = {
queueState: PropTypes.string,
runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string
certification: PropTypes.string,
statistics: PropTypes.object
};
AddNewMovieSearchResult.defaultProps = {
statistics: {
movieFileCount: 0
}
};
export default AddNewMovieSearchResult;

View File

@@ -25,13 +25,13 @@ class ImportMovieFooter extends Component {
const {
defaultMonitor,
defaultQualityProfileId,
defaultQualityProfileIds,
defaultMinimumAvailability
} = props;
this.state = {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
qualityProfileIds: defaultQualityProfileIds,
minimumAvailability: defaultMinimumAvailability
};
}
@@ -39,16 +39,16 @@ class ImportMovieFooter extends Component {
componentDidUpdate(prevProps, prevState) {
const {
defaultMonitor,
defaultQualityProfileId,
defaultQualityProfileIds,
defaultMinimumAvailability,
isMonitorMixed,
isQualityProfileIdMixed,
isQualityProfileIdsMixed,
isMinimumAvailabilityMixed
} = this.props;
const {
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability
} = this.state;
@@ -60,10 +60,10 @@ class ImportMovieFooter extends Component {
newState.monitor = defaultMonitor;
}
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
newState.qualityProfileId = MIXED;
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
newState.qualityProfileId = defaultQualityProfileId;
if (isQualityProfileIdsMixed && qualityProfileIds !== MIXED) {
newState.qualityProfileIds = MIXED;
} else if (!isQualityProfileIdsMixed && qualityProfileIds !== defaultQualityProfileIds) {
newState.qualityProfileIds = defaultQualityProfileIds;
}
if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) {
@@ -94,7 +94,7 @@ class ImportMovieFooter extends Component {
isImporting,
isLookingUpMovie,
isMonitorMixed,
isQualityProfileIdMixed,
isQualityProfileIdsMixed,
isMinimumAvailabilityMixed,
hasUnsearchedItems,
importError,
@@ -105,7 +105,7 @@ class ImportMovieFooter extends Component {
const {
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability
} = this.state;
@@ -148,10 +148,10 @@ class ImportMovieFooter extends Component {
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
value={qualityProfileId}
name="qualityProfileIds"
value={qualityProfileIds}
isDisabled={!selectedCount}
includeMixed={isQualityProfileIdMixed}
includeMixed={isQualityProfileIdsMixed}
onChange={this.onInputChange}
/>
</div>
@@ -257,10 +257,10 @@ ImportMovieFooter.propTypes = {
isImporting: PropTypes.bool.isRequired,
isLookingUpMovie: PropTypes.bool.isRequired,
defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileId: PropTypes.number,
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
defaultMinimumAvailability: PropTypes.string,
isMonitorMixed: PropTypes.bool.isRequired,
isQualityProfileIdMixed: PropTypes.bool.isRequired,
isQualityProfileIdsMixed: PropTypes.bool.isRequired,
isMinimumAvailabilityMixed: PropTypes.bool.isRequired,
hasUnsearchedItems: PropTypes.bool.isRequired,
importError: PropTypes.object,

View File

@@ -18,7 +18,7 @@ function createMapStateToProps() {
(addMovie, importMovie, selectedIds) => {
const {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
qualityProfileIds: defaultQualityProfileIds,
minimumAvailability: defaultMinimumAvailability
} = addMovie.defaults;
@@ -30,7 +30,7 @@ function createMapStateToProps() {
} = importMovie;
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
const isQualityProfileIdsMixed = isMixed(items, selectedIds, defaultQualityProfileIds, 'qualityProfileIds');
const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability');
const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated);
@@ -39,10 +39,10 @@ function createMapStateToProps() {
isLookingUpMovie,
isImporting,
defaultMonitor,
defaultQualityProfileId,
defaultQualityProfileIds,
defaultMinimumAvailability,
isMonitorMixed,
isQualityProfileIdMixed,
isQualityProfileIdsMixed,
isMinimumAvailabilityMixed,
importError,
hasUnsearchedItems

View File

@@ -11,7 +11,7 @@ function ImportMovieRow(props) {
const {
id,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
selectedMovie,
isExistingMovie,
@@ -62,8 +62,8 @@ function ImportMovieRow(props) {
<VirtualTableRowCell className={styles.qualityProfile}>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
value={qualityProfileId}
name="qualityProfileIds"
value={qualityProfileIds}
onChange={onInputChange}
/>
</VirtualTableRowCell>
@@ -74,7 +74,7 @@ function ImportMovieRow(props) {
ImportMovieRow.propTypes = {
id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired,
qualityProfileId: PropTypes.number.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
minimumAvailability: PropTypes.string.isRequired,
selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired,

View File

@@ -15,7 +15,7 @@ class ImportMovieTable extends Component {
const {
unmappedFolders,
defaultMonitor,
defaultQualityProfileId,
defaultQualityProfileIds,
defaultMinimumAvailability,
onMovieLookup,
onSetImportMovieValue
@@ -23,7 +23,7 @@ class ImportMovieTable extends Component {
const values = {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
qualityProfileIds: defaultQualityProfileIds,
minimumAvailability: defaultMinimumAvailability
};
@@ -167,7 +167,7 @@ ImportMovieTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.object),
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileId: PropTypes.number,
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
defaultMinimumAvailability: PropTypes.string,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,

View File

@@ -13,7 +13,7 @@ function createMapStateToProps() {
(addMovie, importMovie, dimensions, allMovies) => {
return {
defaultMonitor: addMovie.defaults.monitor,
defaultQualityProfileId: addMovie.defaults.qualityProfileId,
defaultQualityProfileIds: addMovie.defaults.qualityProfileIds,
defaultMinimumAvailability: addMovie.defaults.minimumAvailability,
items: importMovie.items,
isSmallScreen: dimensions.isSmallScreen,

View File

@@ -5,6 +5,7 @@ import FormInputButton from 'Components/Form/FormInputButton';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Portal from 'Components/Portal';
import { icons, kinds } from 'Helpers/Props';
@@ -242,7 +243,7 @@ class ImportMovieSelectMovie extends Component {
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
ButtonComponent={SpinnerButton}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>

View File

@@ -24,7 +24,7 @@ function createMissingMovieIdsSelector() {
const inCinemas = movie.inCinemas;
if (
!movie.hasFile &&
(!movie.statistics || movie.statistics.movieFileCount === 0) &&
moment(inCinemas).isAfter(start) &&
moment(inCinemas).isBefore(end) &&
isBefore(movie.inCinemas) &&

View File

@@ -1,3 +1,5 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.event {
overflow-x: hidden;
margin: 4px 2px;
@@ -55,6 +57,10 @@
.downloaded {
border-left-color: var(--successColor) !important;
&:global(.fullColor) {
background-color: rgba(39, 194, 76, 0.4) !important;
}
&:global(.colorImpaired) {
border-left-color: color(var(--successColor), saturation(+15%)) !important;
}
@@ -62,28 +68,72 @@
.queue {
border-left-color: var(--purple) !important;
&:global(.fullColor) {
background-color: rgba(122, 67, 182, 0.4) !important;
}
}
.unmonitored {
border-left-color: var(--gray) !important;
&:global(.fullColor) {
background-color: rgba(173, 173, 173, 0.5) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.missingUnmonitored {
border-left-color: var(--warningColor) !important;
&:global(.fullColor) {
background-color: rgba(255, 165, 0, 0.6) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.missingMonitored {
border-left-color: var(--dangerColor) !important;
&:global(.fullColor) {
background-color: rgba(240, 80, 80, 0.6) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.continuing {
.unaired {
border-left-color: var(--primaryColor) !important;
&:global(.fullColor) {
background-color: rgba(93, 156, 236, 0.4) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}

View File

@@ -32,6 +32,7 @@ class CalendarEvent extends Component {
queueItem,
showMovieInformation,
showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode,
date
} = this.props;
@@ -62,7 +63,8 @@ class CalendarEvent extends Component {
styles.event,
styles.link,
styles[statusStyle],
colorImpairedMode && 'colorImpaired'
colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor'
)}
// component="div"
to={link}
@@ -97,7 +99,7 @@ class CalendarEvent extends Component {
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={kinds.WARNING}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
title={translate('QualityCutoffHasNotBeenMet')}
/>
}
@@ -142,11 +144,12 @@ CalendarEvent.propTypes = {
digitalRelease: PropTypes.string,
monitored: PropTypes.bool.isRequired,
certification: PropTypes.string,
hasFile: PropTypes.bool.isRequired,
hasFile: PropTypes.bool,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
showMovieInformation: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired

View File

@@ -9,6 +9,7 @@ import styles from './Legend.css';
function Legend(props) {
const {
showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode
} = props;
@@ -19,7 +20,7 @@ function Legend(props) {
<LegendIconItem
name={translate('CutoffUnmet')}
icon={icons.MOVIE_FILE}
kind={kinds.WARNING}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
/>
);
@@ -31,12 +32,14 @@ function Legend(props) {
<LegendItem
style='ended'
name={translate('DownloadedAndMonitored')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='availNotMonitored'
name={translate('DownloadedButNotMonitored')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -45,12 +48,14 @@ function Legend(props) {
<LegendItem
style='missingMonitored'
name={translate('MissingMonitoredAndConsideredAvailable')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='missingUnmonitored'
name={translate('MissingNotMonitored')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -59,12 +64,14 @@ function Legend(props) {
<LegendItem
style='queue'
name={translate('Queued')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='continuing'
name={translate('Unreleased')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -79,7 +86,9 @@ function Legend(props) {
}
Legend.propTypes = {
view: PropTypes.string.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};

View File

@@ -6,10 +6,12 @@ import Legend from './Legend';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
(state) => state.calendar.view,
createUISettingsSelector(),
(calendarOptions, uiSettings) => {
(calendarOptions, view, uiSettings) => {
return {
...calendarOptions,
view,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}

View File

@@ -8,6 +8,7 @@ function LegendIconItem(props) {
name,
icon,
kind,
darken,
tooltip
} = props;
@@ -19,6 +20,7 @@ function LegendIconItem(props) {
<Icon
className={styles.icon}
name={icon}
darken={darken}
kind={kind}
/>
@@ -31,7 +33,12 @@ LegendIconItem.propTypes = {
name: PropTypes.string.isRequired,
icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
darken: PropTypes.bool.isRequired,
tooltip: PropTypes.string.isRequired
};
LegendIconItem.defaultProps = {
darken: false
};
export default LegendIconItem;

View File

@@ -1,3 +1,5 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.legendItemContainer {
margin-right: 5px;
width: 220px;
@@ -63,10 +65,12 @@
.missingMonitoredColorImpaired {
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
color: var(--white);
}
.missingUnmonitoredColorImpaired {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
color: var(--white);
}
.legendItemText {

View File

@@ -7,6 +7,7 @@ function LegendItem(props) {
const {
name,
style,
fullColorEvents,
colorImpairedMode
} = props;
@@ -16,7 +17,8 @@ function LegendItem(props) {
className={classNames(
styles.legendItem,
styles[style],
colorImpairedMode && 'colorImpaired'
colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor'
)}
/>
<div className={classNames(styles.legendItemText, colorImpairedMode && styles[`${style}ColorImpaired`])}>
@@ -29,6 +31,7 @@ function LegendItem(props) {
LegendItem.propTypes = {
name: PropTypes.string.isRequired,
style: PropTypes.string.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};

View File

@@ -26,14 +26,16 @@ class CalendarOptionsModalContent extends Component {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
enableColorImpairedMode,
fullColorEvents
} = props;
this.state = {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
enableColorImpairedMode,
fullColorEvents
};
}
@@ -94,6 +96,7 @@ class CalendarOptionsModalContent extends Component {
const {
showMovieInformation,
showCutoffUnmetIcon,
fullColorEvents,
onModalClose
} = this.props;
@@ -136,6 +139,18 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('FullColorEvents')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="fullColorEvents"
value={fullColorEvents}
helpText={translate('FullColorEventsHelpText')}
onChange={this.onOptionInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
@@ -162,7 +177,7 @@ class CalendarOptionsModalContent extends Component {
values={weekColumnOptions}
value={calendarWeekColumnHeader}
onChange={this.onGlobalInputChange}
helpText={translate('HelpText')}
helpText={translate('SettingsWeekColumnHeaderHelpText')}
/>
</FormGroup>
@@ -176,7 +191,9 @@ class CalendarOptionsModalContent extends Component {
value={timeFormat}
onChange={this.onGlobalInputChange}
/>
</FormGroup><FormGroup>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
<FormInputGroup
@@ -187,7 +204,6 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onGlobalInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
</ModalBody>
@@ -209,6 +225,7 @@ CalendarOptionsModalContent.propTypes = {
calendarWeekColumnHeader: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
dispatchSetCalendarOption: PropTypes.func.isRequired,
dispatchSaveUISettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired

View File

@@ -22,7 +22,7 @@ function getUrls(state) {
tags
} = state;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v4/calendar/Radarr.ics?`;
if (unmonitored) {
icalUrl += 'unmonitored=true&';

View File

@@ -46,7 +46,7 @@ class AddNewCollectionMovieModalContent extends Component {
onInputChange,
rootFolderPath,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
searchForMovie
} = this.props;
@@ -126,13 +126,13 @@ class AddNewCollectionMovieModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
name="qualityProfileIds"
onChange={this.onQualityProfileIdChange}
{...qualityProfileId}
{...qualityProfileIds}
/>
</FormGroup>
@@ -189,7 +189,7 @@ AddNewCollectionMovieModalContent.propTypes = {
addError: PropTypes.object,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
qualityProfileIds: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,

View File

@@ -25,7 +25,7 @@ function createMapStateToProps() {
const collectionDefaults = {
rootFolderPath: collection.rootFolderPath,
monitor: 'movieOnly',
qualityProfileId: collection.qualityProfileId,
qualityProfileIds: collection.qualityProfileIds,
minimumAvailability: collection.minimumAvailability,
searchForMovie: collection.searchOnAdd,
tags: []
@@ -70,7 +70,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
title,
rootFolderPath,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
searchForMovie,
tags
@@ -81,7 +81,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
title,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
qualityProfileIds: qualityProfileIds.value,
minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value,
tags: tags.value
@@ -109,7 +109,7 @@ AddNewCollectionMovieModalContentConnector.propTypes = {
title: PropTypes.string.isRequired,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
qualityProfileIds: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,

View File

@@ -46,7 +46,7 @@ class EditCollectionModalContent extends Component {
const {
monitored,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
// Id,
rootFolderPath,
@@ -104,12 +104,12 @@ class EditCollectionModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...qualityProfileId}
name="qualityProfileIds"
{...qualityProfileIds}
onChange={onInputChange}
/>
</FormGroup>

View File

@@ -39,7 +39,7 @@ function createMapStateToProps() {
const movieSettings = {
monitored: collection.monitored,
qualityProfileId: collection.qualityProfileId,
qualityProfileIds: collection.qualityProfileIds,
minimumAvailability: collection.minimumAvailability,
rootFolderPath: collection.rootFolderPath,
searchOnAdd: collection.searchOnAdd

View File

@@ -17,11 +17,13 @@ class CollectionMovieLabel extends Component {
status,
monitored,
isAvailable,
hasFile,
onMonitorTogglePress,
isSaving
isSaving,
statistics
} = this.props;
const { movieFileCount } = statistics;
return (
<div className={styles.movie}>
<div className={styles.movieTitle}>
@@ -46,11 +48,11 @@ class CollectionMovieLabel extends Component {
<div
className={classNames(
styles.movieStatus,
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
styles[getStatusStyle(status, monitored, movieFileCount > 0, isAvailable, 'kinds')]
)}
>
{
hasFile ? translate('Downloaded') : translate('Missing')
movieFileCount > 0 ? translate('Downloaded') : translate('Missing')
}
</div>
}
@@ -63,9 +65,9 @@ CollectionMovieLabel.propTypes = {
id: PropTypes.number,
title: PropTypes.string.isRequired,
status: PropTypes.string,
statistics: PropTypes.object.isRequired,
isAvailable: PropTypes.bool,
monitored: PropTypes.bool,
hasFile: PropTypes.bool,
isSaving: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
movieFileId: PropTypes.number,
@@ -75,9 +77,7 @@ CollectionMovieLabel.propTypes = {
CollectionMovieLabel.defaultProps = {
isSaving: false,
statistics: {
episodeFileCount: 0,
totalEpisodeCount: 0,
percentOfEpisodes: 0
movieFileCount: 0
}
};

View File

@@ -96,7 +96,7 @@ class CollectionOverview extends Component {
render() {
const {
monitored,
qualityProfileId,
qualityProfileIds,
rootFolderPath,
genres,
id,
@@ -212,7 +212,7 @@ class CollectionOverview extends Component {
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
qualityProfileId={qualityProfileId}
qualityProfileIds={qualityProfileIds}
/>
}
</span>
@@ -325,7 +325,7 @@ class CollectionOverview extends Component {
CollectionOverview.propTypes = {
id: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
qualityProfileId: PropTypes.number.isRequired,
qualityProfileIds: PropTypes.number.isRequired,
minimumAvailability: PropTypes.string.isRequired,
searchOnAdd: PropTypes.bool.isRequired,
rootFolderPath: PropTypes.string.isRequired,

View File

@@ -2,33 +2,19 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import { kinds } from 'Helpers/Props';
import styles from './FormInputButton.css';
function FormInputButton(props) {
const {
className,
canSpin,
ButtonComponent,
isLastButton,
...otherProps
} = props;
if (canSpin) {
return (
<SpinnerButton
className={classNames(
className,
!isLastButton && styles.middleButton
)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
return (
<Button
<ButtonComponent
className={classNames(
className,
!isLastButton && styles.middleButton
@@ -41,14 +27,14 @@ function FormInputButton(props) {
FormInputButton.propTypes = {
className: PropTypes.string.isRequired,
isLastButton: PropTypes.bool.isRequired,
canSpin: PropTypes.bool.isRequired
ButtonComponent: PropTypes.elementType.isRequired,
isLastButton: PropTypes.bool.isRequired
};
FormInputButton.defaultProps = {
className: styles.button,
isLastButton: true,
canSpin: false
ButtonComponent: Button,
isLastButton: true
};
export default FormInputButton;

View File

@@ -6,7 +6,6 @@
.inputGroup {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
}
.inputContainer {

View File

@@ -20,6 +20,7 @@ import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput';
import PathInputConnector from './PathInputConnector';
import PlexMachineInputConnector from './PlexMachineInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import TagInputConnector from './TagInputConnector';
@@ -62,6 +63,9 @@ function getComponent(type) {
case inputTypes.PATH:
return PathInputConnector;
case inputTypes.PLEX_MACHINE_SELECT:
return PlexMachineInputConnector;
case inputTypes.QUALITY_PROFILE_SELECT:
return QualityProfileSelectInputConnector;

View File

@@ -5,6 +5,7 @@ import { kinds } from 'Helpers/Props';
function OAuthInput(props) {
const {
className,
label,
authorizing,
error,
@@ -12,21 +13,21 @@ function OAuthInput(props) {
} = props;
return (
<div>
<SpinnerErrorButton
kind={kinds.PRIMARY}
isSpinning={authorizing}
error={error}
onPress={onPress}
>
{label}
</SpinnerErrorButton>
</div>
<SpinnerErrorButton
className={className}
kind={kinds.PRIMARY}
isSpinning={authorizing}
error={error}
onPress={onPress}
>
{label}
</SpinnerErrorButton>
);
}
OAuthInput.propTypes = {
label: PropTypes.string.isRequired,
className: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
authorizing: PropTypes.bool.isRequired,
error: PropTypes.object,
onPress: PropTypes.func.isRequired

View File

@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import SelectInput from './SelectInput';
function PlexMachineInput(props) {
const {
isFetching,
isDisabled,
value,
values,
onChange,
...otherProps
} = props;
const helpText = 'Authenticate with plex.tv to show servers to use for authentication';
return (
<>
{
isFetching ?
<LoadingIndicator /> :
<SelectInput
value={value}
values={values}
isDisabled={isDisabled}
onChange={onChange}
helpText={helpText}
{...otherProps}
/>
}
</>
);
}
PlexMachineInput.propTypes = {
isFetching: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
value: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired
};
export default PlexMachineInput;

View File

@@ -0,0 +1,115 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchPlexResources } from 'Store/Actions/settingsActions';
import PlexMachineInput from './PlexMachineInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.oAuth,
(state) => state.settings.plex,
(value, oAuth, plex) => {
let values = [{ key: value, value }];
let isDisabled = true;
if (plex.isPopulated) {
const serverValues = plex.items.filter((item) => item.provides.includes('server')).map((item) => {
return ({
key: item.clientIdentifier,
value: `${item.name} / ${item.owned ? 'Owner' : 'User'} / ${item.clientIdentifier}`
});
});
if (serverValues.find((item) => item.key === value)) {
values = serverValues;
} else {
values = values.concat(serverValues);
}
isDisabled = false;
}
return ({
accessToken: oAuth.result?.accessToken,
values,
isDisabled,
...plex
});
}
);
}
const mapDispatchToProps = {
dispatchFetchPlexResources: fetchPlexResources
};
class PlexMachineInputConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
const {
accessToken,
dispatchFetchPlexResources
} = this.props;
if (accessToken) {
dispatchFetchPlexResources({ accessToken });
}
};
componentDidUpdate(prevProps) {
const {
accessToken,
dispatchFetchPlexResources
} = this.props;
const oldToken = prevProps.accessToken;
if (accessToken && accessToken !== oldToken) {
dispatchFetchPlexResources({ accessToken });
}
}
render() {
const {
isFetching,
isPopulated,
isDisabled,
value,
values,
onChange
} = this.props;
return (
<PlexMachineInput
isFetching={isFetching}
isPopulated={isPopulated}
isDisabled={isDisabled}
value={value}
values={values}
onChange={onChange}
{...this.props}
/>
);
}
}
PlexMachineInputConnector.propTypes = {
dispatchFetchPlexResources: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
error: PropTypes.object,
oAuth: PropTypes.object,
accessToken: PropTypes.string,
onChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(PlexMachineInputConnector);

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import SelectInput from './SelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
@@ -45,40 +45,14 @@ function createMapStateToProps() {
class QualityProfileSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
name,
value,
values
} = this.props;
if (!value || !values.some((v) => v.key === value) ) {
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key)));
if (firstValue) {
this.onChange({ name, value: firstValue.key });
}
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
};
//
// Render
render() {
return (
<SelectInput
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
onChange={this.props.onChange}
/>
);
}
@@ -86,7 +60,7 @@ class QualityProfileSelectInputConnector extends Component {
QualityProfileSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string)]),
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeNoChange: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired

View File

@@ -12,6 +12,10 @@
composes: hasWarning from '~Components/Form/Input.css';
}
.hasButton {
composes: hasButton from '~Components/Form/Input.css';
}
.isDisabled {
opacity: 0.7;
cursor: not-allowed;

View File

@@ -28,6 +28,7 @@ class SelectInput extends Component {
isDisabled,
hasError,
hasWarning,
hasButton,
autoFocus,
onBlur
} = this.props;
@@ -38,6 +39,7 @@ class SelectInput extends Component {
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
hasButton && styles.hasButton,
isDisabled && disabledClassName
)}
disabled={isDisabled}
@@ -80,6 +82,7 @@ SelectInput.propTypes = {
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
hasButton: PropTypes.bool,
autoFocus: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-bitwise */
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
import styles from './UMaskInput.css';
@@ -101,16 +102,16 @@ class UMaskInput extends Component {
</div>
<div className={styles.details}>
<div>
<label>UMask</label>
<label>{translate('UMask')}</label>
<div className={styles.value}>{umask}</div>
</div>
<div>
<label>Folder</label>
<label>{translate('Folder')}</label>
<div className={styles.value}>{folder}</div>
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
</div>
<div>
<label>File</label>
<label>{translate('File')}</label>
<div className={styles.value}>{file}</div>
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
</div>

View File

@@ -12,10 +12,18 @@
.info {
color: var(--infoColor);
&:global(.darken) {
color: color(var(--infoColor) shade(30%));
}
}
.pink {
color: var(--pink);
&:global(.darken) {
color: color(var(--pink) shade(30%));
}
}
.success {

View File

@@ -18,6 +18,7 @@ class Icon extends PureComponent {
kind,
size,
title,
darken,
isSpinning,
...otherProps
} = this.props;
@@ -26,7 +27,8 @@ class Icon extends PureComponent {
<FontAwesomeIcon
className={classNames(
className,
styles[kind]
styles[kind],
darken && 'darken'
)}
icon={name}
spin={isSpinning}
@@ -59,6 +61,7 @@ Icon.propTypes = {
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
darken: PropTypes.bool.isRequired,
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};
@@ -66,6 +69,7 @@ Icon.propTypes = {
Icon.defaultProps = {
kind: kinds.DEFAULT,
size: 14,
darken: false,
isSpinning: false,
fixedWidth: false
};

View File

@@ -12,7 +12,7 @@ import styles from './PageHeaderActionsMenu.css';
function PageHeaderActionsMenu(props) {
const {
formsAuth,
cookieAuth,
onKeyboardShortcutsPress,
onRestartPress,
onShutdownPress
@@ -56,22 +56,20 @@ function PageHeaderActionsMenu(props) {
</MenuItem>
{
formsAuth &&
<div className={styles.separator} />
}
{
formsAuth &&
<MenuItem
to={`${window.Radarr.urlBase}/logout`}
noRouter={true}
>
<Icon
className={styles.itemIcon}
name={icons.LOGOUT}
/>
Logout
</MenuItem>
cookieAuth &&
<>
<div className={styles.separator} />
<MenuItem
to={`${window.Radarr.urlBase}/logout?ReturnUrl=/`}
noRouter={true}
>
<Icon
className={styles.itemIcon}
name={icons.LOGOUT}
/>
Logout
</MenuItem>
</>
}
</MenuContent>
</Menu>
@@ -80,7 +78,7 @@ function PageHeaderActionsMenu(props) {
}
PageHeaderActionsMenu.propTypes = {
formsAuth: PropTypes.bool.isRequired,
cookieAuth: PropTypes.bool.isRequired,
onKeyboardShortcutsPress: PropTypes.func.isRequired,
onRestartPress: PropTypes.func.isRequired,
onShutdownPress: PropTypes.func.isRequired

View File

@@ -10,7 +10,7 @@ function createMapStateToProps() {
(state) => state.system.status,
(status) => {
return {
formsAuth: status.item.authentication === 'forms'
cookieAuth: ['forms', 'oidc', 'plex'].includes(status.item.authentication)
};
}
);

View File

@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
import ColorImpairedContext from 'App/ColorImpairedContext';
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
import SignalRConnector from 'Components/SignalRConnector';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import locationShape from 'Helpers/Props/Shapes/locationShape';
import PageHeader from './Header/PageHeader';
import PageSidebar from './Sidebar/PageSidebar';
@@ -75,6 +76,7 @@ class Page extends Component {
isSmallScreen,
isSidebarVisible,
enableColorImpairedMode,
authenticationEnabled,
onSidebarToggle,
onSidebarVisibleChange
} = this.props;
@@ -109,6 +111,10 @@ class Page extends Component {
isOpen={this.state.isConnectionLostModalOpen}
onModalClose={this.onConnectionLostModalClose}
/>
<AuthenticationRequiredModal
isOpen={!authenticationEnabled}
/>
</div>
</ColorImpairedContext.Provider>
);
@@ -124,6 +130,7 @@ Page.propTypes = {
isUpdated: PropTypes.bool.isRequired,
isDisconnected: PropTypes.bool.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
authenticationEnabled: PropTypes.bool.isRequired,
onResize: PropTypes.func.isRequired,
onSidebarToggle: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired

View File

@@ -11,6 +11,7 @@ import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfil
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ErrorPage from './ErrorPage';
import LoadingPage from './LoadingPage';
import Page from './Page';
@@ -133,18 +134,21 @@ function createMapStateToProps() {
selectErrors,
selectAppProps,
createDimensionsSelector(),
createSystemStatusSelector(),
(
enableColorImpairedMode,
isPopulated,
errors,
app,
dimensions
dimensions,
systemStatus
) => {
return {
...app,
...errors,
isPopulated,
isSmallScreen: dimensions.isSmallScreen,
authenticationEnabled: systemStatus.authentication !== 'none',
enableColorImpairedMode
};
}

View File

@@ -0,0 +1,3 @@
.tags {
flex: 1 0 auto;
}

View File

@@ -0,0 +1,38 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import { kinds } from 'Helpers/Props';
import Label from './Label';
import styles from './QualityProfileList.css';
function QualityProfileList({ qualityProfileIds, qualityProfileList }) {
return (
<div className={styles.tags}>
{
qualityProfileIds.map((t) => {
const qualityProfile = _.find(qualityProfileList, { id: t });
if (!qualityProfile) {
return null;
}
return (
<Label
key={qualityProfile.id}
kind={kinds.INFO}
>
{qualityProfile.name}
</Label>
);
})
}
</div>
);
}
QualityProfileList.propTypes = {
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
qualityProfileList: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default QualityProfileList;

View File

@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import QualityProfileList from './QualityProfileList';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.qualityProfiles.items,
(qualityProfileList) => {
return {
qualityProfileList
};
}
);
}
export default connect(createMapStateToProps)(QualityProfileList);

View File

@@ -73,7 +73,7 @@ function getInfoRowProps(row, props) {
return {
title: translate('Ratings'),
iconName: icons.HEART,
label: `${props.ratings.tmdb.value * 10}%`
label: `${(props.ratings.tmdb.value * 10).toFixed()}%`
};
}

View File

@@ -0,0 +1,34 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModal(props) {
const {
isOpen
} = props;
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<AuthenticationRequiredModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
AuthenticationRequiredModal.propTypes = {
isOpen: PropTypes.bool.isRequired
};
export default AuthenticationRequiredModal;

View File

@@ -0,0 +1,5 @@
.authRequiredAlert {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}

View File

@@ -0,0 +1,267 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import Alert from 'Components/Alert';
import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import OAuthInputConnector from 'Components/Form/OAuthInputConnector';
import Icon from 'Components/Icon';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { icons, inputTypes, kinds } from 'Helpers/Props';
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
import styles from './AuthenticationRequiredModalContent.css';
const oauthData = {
implementation: { value: 'PlexImport' },
configContract: { value: 'PlexListSettings' },
fields: [
{
type: 'textbox',
name: 'accessToken'
},
{
type: 'oAuth',
name: 'signIn',
value: 'startAuth'
}
]
};
function onModalClose() {
// No-op
}
function AuthenticationRequiredModalContent(props) {
const {
isPopulated,
plexServersPopulated,
error,
isSaving,
settings,
onInputChange,
onSavePress,
dispatchFetchStatus
} = props;
const {
authenticationMethod,
authenticationRequired,
username,
password,
plexAuthServer,
plexRequireOwner,
oidcClientId,
oidcClientSecret,
oidcAuthority
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
const showUserPass = authenticationMethod && ['basic', 'forms'].includes(authenticationMethod.value);
const plexEnabled = authenticationMethod && authenticationMethod.value === 'plex';
const oidcEnabled = authenticationMethod && authenticationMethod.value === 'oidc';
const didMount = useRef(false);
useEffect(() => {
if (!isSaving && didMount.current) {
dispatchFetchStatus();
}
didMount.current = true;
}, [isSaving, dispatchFetchStatus]);
return (
<ModalContent
showCloseButton={false}
onModalClose={onModalClose}
>
<ModalHeader>
Authentication Required
</ModalHeader>
<ModalBody>
<Alert
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{authenticationRequiredWarning}
</Alert>
{
isPopulated && !error ?
<div>
<FormGroup>
<FormLabel>Authentication</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText="Require login to access Sonarr"
onChange={onInputChange}
{...authenticationMethod}
/>
</FormGroup>
{
authenticationEnabled ?
<FormGroup>
<FormLabel>Authentication Required</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText="Change which requests authentication is required for. Do not change unless you understand the risks."
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
{
showUserPass &&
<>
<FormGroup>
<FormLabel>Username</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
{...username}
/>
</FormGroup>
<FormGroup>
<FormLabel>Password</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
{...password}
/>
</FormGroup>
</>
}
{
plexEnabled &&
<>
<FormGroup>
<FormLabel>Plex Server</FormLabel>
<FormInputGroup
type={inputTypes.PLEX_MACHINE_SELECT}
name="plexAuthServer"
buttons={[
<FormInputButton
key="auth"
ButtonComponent={OAuthInputConnector}
label={plexServersPopulated ? <Icon name={icons.REFRESH} /> : 'Fetch'}
name="plexAuth"
provider="importList"
providerData={oauthData}
section="settings.importLists"
onChange={onInputChange}
/>
]}
onChange={onInputChange}
{...plexAuthServer}
/>
</FormGroup>
<FormGroup>
<FormLabel>Restrict Access to Server Owner</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="plexRequireOwner"
onChange={onInputChange}
{...plexRequireOwner}
/>
</FormGroup>
</>
}
{
oidcEnabled &&
<>
<FormGroup>
<FormLabel>Authority</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="oidcAuthority"
onChange={onInputChange}
{...oidcAuthority}
/>
</FormGroup>
<FormGroup>
<FormLabel>ClientId</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="oidcClientId"
onChange={onInputChange}
{...oidcClientId}
/>
</FormGroup>
<FormGroup>
<FormLabel>ClientSecret</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="oidcClientSecret"
onChange={onInputChange}
{...oidcClientSecret}
/>
</FormGroup>
</>
}
</div> :
null
}
{
!isPopulated && !error ? <LoadingIndicator /> : null
}
</ModalBody>
<ModalFooter>
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isSaving}
isDisabled={!authenticationEnabled}
onPress={onSavePress}
>
Save
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
AuthenticationRequiredModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired,
plexServersPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
settings: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired
};
export default AuthenticationRequiredModalContent;

View File

@@ -0,0 +1,88 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
const SECTION = 'general';
function createMapStateToProps() {
return createSelector(
createSettingsSectionSelector(SECTION),
(state) => state.settings.plex,
(sectionSettings, plex) => {
return {
...sectionSettings,
plexServersPopulated: plex.isPopulated
};
}
);
}
const mapDispatchToProps = {
dispatchClearPendingChanges: clearPendingChanges,
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
dispatchSaveGeneralSettings: saveGeneralSettings,
dispatchFetchGeneralSettings: fetchGeneralSettings,
dispatchFetchStatus: fetchStatus
};
class AuthenticationRequiredModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchGeneralSettings();
}
componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetGeneralSettingsValue({ name, value });
};
onSavePress = () => {
this.props.dispatchSaveGeneralSettings();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchFetchGeneralSettings,
dispatchSetGeneralSettingsValue,
dispatchSaveGeneralSettings,
...otherProps
} = this.props;
return (
<AuthenticationRequiredModalContent
{...otherProps}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
/>
);
}
}
AuthenticationRequiredModalContentConnector.propTypes = {
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);

View File

@@ -47,6 +47,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.CONTAINS, value: 'contains' },
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' },
{ key: filterTypes.EQUAL, value: 'equal' },
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
{ key: filterTypes.NOT_EQUAL, value: 'not equal' },
{ key: filterTypes.STARTS_WITH, value: 'starts with' },
{ key: filterTypes.NOT_STARTS_WITH, value: 'does not start with' },
{ key: filterTypes.ENDS_WITH, value: 'ends with' },
{ key: filterTypes.NOT_ENDS_WITH, value: 'does not end with' }
]
};

View File

@@ -39,6 +39,22 @@ const filterTypePredicates = {
[filterTypes.NOT_EQUAL]: function(itemValue, filterValue) {
return itemValue !== filterValue;
},
[filterTypes.STARTS_WITH]: function(itemValue, filterValue) {
return itemValue.toLowerCase().startsWith(filterValue.toLowerCase());
},
[filterTypes.NOT_STARTS_WITH]: function(itemValue, filterValue) {
return !itemValue.toLowerCase().startsWith(filterValue.toLowerCase());
},
[filterTypes.ENDS_WITH]: function(itemValue, filterValue) {
return itemValue.toLowerCase().endsWith(filterValue.toLowerCase());
},
[filterTypes.NOT_ENDS_WITH]: function(itemValue, filterValue) {
return !itemValue.toLowerCase().endsWith(filterValue.toLowerCase());
}
};

View File

@@ -10,6 +10,10 @@ export const LESS_THAN = 'lessThan';
export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual';
export const NOT_CONTAINS = 'notContains';
export const NOT_EQUAL = 'notEqual';
export const STARTS_WITH = 'startsWith';
export const NOT_STARTS_WITH = 'notStartsWith';
export const ENDS_WITH = 'endsWith';
export const NOT_ENDS_WITH = 'notEndsWith';
export const all = [
CONTAINS,
@@ -23,5 +27,9 @@ export const all = [
IN_LAST,
NOT_IN_LAST,
IN_NEXT,
NOT_IN_NEXT
NOT_IN_NEXT,
STARTS_WITH,
NOT_STARTS_WITH,
ENDS_WITH,
NOT_ENDS_WITH
];

View File

@@ -9,6 +9,7 @@ export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
export const PATH = 'path';
export const PLEX_MACHINE_SELECT = 'plexMachineSelect';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
@@ -35,6 +36,7 @@ export const all = [
OAUTH,
PASSWORD,
PATH,
PLEX_MACHINE_SELECT,
QUALITY_PROFILE_SELECT,
DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT,

View File

@@ -266,7 +266,7 @@ class InteractiveImportRow extends Component {
anchor={
<Icon name={icons.INTERACTIVE} />
}
title="Formats"
title={translate('Formats')}
body={
<div className={styles.customFormatTooltip}>
<MovieFormats formats={customFormats} />

View File

@@ -2,12 +2,15 @@ import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, sortDirections } from 'Helpers/Props';
import { align, icons, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearchContent.css';
import styles from './InteractiveSearch.css';
const columns = [
{
@@ -22,20 +25,6 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, { name: icons.DANGER }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'title',
label: translate('Title'),
@@ -99,10 +88,24 @@ const columns = [
label: React.createElement(Icon, { name: icons.FLAG }),
isSortable: true,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, { name: icons.DANGER }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
}
];
function InteractiveSearchContent(props) {
function InteractiveSearch(props) {
const {
searchPayload,
isFetching,
@@ -110,44 +113,63 @@ function InteractiveSearchContent(props) {
error,
totalReleasesCount,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
longDateFormat,
timeFormat,
onSortPress,
onFilterSelect,
onGrabPress
} = props;
return (
<div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
filterModalConnectorComponentProps={'movies'}
onFilterSelect={onFilterSelect}
/>
</div>
{
isFetching &&
<LoadingIndicator />
isFetching ? <LoadingIndicator /> : null
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>
!isFetching && error ?
<div>
{translate('UnableToLoadResultsIntSearch')}
</div>
</div> :
null
}
{
!isFetching && isPopulated && !totalReleasesCount &&
<div className={styles.blankpad}>
!isFetching && isPopulated && !totalReleasesCount ?
<div>
{translate('NoResultsFound')}
</div>
</div> :
null
}
{
!!totalReleasesCount && isPopulated && !items.length &&
<div className={styles.blankpad}>
!!totalReleasesCount && isPopulated && !items.length ?
<div>
{translate('AllResultsHiddenFilter')}
</div>
</div> :
null
}
{
isPopulated && !!items.length &&
isPopulated && !!items.length ?
<Table
columns={columns}
sortKey={sortKey}
@@ -170,32 +192,38 @@ function InteractiveSearchContent(props) {
})
}
</TableBody>
</Table>
</Table> :
null
}
{
totalReleasesCount !== items.length && !!items.length &&
totalReleasesCount !== items.length && !!items.length ?
<div className={styles.filteredMessage}>
{translate('SomeResultsHiddenFilter')}
</div>
</div> :
null
}
</div>
);
}
InteractiveSearchContent.propTypes = {
InteractiveSearch.propTypes = {
searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalReleasesCount: 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.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
};
export default InteractiveSearchContent;
export default InteractiveSearch;

View File

@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearchContent from './InteractiveSearchContent';
import InteractiveSearch from './InteractiveSearch';
function createMapStateToProps(appState) {
return createSelector(
@@ -48,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) {
};
}
class InteractiveSearchContentConnector extends Component {
class InteractiveSearchConnector extends Component {
//
// Lifecycle
@@ -79,18 +79,18 @@ class InteractiveSearchContentConnector extends Component {
return (
<InteractiveSearchContent
<InteractiveSearch
{...otherProps}
/>
);
}
}
InteractiveSearchContentConnector.propTypes = {
InteractiveSearchConnector.propTypes = {
searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);

View File

@@ -1,15 +1,20 @@
.cell {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.protocol {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px;
}
.title {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
display: flex;
align-items: center;
justify-content: space-between;
word-break: break-all;
}
.indexer {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 85px;
}
@@ -17,7 +22,9 @@
.quality,
.customFormat,
.language {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
text-align: center;
}
.language {
@@ -25,7 +32,7 @@
}
.customFormatScore {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
font-weight: bold;
@@ -35,34 +42,26 @@
.rejected,
.indexerFlags,
.download {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}
.age,
.size {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
white-space: nowrap;
}
.peers {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px;
}
.title {
composes: cell;
}
.title div {
overflow-wrap: break-word;
}
.history {
composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px;
}

View File

@@ -145,46 +145,6 @@ class InteractiveSearchRow extends Component {
{formatAge(age, ageHours, ageMinutes)}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
/>
</TableRowCell>
<TableRowCell className={styles.rejected}>
{
!!rejections.length &&
<Popover
anchor={
<Icon
name={icons.DANGER}
kind={kinds.DANGER}
/>
}
title={translate('ReleaseRejected')}
body={
<ul>
{
rejections.map((rejection, index) => {
return (
<li key={index}>
{rejection}
</li>
);
})
}
</ul>
}
position={tooltipPositions.BOTTOM}
/>
}
</TableRowCell>
<TableRowCell className={styles.title}>
<Link
to={infoUrl}
@@ -297,6 +257,46 @@ class InteractiveSearchRow extends Component {
}
</TableRowCell>
<TableRowCell className={styles.rejected}>
{
!!rejections.length &&
<Popover
anchor={
<Icon
name={icons.DANGER}
kind={kinds.DANGER}
/>
}
title={translate('ReleaseRejected')}
body={
<ul>
{
rejections.map((rejection, index) => {
return (
<li key={index}>
{rejection}
</li>
);
})
}
</ul>
}
position={tooltipPositions.LEFT}
/>
}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
/>
</TableRowCell>
<ConfirmModal
isOpen={this.state.isConfirmGrabModalOpen}
kind={kinds.WARNING}

View File

@@ -1,16 +0,0 @@
import React from 'react';
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
function InteractiveSearchTable(props) {
return (
<InteractiveSearchContentConnector
searchPayload={props}
/>
);
}
InteractiveSearchTable.propTypes = {
};
export default InteractiveSearchTable;

View File

@@ -54,18 +54,22 @@ class DeleteMovieModalContent extends Component {
const {
title,
path,
hasFile,
sizeOnDisk,
statistics,
onModalClose
} = this.props;
const {
sizeOnDisk,
movieFileCount
} = statistics;
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = this.state.addImportExclusion;
let deleteFilesLabel = hasFile ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [0]);
let deleteFilesLabel = movieFileCount === 1 ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [movieFileCount]);
let deleteFilesHelpText = translate('DeleteFilesHelpText');
if (!hasFile) {
if (movieFileCount === 0) {
deleteFilesLabel = translate('DeleteMovieFolderLabel');
deleteFilesHelpText = translate('DeleteMovieFolderHelpText');
}
@@ -124,9 +128,9 @@ class DeleteMovieModalContent extends Component {
</div>
{
!!hasFile &&
movieFileCount > 0 &&
<div>
{hasFile} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
{movieFileCount} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
</div>
}
</div>
@@ -154,10 +158,16 @@ class DeleteMovieModalContent extends Component {
DeleteMovieModalContent.propTypes = {
title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
hasFile: PropTypes.bool.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
statistics: PropTypes.object.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
DeleteMovieModalContent.defaultProps = {
statistics: {
sizeOnDisk: 0,
movieFileCount: 0
}
};
export default DeleteMovieModalContent;

View File

@@ -69,7 +69,8 @@ class MovieCastPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {

View File

@@ -69,7 +69,8 @@ class MovieCrewPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {

View File

@@ -1,6 +1,7 @@
$hoverScale: 1.05;
.content {
border-radius: 5px;
transition: all 200ms ease-in;
&:hover {

View File

@@ -2,6 +2,10 @@
flex: 1 0 auto;
}
.movie {
padding: 10px;
}
.container {
padding: 10px;
}

View File

@@ -1,12 +1,16 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import dimensions from 'Styles/Variables/dimensions';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
@@ -169,56 +173,50 @@ class MovieCreditPosters extends Component {
render() {
const {
items
items,
itemComponent
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
posterWidth,
posterHeight
} = this.state;
const rowCount = Math.ceil(items.length / columnCount);
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={undefined}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
<div className={styles.sliderContainer}>
<Swiper
slidesPerView='auto'
spaceBetween={10}
slidesPerGroup={3}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.params.navigation.prevEl = this._swiperPrevRef;
swiper.params.navigation.nextEl = this._swiperNextRef;
swiper.navigation.init();
swiper.navigation.update();
}}
>
{items.map((credit) => (
<SwiperSlide key={credit.tmdbId} style={{ width: posterWidth }}>
<MovieCreditPosterConnector
key={credit.order}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={credit.personTmdbId}
personName={credit.personName}
job={credit.job}
character={credit.character}
images={credit.images}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
}
}

View File

@@ -1,3 +0,0 @@
.alternateTitle {
white-space: nowrap;
}

View File

@@ -1,28 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './MovieAlternateTitles.css';
function MovieAlternateTitles({ alternateTitles }) {
return (
<ul>
{
alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
return (
<li
key={alternateTitle}
className={styles.alternateTitle}
>
{alternateTitle}
</li>
);
})
}
</ul>
);
}
MovieAlternateTitles.propTypes = {
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default MovieAlternateTitles;

View File

@@ -5,7 +5,7 @@
.header {
position: relative;
width: 100%;
height: 375px;
height: 425px;
}
.errorMessage {
@@ -39,10 +39,11 @@
}
.poster {
z-index: 2;
flex-shrink: 0;
margin-right: 35px;
width: 217px;
height: 319px;
width: 250px;
height: 368px;
}
.info {

View File

@@ -1,8 +1,8 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import TextTruncate from 'react-text-truncate';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import InfoLabel from 'Components/InfoLabel';
@@ -22,12 +22,11 @@ import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -81,10 +80,10 @@ class MovieDetails extends Component {
isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false,
isInteractiveImportModalOpen: false,
isInteractiveSearchModalOpen: false,
allExpanded: false,
allCollapsed: false,
expandedState: {},
selectedTabIndex: 0,
overviewHeight: 0,
titleWidth: 0
};
@@ -137,6 +136,14 @@ class MovieDetails extends Component {
this.setState({ isEditMovieModalOpen: false });
};
onInteractiveSearchPress = () => {
this.setState({ isInteractiveSearchModalOpen: true });
};
onInteractiveSearchModalClose = () => {
this.setState({ isInteractiveSearchModalOpen: false });
};
onDeleteMoviePress = () => {
this.setState({
isEditMovieModalOpen: false,
@@ -265,7 +272,7 @@ class MovieDetails extends Component {
ratings,
path,
sizeOnDisk,
qualityProfileId,
qualityProfileIds,
monitored,
studio,
genres,
@@ -298,9 +305,9 @@ class MovieDetails extends Component {
isEditMovieModalOpen,
isDeleteMovieModalOpen,
isInteractiveImportModalOpen,
isInteractiveSearchModalOpen,
overviewHeight,
titleWidth,
selectedTabIndex
titleWidth
} = this.state;
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
@@ -326,6 +333,14 @@ class MovieDetails extends Component {
onPress={onSearchPress}
/>
<PageToolbarButton
label={translate('InteractiveSearch')}
iconName={icons.INTERACTIVE}
isSpinning={isSearching}
title={undefined}
onPress={this.onInteractiveSearchPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
@@ -557,7 +572,7 @@ class MovieDetails extends Component {
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
qualityProfileId={qualityProfileId}
qualityProfileIds={qualityProfileIds}
/>
}
</span>
@@ -651,101 +666,39 @@ class MovieDetails extends Component {
</div>
}
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}>
<TabList
className={styles.tabList}
>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('History')}
</Tab>
<FieldSet legend={translate('History')}>
<MovieHistoryTable
movieId={id}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Search')}
</Tab>
<FieldSet legend={translate('Files')}>
<MovieFileEditorTable
movieId={id}
/>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Files')}
</Tab>
<ExtraFileTable
movieId={id}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Titles')}
</Tab>
<FieldSet legend={translate('Cast')}>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</FieldSet>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Cast')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Crew')}
</Tab>
{
selectedTabIndex === 1 &&
<div className={styles.filterIcon}>
<InteractiveSearchFilterMenuConnector />
</div>
}
</TabList>
<TabPanel>
<MovieHistoryTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<InteractiveSearchTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieFileEditorTable
movieId={id}
/>
<ExtraFileTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieTitlesTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
<TabPanel>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
</Tabs>
<FieldSet legend={translate('Crew')}>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</FieldSet>
<FieldSet legend={translate('TitlesAndTranslations')}>
<MovieTitlesTable
movieId={id}
/>
</FieldSet>
</div>
<OrganizePreviewModalConnector
@@ -777,6 +730,12 @@ class MovieDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
<MovieInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
movieId={id}
onModalClose={this.onInteractiveSearchModalClose}
/>
</PageContentBody>
</PageContent>
);
@@ -795,7 +754,7 @@ MovieDetails.propTypes = {
ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
qualityProfileId: PropTypes.number.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
studio: PropTypes.string,

View File

@@ -43,7 +43,7 @@ class MovieTitlesRow extends Component {
}
MovieTitlesRow.propTypes = {
id: PropTypes.number.isRequired,
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
language: PropTypes.object.isRequired,
sourceType: PropTypes.string.isRequired

View File

@@ -0,0 +1,9 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
import styles from './MovieTitlesTable.css';
function MovieTitlesTable(props) {
const {
@@ -7,9 +8,11 @@ function MovieTitlesTable(props) {
} = props;
return (
<MovieTitlesTableContentConnector
{...otherProps}
/>
<div className={styles.container}>
<MovieTitlesTableContentConnector
{...otherProps}
/>
</div>
);
}

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import translate from 'Utilities/String/translate';
@@ -10,7 +9,7 @@ import styles from './MovieTitlesTableContent.css';
const columns = [
{
name: 'altTitle',
label: translate('AlternativeTitle'),
label: translate('Title'),
isVisible: true
},
{
@@ -32,40 +31,25 @@ class MovieTitlesTableContent extends Component {
render() {
const {
isFetching,
isPopulated,
error,
items
titles
} = this.props;
const hasItems = !!items.length;
const hasItems = !!titles.length;
return (
<div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>
{translate('UnableToLoadAltTitle')}
</div>
}
{
isPopulated && !hasItems && !error &&
!hasItems &&
<div className={styles.blankpad}>
{translate('NoAltTitle')}
</div>
}
{
isPopulated && hasItems && !error &&
hasItems &&
<Table columns={columns}>
<TableBody>
{
items.reverse().map((item) => {
titles.reverse().map((item) => {
return (
<MovieTitlesRow
key={item.id}
@@ -83,10 +67,7 @@ class MovieTitlesTableContent extends Component {
}
MovieTitlesTableContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired
titles: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default MovieTitlesTableContent;

View File

@@ -2,13 +2,40 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import MovieTitlesTableContent from './MovieTitlesTableContent';
function createMapStateToProps() {
return createSelector(
(state) => state.movies,
(movies) => {
return movies;
createMovieSelector(),
(movie) => {
let titles = [];
if (movie.alternateTitles) {
titles = movie.alternateTitles.map((title) => {
return {
id: `title_${title.id}`,
title: title.title,
language: title.language || 'Unknown',
sourceType: 'Alternative Title'
};
});
}
if (movie.translations) {
titles = titles.concat(movie.translations.map((title) => {
return {
id: `translation_${title.id}`,
title: title.title,
language: title.language || 'Unknown',
sourceType: 'Translation'
};
}));
}
return {
titles
};
}
);
}
@@ -23,14 +50,14 @@ class MovieTitlesTableContentConnector extends Component {
// Render
render() {
const movie = this.props.items.filter((obj) => {
return obj.id === this.props.movieId;
});
const {
titles
} = this.props;
return (
<MovieTitlesTableContent
{...this.props}
items={movie[0].alternateTitles}
titles={titles}
/>
);
}
@@ -38,7 +65,7 @@ class MovieTitlesTableContentConnector extends Component {
MovieTitlesTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired
titles: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);

View File

@@ -69,7 +69,7 @@ class EditMovieModalContent extends Component {
const {
monitored,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
// Id,
path,
@@ -110,12 +110,12 @@ class EditMovieModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...qualityProfileId}
name="qualityProfileIds"
{...qualityProfileIds}
onChange={onInputChange}
/>
</FormGroup>

View File

@@ -37,7 +37,7 @@ function createMapStateToProps() {
const movieSettings = {
monitored: movie.monitored,
qualityProfileId: movie.qualityProfileId,
qualityProfileIds: movie.qualityProfileIds,
minimumAvailability: movie.minimumAvailability,
path: movie.path,
tags: movie.tags

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
@@ -11,6 +10,7 @@ import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import translate from 'Utilities/String/translate';
import DeleteMovieModal from './Delete/DeleteMovieModal';
import MovieEditorFooterLabel from './MovieEditorFooterLabel';
import QualityProfilesModal from './QualityProfiles/QualityProfilesModal';
import TagsModal from './Tags/TagsModal';
import styles from './MovieEditorFooter.css';
@@ -26,12 +26,13 @@ class MovieEditorFooter extends Component {
this.state = {
monitored: NO_CHANGE,
qualityProfileId: NO_CHANGE,
minimumAvailability: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
savingQualityProfiles: false,
isDeleteMovieModalOpen: false,
isTagsModalOpen: false,
isQualityProfilesModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
};
@@ -46,10 +47,10 @@ class MovieEditorFooter extends Component {
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
qualityProfileId: NO_CHANGE,
minimumAvailability: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false
savingTags: false,
savingQualityProfiles: false
});
}
}
@@ -91,6 +92,17 @@ class MovieEditorFooter extends Component {
});
};
onApplyQualityProfilesPress = (qualityProfileIds) => {
this.setState({
savingQualityProfiles: true,
isQualityProfilesModalOpen: false
});
this.props.onSaveSelected({
qualityProfileIds
});
};
onDeleteSelectedPress = () => {
this.setState({ isDeleteMovieModalOpen: true });
};
@@ -107,6 +119,14 @@ class MovieEditorFooter extends Component {
this.setState({ isTagsModalOpen: false });
};
onQualityProfilesPress = () => {
this.setState({ isQualityProfilesModalOpen: true });
};
onQualityProfilesModalClose = () => {
this.setState({ isQualityProfilesModalOpen: false });
};
onSaveRootFolderPress = () => {
this.setState({
isConfirmMoveModalOpen: false,
@@ -143,11 +163,13 @@ class MovieEditorFooter extends Component {
const {
monitored,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
rootFolderPath,
savingTags,
savingQualityProfiles,
isTagsModalOpen,
isQualityProfilesModalOpen,
isDeleteMovieModalOpen,
isConfirmMoveModalOpen,
destinationRootFolder
@@ -178,17 +200,18 @@ class MovieEditorFooter extends Component {
<div className={styles.inputContainer}>
<MovieEditorFooterLabel
label={translate('QualityProfile')}
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
label={translate('QualityProfiles')}
isSaving={isSaving && qualityProfileIds !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
<SpinnerButton
className={styles.tagsButton}
isSpinning={isSaving && savingTags && savingQualityProfiles}
isDisabled={!selectedCount || isOrganizingMovie}
onPress={this.onQualityProfilesPress}
>
{translate('SetQualityProfiles')}
</SpinnerButton>
</div>
<div className={styles.inputContainer}>
@@ -243,7 +266,7 @@ class MovieEditorFooter extends Component {
<SpinnerButton
className={styles.tagsButton}
isSpinning={isSaving && savingTags}
isSpinning={isSaving && savingTags && savingQualityProfiles}
isDisabled={!selectedCount || isOrganizingMovie}
onPress={this.onTagsPress}
>
@@ -271,6 +294,13 @@ class MovieEditorFooter extends Component {
onModalClose={this.onTagsModalClose}
/>
<QualityProfilesModal
isOpen={isQualityProfilesModalOpen}
movieIds={movieIds}
onApplyQualityProfilesPress={this.onApplyQualityProfilesPress}
onModalClose={this.onQualityProfilesModalClose}
/>
<DeleteMovieModal
isOpen={isDeleteMovieModalOpen}
movieIds={movieIds}

View File

@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import QualityProfilesModalContent from './QualityProfilesModalContent';
function QualityProfilesModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<QualityProfilesModalContent
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
QualityProfilesModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default QualityProfilesModal;

View File

@@ -0,0 +1,12 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.result {
padding-top: 4px;
}

View File

@@ -0,0 +1,98 @@
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, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class QualityProfilesModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
qualityProfileIds: []
};
}
//
// Lifecycle
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
};
onApplyQualityProfilesPress = () => {
const {
qualityProfileIds
} = this.state;
this.props.onApplyQualityProfilesPress(qualityProfileIds);
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
qualityProfileIds
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('QualityProfiles')}
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
value={qualityProfileIds}
onChange={this.onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={this.onApplyQualityProfilesPress}
>
{translate('Apply')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
QualityProfilesModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onApplyQualityProfilesPress: PropTypes.func.isRequired
};
export default QualityProfilesModalContent;

View File

@@ -0,0 +1,9 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
import styles from './MovieHistoryTable.css';
function MovieHistoryTable(props) {
const {
@@ -7,9 +8,11 @@ function MovieHistoryTable(props) {
} = props;
return (
<MovieHistoryTableContentConnector
{...otherProps}
/>
<div className={styles.container}>
<MovieHistoryTableContentConnector
{...otherProps}
/>
</div>
);
}

View File

@@ -21,16 +21,20 @@ class MovieIndexFooter extends PureComponent {
let totalFileSize = 0;
movies.forEach((s) => {
const { statistics = {} } = s;
if (s.hasFile) {
movieFiles += 1;
}
const {
movieFileCount = 0,
sizeOnDisk = 0
} = statistics;
movieFiles += movieFileCount;
if (s.monitored) {
monitored++;
}
totalFileSize += s.sizeOnDisk;
totalFileSize += sizeOnDisk;
});
return (

View File

@@ -14,16 +14,14 @@ function createUnoptimizedSelector() {
monitored,
status,
statistics,
sizeOnDisk,
hasFile
sizeOnDisk
} = s;
return {
monitored,
status,
statistics,
sizeOnDisk,
hasFile
sizeOnDisk
};
});
}

View File

@@ -5,7 +5,6 @@ import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
import createMovieQualityProfileSelector from 'Store/Selectors/createMovieQualityProfileSelector';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
function selectShowSearchAction() {
@@ -29,13 +28,11 @@ function selectShowSearchAction() {
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
createMovieQualityProfileSelector(),
selectShowSearchAction(),
createExecutingCommandsSelector(),
(state) => state.queue.details.items,
(
movie,
qualityProfile,
showSearchAction,
executingCommands,
queueItems
@@ -68,7 +65,6 @@ function createMapStateToProps() {
return {
...movie,
qualityProfile,
showSearchAction,
isRefreshingMovie,
isSearchingMovie,

View File

@@ -91,14 +91,14 @@ class MovieIndexOverview extends Component {
title,
overview,
monitored,
hasFile,
isAvailable,
status,
titleSlug,
statistics,
images,
posterWidth,
posterHeight,
qualityProfile,
qualityProfileIds,
overviewOptions,
showSearchAction,
showRelativeDates,
@@ -119,6 +119,11 @@ class MovieIndexOverview extends Component {
...otherProps
} = this.props;
const {
sizeOnDisk,
movieFileCount
} = statistics;
const {
isEditMovieModalOpen,
isDeleteMovieModalOpen
@@ -169,7 +174,7 @@ class MovieIndexOverview extends Component {
<MovieIndexProgressBar
monitored={monitored}
hasFile={hasFile}
hasFile={movieFileCount > 0}
isAvailable={isAvailable}
status={status}
posterWidth={posterWidth}
@@ -248,11 +253,12 @@ class MovieIndexOverview extends Component {
<MovieIndexOverviewInfo
height={overviewHeight}
monitored={monitored}
qualityProfile={qualityProfile}
qualityProfileIds={qualityProfileIds}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
sizeOnDisk={sizeOnDisk}
{...overviewOptions}
{...otherProps}
/>
@@ -282,15 +288,15 @@ MovieIndexOverview.propTypes = {
title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
rowHeight: PropTypes.number.isRequired,
qualityProfile: PropTypes.object.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
overviewOptions: PropTypes.object.isRequired,
showSearchAction: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
@@ -312,4 +318,11 @@ MovieIndexOverview.propTypes = {
queueState: PropTypes.string
};
MovieIndexOverview.defaultProps = {
statistics: {
movieFileCount: 0,
sizeOnDisk: 0
}
};
export default MovieIndexOverview;

View File

@@ -79,13 +79,13 @@ function getInfoRowProps(row, props) {
};
}
if (name === 'qualityProfileId') {
return {
title: 'Quality Profile',
iconName: icons.PROFILE,
label: props.qualityProfile.name
};
}
// if (name === 'qualityProfileId') {
// return {
// title: 'Quality Profile',
// iconName: icons.PROFILE,
// label: props.qualityProfile.name
// };
// }
if (name === 'added') {
const {

View File

@@ -87,7 +87,6 @@ class MovieIndexPoster extends Component {
youTubeTrailerId,
title,
monitored,
hasFile,
isAvailable,
status,
titleSlug,
@@ -98,7 +97,7 @@ class MovieIndexPoster extends Component {
showTitle,
showMonitored,
showQualityProfile,
qualityProfile,
qualityProfileIds,
showSearchAction,
showRelativeDates,
shortDateFormat,
@@ -117,6 +116,7 @@ class MovieIndexPoster extends Component {
onSelectedChange,
queueStatus,
queueState,
statistics,
...otherProps
} = this.props;
@@ -242,7 +242,7 @@ class MovieIndexPoster extends Component {
<MovieIndexProgressBar
monitored={monitored}
hasFile={hasFile}
hasFile={statistics.movieFileCount > 0}
status={status}
posterWidth={posterWidth}
detailedProgressBar={detailedProgressBar}
@@ -265,12 +265,12 @@ class MovieIndexPoster extends Component {
</div>
}
{
{/* {
showQualityProfile &&
<div className={styles.title}>
{qualityProfile.name}
</div>
}
} */}
{
showCinemaRelease && inCinemas &&
@@ -324,7 +324,7 @@ class MovieIndexPoster extends Component {
}
<MovieIndexPosterInfo
qualityProfile={qualityProfile}
qualityProfileIds={qualityProfileIds}
showQualityProfile={showQualityProfile}
showReleaseDate={showReleaseDate}
showRelativeDates={showRelativeDates}
@@ -357,7 +357,6 @@ MovieIndexPoster.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
@@ -368,7 +367,7 @@ MovieIndexPoster.propTypes = {
showTitle: PropTypes.bool.isRequired,
showMonitored: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired,
qualityProfile: PropTypes.object.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
showSearchAction: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
@@ -389,7 +388,8 @@ MovieIndexPoster.propTypes = {
imdbId: PropTypes.string,
youTubeTrailerId: PropTypes.string,
queueStatus: PropTypes.string,
queueState: PropTypes.string
queueState: PropTypes.string,
statistics: PropTypes.object
};
MovieIndexPoster.defaultProps = {

View File

@@ -25,7 +25,7 @@
}
.originalLanguage,
.qualityProfileId {
.qualityProfileIds {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 1 0 125px;

View File

@@ -32,7 +32,7 @@
}
.originalLanguage,
.qualityProfileId {
.qualityProfileIds {
composes: cell;
flex: 1 0 125px;

View File

@@ -4,6 +4,7 @@ import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import QualityProfileListConnector from 'Components/QualityProfileListConnector';
import RottenTomatoRating from 'Components/RottenTomatoRating';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
@@ -77,8 +78,9 @@ class MovieIndexRow extends Component {
titleSlug,
collection,
studio,
qualityProfile,
qualityProfileIds,
added,
statistics,
year,
inCinemas,
physicalRelease,
@@ -88,7 +90,6 @@ class MovieIndexRow extends Component {
runtime,
minimumAvailability,
path,
sizeOnDisk,
genres,
ratings,
certification,
@@ -107,6 +108,10 @@ class MovieIndexRow extends Component {
movieRuntimeFormat
} = this.props;
const {
sizeOnDisk
} = statistics;
const {
isEditMovieModalOpen,
isDeleteMovieModalOpen
@@ -210,13 +215,15 @@ class MovieIndexRow extends Component {
);
}
if (name === 'qualityProfileId') {
if (name === 'qualityProfileIds') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
{qualityProfile.name}
<QualityProfileListConnector
qualityProfileIds={qualityProfileIds}
/>
</VirtualTableRowCell>
);
}
@@ -498,8 +505,9 @@ MovieIndexRow.propTypes = {
originalLanguage: PropTypes.object.isRequired,
studio: PropTypes.string,
collection: PropTypes.object,
qualityProfile: PropTypes.object.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
added: PropTypes.string,
statistics: PropTypes.object.isRequired,
year: PropTypes.number,
inCinemas: PropTypes.string,
physicalRelease: PropTypes.string,
@@ -507,7 +515,6 @@ MovieIndexRow.propTypes = {
runtime: PropTypes.number,
minimumAvailability: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
ratings: PropTypes.object.isRequired,
certification: PropTypes.string,
@@ -530,6 +537,10 @@ MovieIndexRow.propTypes = {
};
MovieIndexRow.defaultProps = {
statistics: {
movieFileCount: 0,
releaseGroups: []
},
genres: [],
tags: []
};

View File

@@ -9,13 +9,17 @@ function MovieFileStatus(props) {
const {
isAvailable,
monitored,
movieFile,
queueStatus,
queueState,
statistics,
colorImpairedMode
} = props;
const hasMovieFile = !!movieFile;
const {
movieFileCount
} = statistics;
const hasMovieFile = movieFileCount > 0;
const hasReleased = isAvailable;
if (queueStatus) {
@@ -30,12 +34,10 @@ function MovieFileStatus(props) {
}
if (hasMovieFile) {
const quality = movieFile.quality;
return (
<div className={styles.center}>
<span className={styles.ended} />
{quality.quality.name}
Downloaded
</div>
);
}
@@ -79,10 +81,16 @@ function MovieFileStatus(props) {
MovieFileStatus.propTypes = {
isAvailable: PropTypes.bool,
monitored: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
statistics: PropTypes.object,
queueStatus: PropTypes.string,
queueState: PropTypes.string,
colorImpairedMode: PropTypes.bool.isRequired
};
MovieFileStatus.defaultProps = {
statistics: {
movieFileCount: 0
}
};
export default MovieFileStatus;

View File

@@ -16,7 +16,7 @@ function createMapStateToProps() {
isAvailable: movie.isAvailable,
monitored: movie.monitored,
grabbed: movie.grabbed,
movieFile: movie.movieFile,
statistics: movie.statistics,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}

Some files were not shown because too many files have changed in this diff Show More