1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

...

125 Commits

Author SHA1 Message Date
Bogdan 6de0feda65 Filter movies by TmdbId and ImdbId in Select movies Modal 2023-08-26 20:41:54 +03:00
Stevie Robinson 0f699a01f7 Add translations to frontend/InteractiveImport
(cherry picked from commit 060b66aa398f7e676b789354361b6fe95a96ff17)

Closes #9027
2023-08-23 20:20:44 +03:00
Stevie Robinson be20a9d116 Translate Frontend InteractiveSearch
(cherry picked from commit efca70438899c2f22e2be060011b58325e4f4705)

Closes #9027
2023-08-23 19:42:45 +03:00
Stevie Robinson 4c2fcef742 Translate Frontend Parse modal
(cherry picked from commit c14fd2b4a3cc2d0b270c5abe02240b22632e34b5)

Closes #9051
2023-08-23 16:35:12 +03:00
Weblate 15a4c3b742 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Renan da Mota Ciciliato <renanciciliato@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
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/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
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/uk/
Translation: Servarr/Radarr
2023-08-23 05:00:46 +03:00
Bogdan 7b4f908f6d Prevent health checks warnings for disabled notifications
(cherry picked from commit 5a7f42a63e25d6abdb187c37e92a908a6b85fb4d)
2023-08-23 04:59:46 +03:00
Weblate 1b4dd405be Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Renan da Mota Ciciliato <renanciciliato@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2023-08-23 04:59:19 +03:00
Servarr 135de2cad4 Automated API Docs update 2023-08-22 23:59:28 +03:00
Bogdan 174ea347a8 Cleanup InteractiveSearchRowProps 2023-08-22 23:28:34 +03:00
Mark McDowall 9b4f80535e Override release grab modal
New: Option to override release and grab
New: Option to select download client when multiple of the same type are configured

(cherry picked from commit 07f0fbf9a51d54e44681fd0f74df4e048bff561a)
2023-08-22 23:20:47 +03:00
Bogdan 07b69e665d Fix translation usage for IMDbId and TMDBId 2023-08-22 23:20:47 +03:00
Jendrik Weise 99441dfa67 Prevent exception when renaming after script import
(cherry picked from commit 2166e4dce419ae477f2e4ce297bee2d8324a190e)

Closes #9050
2023-08-22 16:32:42 +03:00
Bogdan 8e80c85f03 Revert "Switch to Parallel.ForEach for list processing with MaxParallelism"
This reverts commit 0f93e04186.
2023-08-22 06:05:17 +03:00
Bogdan 429217d1d4 Remove Reddit from issue templates 2023-08-22 02:54:34 +03:00
Servarr 8257e01995 Automated API Docs update 2023-08-21 21:52:23 +03:00
Qstick bd3fad9636 Rename Source to QualitySource 2023-08-20 22:53:11 -05:00
Servarr 3cbdba51e9 Automated API Docs update 2023-08-20 22:16:01 -05:00
Qstick c70ce92ee9 New: Cleanup Alternative Title model and code 2023-08-20 22:07:33 -05:00
Qstick c1a3a8249b Use default MemoryAllocator for ImageSharp resizing 2023-08-20 20:21:13 -05:00
Qstick 0f93e04186 Switch to Parallel.ForEach for list processing with MaxParallelism 2023-08-20 20:21:13 -05:00
Qstick fef666831f Fixed: Ignore case when comparing torrent infohash
(cherry picked from commit 7986488c6d1687b0810b3bcac2c1dae725e770ac)
2023-08-20 16:09:18 -05:00
Qstick 681a36e34f New: Added additional term for matching French language releases
Closes #7209
2023-08-20 15:08:50 -05:00
Servarr 726b71027e Automated API Docs update 2023-08-20 23:03:00 +03:00
Qstick a8feef7e88 Fix using in CalendarController 2023-08-20 14:52:55 -05:00
Qstick 70b725a2dc New: Use file's format title for quality if parsed
Closes #7993

(cherry picked from commit 599ad86657bbb8125c4354000cfc94331041f984)
2023-08-20 14:52:55 -05:00
Qstick 4b3bd86e0f Improvements to Calendar translation mapping 2023-08-20 14:29:23 -05:00
Mark McDowall 3878196f39 New: Calendar filtering by tags
Closes #8502

(cherry picked from commit 62b948b24c4b9c572db225cb19985444d3d80c0f)
2023-08-20 14:29:23 -05:00
Mark McDowall a39cafe404 Improve CF calculation for files without scene name
Fixed: Use original filename instead of complete path when calculating CF for existing file without scene name

(cherry picked from commit 997aabbc3cfc3c9c5c220786d1d08cfceec5e2f2)

Closes #8115
2023-08-20 22:08:27 +03:00
Mark McDowall d9e337f2fb Fixed: Search for newly added movie if disk rescanning is disabled after refresh
(cherry picked from commit 00ab449ebeb53893b8d4c939e072451845a4d69e)

Closes #7543
2023-08-20 22:08:27 +03:00
Qstick 3412e4139e Added table identifier to OrderBy to avoid column ambiguity on joins
Co-Authored-By: Richard <1252123+kharenis@users.noreply.github.com>

(cherry picked from commit c57ceac4debf7419be84096f997ba7b75c906586)
2023-08-20 14:04:26 -05:00
Servarr b7bacf785c Automated API Docs update 2023-08-20 13:59:52 -05:00
Mark McDowall c6e3f3c26c New: Added Mediainfo Video Dynamic Range column for movies
(cherry picked from commit ae0e23fc8ee450a20b43ca622eeccd0759451a2f)

Closes #7247
2023-08-20 21:45:50 +03:00
Mark McDowall e4c5fc5c6e Sync LocalizationService tests with upstream
(cherry picked from commit 9c7fab69fd008a423f3607de71e0c76200e84ea5)

Closes #8978
2023-08-20 21:45:50 +03:00
Mark McDowall 3c42ad0f7f Fixed: Allow Min/Max age to be the same for year auto tagging
(cherry picked from commit fc6ac3ddf191025d4ebe3af542fbd97ef981f0ca)
2023-08-20 21:26:27 +03:00
ItsME6969 5236d46c2b FIxed: Correctly parse German scene bluray REMUXes (#8643) 2023-08-20 11:23:04 -05:00
Servarr 6f54a9e452 Automated API Docs update 2023-08-20 18:40:46 +03:00
Bogdan 4b9107465c New: Add table options for movie files details 2023-08-20 18:27:20 +03:00
Mark McDowall 329e43c331 Fixed: Unknown audio language appearing as 'root'
(cherry picked from commit 5411863f6a00071201dd50dc6d2f33477e6ffb29)
2023-08-20 18:27:20 +03:00
Mark McDowall f05f25af0c Fixed: Invalid audio language leading to UI error
(cherry picked from commit 8fbbe21d814ccdeda7727b5fb83f99ea81f5b225)
2023-08-20 18:27:20 +03:00
Mark McDowall e50abd276e Fixed: Displaying audio and subtitle languages in UI
(cherry picked from commit 139412284276479921632ee5ef1dabe76c5388b4)

Rename LocalizationLanguageResource to avoid collision with LanguageResource

(cherry picked from commit d2cd3f77169887086980feac3bab1f16301d189e)
2023-08-20 18:27:20 +03:00
Mark McDowall 933d9e074c Option to show audio/subtitle language on movie details (first two unique languages will be shown)
(cherry picked from commit c10677dfe7098295fde39517b3983e8f3f22823d)
2023-08-20 18:27:20 +03:00
Weblate 993e4ca298 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2023-08-20 09:21:28 -05:00
bakerboy448 58eb24ff89 New: Default RSS Sync Interval to 30 minutes 2023-08-20 09:14:31 -05:00
Bogdan 9516729385 Fix typo in queryTranslations 2023-08-20 13:09:19 +03:00
Mark McDowall d626f0487d Fixed: Don't reimport the same file from the same release unless grabbed again
(cherry picked from commit 0274778679a8fd485a651eea9d293463528244fd)

Closes #9055
2023-08-20 13:06:42 +03:00
Bogdan 1350ccb236 Fix translations for queue actions and security setting 2023-08-19 23:38:04 +03:00
Bogdan 63d05a6e78 Prevent useless builds 2023-08-19 17:21:34 +03:00
Servarr f60b27355b Automated API Docs update 2023-08-19 13:55:14 +03:00
Stevie Robinson abd63ea2a4 Add info box to Remote Path Mappings Settings
(cherry picked from commit d8f3d7d3eafeafd8d4372db0076a63f935a29218)

Closes #9039
2023-08-19 13:24:39 +03:00
Mark McDowall 655f49b8c9 Fixed: Allow decimals for Custom Format size
(cherry picked from commit 7f5ddff568ce9f87bd45420cbd36690b190bd633)

Closes #9043
Fixes #6147
2023-08-19 13:24:39 +03:00
Stevie Robinson d8c1fe5486 Fix Typo in QualitySource Enum
(cherry picked from commit 1bcef1b4a5dda8a473bb9b3316ea8b09cc1ee022)
2023-08-19 13:14:44 +03:00
Mark McDowall 8afe4e8979 New: Success check mark on blue buttons is now white instead of green
(cherry picked from commit 566fae9d5857a10bd69c718368e7847e5a733faa)
2023-08-19 12:48:45 +03:00
Qstick 1935abbde2 Fix grammar error for collections selection 2023-08-18 23:31:37 -05:00
Mark McDowall fdc6c66f7a Fixed: Ignore IOException deleting download folder after import
(cherry picked from commit d05cb40088a51eef5a2830bf2b55f5d05955578f)
2023-08-18 21:28:38 -05:00
Bogdan def127b93f Fix flaky automation tests 2023-08-18 21:21:17 -05:00
Mark McDowall c75d398f14 New: Status message when downloading metadata in qBittorrent
(cherry picked from commit 8aa872edf4737798d4836f68fbd0697ee0511c41)
2023-08-18 21:14:33 -05:00
nuxen d4fada9b4e fix(parser): added more tests and moved YTS 2023-08-18 21:13:05 -05:00
nuxen 111c081545 fix(parser): not correctly recognizing YIFY RlsGrp 2023-08-18 21:13:05 -05:00
bakerboy448 7f3e7b360b Remove reddit from readme 2023-08-18 12:06:18 +03:00
Weblate 329e37774f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: w2861 <hfagfc@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-08-18 12:05:56 +03:00
Mark McDowall 4a4037323e Fixed: Hidden files being ignored
(cherry picked from commit d493f8762fcb1684b44e182753c21d7a493db787)

Closes #9023
2023-08-18 11:23:22 +03:00
Bogdan 2d72c1ef34 Replace docker detection for cgroup v2
(cherry picked from commit 78d4dee4610c5f3f90cc69469004008aa64900b8)
2023-08-18 10:57:39 +03:00
Qstick 337d01e4ed Add housekeeper for orphaned list movies 2023-08-17 22:47:07 -05:00
Qstick 927ae86e44 Fixed: Don't Clean if no lists synced
Fixes #9011
2023-08-17 22:47:07 -05:00
Qstick fefdd71b6d Fixed: Avoid error in manual interaction notifications 2023-08-17 22:41:24 -05:00
jack-mil 328850627a New: Improved Discord add/delete notifications
(cherry picked from commit 1a4403e0ab9ab824c97acf97174f894b3770f2ef)

Closes #8886
2023-08-17 23:54:05 +03:00
Bogdan f412228383 Change DownloadReport to private 2023-08-17 23:09:54 +03:00
Stevie Robinson dc82d0b6dd Fix RemoveHelpTextWarning > RemoveFromDownloadClientHelpTextWarning
(cherry picked from commit 901b6d20841bfcb2a3724fe27b0fbddf5e41d669)

Closes #8969
2023-08-17 23:09:16 +03:00
Qstick 0e83c42f3a Cleanup other provider status code
(cherry picked from commit c281a7818adce8db728d2a104f4444cb9c0baf2c)
2023-08-17 23:01:59 +03:00
Qstick fa80e8b7a2 New: Notifications (Connect) Status
(cherry picked from commit e3545801721e00d4e5cac3fa534e66dcbe9d2d05)
(cherry picked from commit cb27b05a6c046ca0a6e4998f7e7ecd6b45add1a2)
2023-08-17 23:01:59 +03:00
Bogdan c03453f6f7 Add default update branches as autocomplete values 2023-08-17 22:42:27 +03:00
Mark McDowall 3ffb36a2df Fixed: Don't block updates under docker unless configured in package_info
(cherry picked from commit 5a7e34e291c2715aa67161e5c455d25e80f498df)
2023-08-17 22:42:27 +03:00
Bogdan 0a04fad85b Show warning when using the docker update mechanism
(cherry picked from commit cc538c4b2d33a1734c45c0667776d946596107e9)
2023-08-17 22:42:27 +03:00
Weblate 3c7f7f2e03 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Robert A. Viana <robert.abreu@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-08-17 14:30:16 +03:00
Servarr 32ec9d4872 Automated API Docs update 2023-08-16 21:39:20 -05:00
Qstick c8e04f0c35 Bump Nlog and NUnit 2023-08-16 20:51:25 -05:00
Qstick d6f849ac95 Bump dotnet packages 2023-08-16 20:47:19 -05:00
Qstick fcea483612 Bump Newtonsoft.Json to 13.0.3 2023-08-16 20:43:41 -05:00
Qstick bcd87a3a30 Bump DryIoc to 5.4.1 2023-08-16 20:34:25 -05:00
Qstick e3bcc3da3f Bump Dapper to 2.0.143 2023-08-16 20:33:52 -05:00
Qstick 056c2b5233 Rename Profiles to QualityProfiles 2023-08-16 20:04:42 -05:00
Servarr a946546793 Automated API Docs update 2023-08-15 16:14:16 +03:00
Qstick f9f44aec7a Fixed: Creating new Delay Profile
Fixes #8077

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2023-08-14 22:09:49 -05:00
ricci2511 99ff6aa9c4 New: Convert restrictions to release profiles 2023-08-14 21:26:22 -05:00
Mark McDowall ca93a72d63 New: Show all options when authentication modal is open
(cherry picked from commit c7d6c0f45264944bb5c00374ff025344218ef7eb)
2023-08-14 20:05:03 -05:00
Mark McDowall 0c6eae256b Don't replace private values that haven't been set
(cherry picked from commit 52760e0908fa9852ed8a770f1916bb582eb8c8b4)
2023-08-14 20:05:03 -05:00
Mark McDowall 508a15e09a New: Don't return API Keys and Passwords via the API
(cherry picked from commit 570be882154e73f8ad1de5b16b957bcb964697fd)
2023-08-14 20:05:03 -05:00
Qstick 180dafe696 Handle auth options correctly in Security Settings
(cherry picked from commit 0fad20e327503bac767c4df4c893f5e418866831)
2023-08-14 20:05:03 -05:00
Qstick e3160466e0 Bump SQLite to 3.42.0 (1.0.118) 2023-08-14 20:05:03 -05:00
Marty Zalega 9ccefe0095 Don't lowercase UrlBase in ConfigFileProvider
UrlBase should honour the case it is given.

(cherry picked from commit e1de523c89f7649e64f520b090bbdb2f56cc4b85)
2023-08-14 20:05:03 -05:00
Mark McDowall 104aadfdb7 New: Migrate user passwords to Pbkdf2
(cherry picked from commit 269e72a2193b584476bec338ef41e6fb2e5cbea6)
2023-08-14 20:05:03 -05:00
Qstick 8911386ed0 New: Rework and Require Authentication
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2023-08-14 20:05:03 -05:00
Qstick 1e6540a419 Bump Version to 5
(cherry picked from commit 21afbbc66d294cfeda47b7dacb785a17dae8eb1c)
2023-08-14 20:05:03 -05:00
Weblate 693f8dc391 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: David Molero <contact@dolvem.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: deepserket <deepserket@gmail.com>
Co-authored-by: matt <diabolino7@pm.me>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
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/uk/
Translation: Servarr/Radarr
2023-08-15 01:55:13 +03:00
Servarr 576e1e76af Automated API Docs update 2023-08-15 01:44:17 +03:00
Mark McDowall 1f8877d192 New: Add bypass if above Custom Format Score to Delay Profile
(cherry picked from commit 4ed4ca4804ce973c1b88c1c4ede8ae00547ac834)
2023-08-14 22:14:29 +03:00
Bogdan 8c93123126 New: Default name when adding providers 2023-08-14 02:35:00 +03:00
Bogdan dd614ac005 Use named tokens in frontend translate function 2023-08-13 23:09:58 +03:00
Mark McDowall 82de5d6f9a New: Auto tag series based on Original Language
(cherry picked from commit 2a7964bc16fa32bc0000bb7326795d02cc41bfed)
2023-08-13 21:21:02 +03:00
Bogdan e8e54fdf99 New: Add options to show ratings in movie poster info 2023-08-13 04:51:35 +03:00
Bogdan c3b856401e Add one minute back-off level for all providers
(cherry picked from commit d8f314ff0ef64e8d90b21b7865e46be74db5e570)
2023-08-12 10:04:25 +03:00
Stepan Goremykin 25f6f3ec6d Update FluentAssertions
(cherry picked from commit 951a9ade00d7c9105f03608cb598450d706b826f)
2023-08-12 10:04:25 +03:00
Servarr d28eb47a1a Automated API Docs update 2023-08-11 20:21:44 -05:00
Bogdan 431bc14e76 New: Show Custom Format Score for movies in Files tab
Closes #8818
2023-08-12 03:28:49 +03:00
Bogdan efe5c3beb7 New: Async HttpClient
(cherry picked from commit e12111cee885e23a42308f299ef773e5ae021712)
2023-08-12 02:07:29 +03:00
Bogdan d61ce6112b New: Use HTTP/2 in HttpClient
(cherry picked from commit 9ee09b9186a267844bd99b1a33f5959ba6461750)
2023-08-12 02:07:29 +03:00
Bogdan 531e948687 Align DownloadService with upstream 2023-08-12 02:07:29 +03:00
Weblate 7ad4411e4d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Nir Israel Hen <nirisraelh@gmail.com>
Co-authored-by: PerOHaugstad <perohaugstad@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: matt <diabolino7@pm.me>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
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/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translation: Servarr/Radarr
2023-08-12 01:14:02 +03:00
Robin Dadswell e8e23e41dc bump Npgsql to 7.0.4 2023-08-10 16:45:17 +01:00
Robin Dadswell 0c1fc49d69 Adds Pipeline testing for Postgres15 Databases 2023-08-10 16:45:17 +01:00
Mark McDowall 83632f91e6 New: Add additional logging when renaming extra files
(cherry picked from commit 1ae0dc81f73ef74078f07fd5536a7d9058df649d)

Closes #8966
2023-08-10 12:25:34 +03:00
Bogdan 1bbd08a5a0 New: Show successful grabs in Interactive Search with green icon
(cherry picked from commit 366b2b8b52d8375f1f41719a09893136009a5b48)

Closes #8964
2023-08-10 12:19:45 +03:00
Bogdan 298077940e Fixed: Ensure failing providers are marked as failed when testing all
(cherry picked from commit f6c05d4456a5667398319e249614e2eed115621e)

Closes #8960
2023-08-10 12:14:42 +03:00
Bogdan 4fb632e4fc Fix FileList test 2023-08-10 12:12:17 +03:00
Bogdan 7bcb492572 Fixed: (FileList) Switch to Basic Auth 2023-08-10 01:36:30 +03:00
Weblate a673535417 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albert <zuozl1992@foxmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ivan Mazzoli <dreadtank27@gmail.com>
Co-authored-by: TrojanHorsePower <alaa_alahmad@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: byakurau <byakurau1@gmail.com>
Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
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_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-08-09 18:30:53 +03:00
Mark McDowall e0d70dc341 Fixed: Allow Original Language in Custom Format
(cherry picked from commit 6103c023de0d5b28d96931663ef2185dbd4c5491)
2023-08-09 18:30:29 +03:00
Bogdan aa98b2bac9 Fixed border for actions in health status 2023-08-09 16:14:50 +03:00
Bogdan 145f67d14b Fixed: Detect Docker when using control group v2 2023-08-09 10:47:48 +03:00
Mark McDowall caea810908 Fixed: Allow Unknown Language in Custom Format
(cherry picked from commit 65323d5e872cb87b1f3d16c520aef373f4447915)
2023-08-08 11:59:27 +03:00
Qstick 9a567b93d0 New: Performance tweaks to MovieLookup endpoint 2023-08-06 21:38:18 -05:00
Qstick 6ecd41bc5a Fixed: Error trying to notify user when process not UserInteractive
Closes #8927
2023-08-06 21:24:04 -05:00
Qstick d5b4f0efa9 Cleanup MovieService 2023-08-06 21:17:51 -05:00
Bogdan b337f62a34 Fixed: Add translations for import list movies in Discover 2023-08-06 22:57:51 +03:00
Bogdan c42fc6094d Bump version to 4.7.5 2023-08-06 19:29:18 +03:00
517 changed files with 8264 additions and 4256 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
name: Bug Report name: Bug Report
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first' description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
labels: ['Type: Bug', 'Status: Needs Triage'] labels: ['Type: Bug', 'Status: Needs Triage']
body: body:
- type: checkboxes - type: checkboxes
-3
View File
@@ -3,6 +3,3 @@ contact_links:
- name: Support via Discord - name: Support via Discord
url: https://radarr.video/discord url: https://radarr.video/discord
about: Chat with users and devs on support and setup related topics. about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/radarr
about: Discuss and search thru support topics.
+2 -2
View File
@@ -19,9 +19,9 @@ jobs:
issue-comment: > issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively :wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://radarr.video/discord) to be a support request. Please hop over onto our [Discord](https://radarr.video/discord).
or [Subreddit](https://reddit.com/r/radarr)
close-issue: true close-issue: true
close-reason: 'not planned'
lock-issue: false lock-issue: false
- uses: dessant/support-requests@v3 - uses: dessant/support-requests@v3
with: with:
-1
View File
@@ -35,7 +35,6 @@ Note that only one type of a given movie is supported. If you want both an 4k ve
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr) [![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Radarr)
Note: GitHub Issues are for Bugs and Feature Requests Only Note: GitHub Issues are for Bugs and Feature Requests Only
+131 -7
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '4.7.4' majorVersion: '5.0.0'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -27,6 +27,10 @@ trigger:
include: include:
- develop - develop
- master - master
paths:
exclude:
- .github
- src/Radarr.Api.*/openapi.json
pr: pr:
branches: branches:
@@ -34,6 +38,7 @@ pr:
- develop - develop
paths: paths:
exclude: exclude:
- .github
- src/NzbDrone.Core/Localization/Core - src/NzbDrone.Core/Localization/Core
- src/Radarr.Api.*/openapi.json - src/Radarr.Api.*/openapi.json
@@ -536,8 +541,8 @@ stages:
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres - job: Unit_LinuxCore_Postgres14
displayName: Unit Native LinuxCore with Postgres Database displayName: Unit Native LinuxCore with Postgres14 Database
dependsOn: Prepare dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
@@ -589,7 +594,63 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests' testRunTitle: 'LinuxCore Postgres14 Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres15
displayName: Unit Native LinuxCore with Postgres15 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Radarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests
Radarr__Postgres__Host: 'localhost'
Radarr__Postgres__Port: '5432'
Radarr__Postgres__User: 'radarr'
Radarr__Postgres__Password: 'radarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: |
chmod a+x _tests/ffprobe
displayName: Make ffprobe Executable
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=radarr \
-e POSTGRES_USER=radarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- stage: Integration - stage: Integration
@@ -675,8 +736,8 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres - job: Integration_LinuxCore_Postgres14
displayName: Integration Native LinuxCore with Postgres Database displayName: Integration Native LinuxCore with Postgres14 Database
dependsOn: Prepare dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
@@ -733,7 +794,70 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests' testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres15
displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Radarr.*.linux-core-x64.tar.gz'
Radarr__Postgres__Host: 'localhost'
Radarr__Postgres__Port: '5432'
Radarr__Postgres__User: 'radarr'
Radarr__Postgres__Password: 'radarr'
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=radarr \
-e POSTGRES_USER=radarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
+2 -2
View File
@@ -35,7 +35,7 @@ function QueueDetails(props) {
<Icon <Icon
name={icons.DOWNLOAD} name={icons.DOWNLOAD}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('ImportFailedInterp', [errorMessage])} title={translate('ImportFailedInterp', { errorMessage })}
/> />
); );
} }
@@ -76,7 +76,7 @@ function QueueDetails(props) {
<Icon <Icon
name={icons.DOWNLOADING} name={icons.DOWNLOADING}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DownloadFailedInterp', [errorMessage])} title={translate('DownloadFailedInterp', { errorMessage })}
/> />
); );
} }
@@ -107,14 +107,14 @@ function QueueStatusCell(props) {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING; iconKind = kinds.WARNING;
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails'); const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
title = translate('DownloadWarning', [warningMessage]); title = translate('DownloadWarning', { warningMessage });
} }
if (hasError) { if (hasError) {
if (status === 'completed') { if (status === 'completed') {
iconName = icons.DOWNLOAD; iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
title = translate('ImportFailed', [sourceTitle]); title = translate('ImportFailed', { sourceTitle });
} else { } else {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
@@ -88,12 +88,12 @@ class RemoveQueueItemModal extends Component {
onModalClose={this.onModalClose} onModalClose={this.onModalClose}
> >
<ModalHeader> <ModalHeader>
{translate('Remove')} - {sourceTitle} {translate('RemoveQueueItem', { sourceTitle })}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
{translate('RemoveFromQueueText', [sourceTitle])} {translate('RemoveQueueItemConfirmation', { sourceTitle })}
</div> </div>
{ {
@@ -106,7 +106,7 @@ class RemoveQueueItemModal extends Component {
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="remove" name="remove"
value={remove} value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')} helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
isDisabled={!canIgnore} isDisabled={!canIgnore}
onChange={this.onRemoveChange} onChange={this.onRemoveChange}
/> />
@@ -94,7 +94,7 @@ class RemoveQueueItemsModal extends Component {
<ModalBody> <ModalBody>
<div className={styles.message}> <div className={styles.message}>
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')} {selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
</div> </div>
{ {
@@ -155,7 +155,7 @@ class AddNewMovie extends Component {
!isFetching && !error && !items.length && !!term && !isFetching && !error && !items.length && !!term &&
<div className={styles.message}> <div className={styles.message}>
<div className={styles.noResults}> <div className={styles.noResults}>
{translate('CouldNotFindResults', [term])} {translate('CouldNotFindResults', { term })}
</div> </div>
<div> <div>
{translate('YouCanAlsoSearch')} {translate('YouCanAlsoSearch')}
@@ -119,7 +119,7 @@ class ImportMovie extends Component {
rootFoldersPopulated && rootFoldersPopulated &&
!unmappedFolders.length ? !unmappedFolders.length ?
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
{translate('AllMoviesInPathHaveBeenImported', [path])} {translate('AllMoviesInPathHaveBeenImported', { path })}
</Alert> : </Alert> :
null null
} }
+2
View File
@@ -1,4 +1,5 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState'; import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState'; import CommandAppState from './CommandAppState';
import MovieCollectionAppState from './MovieCollectionAppState'; import MovieCollectionAppState from './MovieCollectionAppState';
import MovieFilesAppState from './MovieFilesAppState'; import MovieFilesAppState from './MovieFilesAppState';
@@ -43,6 +44,7 @@ export interface CustomFilter {
} }
interface AppState { interface AppState {
calendar: CalendarAppState;
commands: CommandAppState; commands: CommandAppState;
interactiveImport: InteractiveImportAppState; interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState; movieCollections: MovieCollectionAppState;
@@ -0,0 +1,9 @@
import AppSectionState from 'App/State/AppSectionState';
import Movie from 'Movie/Movie';
import { FilterBuilderProp } from './AppState';
interface CalendarAppState extends AppSectionState<Movie> {
filterBuilderProps: FilterBuilderProp<Movie>[];
}
export default CalendarAppState;
+3
View File
@@ -22,6 +22,9 @@ export interface MovieIndexAppState {
showQualityProfile: boolean; showQualityProfile: boolean;
showReleaseDate: boolean; showReleaseDate: boolean;
showCinemaRelease: boolean; showCinemaRelease: boolean;
showTmdbRating: boolean;
showImdbRating: boolean;
showRottenTomatoesRating: boolean;
showSearchAction: boolean; showSearchAction: boolean;
}; };
@@ -0,0 +1,56 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setCalendarFilter } from 'Store/Actions/calendarActions';
function createCalendarSelector() {
return createSelector(
(state: AppState) => state.calendar.items,
(calendar) => {
return calendar;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.calendar.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface SeriesIndexFilterModalProps {
isOpen: boolean;
}
export default function CalendarFilterModal(
props: SeriesIndexFilterModalProps
) {
const sectionItems = useSelector(createCalendarSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'calendar';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setCalendarFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}
+5 -1
View File
@@ -14,6 +14,7 @@ import NoMovie from 'Movie/NoMovie';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import CalendarConnector from './CalendarConnector'; import CalendarConnector from './CalendarConnector';
import CalendarFilterModal from './CalendarFilterModal';
import CalendarLinkModal from './iCal/CalendarLinkModal'; import CalendarLinkModal from './iCal/CalendarLinkModal';
import LegendConnector from './Legend/LegendConnector'; import LegendConnector from './Legend/LegendConnector';
import CalendarOptionsModal from './Options/CalendarOptionsModal'; import CalendarOptionsModal from './Options/CalendarOptionsModal';
@@ -83,6 +84,7 @@ class CalendarPage extends Component {
movieIsFetching, movieIsFetching,
movieIsPopulated, movieIsPopulated,
missingMovieIds, missingMovieIds,
customFilters,
isRssSyncExecuting, isRssSyncExecuting,
isSearchingForMissing, isSearchingForMissing,
useCurrentPage, useCurrentPage,
@@ -137,7 +139,8 @@ class CalendarPage extends Component {
isDisabled={!hasMovie} isDisabled={!hasMovie}
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={[]} customFilters={customFilters}
filterModalConnectorComponent={CalendarFilterModal}
onFilterSelect={onFilterSelect} onFilterSelect={onFilterSelect}
/> />
</PageToolbarSection> </PageToolbarSection>
@@ -208,6 +211,7 @@ CalendarPage.propTypes = {
movieIsFetching: PropTypes.bool.isRequired, movieIsFetching: PropTypes.bool.isRequired,
movieIsPopulated: PropTypes.bool.isRequired, movieIsPopulated: PropTypes.bool.isRequired,
missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired, missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired, isRssSyncExecuting: PropTypes.bool.isRequired,
isSearchingForMissing: PropTypes.bool.isRequired, isSearchingForMissing: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired, useCurrentPage: PropTypes.bool.isRequired,
@@ -5,6 +5,7 @@ import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage'; import withCurrentPage from 'Components/withCurrentPage';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions'; import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector'; import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
@@ -59,6 +60,7 @@ function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.calendar.selectedFilterKey, (state) => state.calendar.selectedFilterKey,
(state) => state.calendar.filters, (state) => state.calendar.filters,
createCustomFiltersSelector('calendar'),
createMovieCountSelector(), createMovieCountSelector(),
createUISettingsSelector(), createUISettingsSelector(),
createMissingMovieIdsSelector(), createMissingMovieIdsSelector(),
@@ -67,6 +69,7 @@ function createMapStateToProps() {
( (
selectedFilterKey, selectedFilterKey,
filters, filters,
customFilters,
movieCount, movieCount,
uiSettings, uiSettings,
missingMovieIds, missingMovieIds,
@@ -76,6 +79,7 @@ function createMapStateToProps() {
return { return {
selectedFilterKey, selectedFilterKey,
filters, filters,
customFilters,
colorImpairedMode: uiSettings.enableColorImpairedMode, colorImpairedMode: uiSettings.enableColorImpairedMode,
hasMovie: !!movieCount.count, hasMovie: !!movieCount.count,
movieError: movieCount.error, movieError: movieCount.error,
@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
render() { render() {
const { const {
className,
titleClassName, titleClassName,
descriptionClassName, descriptionClassName,
title, title,
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
} = this.props; } = this.props;
return ( return (
<div> <div className={className}>
<DescriptionListItemTitle <DescriptionListItemTitle
className={titleClassName} className={titleClassName}
> >
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
} }
DescriptionListItem.propTypes = { DescriptionListItem.propTypes = {
className: PropTypes.string,
titleClassName: PropTypes.string, titleClassName: PropTypes.string,
descriptionClassName: PropTypes.string, descriptionClassName: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
@@ -13,6 +13,7 @@ import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector'; import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText'; import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector'; import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput from './KeyValueListInput';
import LanguageSelectInputConnector from './LanguageSelectInputConnector'; import LanguageSelectInputConnector from './LanguageSelectInputConnector';
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput'; import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
@@ -65,6 +66,9 @@ function getComponent(type) {
case inputTypes.QUALITY_PROFILE_SELECT: case inputTypes.QUALITY_PROFILE_SELECT:
return QualityProfileSelectInputConnector; return QualityProfileSelectInputConnector;
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.MOVIE_MONITORED_SELECT: case inputTypes.MOVIE_MONITORED_SELECT:
return MovieMonitoredSelectInput; return MovieMonitoredSelectInput;
@@ -0,0 +1,93 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexers } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers,
(state, { includeAny }) => includeAny,
(indexers, includeAny) => {
const {
isFetching,
isPopulated,
error,
items
} = indexers;
const values = items.sort(sortByName).map((indexer) => ({
key: indexer.id,
value: indexer.name
}));
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexers: fetchIndexers
};
class IndexerSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchIndexers();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
};
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
IndexerSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired
};
IndexerSelectInputConnector.defaultProps = {
includeAny: false
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
+1 -1
View File
@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { value } = this.props; const { value } = this.props;
if (value !== prevProps.value && !this.state.isFocused) { if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
this.setState({ this.setState({
value: value == null ? '' : value.toString() value: value == null ? '' : value.toString()
}); });
@@ -46,13 +46,13 @@ class TextTagInputConnector extends Component {
// to oddities with restrictions (as an example). // to oddities with restrictions (as an example).
const newValue = [...valueArray]; const newValue = [...valueArray];
const newTags = split(tag.name); const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
newTags.forEach((newTag) => { newTags.forEach((newTag) => {
newValue.push(newTag.trim()); newValue.push(newTag.trim());
}); });
onChange({ name, value: newValue.join(',') }); onChange({ name, value: newValue });
}; };
onTagDelete = ({ index }) => { onTagDelete = ({ index }) => {
@@ -67,7 +67,7 @@ class TextTagInputConnector extends Component {
onChange({ onChange({
name, name,
value: newValue.join(',') value: newValue
}); });
}; };
+2 -1
View File
@@ -2,6 +2,7 @@ import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import translate from 'Utilities/String/translate';
import Link from './Link'; import Link from './Link';
import styles from './IconButton.css'; import styles from './IconButton.css';
@@ -23,7 +24,7 @@ function IconButton(props) {
className, className,
isDisabled && styles.isDisabled isDisabled && styles.isDisabled
)} )}
aria-label="Table Options Button" aria-label={translate('TableOptionsButton')}
isDisabled={isDisabled} isDisabled={isDisabled}
{...otherProps} {...otherProps}
> >
@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
render() { render() {
const { const {
kind,
isSpinning, isSpinning,
error, error,
children, children,
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError; const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK; let iconName = icons.CHECK;
let iconKind = kinds.SUCCESS; let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
if (hasWarning) { if (hasWarning) {
iconName = icons.WARNING; iconName = icons.WARNING;
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
return ( return (
<SpinnerButton <SpinnerButton
kind={kind}
isSpinning={isSpinning} isSpinning={isSpinning}
{...otherProps} {...otherProps}
> >
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
} }
SpinnerErrorButton.propTypes = { SpinnerErrorButton.propTypes = {
kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
children: PropTypes.node.isRequired children: PropTypes.node.isRequired
+7
View File
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
import ColorImpairedContext from 'App/ColorImpairedContext'; import ColorImpairedContext from 'App/ColorImpairedContext';
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
import SignalRConnector from 'Components/SignalRConnector'; import SignalRConnector from 'Components/SignalRConnector';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import locationShape from 'Helpers/Props/Shapes/locationShape'; import locationShape from 'Helpers/Props/Shapes/locationShape';
import PageHeader from './Header/PageHeader'; import PageHeader from './Header/PageHeader';
import PageSidebar from './Sidebar/PageSidebar'; import PageSidebar from './Sidebar/PageSidebar';
@@ -75,6 +76,7 @@ class Page extends Component {
isSmallScreen, isSmallScreen,
isSidebarVisible, isSidebarVisible,
enableColorImpairedMode, enableColorImpairedMode,
authenticationEnabled,
onSidebarToggle, onSidebarToggle,
onSidebarVisibleChange onSidebarVisibleChange
} = this.props; } = this.props;
@@ -109,6 +111,10 @@ class Page extends Component {
isOpen={this.state.isConnectionLostModalOpen} isOpen={this.state.isConnectionLostModalOpen}
onModalClose={this.onConnectionLostModalClose} onModalClose={this.onConnectionLostModalClose}
/> />
<AuthenticationRequiredModal
isOpen={!authenticationEnabled}
/>
</div> </div>
</ColorImpairedContext.Provider> </ColorImpairedContext.Provider>
); );
@@ -124,6 +130,7 @@ Page.propTypes = {
isUpdated: PropTypes.bool.isRequired, isUpdated: PropTypes.bool.isRequired,
isDisconnected: PropTypes.bool.isRequired, isDisconnected: PropTypes.bool.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired, enableColorImpairedMode: PropTypes.bool.isRequired,
authenticationEnabled: PropTypes.bool.isRequired,
onResize: PropTypes.func.isRequired, onResize: PropTypes.func.isRequired,
onSidebarToggle: PropTypes.func.isRequired, onSidebarToggle: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired onSidebarVisibleChange: PropTypes.func.isRequired
@@ -11,6 +11,7 @@ import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfil
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ErrorPage from './ErrorPage'; import ErrorPage from './ErrorPage';
import LoadingPage from './LoadingPage'; import LoadingPage from './LoadingPage';
import Page from './Page'; import Page from './Page';
@@ -140,18 +141,21 @@ function createMapStateToProps() {
selectErrors, selectErrors,
selectAppProps, selectAppProps,
createDimensionsSelector(), createDimensionsSelector(),
createSystemStatusSelector(),
( (
enableColorImpairedMode, enableColorImpairedMode,
isPopulated, isPopulated,
errors, errors,
app, app,
dimensions dimensions,
systemStatus
) => { ) => {
return { return {
...app, ...app,
...errors, ...errors,
isPopulated, isPopulated,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
authenticationEnabled: systemStatus.authentication !== 'none',
enableColorImpairedMode enableColorImpairedMode
}; };
} }
@@ -215,7 +215,7 @@ class DiscoverMovieFooter extends Component {
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}> <div className={styles.buttonContainerContent}>
<DiscoverMovieFooterLabel <DiscoverMovieFooterLabel
label={translate('MoviesSelectedInterp', [selectedCount])} label={translate('MoviesSelectedInterp', { count: selectedCount })}
isSaving={false} isSaving={false}
/> />
@@ -47,7 +47,7 @@ function NoDiscoverMovie(props) {
to="/settings/importlists" to="/settings/importlists"
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
> >
{translate('AddList')} {translate('AddImportList')}
</Button> </Button>
</div> </div>
</div> </div>
@@ -0,0 +1,7 @@
enum DownloadProtocol {
Unknown = 'unknown',
Usenet = 'usenet',
Torrent = 'torrent',
}
export default DownloadProtocol;
@@ -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;
@@ -0,0 +1,5 @@
.authRequiredAlert {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}
@@ -1,7 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'addCustomFormatMessage': string; 'authRequiredAlert': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -0,0 +1,153 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import Alert from 'Components/Alert';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import SpinnerButton from 'Components/Link/SpinnerButton';
import 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 { inputTypes, kinds } from 'Helpers/Props';
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
import translate from 'Utilities/String/translate';
import styles from './AuthenticationRequiredModalContent.css';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModalContent(props) {
const {
isPopulated,
error,
isSaving,
settings,
onInputChange,
onSavePress,
dispatchFetchStatus
} = props;
const {
authenticationMethod,
authenticationRequired,
username,
password
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
const didMount = useRef(false);
useEffect(() => {
if (!isSaving && didMount.current) {
dispatchFetchStatus();
}
didMount.current = true;
}, [isSaving, dispatchFetchStatus]);
return (
<ModalContent
showCloseButton={false}
onModalClose={onModalClose}
>
<ModalHeader>
{translate('AuthenticationRequired')}
</ModalHeader>
<ModalBody>
<Alert
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{translate('AuthenticationRequiredWarning')}
</Alert>
{
isPopulated && !error ?
<div>
<FormGroup>
<FormLabel>{translate('Authentication')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
onChange={onInputChange}
{...authenticationMethod}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
{...username}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
{...password}
/>
</FormGroup>
</div> :
null
}
{
!isPopulated && !error ? <LoadingIndicator /> : null
}
</ModalBody>
<ModalFooter>
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isSaving}
isDisabled={!authenticationEnabled}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
AuthenticationRequiredModalContent.propTypes = {
isPopulated: 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;
@@ -0,0 +1,86 @@
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),
(sectionSettings) => {
return {
...sectionSettings
};
}
);
}
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);
@@ -1,14 +1,18 @@
import * as filterTypes from './filterTypes'; import * as filterTypes from './filterTypes';
export const ARRAY = 'array'; export const ARRAY = 'array';
export const CONTAINS = 'contains';
export const DATE = 'date'; export const DATE = 'date';
export const EQUAL = 'equal';
export const EXACT = 'exact'; export const EXACT = 'exact';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const STRING = 'string'; export const STRING = 'string';
export const all = [ export const all = [
ARRAY, ARRAY,
CONTAINS,
DATE, DATE,
EQUAL,
EXACT, EXACT,
NUMBER, NUMBER,
STRING STRING
@@ -20,6 +24,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' } { key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
], ],
[CONTAINS]: [
{ key: filterTypes.CONTAINS, value: 'contains' }
],
[DATE]: [ [DATE]: [
{ key: filterTypes.LESS_THAN, value: 'is before' }, { key: filterTypes.LESS_THAN, value: 'is before' },
{ key: filterTypes.GREATER_THAN, value: 'is after' }, { key: filterTypes.GREATER_THAN, value: 'is after' },
@@ -29,6 +37,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' } { key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
], ],
[EQUAL]: [
{ key: filterTypes.EQUAL, value: 'is' }
],
[EXACT]: [ [EXACT]: [
{ key: filterTypes.EQUAL, value: 'is' }, { key: filterTypes.EQUAL, value: 'is' },
{ key: filterTypes.NOT_EQUAL, value: 'is not' } { key: filterTypes.NOT_EQUAL, value: 'is not' }
+2
View File
@@ -43,6 +43,7 @@ import {
faChevronCircleRight as fasChevronCircleRight, faChevronCircleRight as fasChevronCircleRight,
faChevronCircleUp as fasChevronCircleUp, faChevronCircleUp as fasChevronCircleUp,
faCircle as fasCircle, faCircle as fasCircle,
faCircleDown as fasCircleDown,
faCloud as fasCloud, faCloud as fasCloud,
faCloudDownloadAlt as fasCloudDownloadAlt, faCloudDownloadAlt as fasCloudDownloadAlt,
faCog as fasCog, faCog as fasCog,
@@ -135,6 +136,7 @@ export const CHECK_INDETERMINATE = fasMinus;
export const CHECK_CIRCLE = fasCheckCircle; export const CHECK_CIRCLE = fasCheckCircle;
export const CHECK_SQUARE = fasSquareCheck; export const CHECK_SQUARE = fasSquareCheck;
export const CIRCLE = fasCircle; export const CIRCLE = fasCircle;
export const CIRCLE_DOWN = fasCircleDown;
export const CIRCLE_OUTLINE = farCircle; export const CIRCLE_OUTLINE = farCircle;
export const CLEAR = fasTrashAlt; export const CLEAR = fasTrashAlt;
export const CLIPBOARD = fasCopy; export const CLIPBOARD = fasCopy;
+4
View File
@@ -5,11 +5,13 @@ export const CHECK = 'check';
export const DEVICE = 'device'; export const DEVICE = 'device';
export const KEY_VALUE_LIST = 'keyValueList'; export const KEY_VALUE_LIST = 'keyValueList';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect'; export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const FLOAT = 'float';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
export const PATH = 'path'; export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect'; export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const LANGUAGE_SELECT = 'languageSelect'; export const LANGUAGE_SELECT = 'languageSelect';
@@ -31,11 +33,13 @@ export const all = [
DEVICE, DEVICE,
KEY_VALUE_LIST, KEY_VALUE_LIST,
MOVIE_MONITORED_SELECT, MOVIE_MONITORED_SELECT,
FLOAT,
NUMBER, NUMBER,
OAUTH, OAUTH,
PASSWORD, PASSWORD,
PATH, PATH,
QUALITY_PROFILE_SELECT, QUALITY_PROFILE_SELECT,
INDEXER_SELECT,
DOWNLOAD_CLIENT_SELECT, DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT, ROOT_FOLDER_SELECT,
INDEXER_FLAGS_SELECT, INDEXER_FLAGS_SELECT,
@@ -100,7 +100,7 @@ function InteractiveImportSelectFolderModalContent(
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{modalTitle} - {translate('SelectFolder')} {translate('SelectFolderModalTitle', { modalTitle })}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -118,6 +118,7 @@ const COLUMNS = [
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.DANGER, name: icons.DANGER,
kind: kinds.DANGER, kind: kinds.DANGER,
title: () => translate('Rejections'),
}), }),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
@@ -242,10 +243,23 @@ function InteractiveImportModalContent(
useState<string | null>(null); useState<string | null>(null);
const [selectState, setSelectState] = useSelectState(); const [selectState, setSelectState] = useSelectState();
const [bulkSelectOptions, setBulkSelectOptions] = useState([ const [bulkSelectOptions, setBulkSelectOptions] = useState([
{ key: 'select', value: translate('SelectDotDot'), disabled: true }, {
{ key: 'quality', value: translate('SelectQuality') }, key: 'select',
{ key: 'releaseGroup', value: translate('SelectReleaseGroup') }, value: translate('SelectDropdown'),
{ key: 'language', value: translate('SelectLanguage') }, disabled: true,
},
{
key: 'quality',
value: translate('SelectQuality'),
},
{
key: 'releaseGroup',
value: translate('SelectReleaseGroup'),
},
{
key: 'language',
value: translate('SelectLanguage'),
},
]); ]);
const { allSelected, allUnselected, selectedState } = selectState; const { allSelected, allUnselected, selectedState } = selectState;
const previousIsDeleting = usePrevious(isDeleting); const previousIsDeleting = usePrevious(isDeleting);
@@ -390,7 +404,9 @@ function InteractiveImportModalContent(
const files: InteractiveImportCommandOptions[] = []; const files: InteractiveImportCommandOptions[] = [];
if (finalImportMode === 'chooseImportMode') { if (finalImportMode === 'chooseImportMode') {
setInteractiveImportErrorMessage('An import mode must be selected'); setInteractiveImportErrorMessage(
translate('InteractiveImportNoImportMode')
);
return; return;
} }
@@ -403,21 +419,21 @@ function InteractiveImportModalContent(
if (!movie) { if (!movie) {
setInteractiveImportErrorMessage( setInteractiveImportErrorMessage(
translate('InteractiveImportErrMovie') translate('InteractiveImportNoMovie')
); );
return; return;
} }
if (!quality) { if (!quality) {
setInteractiveImportErrorMessage( setInteractiveImportErrorMessage(
translate('InteractiveImportErrQuality') translate('InteractiveImportNoQuality')
); );
return; return;
} }
if (!languages) { if (!languages) {
setInteractiveImportErrorMessage( setInteractiveImportErrorMessage(
translate('InteractiveImportErrLanguage') translate('InteractiveImportNoLanguage')
); );
return; return;
} }
@@ -605,7 +621,7 @@ function InteractiveImportModalContent(
const errorMessage = getErrorMessage( const errorMessage = getErrorMessage(
error, error,
translate('UnableToLoadManualImportItems') translate('InteractiveImportLoadError')
); );
return ( return (
@@ -685,7 +701,7 @@ function InteractiveImportModalContent(
) : null} ) : null}
{isPopulated && !items.length && !isFetching {isPopulated && !items.length && !isFetching
? translate('NoVideoFilesFoundSelectedFolder') ? translate('InteractiveImportNoFilesFound')
: null} : null}
</ModalBody> </ModalBody>
@@ -781,8 +797,8 @@ function InteractiveImportModalContent(
isOpen={isConfirmDeleteModalOpen} isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedMovieFiles')} title={translate('DeleteSelectedMovieFiles')}
message={translate('DeleteSelectedMovieFilesMessage')} message={translate('DeleteSelectedMovieFilesHelpText')}
confirmLabel="Delete" confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onConfirmDeleteModalClose} onCancel={onConfirmDeleteModalClose}
/> />
@@ -80,16 +80,14 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{modalTitle} - {translate('SelectLanguage')} {translate('SelectLanguageModalTitle', { modalTitle })}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
{isFetching ? <LoadingIndicator /> : null} {isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? ( {!isFetching && error ? (
<Alert kind={kinds.DANGER}> <Alert kind={kinds.DANGER}>{translate('LanguagesLoadError')}</Alert>
{translate('UnableToLoadLanguages')}
</Alert>
) : null} ) : null}
{isPopulated && !error ? ( {isPopulated && !error ? (
@@ -39,12 +39,12 @@ const columns = [
}, },
{ {
name: 'imdbId', name: 'imdbId',
label: () => translate('ImdbId'), label: () => translate('IMDbId'),
isVisible: true, isVisible: true,
}, },
{ {
name: 'tmdbId', name: 'tmdbId',
label: () => translate('TmdbId'), label: () => translate('TMDBId'),
isVisible: true, isVisible: true,
}, },
]; ];
@@ -166,8 +166,11 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
a.sortTitle.localeCompare(b.sortTitle) a.sortTitle.localeCompare(b.sortTitle)
); );
return sorted.filter((item) => return sorted.filter(
item.title.toLowerCase().includes(filter.toLowerCase()) (item) =>
item.title.toLowerCase().includes(filter.toLowerCase()) ||
item.tmdbId.toString().includes(filter) ||
item.imdbId?.includes(filter)
); );
}, [allMovies, filter]); }, [allMovies, filter]);
@@ -28,7 +28,11 @@ class SelectMovieRow extends Component {
</VirtualTableRowCell> </VirtualTableRowCell>
<VirtualTableRowCell className={styles.imdbId}> <VirtualTableRowCell className={styles.imdbId}>
<Label>{this.props.imdbId}</Label> {
this.props.imdbId ?
<Label>{this.props.imdbId}</Label> :
null
}
</VirtualTableRowCell> </VirtualTableRowCell>
<VirtualTableRowCell className={styles.tmdbId}> <VirtualTableRowCell className={styles.tmdbId}>
@@ -43,7 +47,7 @@ SelectMovieRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
tmdbId: PropTypes.number.isRequired, tmdbId: PropTypes.number.isRequired,
imdbId: PropTypes.string.isRequired, imdbId: PropTypes.string,
year: PropTypes.number.isRequired, year: PropTypes.number.isRequired,
onMovieSelect: PropTypes.func.isRequired onMovieSelect: PropTypes.func.isRequired
}; };
@@ -131,9 +131,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
{isFetching && <LoadingIndicator />} {isFetching && <LoadingIndicator />}
{!isFetching && error ? ( {!isFetching && error ? (
<Alert kind={kinds.DANGER}> <Alert kind={kinds.DANGER}>{translate('QualitiesLoadError')}</Alert>
{translate('UnableToLoadQualities')}
</Alert>
) : null} ) : null}
{isPopulated && !error ? ( {isPopulated && !error ? (
@@ -39,7 +39,7 @@ function SelectReleaseGroupModalContent(
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{modalTitle} - {translate('SetReleaseGroup')} {translate('SetReleaseGroupModalTitle', { modalTitle })}
</ModalHeader> </ModalHeader>
<ModalBody <ModalBody
@@ -62,7 +62,7 @@ function SelectReleaseGroupModalContent(
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button onPress={onModalClose}>Cancel</Button> <Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.SUCCESS} onPress={onReleaseGroupSelectWrapper}> <Button kind={kinds.SUCCESS} onPress={onReleaseGroupSelectWrapper}>
{translate('SetReleaseGroup')} {translate('SetReleaseGroup')}
@@ -1,11 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React, { Fragment } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sortDirections } from 'Helpers/Props'; import { icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector'; import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearchContent.css'; import styles from './InteractiveSearchContent.css';
@@ -32,7 +33,11 @@ const columns = [
}, },
{ {
name: 'rejections', name: 'rejections',
label: React.createElement(Icon, { name: icons.DANGER }), columnLabel: () => translate('Rejections'),
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true, isSortable: true,
fixedSortDirection: sortDirections.ASCENDING, fixedSortDirection: sortDirections.ASCENDING,
isVisible: true isVisible: true
@@ -88,6 +93,7 @@ const columns = [
}, },
{ {
name: 'customFormatScore', name: 'customFormatScore',
columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.SCORE, name: icons.SCORE,
title: () => translate('CustomFormatScore') title: () => translate('CustomFormatScore')
@@ -97,7 +103,11 @@ const columns = [
}, },
{ {
name: 'indexerFlags', name: 'indexerFlags',
label: React.createElement(Icon, { name: icons.FLAG }), columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
} }
@@ -119,36 +129,46 @@ function InteractiveSearchContent(props) {
onGrabPress onGrabPress
} = props; } = props;
const errorMessage = getErrorMessage(error);
return ( return (
<div> <div>
{ {
isFetching && isFetching ? <LoadingIndicator /> : null
<LoadingIndicator />
} }
{ {
!isFetching && !!error && !isFetching && error ?
<Alert kind={kinds.DANGER} className={styles.alert}> <Alert kind={kinds.DANGER} className={styles.alert}>
{translate('UnableToLoadResultsIntSearch')} {
</Alert> errorMessage ?
<Fragment>
{translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
</Fragment> :
translate('MovieSearchResultsLoadError')
}
</Alert> :
null
} }
{ {
!isFetching && isPopulated && !totalReleasesCount && !isFetching && isPopulated && !totalReleasesCount ?
<Alert kind={kinds.INFO} className={styles.alert}> <Alert kind={kinds.INFO} className={styles.alert}>
{translate('NoResultsFound')} {translate('NoResultsFound')}
</Alert> </Alert> :
null
} }
{ {
!!totalReleasesCount && isPopulated && !items.length && !!totalReleasesCount && isPopulated && !items.length ?
<Alert kind={kinds.WARNING} className={styles.alert}> <Alert kind={kinds.WARNING} className={styles.alert}>
{translate('AllResultsHiddenFilter')} {translate('AllResultsHiddenFilter')}
</Alert> </Alert> :
null
} }
{ {
isPopulated && !!items.length && isPopulated && !!items.length ?
<Table <Table
columns={columns} columns={columns}
sortKey={sortKey} sortKey={sortKey}
@@ -171,14 +191,16 @@ function InteractiveSearchContent(props) {
}) })
} }
</TableBody> </TableBody>
</Table> </Table> :
null
} }
{ {
totalReleasesCount !== items.length && !!items.length && totalReleasesCount !== items.length && !!items.length ?
<Alert kind={kinds.INFO} className={styles.alert}> <Alert kind={kinds.INFO} className={styles.alert}>
{translate('SomeResultsHiddenFilter')} {translate('SomeResultsHiddenFilter')}
</Alert> </Alert> :
null
} }
</div> </div>
); );
@@ -16,11 +16,11 @@
.quality, .quality,
.customFormat, .customFormat,
.language { .languages {
composes: cell; composes: cell;
} }
.language { .languages {
width: 100px; width: 100px;
} }
@@ -33,8 +33,7 @@
} }
.rejected, .rejected,
.indexerFlags, .indexerFlags {
.download {
composes: cell; composes: cell;
width: 50px; width: 50px;
@@ -70,3 +69,39 @@
.blocklist { .blocklist {
margin-left: 5px; margin-left: 5px;
} }
.download {
composes: cell;
width: 80px;
}
.manualDownloadContent {
position: relative;
display: inline-block;
margin: 0 2px;
width: 22px;
height: 20.39px;
vertical-align: middle;
line-height: 20.39px;
&:hover {
color: var(--iconButtonHoverColor);
}
}
.interactiveIcon {
position: absolute;
top: 4px;
left: 0;
/* width: 100%; */
text-align: center;
}
.downloadIcon {
position: absolute;
top: 7px;
left: 8px;
/* width: 100%; */
text-align: center;
}
@@ -7,10 +7,13 @@ interface CssExports {
'customFormat': string; 'customFormat': string;
'customFormatScore': string; 'customFormatScore': string;
'download': string; 'download': string;
'downloadIcon': string;
'history': string; 'history': string;
'indexer': string; 'indexer': string;
'indexerFlags': string; 'indexerFlags': string;
'language': string; 'interactiveIcon': string;
'languages': string;
'manualDownloadContent': string;
'peers': string; 'peers': string;
'protocol': string; 'protocol': string;
'quality': string; 'quality': string;
@@ -1,353 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import Peers from './Peers';
import styles from './InteractiveSearchRow.css';
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddedToDownloadQueue');
} else if (grabError) {
return grabError;
}
return translate('AddToDownloadQueue');
}
class InteractiveSearchRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isConfirmGrabModalOpen: false
};
}
//
// Listeners
onGrabPress = () => {
const {
guid,
indexerId,
onGrabPress
} = this.props;
onGrabPress({
guid,
indexerId
});
};
onConfirmGrabPress = () => {
this.setState({ isConfirmGrabModalOpen: true });
};
onGrabConfirm = () => {
this.setState({ isConfirmGrabModalOpen: false });
const {
guid,
indexerId,
searchPayload,
onGrabPress
} = this.props;
onGrabPress({
guid,
indexerId,
...searchPayload
});
};
onGrabCancel = () => {
this.setState({ isConfirmGrabModalOpen: false });
};
//
// Render
render() {
const {
protocol,
age,
ageHours,
ageMinutes,
publishDate,
title,
infoUrl,
indexer,
size,
seeders,
leechers,
quality,
customFormats,
customFormatScore,
languages,
indexerFlags,
rejections,
downloadAllowed,
isGrabbing,
isGrabbed,
longDateFormat,
timeFormat,
grabError,
historyGrabbedData,
historyFailedData,
blocklistData
} = this.props;
return (
<TableRow>
<TableRowCell className={styles.protocol}>
<ProtocolLabel
protocol={protocol}
/>
</TableRowCell>
<TableRowCell
className={styles.age}
title={formatDateTime(publishDate, longDateFormat, timeFormat, { includeSeconds: true })}
>
{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}
title={title}
>
<div>
{title}
</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.indexer}>
{indexer}
</TableRowCell>
<TableRowCell className={styles.history}>
{
historyGrabbedData?.date && !historyFailedData?.date &&
<Icon
name={icons.DOWNLOADING}
kind={kinds.DEFAULT}
title={`${translate('Grabbed')}: ${formatDateTime(historyGrabbedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
/>
}
{
historyFailedData?.date &&
<Icon
className={styles.failed}
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={`${translate('Failed')}: ${formatDateTime(historyFailedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
/>
}
{
blocklistData?.date &&
<Icon
className={historyGrabbedData || historyFailedData ? styles.blocklist : ''}
name={icons.BLOCKLIST}
kind={kinds.DANGER}
title={`${translate('Blocklisted')}: ${formatDateTime(blocklistData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
/>
}
</TableRowCell>
<TableRowCell className={styles.size}>
{formatBytes(size)}
</TableRowCell>
<TableRowCell className={styles.peers}>
{
protocol === 'torrent' &&
<Peers
seeders={seeders}
leechers={leechers}
/>
}
</TableRowCell>
<TableRowCell className={styles.language}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
<TableRowCell className={styles.quality}>
<MovieQuality
quality={quality}
/>
</TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
{customFormatScore > 0 && `+${customFormatScore}`}
{customFormatScore < 0 && customFormatScore}
</TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{
!!indexerFlags.length &&
<Popover
anchor={
<Icon
name={icons.FLAG}
kind={kinds.PRIMARY}
/>
}
title={translate('IndexerFlags')}
body={
<ul>
{
indexerFlags.map((flag, index) => {
return (
<li key={index}>
{flag}
</li>
);
})
}
</ul>
}
position={tooltipPositions.BOTTOM}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={this.state.isConfirmGrabModalOpen}
kind={kinds.WARNING}
title={translate('GrabRelease')}
message={translate('GrabReleaseMessageText', [title])}
confirmLabel={translate('Grab')}
onConfirm={this.onGrabConfirm}
onCancel={this.onGrabCancel}
/>
</TableRow>
);
}
}
InteractiveSearchRow.propTypes = {
guid: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
ageHours: PropTypes.number.isRequired,
ageMinutes: PropTypes.number.isRequired,
publishDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
infoUrl: PropTypes.string.isRequired,
indexerId: PropTypes.number.isRequired,
indexer: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
seeders: PropTypes.number,
leechers: PropTypes.number,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
customFormatScore: PropTypes.number.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
downloadAllowed: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
searchPayload: PropTypes.object.isRequired,
onGrabPress: PropTypes.func.isRequired,
historyFailedData: PropTypes.object,
historyGrabbedData: PropTypes.object,
blocklistData: PropTypes.object
};
InteractiveSearchRow.defaultProps = {
rejections: [],
isGrabbing: false,
isGrabbed: false
};
export default InteractiveSearchRow;
@@ -0,0 +1,376 @@
import React, { useCallback, useState } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import MovieBlocklist from 'typings/MovieBlocklist';
import MovieHistory from 'typings/MovieHistory';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
import Peers from './Peers';
import styles from './InteractiveSearchRow.css';
function getDownloadIcon(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddToDownloadQueue');
} else if (grabError) {
return grabError;
}
return translate('AddedToDownloadQueue');
}
interface InteractiveSearchRowProps {
guid: string;
protocol: DownloadProtocol;
age: number;
ageHours: number;
ageMinutes: number;
publishDate: string;
title: string;
infoUrl: string;
indexerId: number;
indexer: string;
size: number;
seeders?: number;
leechers?: number;
quality: QualityModel;
languages: Language[];
customFormats: CustomFormat[];
customFormatScore: number;
mappedMovieId?: number;
rejections: string[];
indexerFlags: string[];
downloadAllowed: boolean;
isGrabbing: boolean;
isGrabbed: boolean;
grabError?: string;
historyFailedData?: MovieHistory;
historyGrabbedData?: MovieHistory;
blocklistData?: MovieBlocklist;
longDateFormat: string;
timeFormat: string;
searchPayload: object;
onGrabPress(...args: unknown[]): void;
}
function InteractiveSearchRow(props: InteractiveSearchRowProps) {
const {
guid,
indexerId,
protocol,
age,
ageHours,
ageMinutes,
publishDate,
title,
infoUrl,
indexer,
size,
seeders,
leechers,
quality,
languages,
customFormatScore,
customFormats,
mappedMovieId,
rejections = [],
indexerFlags = [],
downloadAllowed,
isGrabbing = false,
isGrabbed = false,
longDateFormat,
timeFormat,
grabError,
historyGrabbedData,
historyFailedData,
blocklistData,
searchPayload,
onGrabPress,
} = props;
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
const onGrabPressWrapper = useCallback(() => {
if (downloadAllowed) {
onGrabPress({
guid,
indexerId,
});
return;
}
setIsConfirmGrabModalOpen(true);
}, [
guid,
indexerId,
downloadAllowed,
onGrabPress,
setIsConfirmGrabModalOpen,
]);
const onGrabConfirm = useCallback(() => {
setIsConfirmGrabModalOpen(false);
onGrabPress({
guid,
indexerId,
...searchPayload,
});
}, [guid, indexerId, searchPayload, onGrabPress, setIsConfirmGrabModalOpen]);
const onGrabCancel = useCallback(() => {
setIsConfirmGrabModalOpen(false);
}, [setIsConfirmGrabModalOpen]);
const onOverridePress = useCallback(() => {
setIsOverrideModalOpen(true);
}, [setIsOverrideModalOpen]);
const onOverrideModalClose = useCallback(() => {
setIsOverrideModalOpen(false);
}, [setIsOverrideModalOpen]);
return (
<TableRow>
<TableRowCell className={styles.protocol}>
<ProtocolLabel protocol={protocol} />
</TableRowCell>
<TableRowCell
className={styles.age}
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
includeSeconds: true,
})}
>
{formatAge(age, ageHours, ageMinutes)}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</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.RIGHT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.title}>
<Link to={infoUrl} title={title}>
<div>{title}</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.indexer}>{indexer}</TableRowCell>
<TableRowCell className={styles.history}>
{historyGrabbedData?.date && !historyFailedData?.date ? (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DEFAULT}
title={`${translate('Grabbed')}: ${formatDateTime(
historyGrabbedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
/>
) : null}
{historyFailedData?.date ? (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={`${translate('Failed')}: ${formatDateTime(
historyFailedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
/>
) : null}
{blocklistData?.date ? (
<Icon
className={
historyGrabbedData || historyFailedData ? styles.blocklist : ''
}
name={icons.BLOCKLIST}
kind={kinds.DANGER}
title={`${translate('Blocklisted')}: ${formatDateTime(
blocklistData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.size}>{formatBytes(size)}</TableRowCell>
<TableRowCell className={styles.peers}>
{protocol === 'torrent' ? (
<Peers seeders={seeders} leechers={leechers} />
) : null}
</TableRowCell>
<TableRowCell className={styles.languages}>
<MovieLanguage languages={languages} />
</TableRowCell>
<TableRowCell className={styles.quality}>
<MovieQuality quality={quality} />
</TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
/>
</TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{indexerFlags.length ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={
<ul>
{indexerFlags.map((flag, index) => {
return <li key={index}>{flag}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<ConfirmModal
isOpen={isConfirmGrabModalOpen}
kind={kinds.WARNING}
title={translate('GrabRelease')}
message={translate('GrabReleaseMessageText', { title })}
confirmLabel={translate('Grab')}
onConfirm={onGrabConfirm}
onCancel={onGrabCancel}
/>
<OverrideMatchModal
isOpen={isOverrideModalOpen}
title={title}
indexerId={indexerId}
guid={guid}
movieId={mappedMovieId}
languages={languages}
quality={quality}
protocol={protocol}
isGrabbing={isGrabbing}
grabError={grabError}
onModalClose={onOverrideModalClose}
/>
</TableRow>
);
}
export default InteractiveSearchRow;
@@ -0,0 +1,31 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { sizes } from 'Helpers/Props';
import SelectDownloadClientModalContent from './SelectDownloadClientModalContent';
interface SelectDownloadClientModalProps {
isOpen: boolean;
protocol: DownloadProtocol;
modalTitle: string;
onDownloadClientSelect(downloadClientId: number): void;
onModalClose(): void;
}
function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } =
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
<SelectDownloadClientModalContent
protocol={protocol}
modalTitle={modalTitle}
onDownloadClientSelect={onDownloadClientSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectDownloadClientModal;
@@ -0,0 +1,74 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import Button from 'Components/Link/Button';
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { kinds } from 'Helpers/Props';
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
import translate from 'Utilities/String/translate';
import SelectDownloadClientRow from './SelectDownloadClientRow';
interface SelectDownloadClientModalContentProps {
protocol: DownloadProtocol;
modalTitle: string;
onDownloadClientSelect(downloadClientId: number): void;
onModalClose(): void;
}
function SelectDownloadClientModalContent(
props: SelectDownloadClientModalContentProps
) {
const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props;
const { isFetching, isPopulated, error, items } = useSelector(
createEnabledDownloadClientsSelector(protocol)
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('SelectDownloadClientModalTitle', { modalTitle })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('DownloadClientsLoadError')}
</Alert>
) : null}
{isPopulated && !error ? (
<Form>
{items.map((downloadClient) => {
const { id, name, priority } = downloadClient;
return (
<SelectDownloadClientRow
key={id}
id={id}
name={name}
priority={priority}
onDownloadClientSelect={onDownloadClientSelect}
/>
);
})}
</Form>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectDownloadClientModalContent;
@@ -0,0 +1,6 @@
.downloadClient {
display: flex;
justify-content: space-between;
padding: 8px;
border-bottom: 1px solid var(--borderColor);
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'downloadClient': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import Link from 'Components/Link/Link';
import translate from 'Utilities/String/translate';
import styles from './SelectDownloadClientRow.css';
interface SelectSeasonRowProps {
id: number;
name: string;
priority: number;
onDownloadClientSelect(downloadClientId: number): unknown;
}
function SelectDownloadClientRow(props: SelectSeasonRowProps) {
const { id, name, priority, onDownloadClientSelect } = props;
const onSeasonSelectWrapper = useCallback(() => {
onDownloadClientSelect(id);
}, [id, onDownloadClientSelect]);
return (
<Link
className={styles.downloadClient}
component="div"
onPress={onSeasonSelectWrapper}
>
<div>{name}</div>
<div>{translate('PrioritySettings', { priority })}</div>
</Link>
);
}
export default SelectDownloadClientRow;
@@ -0,0 +1,17 @@
.link {
composes: link from '~Components/Link/Link.css';
width: 100%;
}
.placeholder {
display: inline-block;
margin: -2px 0;
width: 100%;
outline: 2px dashed var(--dangerColor);
outline-offset: -2px;
}
.optional {
outline: 2px dashed var(--gray);
}
@@ -0,0 +1,9 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'link': string;
'optional': string;
'placeholder': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,35 @@
import classNames from 'classnames';
import React from 'react';
import Link from 'Components/Link/Link';
import styles from './OverrideMatchData.css';
interface OverrideMatchDataProps {
value?: string | number | JSX.Element | JSX.Element[];
isDisabled?: boolean;
isOptional?: boolean;
onPress: () => void;
}
function OverrideMatchData(props: OverrideMatchDataProps) {
const { value, isDisabled = false, isOptional, onPress } = props;
return (
<Link className={styles.link} isDisabled={isDisabled} onPress={onPress}>
{(value == null || (Array.isArray(value) && value.length === 0)) &&
!isDisabled ? (
<span
className={classNames(
styles.placeholder,
isOptional && styles.optional
)}
>
&nbsp;
</span>
) : (
value
)}
</Link>
);
}
export default OverrideMatchData;
@@ -0,0 +1,56 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { sizes } from 'Helpers/Props';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import OverrideMatchModalContent from './OverrideMatchModalContent';
interface OverrideMatchModalProps {
isOpen: boolean;
title: string;
indexerId: number;
guid: string;
movieId?: number;
languages: Language[];
quality: QualityModel;
protocol: DownloadProtocol;
isGrabbing: boolean;
grabError?: string;
onModalClose(): void;
}
function OverrideMatchModal(props: OverrideMatchModalProps) {
const {
isOpen,
title,
indexerId,
guid,
movieId,
languages,
quality,
protocol,
isGrabbing,
grabError,
onModalClose,
} = props;
return (
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
<OverrideMatchModalContent
title={title}
indexerId={indexerId}
guid={guid}
movieId={movieId}
languages={languages}
quality={quality}
protocol={protocol}
isGrabbing={isGrabbing}
grabError={grabError}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default OverrideMatchModal;
@@ -0,0 +1,49 @@
.label {
composes: label from '~Components/Label.css';
cursor: pointer;
}
.item {
display: block;
margin-bottom: 5px;
margin-left: 50px;
}
.footer {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
display: flex;
justify-content: space-between;
overflow: hidden;
}
.error {
margin-right: 20px;
color: var(--dangerColor);
word-break: break-word;
}
.buttons {
display: flex;
}
@media only screen and (max-width: $breakpointSmall) {
.item {
margin-left: 0;
}
.footer {
display: block;
}
.error {
margin-right: 0;
margin-bottom: 10px;
}
.buttons {
justify-content: space-between;
flex-grow: 1;
}
}
@@ -0,0 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'buttons': string;
'error': string;
'footer': string;
'item': string;
'label': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,299 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
import usePrevious from 'Helpers/Hooks/usePrevious';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import Language from 'Language/Language';
import Movie from 'Movie/Movie';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import { QualityModel } from 'Quality/Quality';
import { grabRelease } from 'Store/Actions/releaseActions';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
import { createMovieSelectorForHook } from 'Store/Selectors/createMovieSelector';
import translate from 'Utilities/String/translate';
import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal';
import OverrideMatchData from './OverrideMatchData';
import styles from './OverrideMatchModalContent.css';
type SelectType =
| 'select'
| 'movie'
| 'quality'
| 'language'
| 'downloadClient';
interface OverrideMatchModalContentProps {
indexerId: number;
title: string;
guid: string;
movieId?: number;
languages: Language[];
quality: QualityModel;
protocol: DownloadProtocol;
isGrabbing: boolean;
grabError?: string;
onModalClose(): void;
}
function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
const modalTitle = translate('ManualGrab');
const {
indexerId,
title,
guid,
protocol,
isGrabbing,
grabError,
onModalClose,
} = props;
const [movieId, setMovieId] = useState(props.movieId);
const [languages, setLanguages] = useState(props.languages);
const [quality, setQuality] = useState(props.quality);
const [downloadClientId, setDownloadClientId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
);
const previousIsGrabbing = usePrevious(isGrabbing);
const dispatch = useDispatch();
const movie: Movie | undefined = useSelector(
createMovieSelectorForHook(movieId)
);
const { items: downloadClients } = useSelector(
createEnabledDownloadClientsSelector(protocol)
);
const onSelectModalClose = useCallback(() => {
setSelectModalOpen(null);
}, [setSelectModalOpen]);
const onSelectMoviePress = useCallback(() => {
setSelectModalOpen('movie');
}, [setSelectModalOpen]);
const onMovieSelect = useCallback(
(m: Movie) => {
setMovieId(m.id);
setSelectModalOpen(null);
},
[setMovieId, setSelectModalOpen]
);
const onSelectQualityPress = useCallback(() => {
setSelectModalOpen('quality');
}, [setSelectModalOpen]);
const onQualitySelect = useCallback(
(quality: QualityModel) => {
setQuality(quality);
setSelectModalOpen(null);
},
[setQuality, setSelectModalOpen]
);
const onSelectLanguagesPress = useCallback(() => {
setSelectModalOpen('language');
}, [setSelectModalOpen]);
const onLanguagesSelect = useCallback(
(languages: Language[]) => {
setLanguages(languages);
setSelectModalOpen(null);
},
[setLanguages, setSelectModalOpen]
);
const onSelectDownloadClientPress = useCallback(() => {
setSelectModalOpen('downloadClient');
}, [setSelectModalOpen]);
const onDownloadClientSelect = useCallback(
(downloadClientId: number) => {
setDownloadClientId(downloadClientId);
setSelectModalOpen(null);
},
[setDownloadClientId, setSelectModalOpen]
);
const onGrabPress = useCallback(() => {
if (!movieId) {
setError(translate('OverrideGrabNoMovie'));
return;
} else if (!quality) {
setError(translate('OverrideGrabNoQuality'));
return;
} else if (!languages.length) {
setError(translate('OverrideGrabNoLanguage'));
return;
}
dispatch(
grabRelease({
indexerId,
guid,
movieId,
quality,
languages,
downloadClientId,
shouldOverride: true,
})
);
}, [
indexerId,
guid,
movieId,
quality,
languages,
downloadClientId,
setError,
dispatch,
]);
useEffect(() => {
if (!isGrabbing && previousIsGrabbing) {
onModalClose();
}
}, [isGrabbing, previousIsGrabbing, onModalClose]);
useEffect(
() => {
dispatch(fetchDownloadClients());
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('OverrideGrabModalTitle', { title })}
</ModalHeader>
<ModalBody>
<DescriptionList>
<DescriptionListItem
className={styles.item}
title={translate('Movie')}
data={
<OverrideMatchData
value={movie?.title}
onPress={onSelectMoviePress}
/>
}
/>
<DescriptionListItem
className={styles.item}
title={translate('Quality')}
data={
<OverrideMatchData
value={
<MovieQuality className={styles.label} quality={quality} />
}
onPress={onSelectQualityPress}
/>
}
/>
<DescriptionListItem
className={styles.item}
title={translate('Languages')}
data={
<OverrideMatchData
value={
<MovieLanguage
className={styles.label}
languages={languages}
/>
}
onPress={onSelectLanguagesPress}
/>
}
/>
{downloadClients.length > 1 ? (
<DescriptionListItem
className={styles.item}
title={translate('DownloadClient')}
data={
<OverrideMatchData
value={
downloadClients.find(
(downloadClient) => downloadClient.id === downloadClientId
)?.name ?? translate('Default')
}
onPress={onSelectDownloadClientPress}
/>
}
/>
) : null}
</DescriptionList>
</ModalBody>
<ModalFooter className={styles.footer}>
<div className={styles.error}>{error || grabError}</div>
<div className={styles.buttons}>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isGrabbing}
error={grabError}
onPress={onGrabPress}
>
{translate('GrabRelease')}
</SpinnerErrorButton>
</div>
</ModalFooter>
<SelectMovieModal
isOpen={selectModalOpen === 'movie'}
modalTitle={modalTitle}
onMovieSelect={onMovieSelect}
onModalClose={onSelectModalClose}
/>
<SelectQualityModal
isOpen={selectModalOpen === 'quality'}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
modalTitle={modalTitle}
onQualitySelect={onQualitySelect}
onModalClose={onSelectModalClose}
/>
<SelectLanguageModal
isOpen={selectModalOpen === 'language'}
languageIds={languages ? languages.map((l) => l.id) : []}
modalTitle={modalTitle}
onLanguagesSelect={onLanguagesSelect}
onModalClose={onSelectModalClose}
/>
<SelectDownloadClientModal
isOpen={selectModalOpen === 'downloadClient'}
protocol={protocol}
modalTitle={modalTitle}
onDownloadClientSelect={onDownloadClientSelect}
onModalClose={onSelectModalClose}
/>
</ModalContent>
);
}
export default OverrideMatchModalContent;
@@ -117,7 +117,7 @@ class DeleteMovieModalContent extends Component {
deleteFiles && deleteFiles &&
<div className={styles.deleteFilesMessage}> <div className={styles.deleteFilesMessage}>
<div> <div>
{translate('DeleteTheMovieFolder', [path])} {translate('DeleteTheMovieFolder', { path })}
</div> </div>
{ {
+1 -1
View File
@@ -654,7 +654,7 @@ class MovieDetails extends Component {
</div> </div>
} }
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}> <Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
<TabList <TabList
className={styles.tabList} className={styles.tabList}
> >
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import MovieLanguage from 'Movie/MovieLanguage';
import titleCase from 'Utilities/String/titleCase'; import titleCase from 'Utilities/String/titleCase';
class MovieTitlesRow extends Component { class MovieTitlesRow extends Component {
@@ -13,13 +12,9 @@ class MovieTitlesRow extends Component {
render() { render() {
const { const {
title, title,
language,
sourceType sourceType
} = this.props; } = this.props;
// TODO - Fix languages to all take arrays
const languages = [language];
return ( return (
<TableRow> <TableRow>
@@ -27,12 +22,6 @@ class MovieTitlesRow extends Component {
{title} {title}
</TableRowCell> </TableRowCell>
<TableRowCell>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
<TableRowCell> <TableRowCell>
{titleCase(sourceType)} {titleCase(sourceType)}
</TableRowCell> </TableRowCell>
@@ -45,7 +34,6 @@ class MovieTitlesRow extends Component {
MovieTitlesRow.propTypes = { MovieTitlesRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
language: PropTypes.object.isRequired,
sourceType: PropTypes.string.isRequired sourceType: PropTypes.string.isRequired
}; };
@@ -13,11 +13,6 @@ const columns = [
label: () => translate('AlternativeTitle'), label: () => translate('AlternativeTitle'),
isVisible: true isVisible: true
}, },
{
name: 'language',
label: () => translate('Language'),
isVisible: true
},
{ {
name: 'sourceType', name: 'sourceType',
label: () => translate('Type'), label: () => translate('Type'),
@@ -9,3 +9,9 @@
word-break: break-word; word-break: break-word;
} }
.customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
}
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'customFormatScore': string;
'sourceTitle': string; 'sourceTitle': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
+11 -3
View File
@@ -7,7 +7,8 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
@@ -106,8 +107,15 @@ class MovieHistoryRow extends Component {
/> />
</TableRowCell> </TableRowCell>
<TableRowCell> <TableRowCell className={styles.customFormatScore}>
{formatCustomFormatScore(customFormatScore)} <Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
/>
</TableRowCell> </TableRowCell>
<RelativeDateCellConnector <RelativeDateCellConnector
@@ -100,6 +100,15 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
{translate('DigitalRelease')} {translate('DigitalRelease')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem
name="tmdbRating"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('TmdbRating')}
</SortMenuItem>
<SortMenuItem <SortMenuItem
name="imdbRating" name="imdbRating"
sortKey={sortKey} sortKey={sortKey}
@@ -110,12 +119,12 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
name="tmdbRating" name="rottenTomatoesRating"
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('TmdbRating')} {translate('RottenTomatoesRating')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -2,10 +2,13 @@ import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames'; import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import RottenTomatoRating from 'Components/RottenTomatoRating';
import TmdbRating from 'Components/TmdbRating';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
@@ -44,6 +47,9 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
showQualityProfile, showQualityProfile,
showCinemaRelease, showCinemaRelease,
showReleaseDate, showReleaseDate,
showTmdbRating,
showImdbRating,
showRottenTomatoesRating,
showSearchAction, showSearchAction,
} = useSelector(selectPosterOptions); } = useSelector(selectPosterOptions);
@@ -257,6 +263,24 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
</div> </div>
) : null} ) : null}
{showTmdbRating && !!ratings.tmdb ? (
<div className={styles.title}>
<TmdbRating ratings={ratings} iconSize={12} />
</div>
) : null}
{showImdbRating && !!ratings.imdb ? (
<div className={styles.title}>
<ImdbRating ratings={ratings} iconSize={12} />
</div>
) : null}
{showRottenTomatoesRating && !!ratings.rottenTomatoes ? (
<div className={styles.title}>
<RottenTomatoRating ratings={ratings} iconSize={12} />
</div>
) : null}
<MovieIndexPosterInfo <MovieIndexPosterInfo
studio={studio} studio={studio}
qualityProfile={qualityProfile} qualityProfile={qualityProfile}
@@ -279,6 +303,9 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
certification={certification} certification={certification}
originalTitle={originalTitle} originalTitle={originalTitle}
originalLanguage={originalLanguage} originalLanguage={originalLanguage}
showTmdbRating={showTmdbRating}
showImdbRating={showImdbRating}
showRottenTomatoesRating={showRottenTomatoesRating}
/> />
<EditMovieModalConnector <EditMovieModalConnector
@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating'; import ImdbRating from 'Components/ImdbRating';
import RottenTomatoRating from 'Components/RottenTomatoRating';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import { Language, Ratings } from 'Movie/Movie'; import { Language, Ratings } from 'Movie/Movie';
@@ -33,6 +34,9 @@ interface MovieIndexPosterInfoProps {
shortDateFormat: string; shortDateFormat: string;
longDateFormat: string; longDateFormat: string;
timeFormat: string; timeFormat: string;
showTmdbRating: boolean;
showImdbRating: boolean;
showRottenTomatoesRating: boolean;
} }
function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) { function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
@@ -58,6 +62,9 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
shortDateFormat, shortDateFormat,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
showTmdbRating,
showImdbRating,
showRottenTomatoesRating,
} = props; } = props;
if (sortKey === 'studio' && studio) { if (sortKey === 'studio' && studio) {
@@ -163,7 +170,15 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
); );
} }
if (sortKey === 'imdbRating' && !!ratings.imdb) { if (!showTmdbRating && sortKey === 'tmdbRating' && !!ratings.tmdb) {
return (
<div className={styles.info}>
<TmdbRating ratings={ratings} iconSize={12} />
</div>
);
}
if (!showImdbRating && sortKey === 'imdbRating' && !!ratings.imdb) {
return ( return (
<div className={styles.info}> <div className={styles.info}>
<ImdbRating ratings={ratings} iconSize={12} /> <ImdbRating ratings={ratings} iconSize={12} />
@@ -171,10 +186,14 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
); );
} }
if (sortKey === 'tmdbRating' && !!ratings.tmdb) { if (
!showRottenTomatoesRating &&
sortKey === 'rottenTomatoesRating' &&
!!ratings.rottenTomatoes
) {
return ( return (
<div className={styles.info}> <div className={styles.info}>
<TmdbRating ratings={ratings} iconSize={12} /> <RottenTomatoRating ratings={ratings} iconSize={12} />
</div> </div>
); );
} }
@@ -145,6 +145,9 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
showQualityProfile, showQualityProfile,
showCinemaRelease, showCinemaRelease,
showReleaseDate, showReleaseDate,
showTmdbRating,
showImdbRating,
showRottenTomatoesRating,
} = posterOptions; } = posterOptions;
const nextAiringHeight = 19; const nextAiringHeight = 19;
@@ -176,12 +179,22 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
heights.push(19); heights.push(19);
} }
if (showTmdbRating) {
heights.push(19);
}
if (showImdbRating) {
heights.push(19);
}
if (showRottenTomatoesRating) {
heights.push(19);
}
switch (sortKey) { switch (sortKey) {
case 'studio': case 'studio':
case 'added': case 'added':
case 'year': case 'year':
case 'imdbRating':
case 'tmdbRating':
case 'path': case 'path':
case 'sizeOnDisk': case 'sizeOnDisk':
case 'originalTitle': case 'originalTitle':
@@ -204,6 +217,21 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
heights.push(19); heights.push(19);
} }
break; break;
case 'imdbRating':
if (!showImdbRating) {
heights.push(19);
}
break;
case 'tmdbRating':
if (!showTmdbRating) {
heights.push(19);
}
break;
case 'rottenTomatoesRating':
if (!showRottenTomatoesRating) {
heights.push(19);
}
break;
default: default:
// No need to add a height of 0 // No need to add a height of 0
} }
@@ -54,6 +54,9 @@ function MovieIndexPosterOptionsModalContent(
showQualityProfile, showQualityProfile,
showCinemaRelease, showCinemaRelease,
showReleaseDate, showReleaseDate,
showTmdbRating,
showImdbRating,
showRottenTomatoesRating,
showSearchAction, showSearchAction,
} = posterOptions; } = posterOptions;
@@ -156,6 +159,42 @@ function MovieIndexPosterOptionsModalContent(
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>{translate('ShowTmdbRating')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showTmdbRating"
value={showTmdbRating}
helpText={translate('ShowTmdbRatingHelpText')}
onChange={onPosterOptionChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowImdbRating')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showImdbRating"
value={showImdbRating}
helpText={translate('ShowImdbRatingHelpText')}
onChange={onPosterOptionChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowRottenTomatoesRating')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showRottenTomatoesRating"
value={showRottenTomatoesRating}
helpText={translate('ShowRottenTomatoesRatingHelpText')}
onChange={onPosterOptionChange}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('ShowSearch')}</FormLabel> <FormLabel>{translate('ShowSearch')}</FormLabel>
@@ -212,7 +212,7 @@ function EditMoviesModalContent(props: EditMoviesModalContentProps) {
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('MoviesSelectedInterp', [selectedCount])} {translate('MoviesSelectedInterp', { count: selectedCount })}
</div> </div>
<div> <div>
@@ -191,7 +191,7 @@ function MovieIndexSelectFooter() {
</div> </div>
<div className={styles.selected}> <div className={styles.selected}>
{translate('MoviesSelectedInterp', [selectedCount])} {translate('MoviesSelectedInterp', { count: selectedCount })}
</div> </div>
<EditMoviesModal <EditMoviesModal
@@ -65,7 +65,7 @@ function OrganizeMoviesModalContent(props: OrganizeMoviesModalContentProps) {
</Alert> </Alert>
<div className={styles.message}> <div className={styles.message}>
{translate('OrganizeConfirm', [movieTitles.length])} {translate('OrganizeConfirm', { count: movieTitles.length })}
</div> </div>
<ul> <ul>
@@ -111,7 +111,7 @@ class FileEditModalContent extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<Alert kind={kinds.DANGER}> <Alert kind={kinds.DANGER}>
{translate('UnableToLoadQualities')} {translate('QualitiesLoadError')}
</Alert> </Alert>
} }
@@ -28,14 +28,31 @@
white-space: nowrap; white-space: nowrap;
} }
.languages,
.audio,
.video,
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 100px; width: 100px;
} }
.audioLanguages,
.videoDynamicRangeType,
.subtitles {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 165px;
}
.releaseGroup { .releaseGroup {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 120px; width: 120px;
} }
.customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
}
@@ -3,14 +3,21 @@
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'age': string; 'age': string;
'audio': string;
'audioLanguages': string;
'customFormatScore': string;
'download': string; 'download': string;
'formats': string; 'formats': string;
'language': string; 'language': string;
'languages': string;
'quality': string; 'quality': string;
'rejected': string; 'rejected': string;
'relativePath': string; 'relativePath': string;
'releaseGroup': string; 'releaseGroup': string;
'size': string; 'size': string;
'subtitles': string;
'video': string;
'videoDynamicRangeType': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -4,7 +4,8 @@ import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
@@ -12,6 +13,7 @@ import FileEditModal from 'MovieFile/Edit/FileEditModal';
import MediaInfoConnector from 'MovieFile/MediaInfoConnector'; import MediaInfoConnector from 'MovieFile/MediaInfoConnector';
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes'; import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import FileDetailsModal from '../FileDetailsModal'; import FileDetailsModal from '../FileDetailsModal';
import MovieFileRowCellPlaceholder from './MovieFileRowCellPlaceholder'; import MovieFileRowCellPlaceholder from './MovieFileRowCellPlaceholder';
@@ -78,7 +80,9 @@ class MovieFileEditorRow extends Component {
quality, quality,
qualityCutoffNotMet, qualityCutoffNotMet,
customFormats, customFormats,
languages customFormatScore,
languages,
columns
} = this.props; } = this.props;
const { const {
@@ -88,107 +92,228 @@ class MovieFileEditorRow extends Component {
} = this.state; } = this.state;
const showQualityPlaceholder = !quality; const showQualityPlaceholder = !quality;
const showLanguagePlaceholder = !languages; const showLanguagePlaceholder = !languages;
return ( return (
<TableRow> <TableRow>
<TableRowCell {
className={styles.relativePath} columns.map((column) => {
title={relativePath} const {
> name,
{relativePath} isVisible
</TableRowCell> } = column;
<TableRowCell> if (!isVisible) {
<MediaInfoConnector return null;
movieFileId={id} }
type={mediaInfoTypes.VIDEO}
/>
</TableRowCell>
<TableRowCell> if (name === 'relativePath') {
<MediaInfoConnector return (
movieFileId={id} <TableRowCell
type={mediaInfoTypes.AUDIO} key={name}
/> className={styles.relativePath}
</TableRowCell> title={relativePath}
>
{relativePath}
</TableRowCell>
);
}
<TableRowCell if (name === 'customFormats') {
className={styles.size} return (
title={size} <TableRowCell key={name}>
> <MovieFormats
{formatBytes(size)} formats={customFormats}
</TableRowCell> />
</TableRowCell>
);
}
<TableRowCell if (name === 'customFormatScore') {
className={styles.language} return (
> <TableRowCell
{ key={name}
showLanguagePlaceholder && className={styles.customFormatScore}
<MovieFileRowCellPlaceholder /> >
} <Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
/>
</TableRowCell>
);
}
{ if (name === 'languages') {
!showLanguagePlaceholder && !!languages && return (
<MovieLanguage <TableRowCell
className={styles.label} key={name}
languages={languages} className={styles.languages}
/> >
} {
</TableRowCell> showLanguagePlaceholder ?
<MovieFileRowCellPlaceholder /> :
null
}
<TableRowCell {
className={styles.quality} !showLanguagePlaceholder && !!languages &&
> <MovieLanguage
{ className={styles.label}
showQualityPlaceholder && languages={languages}
<MovieFileRowCellPlaceholder /> />
} }
</TableRowCell>
);
}
{ if (name === 'quality') {
!showQualityPlaceholder && !!quality && return (
<MovieQuality <TableRowCell
className={styles.label} key={name}
quality={quality} className={styles.quality}
isCutoffNotMet={qualityCutoffNotMet} >
/> {
} showQualityPlaceholder ?
</TableRowCell> <MovieFileRowCellPlaceholder /> :
null
}
<TableRowCell {
className={styles.releaseGroup} !showQualityPlaceholder && !!quality &&
> <MovieQuality
{releaseGroup} className={styles.label}
</TableRowCell> quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
}
</TableRowCell>
);
}
<TableRowCell if (name === 'audioInfo') {
className={styles.formats} return (
> <TableRowCell
<MovieFormats key={name}
formats={customFormats} className={styles.audio}
/> >
</TableRowCell> <MediaInfoConnector
type={mediaInfoTypes.AUDIO}
movieFileId={id}
/>
</TableRowCell>
);
}
<TableRowCell className={styles.actions}> if (name === 'audioLanguages') {
<IconButton return (
title={translate('EditMovieFile')} <TableRowCell
name={icons.EDIT} key={name}
onPress={this.onFileEditPress} className={styles.audioLanguages}
/> >
<MediaInfoConnector
type={mediaInfoTypes.AUDIO_LANGUAGES}
movieFileId={id}
/>
</TableRowCell>
);
}
<IconButton if (name === 'subtitleLanguages') {
title={translate('Details')} return (
name={icons.MEDIA_INFO} <TableRowCell
onPress={this.onFileDetailsPress} key={name}
/> className={styles.subtitles}
>
<MediaInfoConnector
type={mediaInfoTypes.SUBTITLES}
movieFileId={id}
/>
</TableRowCell>
);
}
<IconButton if (name === 'videoCodec') {
title={translate('DeleteFile')} return (
name={icons.REMOVE} <TableRowCell
onPress={this.onDeletePress} key={name}
/> className={styles.video}
</TableRowCell> >
<MediaInfoConnector
type={mediaInfoTypes.VIDEO}
movieFileId={id}
/>
</TableRowCell>
);
}
if (name === 'videoDynamicRangeType') {
return (
<TableRowCell
key={name}
className={styles.videoDynamicRangeType}
>
<MediaInfoConnector
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
movieFileId={id}
/>
</TableRowCell>
);
}
if (name === 'size') {
return (
<TableRowCell
key={name}
className={styles.size}
title={size}
>
{formatBytes(size)}
</TableRowCell>
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell
key={name}
className={styles.releaseGroup}
>
{releaseGroup}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell key={name} className={styles.actions}>
<IconButton
title={translate('EditMovieFile')}
name={icons.EDIT}
onPress={this.onFileEditPress}
/>
<IconButton
title={translate('Details')}
name={icons.MEDIA_INFO}
onPress={this.onFileDetailsPress}
/>
<IconButton
title={translate('DeleteFile')}
name={icons.REMOVE}
onPress={this.onDeletePress}
/>
</TableRowCell>
);
}
return null;
})
}
<FileDetailsModal <FileDetailsModal
isOpen={isFileDetailsModalOpen} isOpen={isFileDetailsModalOpen}
@@ -207,7 +332,7 @@ class MovieFileEditorRow extends Component {
ids={[id]} ids={[id]}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedMovieFiles')} title={translate('DeleteSelectedMovieFiles')}
message={translate('DeleteSelectedMovieFilesMessage')} message={translate('DeleteSelectedMovieFilesHelpText')}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDelete} onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose} onCancel={this.onConfirmDeleteModalClose}
@@ -225,10 +350,16 @@ MovieFileEditorRow.propTypes = {
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
releaseGroup: PropTypes.string, releaseGroup: PropTypes.string,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired, customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
customFormatScore: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired, languages: PropTypes.arrayOf(PropTypes.object).isRequired,
mediaInfo: PropTypes.object, mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired onDeletePress: PropTypes.func.isRequired
}; };
MovieFileEditorRow.defaultProps = {
customFormats: []
};
export default MovieFileEditorRow; export default MovieFileEditorRow;
@@ -1,61 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieFileEditorRow from './MovieFileEditorRow'; import MovieFileEditorRow from './MovieFileEditorRow';
import styles from './MovieFileEditorTableContent.css'; import styles from './MovieFileEditorTableContent.css';
const columns = [
{
name: 'title',
label: () => translate('RelativePath'),
isVisible: true
},
{
name: 'videoCodec',
label: () => translate('VideoCodec'),
isVisible: true
},
{
name: 'audioInfo',
label: () => translate('AudioInfo'),
isVisible: true
},
{
name: 'size',
label: () => translate('Size'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'releaseGroup',
label: () => translate('ReleaseGroup'),
isVisible: true
},
{
name: 'quality.customFormats',
label: () => translate('Formats'),
isVisible: true
},
{
name: 'action',
label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
isVisible: true
}
];
class MovieFileEditorTableContent extends Component { class MovieFileEditorTableContent extends Component {
// //
@@ -63,7 +12,9 @@ class MovieFileEditorTableContent extends Component {
render() { render() {
const { const {
items items,
columns,
onTableOptionChange
} = this.props; } = this.props;
return ( return (
@@ -77,13 +28,17 @@ class MovieFileEditorTableContent extends Component {
{ {
!!items.length && !!items.length &&
<Table columns={columns}> <Table
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<TableBody> <TableBody>
{ {
items.map((item) => { items.map((item) => {
return ( return (
<MovieFileEditorRow <MovieFileEditorRow
key={item.id} key={item.id}
columns={columns}
{...item} {...item}
onDeletePress={this.props.onDeletePress} onDeletePress={this.props.onDeletePress}
/> />
@@ -103,6 +58,8 @@ MovieFileEditorTableContent.propTypes = {
movieId: PropTypes.number, movieId: PropTypes.number,
isDeleting: PropTypes.bool.isRequired, isDeleting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onDeletePress: PropTypes.func.isRequired onDeletePress: PropTypes.func.isRequired
}; };
@@ -1,9 +1,8 @@
/* eslint max-params: 0 */
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { deleteMovieFile, updateMovieFiles } from 'Store/Actions/movieFileActions'; import { deleteMovieFile, setMovieFilesTableOption, updateMovieFiles } from 'Store/Actions/movieFileActions';
import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector'; import createMovieSelector from 'Store/Selectors/createMovieSelector';
import getQualities from 'Utilities/Quality/getQualities'; import getQualities from 'Utilities/Quality/getQualities';
@@ -30,6 +29,7 @@ function createMapStateToProps() {
return { return {
items: filesForMovie, items: filesForMovie,
columns: movieFiles.columns,
isDeleting: movieFiles.isDeleting, isDeleting: movieFiles.isDeleting,
isSaving: movieFiles.isSaving, isSaving: movieFiles.isSaving,
error: null, error: null,
@@ -54,6 +54,10 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(updateMovieFiles(updateProps)); dispatch(updateMovieFiles(updateProps));
}, },
onTableOptionChange(payload) {
dispatch(setMovieFilesTableOption(payload));
},
onDeletePress(movieFileId) { onDeletePress(movieFileId) {
dispatch(deleteMovieFile({ dispatch(deleteMovieFile({
id: movieFileId id: movieFileId
+60 -8
View File
@@ -1,36 +1,77 @@
import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import getLanguageName from 'Utilities/String/getLanguageName';
import translate from 'Utilities/String/translate';
import * as mediaInfoTypes from './mediaInfoTypes'; import * as mediaInfoTypes from './mediaInfoTypes';
function formatLanguages(languages) {
if (!languages) {
return null;
}
const splitLanguages = _.uniq(languages.split('/')).map((l) => {
const simpleLanguage = l.split('_')[0];
if (simpleLanguage === 'und') {
return translate('Unknown');
}
return getLanguageName(simpleLanguage);
});
if (splitLanguages.length > 3) {
return (
<span title={splitLanguages.join(', ')}>
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more
</span>
);
}
return (
<span>
{splitLanguages.join(', ')}
</span>
);
}
function MediaInfo(props) { function MediaInfo(props) {
const { const {
type, type,
audioChannels, audioChannels,
audioCodec, audioCodec,
videoCodec audioLanguages,
subtitles,
videoCodec,
videoDynamicRangeType
} = props; } = props;
if (type === mediaInfoTypes.AUDIO) { if (type === mediaInfoTypes.AUDIO) {
return ( return (
<span> <span>
{ {
!!audioCodec && audioCodec ? audioCodec : ''
audioCodec
} }
{ {
!!audioCodec && !!audioChannels && audioCodec && audioChannels ? ' - ' : ''
' - '
} }
{ {
!!audioChannels && audioChannels ? audioChannels.toFixed(1) : ''
audioChannels.toFixed(1)
} }
</span> </span>
); );
} }
if (type === mediaInfoTypes.AUDIO_LANGUAGES) {
return formatLanguages(audioLanguages);
}
if (type === mediaInfoTypes.SUBTITLES) {
return formatLanguages(subtitles);
}
if (type === mediaInfoTypes.VIDEO) { if (type === mediaInfoTypes.VIDEO) {
return ( return (
<span> <span>
@@ -39,6 +80,14 @@ function MediaInfo(props) {
); );
} }
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
return (
<span>
{videoDynamicRangeType}
</span>
);
}
return null; return null;
} }
@@ -46,7 +95,10 @@ MediaInfo.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
audioChannels: PropTypes.number, audioChannels: PropTypes.number,
audioCodec: PropTypes.string, audioCodec: PropTypes.string,
videoCodec: PropTypes.string audioLanguages: PropTypes.string,
subtitles: PropTypes.string,
videoCodec: PropTypes.string,
videoDynamicRangeType: PropTypes.string
}; };
export default MediaInfo; export default MediaInfo;
@@ -94,7 +94,7 @@ class SelectQualityModalContent extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<Alert kind={kinds.DANGER}> <Alert kind={kinds.DANGER}>
{translate('UnableToLoadQualities')} {translate('QualitiesLoadError')}
</Alert> </Alert>
} }
+3
View File
@@ -1,2 +1,5 @@
export const AUDIO = 'audio'; export const AUDIO = 'audio';
export const AUDIO_LANGUAGES = 'audioLanguages';
export const SUBTITLES = 'subtitles';
export const VIDEO = 'video'; export const VIDEO = 'video';
export const VIDEO_DYNAMIC_RANGE_TYPE = 'videoDynamicRangeType';
+6 -8
View File
@@ -9,6 +9,7 @@ import PageContentBody from 'Components/Page/PageContentBody';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import { clear, fetch } from 'Store/Actions/parseActions'; import { clear, fetch } from 'Store/Actions/parseActions';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import ParseResult from './ParseResult'; import ParseResult from './ParseResult';
import parseStateSelector from './parseStateSelector'; import parseStateSelector from './parseStateSelector';
import styles from './Parse.css'; import styles from './Parse.css';
@@ -50,7 +51,7 @@ function Parse() {
); );
return ( return (
<PageContent title="Parse"> <PageContent title={translate('Parse')}>
<PageContentBody> <PageContentBody>
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.inputIconContainer}> <div className={styles.inputIconContainer}>
@@ -76,7 +77,7 @@ function Parse() {
{!isFetching && !!error ? ( {!isFetching && !!error ? (
<div className={styles.message}> <div className={styles.message}>
<div className={styles.helpText}> <div className={styles.helpText}>
Error parsing, please try again. {translate('ParseModalErrorParsing')}
</div> </div>
<div>{getErrorMessage(error)}</div> <div>{getErrorMessage(error)}</div>
</div> </div>
@@ -84,7 +85,7 @@ function Parse() {
{!isFetching && title && !error && !item.parsedMovieInfo ? ( {!isFetching && title && !error && !item.parsedMovieInfo ? (
<div className={styles.message}> <div className={styles.message}>
Unable to parse the provided title, please try again. {translate('ParseModalUnableToParse')}
</div> </div>
) : null} ) : null}
@@ -95,12 +96,9 @@ function Parse() {
{title ? null : ( {title ? null : (
<div className={styles.message}> <div className={styles.message}>
<div className={styles.helpText}> <div className={styles.helpText}>
Enter a release title in the input above {translate('ParseModalHelpText')}
</div>
<div>
Radarr will attempt to parse the title and show you details about
it
</div> </div>
<div>{translate('ParseModalHelpTextDetails')}</div>
</div> </div>
)} )}
</PageContentBody> </PageContentBody>
+4 -7
View File
@@ -86,7 +86,7 @@ function ParseModalContent(props: ParseModalContentProps) {
{!isFetching && !!error ? ( {!isFetching && !!error ? (
<div className={styles.message}> <div className={styles.message}>
<div className={styles.helpText}> <div className={styles.helpText}>
Error parsing, please try again. {translate('ParseModalErrorParsing')}
</div> </div>
<div>{getErrorMessage(error)}</div> <div>{getErrorMessage(error)}</div>
</div> </div>
@@ -94,7 +94,7 @@ function ParseModalContent(props: ParseModalContentProps) {
{!isFetching && title && !error && !item.parsedMovieInfo ? ( {!isFetching && title && !error && !item.parsedMovieInfo ? (
<div className={styles.message}> <div className={styles.message}>
Unable to parse the provided title, please try again. {translate('ParseModalUnableToParse')}
</div> </div>
) : null} ) : null}
@@ -105,12 +105,9 @@ function ParseModalContent(props: ParseModalContentProps) {
{title ? null : ( {title ? null : (
<div className={styles.message}> <div className={styles.message}>
<div className={styles.helpText}> <div className={styles.helpText}>
Enter a release title in the input above {translate('ParseModalHelpText')}
</div>
<div>
Radarr will attempt to parse the title and show you details about
it
</div> </div>
<div>{translate('ParseModalHelpTextDetails')}</div>
</div> </div>
)} )}
</ModalBody> </ModalBody>
+11 -5
View File
@@ -72,11 +72,11 @@ function ParseResult(props: ParseResultProps) {
/> />
{tmdbId ? ( {tmdbId ? (
<ParseResultItem title={translate('TmdbId')} data={tmdbId} /> <ParseResultItem title={translate('TMDBId')} data={tmdbId} />
) : null} ) : null}
{imdbId ? ( {imdbId ? (
<ParseResultItem title={translate('ImdbId')} data={imdbId} /> <ParseResultItem title={translate('IMDbId')} data={imdbId} />
) : null} ) : null}
</FieldSet> </FieldSet>
@@ -98,7 +98,7 @@ function ParseResult(props: ParseResultProps) {
<ParseResultItem <ParseResultItem
title={translate('Repack')} title={translate('Repack')}
data={quality.revision.isRepack ? 'True' : '-'} data={quality.revision.isRepack ? translate('True') : '-'}
/> />
</div> </div>
@@ -112,7 +112,7 @@ function ParseResult(props: ParseResultProps) {
<ParseResultItem <ParseResultItem
title={translate('Real')} title={translate('Real')}
data={quality.revision.real ? 'True' : '-'} data={quality.revision.real ? translate('True') : '-'}
/> />
</div> </div>
</div> </div>
@@ -150,7 +150,13 @@ function ParseResult(props: ParseResultProps) {
<ParseResultItem <ParseResultItem
title={translate('CustomFormats')} title={translate('CustomFormats')}
data={<MovieFormats formats={customFormats} />} data={
customFormats?.length ? (
<MovieFormats formats={customFormats} />
) : (
'-'
)
}
/> />
<ParseResultItem <ParseResultItem
+1 -1
View File
@@ -1,5 +1,5 @@
export enum QualitySource { export enum QualitySource {
Unknown = 'unkonwn', Unknown = 'unknown',
Television = 'television', Television = 'television',
TelevisionRaw = 'televisionRaw', TelevisionRaw = 'televisionRaw',
Web = 'web', Web = 'web',
+1 -1
View File
@@ -83,7 +83,7 @@ function RootFolderRow(props: RootFolderRowProps) {
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteRootFolder')} title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', [path])} message={translate('DeleteRootFolderMessageText', { path })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen} isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCustomFormat')} title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', [name])} message={translate('DeleteCustomFormatMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat} onConfirm={this.onConfirmDeleteCustomFormat}
@@ -41,7 +41,7 @@ function EditSpecificationModalContent(props) {
return ( return (
<ModalContent onModalClose={onCancelPress}> <ModalContent onModalClose={onCancelPress}>
<ModalHeader> <ModalHeader>
{`${id ? 'Edit' : 'Add'} Condition - ${implementationName}`} {id ? translate('EditConditionImplementation', { implementationName }) : translate('AddConditionImplementation', { implementationName })}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -115,7 +115,7 @@ class Specification extends Component {
isOpen={this.state.isDeleteSpecificationModalOpen} isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCondition')} title={translate('DeleteCondition')}
message={translate('DeleteConditionMessageText', [name])} message={translate('DeleteConditionMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification} onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose} onCancel={this.onDeleteSpecificationModalClose}
@@ -92,7 +92,7 @@ class DownloadClient extends Component {
kind={kinds.DISABLED} kind={kinds.DISABLED}
outline={true} outline={true}
> >
{translate('PrioritySettings', [priority])} {translate('PrioritySettings', { priority })}
</Label> </Label>
} }
</div> </div>
@@ -113,7 +113,7 @@ class DownloadClient extends Component {
isOpen={this.state.isDeleteDownloadClientModalOpen} isOpen={this.state.isDeleteDownloadClientModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteDownloadClient')} title={translate('DeleteDownloadClient')}
message={translate('DeleteDownloadClientMessageText', [name])} message={translate('DeleteDownloadClientMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteDownloadClient} onConfirm={this.onConfirmDeleteDownloadClient}
onCancel={this.onDeleteDownloadClientModalClose} onCancel={this.onDeleteDownloadClientModalClose}
@@ -58,7 +58,7 @@ class EditDownloadClientModalContent extends Component {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{`${id ? translate('Edit') : translate('Add')} ${translate('DownloadClient')} - ${implementationName}`} {id ? translate('EditDownloadClientImplementation', { implementationName }) : translate('AddDownloadClientImplementation', { implementationName })}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -180,7 +180,7 @@ function ManageDownloadClientsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountDownloadClientsSelected', [selectedCount])} {translate('CountDownloadClientsSelected', { count: selectedCount })}
</div> </div>
<div> <div>
@@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedDownloadClients')} title={translate('DeleteSelectedDownloadClients')}
message={translate('DeleteSelectedDownloadClientsMessageText', [ message={translate('DeleteSelectedDownloadClientsMessageText', {
selectedIds.length, count: selectedIds.length,
])} })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -1,10 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageSectionContent from 'Components/Page/PageSectionContent'; import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector'; import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
import RemotePathMapping from './RemotePathMapping'; import RemotePathMapping from './RemotePathMapping';
@@ -50,6 +52,11 @@ class RemotePathMappings extends Component {
errorMessage={translate('UnableToLoadRemotePathMappings')} errorMessage={translate('UnableToLoadRemotePathMappings')}
{...otherProps} {...otherProps}
> >
<Alert kind={kinds.INFO}>
<InlineMarkdown data={translate('RemotePathMappingsInfo', { app: 'Radarr', wikiLink: 'https://wiki.servarr.com/radarr/settings#remote-path-mappings' })} />
</Alert>
<div className={styles.remotePathMappingsHeader}> <div className={styles.remotePathMappingsHeader}>
<div className={styles.host}> <div className={styles.host}>
{translate('Host')} {translate('Host')}
@@ -11,12 +11,20 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import { icons, inputTypes, kinds } from 'Helpers/Props'; import { icons, inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const authenticationMethodOptions = [ export const authenticationMethodOptions = [
{ {
key: 'none', key: 'none',
get value() { get value() {
return translate('None'); return translate('None');
} },
isDisabled: true
},
{
key: 'external',
get value() {
return translate('External');
},
isHidden: true
}, },
{ {
key: 'basic', key: 'basic',
@@ -32,6 +40,21 @@ const authenticationMethodOptions = [
} }
]; ];
export const authenticationRequiredOptions = [
{
key: 'enabled',
get value() {
return translate('Enabled');
}
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
}
}
];
const certificateValidationOptions = [ const certificateValidationOptions = [
{ {
key: 'enabled', key: 'enabled',
@@ -42,7 +65,7 @@ const certificateValidationOptions = [
{ {
key: 'disabledForLocalAddresses', key: 'disabledForLocalAddresses',
get value() { get value() {
return translate('CertValidationNoLocal'); return translate('DisabledForLocalAddresses');
} }
}, },
{ {
@@ -98,6 +121,7 @@ class SecuritySettings extends Component {
const { const {
authenticationMethod, authenticationMethod,
authenticationRequired,
username, username,
password, password,
apiKey, apiKey,
@@ -116,13 +140,31 @@ class SecuritySettings extends Component {
name="authenticationMethod" name="authenticationMethod"
values={authenticationMethodOptions} values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')} helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={translate('AuthenticationRequiredWarning')}
onChange={onInputChange} onChange={onInputChange}
{...authenticationMethod} {...authenticationMethod}
/> />
</FormGroup> </FormGroup>
{ {
authenticationEnabled && authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup> <FormGroup>
<FormLabel>{translate('Username')}</FormLabel> <FormLabel>{translate('Username')}</FormLabel>
@@ -132,11 +174,12 @@ class SecuritySettings extends Component {
onChange={onInputChange} onChange={onInputChange}
{...username} {...username}
/> />
</FormGroup> </FormGroup> :
null
} }
{ {
authenticationEnabled && authenticationEnabled ?
<FormGroup> <FormGroup>
<FormLabel>{translate('Password')}</FormLabel> <FormLabel>{translate('Password')}</FormLabel>
@@ -146,7 +189,8 @@ class SecuritySettings extends Component {
onChange={onInputChange} onChange={onInputChange}
{...password} {...password}
/> />
</FormGroup> </FormGroup> :
null
} }
<FormGroup> <FormGroup>
@@ -8,6 +8,12 @@ import { inputTypes, sizes } from 'Helpers/Props';
import titleCase from 'Utilities/String/titleCase'; import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const branchValues = [
'master',
'develop',
'nightly'
];
function UpdateSettings(props) { function UpdateSettings(props) {
const { const {
advancedSettings, advancedSettings,
@@ -52,11 +58,12 @@ function UpdateSettings(props) {
<FormLabel>{translate('Branch')}</FormLabel> <FormLabel>{translate('Branch')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.AUTO_COMPLETE}
name="branch" name="branch"
helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')} helpText={usingExternalUpdateMechanism ? translate('BranchUpdateMechanism') : translate('BranchUpdate')}
helpLink="https://wiki.servarr.com/radarr/settings#updates" helpLink="https://wiki.servarr.com/radarr/settings#updates"
{...branch} {...branch}
values={branchValues}
onChange={onInputChange} onChange={onInputChange}
readOnly={usingExternalUpdateMechanism} readOnly={usingExternalUpdateMechanism}
/> />
@@ -76,6 +83,7 @@ function UpdateSettings(props) {
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="updateAutomatically" name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')} helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Radarr' }) : undefined}
onChange={onInputChange} onChange={onInputChange}
{...updateAutomatically} {...updateAutomatically}
/> />

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