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

Compare commits

...

119 Commits

Author SHA1 Message Date
Robin Dadswell d63b3ca59d fixed issues with test 2024-11-19 15:29:49 +00:00
Robin Dadswell c8faab9928 fixup tests
local build not working, need to work out why - it's to do with a nuget restore
2024-11-18 17:16:18 +00:00
Robin Dadswell 0a6b0ee959 fix macos image version 2024-11-18 15:53:45 +00:00
Robin Dadswell b993f70d2c Version Bump to force build 2024-11-18 15:48:21 +00:00
Qstick 85544ca8f6 fixup! New: Multiple Quality Profiles and Files Per Movie 2023-10-14 20:42:57 -05:00
Qstick 9e7ad678b0 Add api endpoint to generate the required login cookie
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2023-10-14 17:44:48 -05:00
Robin Dadswell 0aebd90ac9 Fixed: Another attempted fix on ODIC correlation error 2023-10-14 17:44:48 -05:00
Robin Dadswell 76bed80060 Fixed: Alternative fix on ODIC correlation error 2023-10-14 17:44:48 -05:00
Robin Dadswell 669b50dc72 New: SSO goes straight to authentication provider 2023-10-14 17:44:48 -05:00
ta264 18fc1413c3 New: OIDC and Plex authentication methods
(cherry picked from commit 3ff3de6b90704fba266833115cd9d03ace99aae9)
2023-10-14 17:44:47 -05:00
Qstick 775b1ba9cf Build Branch [REVERT] 2023-10-14 17:44:47 -05:00
Qstick 5ad3f96e0f New: Multiple Quality Profiles and Files Per Movie 2023-10-14 17:44:47 -05:00
Qstick b024fcf5ee Bump Version to 6 2023-10-14 17:36:43 -05:00
Qstick 3d46bd2d8f Revert cover mapping for collections, optimize translation mapping 2023-10-07 15:43:59 -05:00
Bogdan 017f272201 Log Notifiarr errors as warnings 2023-10-07 22:56:22 +03:00
Bogdan c221e2097a Prevent NullRef for cases when media covers have nullable urls 2023-10-05 02:42:18 +03:00
Bogdan a61804e949 Fixed localization test 2023-10-05 01:45:55 +03:00
Bogdan cb2bed93cb Fixed: Sorting by movie title in Blocklist and History
Fixes #9234
2023-10-05 01:02:35 +03:00
Weblate 2bea61bae5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Garkus98 <ivan12061998@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-10-04 08:43:53 +03:00
Mark McDowall 7922109f01 Fixed a potential issue when extra files for multiple movies have the same relative path
(cherry picked from commit a6a68b4cae7688506c45ff6cf10989fe6596c274)

Closes #7222
2023-10-04 00:09:58 +03:00
Mark McDowall 46dd72e0cd New: Validate that naming formats don't contain illegal characters
(cherry picked from commit 145c644c9d8f1636027da8a782a7e74f3182c678)

Closes #5382
2023-10-03 23:49:30 +03:00
Mark McDowall 4e3535f1fe Fixed Misaligned table border
(cherry picked from commit aa938d911b61b08185dc57a0887f3f33e3c6e1f2)

Closes #8176
2023-10-03 23:36:45 +03:00
Mark McDowall 3468f1144d New: Calendar month view will scroll to today on load and press
(cherry picked from commit 7c0d3444376caa8a116b5c2084821c326beb01a1)

Closes #8501
2023-10-03 21:42:50 +03:00
Bogdan 572c410f54 Add runtime param to ServerSideNotificationService 2023-10-02 18:16:43 +03:00
Bogdan 1762a189d2 Fixed: (PassThePopcorn) Disable grouping 2023-10-02 03:38:24 +03:00
Mark McDowall e2f5f2f73a Fixed: Completed downloads in Qbit missing import path
(cherry picked from commit 35365665cfd436ac276dd9591e23333bd26cf789)

Closes #9221
2023-10-01 17:13:24 +03:00
Weblate ade387ba74 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mr cmuc <github@nextcos.de>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translation: Servarr/Radarr
2023-10-01 17:11:28 +03:00
Stevie Robinson 6b9a622328 Fixed: qBittorent history retention to allow at least 14 days seeding
(cherry picked from commit 33b87acabf2b4c71ee24cda1a466dec6f4f76996)
2023-10-01 17:11:05 +03:00
bakerboy448 ba5028bebb Fixed: Only apply remote path mappings for completed items in Qbit
(cherry picked from commit 583eb52ddc01b608ab6cb17e863a8830c17b7b75)
2023-10-01 17:10:47 +03:00
Stevie Robinson 33d1d1f875 Fixed: SABnzbd history retention to allow at least 14 days
(cherry picked from commit a3938d8e0264b48b35f4715cbc15329fb489218a)

Closes #9217
2023-09-27 23:14:34 +03:00
Weblate fb60dcb5bf Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Jaspils <jasperkemper@gmail.com>
Co-authored-by: SKAL <sir_kalot@yahoo.it>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 宿命 <331874545@qq.com>
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/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-09-27 19:45:22 +03:00
Bogdan ddf23530fc Bump version to 5.0.3 2023-09-24 16:22:06 +03:00
Bogdan 30b1edbff0 Add content summary for bulk movie info 2023-09-20 20:58:39 +03:00
Bogdan f20c260a4f Avoid returning null in static resource mapper Task 2023-09-19 20:15:59 +03:00
Bogdan 2fcbac49c7 Fixed: Add support for TMDb in Plex Watchlist RSS
Fixes #9208
2023-09-19 19:10:20 +03:00
Mark McDowall 3248e7f476 Fixed: Pushed releases not being properly rejected
(cherry picked from commit 07f816ffb18ac34090c2f8ba25147737299b361d)

Closes #9204
2023-09-19 14:04:01 +03:00
Mark McDowall ce145a3050 Optional 'downloadClientId' for pushed releases
(cherry picked from commit fa5bfc3742c24c5730b77bf8178a423d98fdf50e)

Closes #9190
2023-09-19 14:01:45 +03:00
Bogdan 3bc4197b4a Preserve the protocol for fanart images
(cherry picked from commit d8633b968830fd20d73612e9f0d0559b0bcb304c)

Closes #9205
2023-09-19 14:01:39 +03:00
Bogdan 552b8f91d2 Fixed: Skip parsing releases without title
(cherry picked from commit c7824bb593291634bf14a5f7aa689666969b03bf)
2023-09-19 13:58:11 +03:00
Bogdan e9e36ae56a Revert "New: Optional 'downloadClientId' for pushed releases"
This reverts commit 4e01fa57fd.

Closes #9202
2023-09-19 04:07:27 +03:00
Bogdan 450d6c0c80 Replace support-requests with label-actions 2023-09-19 02:05:48 +03:00
Servarr 9eece2965a Automated API Docs update 2023-09-18 16:04:17 +03:00
Stevie Robinson cd5d4f993a Add health check for dl clients removing completed downloads + enable for sab and qbit
(cherry picked from commit 7f2cd8a0e99b537a1c616998514bacdd8468a016)

Closes #9195
2023-09-18 15:28:13 +03:00
Bogdan fe7203815d Preserve the protocol in Movie Image
(cherry picked from commit a2f16bddfd7dfba207c5feaaf472913c38dc3e25)

Closes #9198
2023-09-18 15:24:06 +03:00
Mark McDowall 4e01fa57fd New: Optional 'downloadClientId' for pushed releases
(cherry picked from commit fa5bfc3742c24c5730b77bf8178a423d98fdf50e)

Closes #9190
2023-09-18 15:18:55 +03:00
Bogdan bbeb4d7b5f Log request failures in Notifiarr 2023-09-18 14:57:05 +03:00
Mark McDowall 49dac0ebaa New: Don't treat 400 responses from Notifiarr as errors
(cherry picked from commit 5eb420bbe12f59d0a5392abf3d351be28ca210e6)

Closes #9194
2023-09-18 14:53:09 +03:00
Mark McDowall ea8f5c7b9f Fixed: Skip free space check only applies during import
(cherry picked from commit 5ff254b6468fc66a337dc0a13f4be53a997c52fd)

Closes #9191
2023-09-18 14:48:55 +03:00
Mark McDowall 24a17a9240 Fixed: Don't try to create metadata images if source files doesn't exist
(cherry picked from commit 9a1022386a031c928fc0495d6ab990ebce605ec1)

Closes #9189
2023-09-18 14:47:03 +03:00
Weblate 97c2d4f9db Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Daghriry <mdaghriri@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Gyuyeop Kim <rlarbduq777@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mati300m <mateusz.smolec@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
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/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-09-18 14:43:29 +03:00
Qstick b7cafb2917 Fixed: Show correct error on unauthorized caps call
(cherry picked from commit f2b0fc946e1fb1b4649f1b46a003bd2add09a461)
2023-09-18 14:42:50 +03:00
Mark McDowall 2a2667a2ec Fixed: Don't allow quality profile to be created without all qualities
(cherry picked from commit 32e1ae2f64827272d351991838200884876e52b4)
2023-09-18 14:42:06 +03:00
Bogdan 27da524391 Use async requests for media cover proxy
(cherry picked from commit ad1f185330a30a2a9d27c9d3f18d384e66727c2a)

Closes #9183
2023-09-18 03:46:52 +03:00
Bogdan 4bd1c14db9 Log exceptions for failed fetches in Radarr import lists
Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
(cherry picked from commit c1d9187bb66c0524048020613d816918b84b5532)

Closes #9186
2023-09-18 03:46:46 +03:00
Bogdan 608e2e7307 Use await on reading the response content
(cherry picked from commit 82d586e7015d7ea06356ca436024a8af5a4fb677)
2023-09-18 03:22:14 +03:00
Qstick cff54d76b9 Fix regression in collections endpoint speed 2023-09-17 00:14:28 -05:00
Mark McDowall 3244282a83 Less logging when no import lists are enabled
(cherry picked from commit 7be4840f028f24e3920bd395a4e15eb5e643e46f)

Closes #9040
2023-09-15 15:22:59 +03:00
Weblate 1d488df242 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Akashi2020 <dieux02400@gmail.com>
Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Anthony Veaudry <anthonyveaudry@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Herve Lauwerier <hervelauwerier@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
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/zh_CN/
Translation: Servarr/Radarr
2023-09-14 23:34:25 +03:00
Bogdan 22927224c6 Fix lint issues in AppUpdatedModalContent 2023-09-14 23:32:33 +03:00
Stevie Robinson 51149bccdd Translate Updated and Connection Lost Modals in frontend
(cherry picked from commit 074aa6f4457bf83173e6ba7209c452a6e0659a35)

Closes #8994
2023-09-14 23:04:49 +03:00
Stevie Robinson 4bbc166040 Translate Frontend Organize + Rename modal
(cherry picked from commit 866fbc7f093c34e4414734f90e756e2f6870d84a)

Closes #9054
2023-09-14 23:04:44 +03:00
Bogdan 11c7446cbe Save Whitelisted Subtitle Tags as string
Fixes #9134
2023-09-14 22:37:53 +03:00
Mark McDowall dce637905a Mock debouncer for testing
(cherry picked from commit bb7b2808e2f70389157408809ec47cc8860b4938)
2023-09-14 22:19:12 +03:00
Mark McDowall 7d85922f8d Fixed: Duplicate notifications for failed health checks
(cherry picked from commit c0e54773e213f526a5046fa46aa7b57532471128)
2023-09-14 22:19:12 +03:00
Bogdan 80f6033595 Fixed: Ignore invalid mount points
Fixes #9176
2023-09-13 22:21:43 +03:00
Weblate 78b8747b50 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Richard de Souza Leite <rs9010482@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: volrod64 <sebsogamer@gmail.com>
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/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-09-13 19:16:37 +03:00
Stevie Robinson c2df194d49 Translate Frontend Utilities
(cherry picked from commit 3f0e8ce8634b085d49638d11df96109797ac9cef)

Closes #9129
2023-09-12 20:11:18 +03:00
Stevie Robinson 4a41c67dfe Fix missing translations and correct some keys
(cherry picked from commit ce4ac7594192fa8f63ba239607be61d229c24ce1)
2023-09-12 19:33:32 +03:00
Stevie Robinson 85d51e485a Translate Activity pages
(cherry picked from commit 322836e2b3726b653bf4a7f01ea7018ef44a77bd)
2023-09-12 19:33:32 +03:00
Bogdan 50e2e9edef Add custom format score to history details 2023-09-12 19:33:32 +03:00
Bogdan 703c251b5c Fix some translations 2023-09-12 19:33:32 +03:00
Mark McDowall a798556d32 New: Show detailed queue status on Calendar
(cherry picked from commit 8fff59ff107d9a9fcfc0de1acb6aa635565e5d9b)
2023-09-12 19:33:32 +03:00
Bogdan 69253a4ac4 Fix status style for unmonitored events 2023-09-12 12:05:59 +03:00
Mark McDowall 4e827e726f New: Calendar option for full color events
(cherry picked from commit 0210b5c5c1b5c56dce6f4c9f3f56366adba950d3)
2023-09-11 22:54:45 +03:00
Bogdan e3abda9afc Ensure the images are mapped correctly after updating a movie 2023-09-11 18:53:34 +03:00
Steven Lu 0386ea9b71 Updating Steven Lu movies URL 2023-09-11 17:49:35 +03:00
Bogdan f0fcd23248 Fix custom formats with indexer flags in queue 2023-09-11 11:10:38 +03:00
Mark McDowall 18f22d7ada Fixed: Parsing of multiple languages from Newznab/Torznab indexer releases
(cherry picked from commit 2a241294b5eeb9e95c46e030828191da09d05e88)

Closes #9159
2023-09-11 10:45:23 +03:00
Servarr 1c4b5f2abf Automated API Docs update 2023-09-10 22:17:06 -04:00
Mark McDowall b48b970f25 Map Clearlogo images to cover type
(cherry picked from commit 809788eb2ed7e448f7f23101ae535533de57cd02)
2023-09-10 22:05:03 -04:00
Qstick e715557a0d Fixed: Tweak all movies endpoint for better performance from Radarr lists 2023-09-10 22:04:23 -04:00
Qstick 248ac9619c Fixed: Images for some movies not downloading
Closes #8006

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2023-09-10 22:04:23 -04:00
Denis Gheorghescu feff609685 New: Add support for Pushcut notifications 2023-09-10 23:29:54 +03:00
Bogdan 07cfbb59da Add media proxy for collections, credits and discovery images 2023-09-10 22:41:26 +03:00
Weblate 9db0058114 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: Shiessis <shiessis@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/es/
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/
Translation: Servarr/Radarr
2023-09-10 20:29:24 +03:00
Weblate 8d7f6b9de8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: ChewyGlitter <lulu3dddsss@gmail.com>
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: He Zhu <zhuhe202@qq.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/fr/
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/zh_CN/
Translation: Servarr/Radarr
2023-09-10 00:26:49 -05:00
Bogdan 28c566a071 Hide obsolete indexer flags from UI 2023-09-10 08:18:23 +03:00
Bogdan e5963c9ee1 Add rename deprecated indexer flags migration 2023-09-09 13:13:56 +03:00
Bogdan 336cb4a2bc Improve messaging for failed movie details data 2023-09-09 13:13:56 +03:00
Bogdan ff3d38a515 New: Add support for additional Torznab indexer flags 2023-09-09 07:54:37 +03:00
Bogdan a2bde5e016 Fixed: Calculating seed time for qBittorrent
(cherry picked from commit 1b3ff64cc521396f9f1623617052c497649325a8)
2023-09-08 04:25:55 +03:00
Bogdan cb04ef960e Fixed: macOS version detection
(cherry picked from commit 060be6177a5477c94823e6a423c42064dedc1afb)
2023-09-08 04:14:47 +03:00
Servarr ba732847ef Automated API Docs update 2023-09-06 12:37:12 +03:00
Bogdan 1865257544 Include indexer flags to show the correct custom formats in movie file history 2023-09-06 12:29:37 +03:00
Bogdan 58e0b19d06 Include indexer flags to show the correct custom formats in movie imported history 2023-09-06 09:56:10 +03:00
Bogdan 05c5bcbe15 Bump ImageSharp 2023-09-05 03:18:27 +03:00
Bogdan d6749a0c8e Migrate to merged proposals now included in babel/present-env
Bump core-js too.
2023-09-04 00:50:20 +03:00
Bogdan 72fe25d7b2 Bump dotnet to 6.0.21 2023-09-04 00:50:20 +03:00
Bogdan 0598d46ee8 Bump version to 5.0.2 2023-09-03 07:28:12 +03:00
Bogdan e73d05c0fe Check if import list items are valid before importing
Co-authored-by: Qstick <qstick@gmail.com>
2023-09-03 04:21:10 +03:00
Bakerboy448 9a0ca650a3 Fixed: Dont Parse 24-bit as group 2023-09-02 20:16:40 -05:00
Qstick ac6da13a82 Avoid double ToList conversion in ImportList fetch 2023-09-02 15:17:06 -05:00
Bogdan 61ffc50b7f Use paths relative to the frontend source root 2023-09-02 05:41:39 +03:00
Stevie Robinson fcd8a4a873 Translate Frontend Store
Fixed indexer copy translation

(cherry picked from commit d31fcbb2dfe98a540a359b98b204d101d554cf03)
(cherry picked from commit 276352dda478aaa7be7d0fc744548adddec6e6b6)

Closes #9118
Closes #9121
2023-09-02 05:37:13 +03:00
Stevie Robinson 093bb94e42 Fixed: Fallback to English translations if invalid UI language in config
(cherry picked from commit 4c7201741276eccaea2fb1f33daecc31e8b2d54e)

Close #9112
2023-09-02 05:29:36 +03:00
Bogdan 14b9dd77af Display a not allowed cursor for disabled select options 2023-09-02 05:18:35 +03:00
Bogdan d6b62e738a Fixed: Increase timeout when downloading updates
(cherry picked from commit 467ce70291c793042ffb3ed8942c42e7bc1424d5)
2023-09-01 04:04:07 +03:00
Bogdan 53254f6aeb Fix showing Grab ID in history details modal 2023-08-29 22:36:33 +03:00
Bogdan 756384d94a Simplify user check in Authentication Required for newer installs 2023-08-29 19:57:48 +03:00
Bogdan 7f172dcfd1 Fix user check in Authentication Required for newer installs 2023-08-29 18:54:12 +03:00
Mark McDowall 02998cd59a Fixed: Auto Tag required not showing in the UI correctly
(cherry picked from commit 08939f2fb4b6f7aca4916d8bd3b60535f3a992da)
2023-08-29 08:31:10 +03:00
Stevie Robinson 37aa739611 Add info box under health messages
(cherry picked from commit 936ef9f461c3e80a36099508c420dc44d8cd293c)

Closes #9095
2023-08-29 05:43:12 +03:00
Bogdan 0e2c98827f Remove column labels from Interactive Search 2023-08-29 05:31:50 +03:00
Bogdan 27f45b8fd6 Add missing app name token for translations 2023-08-29 03:29:46 +03:00
Bogdan 2210ce9394 Improve messaging in Authentication Required modal 2023-08-29 01:42:30 +03:00
Bogdan bbef1590a3 Remove duplicate Language interface 2023-08-29 01:35:55 +03:00
Bogdan feb3131ad4 Fix download tooltip in interactive search 2023-08-28 03:36:32 +03:00
Weblate 89f5595e64 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ChewyGlitter <lulu3dddsss@gmail.com>
Co-authored-by: DavidJares <david.jares@me.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/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-08-27 19:20:29 +03:00
Bogdan cf9cff61b5 Bump version to 5.0.1 2023-08-27 08:02:02 +03:00
472 changed files with 9883 additions and 4968 deletions
+16
View File
@@ -0,0 +1,16 @@
# Configuration for Label Actions - https://github.com/dessant/label-actions
'Type: Support':
comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
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).
close: true
close-reason: 'not planned'
'Status: Logs Needed':
comment: >
:wave: @{issue-author}, In order to help you further we'll need to see logs.
You'll need to enable trace logging and replicate the problem that you encountered.
Guidance on how to enable trace logging can be found in
our [troubleshooting guide](https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files).
+17
View File
@@ -0,0 +1,17 @@
name: 'Label Actions'
on:
issues:
types: [labeled, unlabeled]
permissions:
contents: read
issues: write
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/label-actions@v3
with:
process-only: 'issues'
-36
View File
@@ -1,36 +0,0 @@
name: 'Support requests'
on:
issues:
types: [labeled, unlabeled, reopened]
permissions: {}
jobs:
support:
permissions:
issues: write # to modify issues
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v3
with:
github-token: ${{ github.token }}
support-label: 'Type: Support'
issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
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).
close-issue: true
close-reason: 'not planned'
lock-issue: false
- uses: dessant/support-requests@v3
with:
github-token: ${{ github.token }}
support-label: 'Status: Logs Needed'
issue-comment: >
:wave: @{issue-author}, In order to help you further we'll need to see logs.
You'll need to enable trace logging and replicate the problem that you encountered.
Guidance on how to enable trace logging can be found in
our [troubleshooting guide](https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files).
close-issue: false
lock-issue: false
+4 -3
View File
@@ -9,24 +9,25 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.0.0'
majorVersion: '6.0.1'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.408'
dotnetVersion: '6.0.413'
nodeVersion: '16.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11'
macImage: 'macOS-13'
trigger:
branches:
include:
- develop
- master
- zeus
paths:
exclude:
- .github
+4 -4
View File
@@ -4,14 +4,14 @@ module.exports = {
plugins: [
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
['@babel/plugin-transform-optional-chaining', { loose }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
// Stage 2
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-transform-export-namespace-from',
// Stage 3
['@babel/plugin-proposal-class-properties', { loose }],
['@babel/plugin-transform-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import'
],
env: {
+2 -2
View File
@@ -158,7 +158,7 @@ class Blocklist extends Component {
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadBlocklist')}
{translate('BlocklistLoadError')}
</Alert>
}
@@ -210,7 +210,7 @@ class Blocklist extends Component {
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title={translate('RemoveSelected')}
message={translate('RemoveSelectedItemBlocklistMessageText')}
message={translate('RemoveSelectedBlocklistMessageText')}
confirmLabel={translate('RemoveSelected')}
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
@@ -82,7 +82,7 @@ class BlocklistRow extends Component {
return null;
}
if (name === 'movies.sortTitle') {
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink
@@ -7,6 +7,7 @@ import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionList
import Link from 'Components/Link/Link';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
@@ -15,6 +16,7 @@ function HistoryDetails(props) {
eventType,
sourceTitle,
data,
downloadId,
shortDateFormat,
timeFormat
} = props;
@@ -23,11 +25,11 @@ function HistoryDetails(props) {
const {
indexer,
releaseGroup,
movieMatchType,
customFormatScore,
nzbInfoUrl,
downloadClient,
downloadClientName,
downloadId,
movieMatchType,
age,
ageHours,
ageMinutes,
@@ -45,33 +47,31 @@ function HistoryDetails(props) {
/>
{
!!indexer &&
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/>
/> :
null
}
{
!!releaseGroup &&
releaseGroup ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseGroup')}
data={releaseGroup}
/>
/> :
null
}
{
!!nzbInfoUrl &&
<span>
<DescriptionListItemTitle>
Info URL
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span>
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
{
@@ -84,6 +84,20 @@ function HistoryDetails(props) {
null
}
{
nzbInfoUrl ?
<span>
<DescriptionListItemTitle>
{translate('InfoUrl')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span> :
null
}
{
downloadClientNameInfo ?
<DescriptionListItem
@@ -94,27 +108,30 @@ function HistoryDetails(props) {
}
{
!!downloadId &&
downloadId ?
<DescriptionListItem
title={translate('GrabID')}
title={translate('GrabId')}
data={downloadId}
/>
/> :
null
}
{
!!indexer &&
indexer ?
<DescriptionListItem
title={translate('AgeWhenGrabbed')}
data={formatAge(age, ageHours, ageMinutes)}
/>
/> :
null
}
{
!!publishedDate &&
publishedDate ?
<DescriptionListItem
title={translate('PublishedDate')}
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/>
/> :
null
}
</DescriptionList>
);
@@ -134,11 +151,21 @@ function HistoryDetails(props) {
/>
{
!!message &&
downloadId ?
<DescriptionListItem
title={translate('GrabId')}
data={downloadId}
/> :
null
}
{
message ?
<DescriptionListItem
title={translate('Message')}
data={message}
/>
/> :
null
}
</DescriptionList>
);
@@ -146,6 +173,7 @@ function HistoryDetails(props) {
if (eventType === 'downloadFolderImported') {
const {
customFormatScore,
droppedPath,
importedPath
} = data;
@@ -159,21 +187,32 @@ function HistoryDetails(props) {
/>
{
!!droppedPath &&
droppedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Source')}
data={droppedPath}
/>
/> :
null
}
{
!!importedPath &&
importedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ImportedTo')}
data={importedPath}
/>
/> :
null
}
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
</DescriptionList>
);
@@ -181,20 +220,21 @@ function HistoryDetails(props) {
if (eventType === 'movieFileDeleted') {
const {
reason
reason,
customFormatScore
} = data;
let reasonMessage = '';
switch (reason) {
case 'Manual':
reasonMessage = translate('FileWasDeletedByViaUI');
reasonMessage = translate('DeletedReasonManual');
break;
case 'MissingFromDisk':
reasonMessage = translate('MissingFromDisk');
reasonMessage = translate('DeletedReasonMissingFromDisk');
break;
case 'Upgrade':
reasonMessage = translate('FileWasDeletedByUpgrade');
reasonMessage = translate('DeletedReasonUpgrade');
break;
default:
reasonMessage = '';
@@ -211,6 +251,15 @@ function HistoryDetails(props) {
title={translate('Reason')}
data={reasonMessage}
/>
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
</DescriptionList>
);
}
@@ -262,11 +311,21 @@ function HistoryDetails(props) {
/>
{
!!message &&
downloadId ?
<DescriptionListItem
title={translate('GrabId')}
data={downloadId}
/> :
null
}
{
message ?
<DescriptionListItem
title={translate('Message')}
data={message}
/>
/> :
null
}
</DescriptionList>
);
@@ -287,6 +346,7 @@ HistoryDetails.propTypes = {
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
@@ -15,19 +15,19 @@ import styles from './HistoryDetailsModal.css';
function getHeaderTitle(eventType) {
switch (eventType) {
case 'grabbed':
return 'Grabbed';
return translate('Grabbed');
case 'downloadFailed':
return 'Download Failed';
return translate('DownloadFailed');
case 'downloadFolderImported':
return 'Movie Imported';
return translate('MovieImported');
case 'movieFileDeleted':
return 'Movie File Deleted';
return translate('MovieFileDeleted');
case 'movieFileRenamed':
return 'Movie File Renamed';
return translate('MovieFileRenamed');
case 'downloadIgnored':
return 'Download Ignored';
return translate('DownloadIgnored');
default:
return 'Unknown';
return translate('Unknown');
}
}
@@ -37,6 +37,7 @@ function HistoryDetailsModal(props) {
eventType,
sourceTitle,
data,
downloadId,
isMarkingAsFailed,
shortDateFormat,
timeFormat,
@@ -59,6 +60,7 @@ function HistoryDetailsModal(props) {
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
/>
@@ -73,7 +75,7 @@ function HistoryDetailsModal(props) {
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
Mark as Failed
{translate('MarkAsFailed')}
</SpinnerButton>
}
@@ -93,6 +95,7 @@ HistoryDetailsModal.propTypes = {
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
isMarkingAsFailed: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
+2 -2
View File
@@ -85,7 +85,7 @@ class History extends Component {
{
!isFetchingAny && hasError &&
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadHistory')}
{translate('HistoryLoadError')}
</Alert>
}
@@ -95,7 +95,7 @@ class History extends Component {
isPopulated && !hasError && !items.length &&
<Alert kind={kinds.INFO}>
{translate('NoHistory')}
{translate('NoHistoryFound')}
</Alert>
}
@@ -3,6 +3,7 @@ import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css';
function getIconName(eventType) {
@@ -38,21 +39,21 @@ function getIconKind(eventType) {
function getTooltip(eventType, data) {
switch (eventType) {
case 'grabbed':
return `Movie grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
return translate('MovieGrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
case 'movieFolderImported':
return 'Movie imported from movie folder';
return translate('MovieFolderImportedTooltip');
case 'downloadFolderImported':
return 'Movie downloaded successfully and picked up from download client';
return translate('MovieImportedTooltip');
case 'downloadFailed':
return 'Movie download failed';
return translate('MovieDownloadFailedTooltip');
case 'movieFileDeleted':
return 'Movie file deleted';
return translate('MovieFileDeletedTooltip');
case 'movieFileRenamed':
return 'Movie file renamed';
return translate('MovieFileRenamedTooltip');
case 'downloadIgnored':
return 'Movie Download Ignored';
return translate('MovieDownloadIgnoredTooltip');
default:
return 'Unknown event';
return translate('UnknownEventTooltip');
}
}
+10 -5
View File
@@ -64,6 +64,7 @@ class HistoryRow extends Component {
sourceTitle,
date,
data,
downloadId,
isMarkingAsFailed,
columns,
shortDateFormat,
@@ -98,7 +99,7 @@ class HistoryRow extends Component {
);
}
if (name === 'movies.sortTitle') {
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink
@@ -216,10 +217,12 @@ class HistoryRow extends Component {
key={name}
className={styles.details}
>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
<div className={styles.actionContents}>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
</div>
</TableRowCell>
);
}
@@ -233,6 +236,7 @@ class HistoryRow extends Component {
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
isMarkingAsFailed={isMarkingAsFailed}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
@@ -257,6 +261,7 @@ HistoryRow.propTypes = {
sourceTitle: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
isMarkingAsFailed: PropTypes.bool,
markAsFailedError: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
+1 -1
View File
@@ -233,7 +233,7 @@ class Queue extends Component {
{
!isRefreshing && hasError ?
<Alert kind={kinds.DANGER}>
{translate('FailedToLoadQueue')}
{translate('QueueLoadError')}
</Alert> :
null
}
@@ -0,0 +1,5 @@
.progressBarContainer {
display: flex;
justify-content: center;
width: 100%;
}
+7
View File
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'progressBarContainer': string;
}
export const cssExports: CssExports;
export default cssExports;
+38 -82
View File
@@ -1,116 +1,71 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import Popover from 'Components/Tooltip/Popover';
import { icons, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import QueueStatus from './QueueStatus';
import styles from './QueueDetails.css';
function QueueDetails(props) {
const {
title,
size,
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
statusMessages,
errorMessage,
progressBar
} = props;
const progress = size ? (100 - sizeleft / size * 100) : 0;
const isDownloading = status === 'downloading';
const isPaused = status === 'paused';
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
if (status === 'pending') {
return (
<Icon
name={icons.PENDING}
title={translate('ReleaseWillBeProcessedInterp', [moment(estimatedCompletionTime).fromNow()])}
/>
);
}
if (
(isDownloading || isPaused) &&
!hasWarning &&
!hasError
) {
const state = isPaused ? translate('Paused') : translate('Downloading');
if (status === 'completed') {
if (errorMessage) {
if (progress < 5) {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.DANGER}
title={translate('ImportFailedInterp', { errorMessage })}
name={icons.DOWNLOADING}
title={`${state} - ${progress.toFixed(1)}% ${title}`}
/>
);
}
if (trackedDownloadStatus === 'warning') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.WARNING}
title={translate('UnableToImportCheckLogs')}
/>
);
}
if (trackedDownloadState === 'importPending') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={`${translate('Downloaded')} - ${translate('WaitingToImport')}`}
/>
);
}
if (trackedDownloadState === 'importing') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={`${translate('Downloaded')} - ${translate('Importing')}`}
/>
);
}
}
if (errorMessage) {
return (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={translate('DownloadFailedInterp', { errorMessage })}
<Popover
className={styles.progressBarContainer}
anchor={progressBar}
title={`${state} - ${progress.toFixed(1)}%`}
body={
<div>{title}</div>
}
position={tooltipPositions.LEFT}
/>
);
}
if (status === 'failed') {
return (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={translate('DownloadFailedCheckDownloadClientForMoreDetails')}
/>
);
}
if (status === 'warning') {
return (
<Icon
name={icons.DOWNLOADING}
kind={kinds.WARNING}
title={translate('DownloadWarningCheckDownloadClientForMoreDetails')}
/>
);
}
if (progress < 5) {
return (
<Icon
name={icons.DOWNLOADING}
title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}
/>
);
}
return progressBar;
return (
<QueueStatus
sourceTitle={title}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
position={tooltipPositions.LEFT}
/>
);
}
QueueDetails.propTypes = {
@@ -121,6 +76,7 @@ QueueDetails.propTypes = {
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired
};
+1 -1
View File
@@ -61,7 +61,7 @@ class QueueOptions extends Component {
type={inputTypes.CHECK}
name="includeUnknownMovieItems"
value={includeUnknownMovieItems}
helpText={translate('IncludeUnknownMovieItemsHelpText')}
helpText={translate('ShowUnknownMovieItemsHelpText')}
onChange={this.onOptionChange}
/>
</FormGroup>
@@ -0,0 +1,3 @@
.noMessages {
margin-bottom: 10px;
}
+7
View File
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'noMessages': string;
}
export const cssExports: CssExports;
export default cssExports;
+162
View File
@@ -0,0 +1,162 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './QueueStatus.css';
function getDetailedPopoverBody(statusMessages) {
return (
<div>
{
statusMessages.map(({ title, messages }) => {
return (
<div
key={title}
className={messages.length ? undefined: styles.noMessages}
>
{title}
<ul>
{
messages.map((message) => {
return (
<li key={message}>
{message}
</li>
);
})
}
</ul>
</div>
);
})
}
</div>
);
}
function QueueStatus(props) {
const {
sourceTitle,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage,
position,
canFlip
} = props;
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind = kinds.DEFAULT;
let title = translate('Downloading');
if (status === 'paused') {
iconName = icons.PAUSED;
title = translate('Paused');
}
if (status === 'queued') {
iconName = icons.QUEUED;
title = translate('Queued');
}
if (status === 'completed') {
iconName = icons.DOWNLOADED;
title = translate('Downloaded');
if (trackedDownloadState === 'importPending') {
title += ` - ${translate('WaitingToImport')}`;
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ` - ${translate('Importing')}`;
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ` - ${translate('WaitingToProcess')}`;
iconKind = kinds.DANGER;
}
}
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'delay') {
iconName = icons.PENDING;
title = translate('Pending');
}
if (status === 'downloadClientUnavailable') {
iconName = icons.PENDING;
iconKind = kinds.WARNING;
title = translate('PendingDownloadClientUnavailable');
}
if (status === 'failed') {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;
title = translate('DownloadFailed');
}
if (status === 'warning') {
iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING;
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
title = translate('DownloadWarning', { warningMessage });
}
if (hasError) {
if (status === 'completed') {
iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER;
title = translate('ImportFailed', { sourceTitle });
} else {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;
title = translate('DownloadFailed');
}
}
return (
<Popover
anchor={
<Icon
name={iconName}
kind={iconKind}
/>
}
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
position={position}
canFlip={canFlip}
/>
);
}
QueueStatus.propTypes = {
sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
canFlip: PropTypes.bool.isRequired
};
QueueStatus.defaultProps = {
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading',
canFlip: false
};
export default QueueStatus;
+9 -118
View File
@@ -1,39 +1,11 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css';
function getDetailedPopoverBody(statusMessages) {
return (
<div>
{
statusMessages.map(({ title, messages }) => {
return (
<div key={title}>
{title}
<ul>
{
messages.map((message) => {
return (
<li key={message}>
{message}
</li>
);
})
}
</ul>
</div>
);
})
}
</div>
);
}
function QueueStatusCell(props) {
const {
sourceTitle,
@@ -44,97 +16,16 @@ function QueueStatusCell(props) {
errorMessage
} = props;
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind = kinds.DEFAULT;
let title = translate('Downloading');
if (status === 'paused') {
iconName = icons.PAUSED;
title = translate('Paused');
}
if (status === 'queued') {
iconName = icons.QUEUED;
title = translate('Queued');
}
if (status === 'completed') {
iconName = icons.DOWNLOADED;
title = translate('Downloaded');
if (trackedDownloadState === 'importPending') {
title += ` - ${translate('WaitingToImport')}`;
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ` - ${translate('Importing')}`;
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ` - ${translate('WaitingToProcess')}`;
iconKind = kinds.DANGER;
}
}
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'delay') {
iconName = icons.PENDING;
title = translate('Pending');
}
if (status === 'DownloadClientUnavailable') {
iconName = icons.PENDING;
iconKind = kinds.WARNING;
title = `${translate('Pending')} - ${translate('DownloadClientUnavailable')}`;
}
if (status === 'failed') {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;
title = translate('DownloadFailed');
}
if (status === 'warning') {
iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING;
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
title = translate('DownloadWarning', { warningMessage });
}
if (hasError) {
if (status === 'completed') {
iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER;
title = translate('ImportFailed', { sourceTitle });
} else {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;
title = translate('DownloadFailed');
}
}
return (
<TableRowCell className={styles.status}>
<Popover
anchor={
<Icon
name={iconName}
kind={iconKind}
/>
}
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
<QueueStatus
sourceTitle={sourceTitle}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
position={tooltipPositions.RIGHT}
canFlip={false}
/>
</TableRowCell>
);
@@ -115,11 +115,12 @@ class RemoveQueueItemModal extends Component {
<FormGroup>
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistHelpText')}
helpText={translate('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
@@ -149,7 +150,7 @@ class RemoveQueueItemModal extends Component {
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
Remove
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
@@ -123,7 +123,7 @@ class RemoveQueueItemsModal extends Component {
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistHelpText')}
helpText={translate('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
+2 -2
View File
@@ -27,7 +27,7 @@ function TimeleftCell(props) {
return (
<TableRowCell
className={styles.timeleft}
title={translate('DelayingDownloadUntilInterp', [date, time])}
title={translate('DelayingDownloadUntil', { date, time })}
>
-
</TableRowCell>
@@ -41,7 +41,7 @@ function TimeleftCell(props) {
return (
<TableRowCell
className={styles.timeleft}
title={translate('RetryingDownloadInterp', [date, time])}
title={translate('RetryingDownloadOn', { date, time })}
>
-
</TableRowCell>
@@ -20,10 +20,6 @@ class AddNewMovieModalContent extends Component {
//
// Listeners
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
};
onAddMoviePress = () => {
this.props.onAddMoviePress();
};
@@ -40,7 +36,7 @@ class AddNewMovieModalContent extends Component {
isAdding,
rootFolderPath,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
searchForMovie,
folder,
@@ -130,9 +126,9 @@ class AddNewMovieModalContent extends Component {
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
onChange={this.onQualityProfileIdChange}
{...qualityProfileId}
name="qualityProfileIds"
onChange={onInputChange}
{...qualityProfileIds}
/>
</FormGroup>
@@ -189,7 +185,7 @@ AddNewMovieModalContent.propTypes = {
addError: PropTypes.object,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
@@ -58,7 +58,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId,
rootFolderPath,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
searchForMovie,
tags
@@ -68,7 +68,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
qualityProfileIds: qualityProfileIds.value,
minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value,
tags: tags.value
@@ -93,7 +93,7 @@ AddNewMovieModalContentConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
qualityProfileIds: PropTypes.arrayOf(PropTypes.object),
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,
@@ -72,15 +72,19 @@ class AddNewMovieSearchResult extends Component {
colorImpairedMode,
id,
monitored,
hasFile,
isAvailable,
queueStatus,
queueState,
runtime,
movieRuntimeFormat,
certification
certification,
statistics
} = this.props;
const {
movieFileCount
} = statistics;
const {
isNewAddMovieModalOpen
} = this.state;
@@ -121,7 +125,7 @@ class AddNewMovieSearchResult extends Component {
isExistingMovie &&
<MovieIndexProgressBar
monitored={monitored}
hasFile={hasFile}
hasFile={movieFileCount > 0}
status={status}
width={posterWidth}
detailedProgressBar={true}
@@ -234,7 +238,7 @@ class AddNewMovieSearchResult extends Component {
{
isExistingMovie && isSmallScreen &&
<MovieStatusLabel
hasMovieFiles={hasFile}
hasMovieFiles={movieFileCount > 0}
monitored={monitored}
isAvailable={isAvailable}
id={id}
@@ -291,7 +295,14 @@ AddNewMovieSearchResult.propTypes = {
queueState: PropTypes.string,
runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string
certification: PropTypes.string,
statistics: PropTypes.object
};
AddNewMovieSearchResult.defaultProps = {
statistics: {
movieFileCount: 0
}
};
export default AddNewMovieSearchResult;
@@ -25,13 +25,13 @@ class ImportMovieFooter extends Component {
const {
defaultMonitor,
defaultQualityProfileId,
defaultQualityProfileIds,
defaultMinimumAvailability
} = props;
this.state = {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
qualityProfileIds: defaultQualityProfileIds,
minimumAvailability: defaultMinimumAvailability
};
}
@@ -39,16 +39,16 @@ class ImportMovieFooter extends Component {
componentDidUpdate(prevProps, prevState) {
const {
defaultMonitor,
defaultQualityProfileId,
defaultQualityProfileIds,
defaultMinimumAvailability,
isMonitorMixed,
isQualityProfileIdMixed,
isQualityProfileIdsMixed,
isMinimumAvailabilityMixed
} = this.props;
const {
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability
} = this.state;
@@ -60,10 +60,10 @@ class ImportMovieFooter extends Component {
newState.monitor = defaultMonitor;
}
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
newState.qualityProfileId = MIXED;
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
newState.qualityProfileId = defaultQualityProfileId;
if (isQualityProfileIdsMixed && qualityProfileIds !== MIXED) {
newState.qualityProfileIds = MIXED;
} else if (!isQualityProfileIdsMixed && qualityProfileIds !== defaultQualityProfileIds) {
newState.qualityProfileIds = defaultQualityProfileIds;
}
if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) {
@@ -94,7 +94,7 @@ class ImportMovieFooter extends Component {
isImporting,
isLookingUpMovie,
isMonitorMixed,
isQualityProfileIdMixed,
isQualityProfileIdsMixed,
isMinimumAvailabilityMixed,
hasUnsearchedItems,
importError,
@@ -105,7 +105,7 @@ class ImportMovieFooter extends Component {
const {
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability
} = this.state;
@@ -148,10 +148,10 @@ class ImportMovieFooter extends Component {
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
value={qualityProfileId}
name="qualityProfileIds"
value={qualityProfileIds}
isDisabled={!selectedCount}
includeMixed={isQualityProfileIdMixed}
includeMixed={isQualityProfileIdsMixed}
onChange={this.onInputChange}
/>
</div>
@@ -257,10 +257,10 @@ ImportMovieFooter.propTypes = {
isImporting: PropTypes.bool.isRequired,
isLookingUpMovie: PropTypes.bool.isRequired,
defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileId: PropTypes.number,
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
defaultMinimumAvailability: PropTypes.string,
isMonitorMixed: PropTypes.bool.isRequired,
isQualityProfileIdMixed: PropTypes.bool.isRequired,
isQualityProfileIdsMixed: PropTypes.bool.isRequired,
isMinimumAvailabilityMixed: PropTypes.bool.isRequired,
hasUnsearchedItems: PropTypes.bool.isRequired,
importError: PropTypes.object,
@@ -18,7 +18,7 @@ function createMapStateToProps() {
(addMovie, importMovie, selectedIds) => {
const {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
qualityProfileIds: defaultQualityProfileIds,
minimumAvailability: defaultMinimumAvailability
} = addMovie.defaults;
@@ -30,7 +30,7 @@ function createMapStateToProps() {
} = importMovie;
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
const isQualityProfileIdsMixed = isMixed(items, selectedIds, defaultQualityProfileIds, 'qualityProfileIds');
const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability');
const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated);
@@ -39,10 +39,10 @@ function createMapStateToProps() {
isLookingUpMovie,
isImporting,
defaultMonitor,
defaultQualityProfileId,
defaultQualityProfileIds,
defaultMinimumAvailability,
isMonitorMixed,
isQualityProfileIdMixed,
isQualityProfileIdsMixed,
isMinimumAvailabilityMixed,
importError,
hasUnsearchedItems
@@ -11,7 +11,7 @@ function ImportMovieRow(props) {
const {
id,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
selectedMovie,
isExistingMovie,
@@ -62,8 +62,8 @@ function ImportMovieRow(props) {
<VirtualTableRowCell className={styles.qualityProfile}>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
value={qualityProfileId}
name="qualityProfileIds"
value={qualityProfileIds}
onChange={onInputChange}
/>
</VirtualTableRowCell>
@@ -74,7 +74,7 @@ function ImportMovieRow(props) {
ImportMovieRow.propTypes = {
id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired,
qualityProfileId: PropTypes.number.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
minimumAvailability: PropTypes.string.isRequired,
selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired,
@@ -15,7 +15,7 @@ class ImportMovieTable extends Component {
const {
unmappedFolders,
defaultMonitor,
defaultQualityProfileId,
defaultQualityProfileIds,
defaultMinimumAvailability,
onMovieLookup,
onSetImportMovieValue
@@ -23,7 +23,7 @@ class ImportMovieTable extends Component {
const values = {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
qualityProfileIds: defaultQualityProfileIds,
minimumAvailability: defaultMinimumAvailability
};
@@ -167,7 +167,7 @@ ImportMovieTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.object),
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileId: PropTypes.number,
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number),
defaultMinimumAvailability: PropTypes.string,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
@@ -13,7 +13,7 @@ function createMapStateToProps() {
(addMovie, importMovie, dimensions, allMovies) => {
return {
defaultMonitor: addMovie.defaults.monitor,
defaultQualityProfileId: addMovie.defaults.qualityProfileId,
defaultQualityProfileIds: addMovie.defaults.qualityProfileIds,
defaultMinimumAvailability: addMovie.defaults.minimumAvailability,
items: importMovie.items,
isSmallScreen: dimensions.isSmallScreen,
@@ -5,6 +5,7 @@ import FormInputButton from 'Components/Form/FormInputButton';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Portal from 'Components/Portal';
import { icons, kinds } from 'Helpers/Props';
@@ -242,7 +243,7 @@ class ImportMovieSelectMovie extends Component {
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
ButtonComponent={SpinnerButton}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>
@@ -1,6 +1,7 @@
.version {
margin: 0 3px;
font-weight: bold;
font-family: var(--defaultFontFamily);
}
.maintenance {
+6 -5
View File
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
@@ -64,20 +65,20 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('RadarrUpdated')}
{translate('AppUpdated', { appName: 'Radarr' })}
</ModalHeader>
<ModalBody>
<div dangerouslySetInnerHTML={{ __html: translate('VersionUpdateText', [`<span className=${styles.version}>${version}</span>`]) }} />
<div>
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Radarr', version })} blockClassName={styles.version} />
</div>
{
isPopulated && !error && !!update &&
<div>
{
!update.changes &&
<div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
}
{
+2 -2
View File
@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
{translate('ConnectionLostMessage')}
{translate('ConnectionLostToBackend', { appName: 'Radarr' })}
</div>
<div className={styles.automatic}>
{translate('ConnectionLostAutomaticMessage')}
{translate('ConnectionLostReconnect', { appName: 'Radarr' })}
</div>
</ModalBody>
<ModalFooter>
@@ -1,7 +1,7 @@
import AppSectionState from 'App/State/AppSectionState';
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
import ImportMode from '../../InteractiveImport/ImportMode';
import InteractiveImport from '../../InteractiveImport/InteractiveImport';
import ImportMode from 'InteractiveImport/ImportMode';
import InteractiveImport from 'InteractiveImport/InteractiveImport';
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
originalItems: InteractiveImport[];
@@ -8,6 +8,7 @@ import Language from 'Language/Language';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings';
@@ -35,12 +36,14 @@ export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
downloadClients: DownloadClientAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
notifications: NotificationAppState;
+21 -6
View File
@@ -1,17 +1,28 @@
.event {
display: flex;
overflow-x: hidden;
position: relative;
padding: 5px;
border-bottom: 1px solid var(--borderColor);
font-size: $defaultFontSize;
}
.underlay {
@add-mixin cover;
&:hover {
background-color: var(--tableRowHoverBackgroundColor);
}
}
.link {
composes: link from '~Calendar/Events/CalendarEvent.css';
.overlay {
@add-mixin linkOverlay;
position: relative;
display: flex;
overflow-x: hidden;
font-size: $defaultFontSize;
&:global(.colorImpaired) {
border-left-width: 5px;
}
}
.eventWrapper {
@@ -44,6 +55,8 @@
.statusIcon {
margin-left: 3px;
cursor: default;
pointer-events: all;
}
/*
@@ -95,6 +108,8 @@
}
}
.dateIcon {
.releaseIcon {
margin-right: 20px;
width: 25px;
text-align: right;
}
+3 -2
View File
@@ -3,18 +3,19 @@
interface CssExports {
'continuing': string;
'date': string;
'dateIcon': string;
'downloaded': string;
'event': string;
'eventWrapper': string;
'genres': string;
'link': string;
'missingMonitored': string;
'missingUnmonitored': string;
'movieTitle': string;
'overlay': string;
'queue': string;
'releaseIcon': string;
'statusIcon': string;
'time': string;
'underlay': string;
'unmonitored': string;
}
export const cssExports: CssExports;
+12 -15
View File
@@ -87,25 +87,24 @@ class AgendaEvent extends Component {
const link = `/movie/${titleSlug}`;
return (
<div>
<div className={styles.event}>
<Link
className={classNames(
styles.event,
styles.link
)}
className={styles.underlay}
to={link}
>
<div className={styles.dateIcon}>
/>
<div className={styles.overlay}>
<div className={styles.date}>
{(showDate) ? startTime.format(longDateFormat) : null}
</div>
<div className={styles.releaseIcon}>
<Icon
name={releaseIcon}
kind={kinds.DEFAULT}
/>
</div>
<div className={styles.date}>
{(showDate) ? startTime.format(longDateFormat) : null}
</div>
<div
className={classNames(
styles.eventWrapper,
@@ -143,9 +142,7 @@ class AgendaEvent extends Component {
}
{
showCutoffUnmetIcon &&
!!movieFile &&
movieFile.qualityCutoffNotMet &&
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet &&
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
@@ -154,7 +151,7 @@ class AgendaEvent extends Component {
/>
}
</div>
</Link>
</div>
</div>
);
}
@@ -25,7 +25,7 @@ function createMissingMovieIdsSelector() {
const inCinemas = movie.inCinemas;
if (
!movie.hasFile &&
(!movie.statistics || movie.statistics.movieFileCount === 0) &&
moment(inCinemas).isAfter(start) &&
moment(inCinemas).isBefore(end) &&
isBefore(movie.inCinemas) &&
-64
View File
@@ -1,64 +0,0 @@
import classNames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import React from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
import styles from './CalendarDay.css';
function CalendarDay(props) {
const {
date,
time,
isTodaysDate,
events,
view,
onEventModalOpenToggle
} = props;
return (
<div className={classNames(
styles.day,
view === calendarViews.DAY && styles.isSingleDay
)}
>
{
view === calendarViews.MONTH &&
<div className={classNames(
styles.dayOfMonth,
isTodaysDate && styles.isToday,
!moment(date).isSame(moment(time), 'month') && styles.isDifferentMonth
)}
>
{moment(date).date()}
</div>
}
<div>
{
events.map((event) => {
return (
<CalendarEventConnector
key={event.id}
movieId={event.id}
date={date}
{...event}
onEventModalOpenToggle={onEventModalOpenToggle}
/>
);
})
}
</div>
</div>
);
}
CalendarDay.propTypes = {
date: PropTypes.string.isRequired,
time: PropTypes.string.isRequired,
isTodaysDate: PropTypes.bool.isRequired,
events: PropTypes.arrayOf(PropTypes.object).isRequired,
view: PropTypes.string.isRequired,
onEventModalOpenToggle: PropTypes.func.isRequired
};
export default CalendarDay;
+67
View File
@@ -0,0 +1,67 @@
import classNames from 'classnames';
import moment from 'moment';
import React from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
import CalendarEvent from 'typings/CalendarEvent';
import styles from './CalendarDay.css';
interface CalendarDayProps {
date: string;
time: string;
isTodaysDate: boolean;
events: CalendarEvent[];
view: string;
onEventModalOpenToggle(...args: unknown[]): unknown;
}
function CalendarDay(props: CalendarDayProps) {
const { date, time, isTodaysDate, events, view, onEventModalOpenToggle } =
props;
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (isTodaysDate && view === calendarViews.MONTH && ref.current) {
ref.current.scrollIntoView();
}
}, [time, isTodaysDate, view]);
return (
<div
ref={ref}
className={classNames(
styles.day,
view === calendarViews.DAY && styles.isSingleDay
)}
>
{view === calendarViews.MONTH && (
<div
className={classNames(
styles.dayOfMonth,
isTodaysDate && styles.isToday,
!moment(date).isSame(moment(time), 'month') &&
styles.isDifferentMonth
)}
>
{moment(date).date()}
</div>
)}
<div>
{events.map((event) => {
return (
<CalendarEventConnector
key={event.id}
{...event}
movieId={event.id}
date={date as string}
onEventModalOpenToggle={onEventModalOpenToggle}
/>
);
})}
</div>
</div>
);
}
export default CalendarDay;
+72 -15
View File
@@ -1,9 +1,22 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.event {
overflow-x: hidden;
position: relative;
margin: 4px 2px;
padding: 5px;
border-bottom: 1px solid var(--calendarBorderColor);
border-left: 4px solid var(--calendarBorderColor);
}
.underlay {
@add-mixin cover;
}
.overlay {
@add-mixin linkOverlay;
position: relative;
overflow-x: hidden;
font-size: 12px;
&:global(.colorImpaired) {
@@ -11,18 +24,6 @@
}
}
.link {
composes: link from '~Components/Link/Link.css';
display: block;
color: var(--defaultColor);
&:hover {
color: var(--defaultColor);
text-decoration: none;
}
}
.info,
.movieInfo {
display: flex;
@@ -44,8 +45,15 @@
font-size: $defaultFontSize;
}
.statusContainer {
display: flex;
align-items: center;
}
.statusIcon {
margin-left: 3px;
cursor: default;
pointer-events: all;
}
/*
@@ -55,35 +63,84 @@
.downloaded {
border-left-color: var(--successColor) !important;
&:global(.fullColor) {
background-color: rgba(39, 194, 76, 0.4) !important;
}
&:global(.colorImpaired) {
border-left-color: color(var(--successColor), saturation(+15%)) !important;
border-left-color: color(#27c24c saturation(+15%)) !important;
}
}
.queue {
border-left-color: var(--purple) !important;
&:global(.fullColor) {
background-color: rgba(122, 67, 182, 0.4) !important;
}
}
.unmonitored {
border-left-color: var(--gray) !important;
&:global(.fullColor) {
background-color: rgba(173, 173, 173, 0.5) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.missingUnmonitored {
border-left-color: var(--warningColor) !important;
&:global(.fullColor) {
background-color: rgba(255, 165, 0, 0.6) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.missingMonitored {
border-left-color: var(--dangerColor) !important;
&:global(.fullColor) {
background-color: rgba(240, 80, 80, 0.6) !important;
}
&:global(.colorImpaired) {
border-left-color: color(#f05050 saturation(+15%)) !important;
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.continuing {
border-left-color: var(--primaryColor) !important;
&:global(.fullColor) {
background-color: rgba(93, 156, 236, 0.4) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
+3 -1
View File
@@ -6,13 +6,15 @@ interface CssExports {
'event': string;
'genres': string;
'info': string;
'link': string;
'missingMonitored': string;
'missingUnmonitored': string;
'movieInfo': string;
'movieTitle': string;
'overlay': string;
'queue': string;
'statusContainer': string;
'statusIcon': string;
'underlay': string;
'unmonitored': string;
}
export const cssExports: CssExports;
+59 -49
View File
@@ -25,6 +25,7 @@ class CalendarEvent extends Component {
title,
titleSlug,
genres,
date,
monitored,
certification,
hasFile,
@@ -32,8 +33,8 @@ class CalendarEvent extends Component {
queueItem,
showMovieInformation,
showCutoffUnmetIcon,
colorImpairedMode,
date
fullColorEvents,
colorImpairedMode
} = this.props;
const isDownloading = !!(queueItem || grabbed);
@@ -56,64 +57,71 @@ class CalendarEvent extends Component {
}
return (
<div>
<div
className={classNames(
styles.event,
styles[statusStyle],
colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor'
)}
>
<Link
className={classNames(
styles.event,
styles.link,
styles[statusStyle],
colorImpairedMode && 'colorImpaired'
)}
// component="div"
className={styles.underlay}
to={link}
>
/>
<div className={styles.overlay} >
<div className={styles.info}>
<div className={styles.movieTitle}>
{title}
</div>
{
!!queueItem &&
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
/>
</span>
}
<div className={styles.statusContainer}>
{
queueItem ?
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
/>
</span> :
null
}
{
!queueItem && grabbed &&
<Icon
className={styles.statusIcon}
name={icons.DOWNLOADING}
title={translate('MovieIsDownloading')}
/>
}
{
!queueItem && grabbed ?
<Icon
className={styles.statusIcon}
name={icons.DOWNLOADING}
title={translate('MovieIsDownloading')}
/> :
null
}
{
showCutoffUnmetIcon &&
!!movieFile &&
movieFile.qualityCutoffNotMet &&
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={kinds.WARNING}
title={translate('QualityCutoffHasNotBeenMet')}
/>
}
{
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet ?
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={kinds.WARNING}
title={translate('QualityCutoffHasNotBeenMet')}
/> :
null
}
</div>
</div>
{
showMovieInformation &&
showMovieInformation ?
<div className={styles.movieInfo}>
<div className={styles.genres}>
{joinedGenres}
</div>
</div>
</div> :
null
}
{
showMovieInformation &&
showMovieInformation ?
<div className={styles.movieInfo}>
<div className={styles.genres}>
{eventType.join(', ')}
@@ -121,10 +129,10 @@ class CalendarEvent extends Component {
<div>
{certification}
</div>
</div>
</div> :
null
}
</Link>
</div>
</div>
);
}
@@ -140,16 +148,18 @@ CalendarEvent.propTypes = {
inCinemas: PropTypes.string,
physicalRelease: PropTypes.string,
digitalRelease: PropTypes.string,
date: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
certification: PropTypes.string,
hasFile: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
showMovieInformation: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired
// These props come from the connector, not marked as required to appease TS for now.
showMovieInformation: PropTypes.bool,
showCutoffUnmetIcon: PropTypes.bool,
fullColorEvents: PropTypes.bool,
timeFormat: PropTypes.string,
colorImpairedMode: PropTypes.bool
};
CalendarEvent.defaultProps = {
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import QueueDetails from 'Activity/Queue/QueueDetails';
import CircularProgressBar from 'Components/CircularProgressBar';
import translate from 'Utilities/String/translate';
function CalendarEventQueueDetails(props) {
const {
@@ -13,6 +12,7 @@ function CalendarEventQueueDetails(props) {
status,
trackedDownloadState,
trackedDownloadStatus,
statusMessages,
errorMessage
} = props;
@@ -27,16 +27,15 @@ function CalendarEventQueueDetails(props) {
status={status}
trackedDownloadState={trackedDownloadState}
trackedDownloadStatus={trackedDownloadStatus}
statusMessages={statusMessages}
errorMessage={errorMessage}
progressBar={
<div title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}>
<CircularProgressBar
progress={progress}
size={20}
strokeWidth={2}
strokeColor={'#7a43b6'}
/>
</div>
<CircularProgressBar
progress={progress}
size={20}
strokeWidth={2}
strokeColor={'#7a43b6'}
/>
}
/>
);
@@ -50,6 +49,7 @@ CalendarEventQueueDetails.propTypes = {
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string
};
+25 -7
View File
@@ -8,18 +8,21 @@ import styles from './Legend.css';
function Legend(props) {
const {
view,
showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode
} = props;
const iconsToShow = [];
const isAgendaView = view === 'agenda';
if (showCutoffUnmetIcon) {
iconsToShow.push(
<LegendIconItem
name={translate('CutoffUnmet')}
icon={icons.MOVIE_FILE}
kind={kinds.WARNING}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
/>
);
@@ -29,45 +32,58 @@ function Legend(props) {
<div className={styles.legend}>
<div>
<LegendItem
style='ended'
status="downloaded"
name={translate('DownloadedAndMonitored')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='availNotMonitored'
status="unmonitored"
name={translate('DownloadedButNotMonitored')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
<div>
<LegendItem
style='missingMonitored'
status="missingMonitored"
name={translate('MissingMonitoredAndConsideredAvailable')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='missingUnmonitored'
status="missingUnmonitored"
name={translate('MissingNotMonitored')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
<div>
<LegendItem
style='queue'
status="queue"
name={translate('Queued')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
style='continuing'
status="continuing"
name={translate('Unreleased')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
{
iconsToShow.length > 0 &&
<div>
@@ -79,7 +95,9 @@ function Legend(props) {
}
Legend.propTypes = {
view: PropTypes.string.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};
@@ -6,10 +6,12 @@ import Legend from './Legend';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
(state) => state.calendar.view,
createUISettingsSelector(),
(calendarOptions, uiSettings) => {
(calendarOptions, view, uiSettings) => {
return {
...calendarOptions,
view,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
@@ -8,6 +8,7 @@ function LegendIconItem(props) {
name,
icon,
kind,
darken,
tooltip
} = props;
@@ -19,6 +20,7 @@ function LegendIconItem(props) {
<Icon
className={styles.icon}
name={icon}
darken={darken}
kind={kind}
/>
@@ -31,7 +33,12 @@ LegendIconItem.propTypes = {
name: PropTypes.string.isRequired,
icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
darken: PropTypes.bool.isRequired,
tooltip: PropTypes.string.isRequired
};
LegendIconItem.defaultProps = {
darken: false
};
export default LegendIconItem;
+21 -58
View File
@@ -1,74 +1,37 @@
.legendItemContainer {
margin-right: 5px;
width: 220px;
}
.legendItem {
display: inline-flex;
margin-top: -1px;
vertical-align: middle;
line-height: 16px;
margin: 3px 0;
margin-right: 6px;
padding-left: 5px;
width: 220px;
border-left-width: 4px;
border-left-style: solid;
cursor: default;
}
.legendItemColor {
margin-right: 8px;
width: 30px;
height: 16px;
border-radius: 4px;
/*
* Status
*/
.downloaded {
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
}
.queue {
composes: legendItemColor;
background-color: var(--queueColor);
composes: queue from '~Calendar/Events/CalendarEvent.css';
}
.continuing {
composes: legendItemColor;
background-color: var(--primaryColor);
}
.availNotMonitored {
composes: legendItemColor;
background-color: var(--darkGray);
}
.ended {
composes: legendItemColor;
background-color: var(--successColor);
}
.missingMonitored {
composes: legendItemColor;
background-color: var(--dangerColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(var(--dangerColor) shade(5%)), color(var(--dangerColor) shade(5%)) 5px, color(var(--dangerColor) shade(15%)) 5px, color(var(--dangerColor) shade(15%)) 10px);
}
.unmonitored {
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
}
.missingUnmonitored {
composes: legendItemColor;
background-color: var(--warningColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--warningColor), var(--warningColor) 5px, color(var(--warningColor) tint(15%)) 5px, color(var(--warningColor) tint(15%)) 10px);
}
composes: missingUnmonitored from '~Calendar/Events/CalendarEvent.css';
}
.missingMonitoredColorImpaired {
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
.missingMonitored {
composes: missingMonitored from '~Calendar/Events/CalendarEvent.css';
}
.missingUnmonitoredColorImpaired {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
.legendItemText {
display: inline-block;
.continuing {
composes: continuing from '~Calendar/Events/CalendarEvent.css';
}
+2 -7
View File
@@ -1,18 +1,13 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'availNotMonitored': string;
'continuing': string;
'ended': string;
'downloaded': string;
'legendItem': string;
'legendItemColor': string;
'legendItemContainer': string;
'legendItemText': string;
'missingMonitored': string;
'missingMonitoredColorImpaired': string;
'missingUnmonitored': string;
'missingUnmonitoredColorImpaired': string;
'queue': string;
'unmonitored': string;
}
export const cssExports: CssExports;
export default cssExports;
+15 -13
View File
@@ -6,29 +6,31 @@ import styles from './LegendItem.css';
function LegendItem(props) {
const {
name,
style,
status,
isAgendaView,
fullColorEvents,
colorImpairedMode
} = props;
return (
<div className={styles.legendItemContainer}>
<div
className={classNames(
styles.legendItem,
styles[style],
colorImpairedMode && 'colorImpaired'
)}
/>
<div className={classNames(styles.legendItemText, colorImpairedMode && styles[`${style}ColorImpaired`])}>
{name}
</div>
<div
className={classNames(
styles.legendItem,
styles[status],
colorImpairedMode && 'colorImpaired',
fullColorEvents && !isAgendaView && 'fullColor'
)}
>
{name}
</div>
);
}
LegendItem.propTypes = {
name: PropTypes.string.isRequired,
style: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
isAgendaView: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};
@@ -26,14 +26,16 @@ class CalendarOptionsModalContent extends Component {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
enableColorImpairedMode,
fullColorEvents
} = props;
this.state = {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
enableColorImpairedMode,
fullColorEvents
};
}
@@ -94,6 +96,7 @@ class CalendarOptionsModalContent extends Component {
const {
showMovieInformation,
showCutoffUnmetIcon,
fullColorEvents,
onModalClose
} = this.props;
@@ -136,6 +139,18 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('FullColorEvents')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="fullColorEvents"
value={fullColorEvents}
helpText={translate('FullColorEventsHelpText')}
onChange={this.onOptionInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
@@ -176,7 +191,9 @@ class CalendarOptionsModalContent extends Component {
value={timeFormat}
onChange={this.onGlobalInputChange}
/>
</FormGroup><FormGroup>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
<FormInputGroup
@@ -187,7 +204,6 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onGlobalInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
</ModalBody>
@@ -209,6 +225,7 @@ CalendarOptionsModalContent.propTypes = {
calendarWeekColumnHeader: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
dispatchSetCalendarOption: PropTypes.func.isRequired,
dispatchSaveUISettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
@@ -46,7 +46,7 @@ class AddNewCollectionMovieModalContent extends Component {
onInputChange,
rootFolderPath,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
searchForMovie
} = this.props;
@@ -126,13 +126,13 @@ class AddNewCollectionMovieModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
name="qualityProfileIds"
onChange={this.onQualityProfileIdChange}
{...qualityProfileId}
{...qualityProfileIds}
/>
</FormGroup>
@@ -189,7 +189,7 @@ AddNewCollectionMovieModalContent.propTypes = {
addError: PropTypes.object,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
qualityProfileIds: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
@@ -25,7 +25,7 @@ function createMapStateToProps() {
const collectionDefaults = {
rootFolderPath: collection.rootFolderPath,
monitor: 'movieOnly',
qualityProfileId: collection.qualityProfileId,
qualityProfileIds: collection.qualityProfileIds,
minimumAvailability: collection.minimumAvailability,
searchForMovie: collection.searchOnAdd,
tags: collection.tags || []
@@ -70,7 +70,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
title,
rootFolderPath,
monitor,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
searchForMovie,
tags
@@ -81,7 +81,7 @@ class AddNewCollectionMovieModalContentConnector extends Component {
title,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
qualityProfileIds: qualityProfileIds.value,
minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value,
tags: tags.value
@@ -109,7 +109,7 @@ AddNewCollectionMovieModalContentConnector.propTypes = {
title: PropTypes.string.isRequired,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
qualityProfileIds: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,
@@ -46,7 +46,7 @@ class EditCollectionModalContent extends Component {
const {
monitored,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
// Id,
rootFolderPath,
@@ -105,12 +105,12 @@ class EditCollectionModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...qualityProfileId}
name="qualityProfileIds"
{...qualityProfileIds}
onChange={onInputChange}
/>
</FormGroup>
@@ -39,7 +39,7 @@ function createMapStateToProps() {
const movieSettings = {
monitored: collection.monitored,
qualityProfileId: collection.qualityProfileId,
qualityProfileIds: collection.qualityProfileIds,
minimumAvailability: collection.minimumAvailability,
rootFolderPath: collection.rootFolderPath,
tags: collection.tags,
@@ -17,11 +17,13 @@ class CollectionMovieLabel extends Component {
status,
monitored,
isAvailable,
hasFile,
onMonitorTogglePress,
isSaving
isSaving,
statistics
} = this.props;
const { movieFileCount } = statistics;
return (
<div className={styles.movie}>
<div className={styles.movieTitle}>
@@ -46,11 +48,11 @@ class CollectionMovieLabel extends Component {
<div
className={classNames(
styles.movieStatus,
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
styles[getStatusStyle(status, monitored, movieFileCount > 0, isAvailable, 'kinds')]
)}
>
{
hasFile ? translate('Downloaded') : translate('Missing')
movieFileCount > 0 ? translate('Downloaded') : translate('Missing')
}
</div>
}
@@ -63,9 +65,9 @@ CollectionMovieLabel.propTypes = {
id: PropTypes.number,
title: PropTypes.string.isRequired,
status: PropTypes.string,
statistics: PropTypes.object.isRequired,
isAvailable: PropTypes.bool,
monitored: PropTypes.bool,
hasFile: PropTypes.bool,
isSaving: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
movieFileId: PropTypes.number,
@@ -75,9 +77,7 @@ CollectionMovieLabel.propTypes = {
CollectionMovieLabel.defaultProps = {
isSaving: false,
statistics: {
episodeFileCount: 0,
totalEpisodeCount: 0,
percentOfEpisodes: 0
movieFileCount: 0
}
};
@@ -96,7 +96,7 @@ class CollectionOverview extends Component {
render() {
const {
monitored,
qualityProfileId,
qualityProfileIds,
rootFolderPath,
genres,
id,
@@ -212,7 +212,7 @@ class CollectionOverview extends Component {
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
qualityProfileId={qualityProfileId}
qualityProfileIds={qualityProfileIds}
/>
}
</span>
@@ -325,7 +325,7 @@ class CollectionOverview extends Component {
CollectionOverview.propTypes = {
id: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
qualityProfileId: PropTypes.number.isRequired,
qualityProfileIds: PropTypes.number.isRequired,
minimumAvailability: PropTypes.string.isRequired,
searchOnAdd: PropTypes.bool.isRequired,
rootFolderPath: PropTypes.string.isRequired,
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import TagInputTag from 'Components/Form/TagInputTag';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './FilterBuilderRowValueTag.css';
function FilterBuilderRowValueTag(props) {
@@ -15,10 +16,11 @@ function FilterBuilderRowValueTag(props) {
/>
{
!props.isLastTag &&
<span className={styles.or}>
or
</span>
props.isLastTag ?
null :
<div className={styles.or}>
{translate('Or')}
</div>
}
</span>
);
@@ -9,6 +9,10 @@
&:hover {
background-color: var(--inputHoverBackgroundColor);
}
&.isDisabled {
cursor: not-allowed;
}
}
.optionCheck {
@@ -2,33 +2,19 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import { kinds } from 'Helpers/Props';
import styles from './FormInputButton.css';
function FormInputButton(props) {
const {
className,
canSpin,
ButtonComponent,
isLastButton,
...otherProps
} = props;
if (canSpin) {
return (
<SpinnerButton
className={classNames(
className,
!isLastButton && styles.middleButton
)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
return (
<Button
<ButtonComponent
className={classNames(
className,
!isLastButton && styles.middleButton
@@ -41,14 +27,14 @@ function FormInputButton(props) {
FormInputButton.propTypes = {
className: PropTypes.string.isRequired,
isLastButton: PropTypes.bool.isRequired,
canSpin: PropTypes.bool.isRequired
ButtonComponent: PropTypes.elementType.isRequired,
isLastButton: PropTypes.bool.isRequired
};
FormInputButton.defaultProps = {
className: styles.button,
isLastButton: true,
canSpin: false
ButtonComponent: Button,
isLastButton: true
};
export default FormInputButton;
@@ -6,7 +6,6 @@
.inputGroup {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
}
.inputContainer {
@@ -12,7 +12,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
@@ -21,6 +21,7 @@ import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput';
import PathInputConnector from './PathInputConnector';
import PlexMachineInputConnector from './PlexMachineInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import TagInputConnector from './TagInputConnector';
@@ -63,6 +64,9 @@ function getComponent(type) {
case inputTypes.PATH:
return PathInputConnector;
case inputTypes.PLEX_MACHINE_SELECT:
return PlexMachineInputConnector;
case inputTypes.QUALITY_PROFILE_SELECT:
return QualityProfileSelectInputConnector;
@@ -76,7 +80,7 @@ function getComponent(type) {
return RootFolderSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector;
return IndexerFlagsSelectInput;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
@@ -0,0 +1,60 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import EnhancedSelectInput from './EnhancedSelectInput';
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
const selectIndexerFlagsValues = (selectedFlags: number) =>
createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => {
const value = indexerFlags.items
.filter(
// eslint-disable-next-line no-bitwise
(item) => (selectedFlags & item.id) === item.id
)
.map(({ id }) => id);
const values = indexerFlags.items.map(({ id, name }) => ({
key: id,
value: name,
}));
return {
value,
values,
};
}
);
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
const { indexerFlags, onChange } = props;
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
const onChangeWrapper = useCallback(
({ name, value }: { name: string; value: number[] }) => {
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
onChange({ name, value: indexerFlags });
},
[onChange]
);
return (
<EnhancedSelectInput
{...props}
value={value}
values={values}
onChange={onChangeWrapper}
/>
);
}
export default IndexerFlagsSelectInput;
@@ -1,70 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { indexerFlags }) => indexerFlags,
(state) => state.settings.indexerFlags,
(selectedFlags, indexerFlags) => {
const value = [];
indexerFlags.items.forEach((item) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & item.id) === item.id) {
value.push(item.id);
}
});
const values = indexerFlags.items.map(({ id, name }) => {
return {
key: id,
value: name
};
});
return {
value,
values
};
}
);
}
class IndexerFlagsSelectInputConnector extends Component {
onChange = ({ name, value }) => {
let indexerFlags = 0;
value.forEach((flagId) => {
indexerFlags += flagId;
});
this.props.onChange({ name, value: indexerFlags });
};
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
IndexerFlagsSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
indexerFlags: PropTypes.number.isRequired,
value: PropTypes.arrayOf(PropTypes.number).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps)(IndexerFlagsSelectInputConnector);
+12 -11
View File
@@ -5,6 +5,7 @@ import { kinds } from 'Helpers/Props';
function OAuthInput(props) {
const {
className,
label,
authorizing,
error,
@@ -12,21 +13,21 @@ function OAuthInput(props) {
} = props;
return (
<div>
<SpinnerErrorButton
kind={kinds.PRIMARY}
isSpinning={authorizing}
error={error}
onPress={onPress}
>
{label}
</SpinnerErrorButton>
</div>
<SpinnerErrorButton
className={className}
kind={kinds.PRIMARY}
isSpinning={authorizing}
error={error}
onPress={onPress}
>
{label}
</SpinnerErrorButton>
);
}
OAuthInput.propTypes = {
label: PropTypes.string.isRequired,
className: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
authorizing: PropTypes.bool.isRequired,
error: PropTypes.object,
onPress: PropTypes.func.isRequired
@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import SelectInput from './SelectInput';
function PlexMachineInput(props) {
const {
isFetching,
isDisabled,
value,
values,
onChange,
...otherProps
} = props;
const helpText = 'Authenticate with plex.tv to show servers to use for authentication';
return (
<>
{
isFetching ?
<LoadingIndicator /> :
<SelectInput
value={value}
values={values}
isDisabled={isDisabled}
onChange={onChange}
helpText={helpText}
{...otherProps}
/>
}
</>
);
}
PlexMachineInput.propTypes = {
isFetching: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
value: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired
};
export default PlexMachineInput;
@@ -0,0 +1,115 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchPlexResources } from 'Store/Actions/settingsActions';
import PlexMachineInput from './PlexMachineInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.oAuth,
(state) => state.settings.plex,
(value, oAuth, plex) => {
let values = [{ key: value, value }];
let isDisabled = true;
if (plex.isPopulated) {
const serverValues = plex.items.filter((item) => item.provides.includes('server')).map((item) => {
return ({
key: item.clientIdentifier,
value: `${item.name} / ${item.owned ? 'Owner' : 'User'} / ${item.clientIdentifier}`
});
});
if (serverValues.find((item) => item.key === value)) {
values = serverValues;
} else {
values = values.concat(serverValues);
}
isDisabled = false;
}
return ({
accessToken: oAuth.result?.accessToken,
values,
isDisabled,
...plex
});
}
);
}
const mapDispatchToProps = {
dispatchFetchPlexResources: fetchPlexResources
};
class PlexMachineInputConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
const {
accessToken,
dispatchFetchPlexResources
} = this.props;
if (accessToken) {
dispatchFetchPlexResources({ accessToken });
}
};
componentDidUpdate(prevProps) {
const {
accessToken,
dispatchFetchPlexResources
} = this.props;
const oldToken = prevProps.accessToken;
if (accessToken && accessToken !== oldToken) {
dispatchFetchPlexResources({ accessToken });
}
}
render() {
const {
isFetching,
isPopulated,
isDisabled,
value,
values,
onChange
} = this.props;
return (
<PlexMachineInput
isFetching={isFetching}
isPopulated={isPopulated}
isDisabled={isDisabled}
value={value}
values={values}
onChange={onChange}
{...this.props}
/>
);
}
}
PlexMachineInputConnector.propTypes = {
dispatchFetchPlexResources: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
error: PropTypes.object,
oAuth: PropTypes.object,
accessToken: PropTypes.string,
onChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(PlexMachineInputConnector);
@@ -47,32 +47,6 @@ function createMapStateToProps() {
class QualityProfileSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
name,
value,
values
} = this.props;
if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
if (firstValue) {
this.onChange({ name, value: firstValue.key });
}
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
};
//
// Render
@@ -80,7 +54,7 @@ class QualityProfileSelectInputConnector extends Component {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
onChange={this.props.onChange}
/>
);
}
@@ -88,7 +62,7 @@ class QualityProfileSelectInputConnector extends Component {
QualityProfileSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
value: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.number), PropTypes.arrayOf(PropTypes.string)]),
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeNoChange: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
@@ -12,6 +12,10 @@
composes: hasWarning from '~Components/Form/Input.css';
}
.hasButton {
composes: hasButton from '~Components/Form/Input.css';
}
.isDisabled {
opacity: 0.7;
cursor: not-allowed;
+1
View File
@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'hasButton': string;
'hasError': string;
'hasWarning': string;
'isDisabled': string;
@@ -28,6 +28,7 @@ class SelectInput extends Component {
isDisabled,
hasError,
hasWarning,
hasButton,
autoFocus,
onBlur
} = this.props;
@@ -38,6 +39,7 @@ class SelectInput extends Component {
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
hasButton && styles.hasButton,
isDisabled && disabledClassName
)}
disabled={isDisabled}
@@ -80,6 +82,7 @@ SelectInput.propTypes = {
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
hasButton: PropTypes.bool,
autoFocus: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onBlur: PropTypes.func
+1 -1
View File
@@ -102,7 +102,7 @@ class UMaskInput extends Component {
</div>
<div className={styles.details}>
<div>
<label>{translate('UMask')}</label>
<label>{translate('Umask')}</label>
<div className={styles.value}>{umask}</div>
</div>
<div>
+8
View File
@@ -12,10 +12,18 @@
.info {
color: var(--infoColor);
&:global(.darken) {
color: color(var(--infoColor) shade(30%));
}
}
.pink {
color: var(--pink);
&:global(.darken) {
color: color(var(--pink) shade(30%));
}
}
.success {
+5 -1
View File
@@ -18,6 +18,7 @@ class Icon extends PureComponent {
kind,
size,
title,
darken,
isSpinning,
...otherProps
} = this.props;
@@ -26,7 +27,8 @@ class Icon extends PureComponent {
<FontAwesomeIcon
className={classNames(
className,
styles[kind]
styles[kind],
darken && 'darken'
)}
icon={name}
spin={isSpinning}
@@ -59,6 +61,7 @@ Icon.propTypes = {
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
darken: PropTypes.bool.isRequired,
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};
@@ -66,6 +69,7 @@ Icon.propTypes = {
Icon.defaultProps = {
kind: kinds.DEFAULT,
size: 14,
darken: false,
isSpinning: false,
fixedWidth: false
};
@@ -10,7 +10,8 @@ class InlineMarkdown extends Component {
render() {
const {
className,
data
data,
blockClassName
} = this.props;
// For now only replace links or code blocks (not both)
@@ -47,7 +48,7 @@ class InlineMarkdown extends Component {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<code key={`code-${match.index}`}>{match[0].substring(1, match[0].length - 1)}</code>);
markdownBlocks.push(<code key={`code-${match.index}`} className={blockClassName ?? null}>{match[0].substring(1, match[0].length - 1)}</code>);
endIndex = match.index + match[0].length;
}
@@ -66,7 +67,8 @@ class InlineMarkdown extends Component {
InlineMarkdown.propTypes = {
className: PropTypes.string,
data: PropTypes.string
data: PropTypes.string,
blockClassName: PropTypes.string
};
export default InlineMarkdown;
@@ -12,7 +12,7 @@ import styles from './PageHeaderActionsMenu.css';
function PageHeaderActionsMenu(props) {
const {
formsAuth,
cookieAuth,
onKeyboardShortcutsPress,
onRestartPress,
onShutdownPress
@@ -56,22 +56,20 @@ function PageHeaderActionsMenu(props) {
</MenuItem>
{
formsAuth &&
<div className={styles.separator} />
}
{
formsAuth &&
<MenuItem
to={`${window.Radarr.urlBase}/logout`}
noRouter={true}
>
<Icon
className={styles.itemIcon}
name={icons.LOGOUT}
/>
Logout
</MenuItem>
cookieAuth &&
<>
<div className={styles.separator} />
<MenuItem
to={`${window.Radarr.urlBase}/logout?ReturnUrl=/`}
noRouter={true}
>
<Icon
className={styles.itemIcon}
name={icons.LOGOUT}
/>
Logout
</MenuItem>
</>
}
</MenuContent>
</Menu>
@@ -80,7 +78,7 @@ function PageHeaderActionsMenu(props) {
}
PageHeaderActionsMenu.propTypes = {
formsAuth: PropTypes.bool.isRequired,
cookieAuth: PropTypes.bool.isRequired,
onKeyboardShortcutsPress: PropTypes.func.isRequired,
onRestartPress: PropTypes.func.isRequired,
onShutdownPress: PropTypes.func.isRequired
@@ -10,7 +10,7 @@ function createMapStateToProps() {
(state) => state.system.status,
(status) => {
return {
formsAuth: status.item.authentication === 'forms'
cookieAuth: ['forms', 'oidc', 'plex'].includes(status.item.authentication)
};
}
);
@@ -0,0 +1,3 @@
.tags {
flex: 1 0 auto;
}
+7
View File
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'tags': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,46 @@
import _ from 'lodash';
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { kinds } from 'Helpers/Props';
import Label from './Label';
import styles from './QualityProfileList.css';
interface QualityProfileListProps {
qualityProfileIds: number[];
}
function QualityProfileList(props: QualityProfileListProps) {
const { qualityProfileIds } = props;
const { qualityProfileList } = useSelector(
createSelector(
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfileList) => {
return {
qualityProfileList,
};
}
)
);
return (
<div className={styles.tags}>
{qualityProfileIds.map((t) => {
const qualityProfile = _.find(qualityProfileList, { id: t });
if (!qualityProfile) {
return null;
}
return (
<Label key={qualityProfile.id} kind={kinds.INFO}>
{qualityProfile.name}
</Label>
);
})}
</div>
);
}
export default QualityProfileList;
@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import QualityProfileList from './QualityProfileList';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.qualityProfiles.items,
(qualityProfileList) => {
return {
qualityProfileList
};
}
);
}
export default connect(createMapStateToProps)(QualityProfileList);
@@ -51,9 +51,9 @@ class TableOptionsModal extends Component {
let pageSizeError = null;
if (value < 5) {
pageSizeError = 'Page size must be at least 5';
pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' });
} else if (value > 250) {
pageSizeError = 'Page size must not exceed 250';
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: '250' });
} else {
this.props.onTableOptionChange({ pageSize: value });
}
@@ -145,13 +145,13 @@ class TableOptionsModal extends Component {
{
hasPageSize ?
<FormGroup>
<FormLabel>{translate('PageSize')}</FormLabel>
<FormLabel>{translate('TablePageSize')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="pageSize"
value={pageSize || 0}
helpText={translate('PageSizeHelpText')}
helpText={translate('TablePageSizeHelpText')}
errors={pageSizeError ? [{ message: pageSizeError }] : undefined}
onChange={this.onPageSizeChange}
/>
@@ -2,19 +2,38 @@ import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import Alert from 'Components/Alert';
import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import OAuthInputConnector from 'Components/Form/OAuthInputConnector';
import Icon from 'Components/Icon';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { icons, inputTypes, kinds } from 'Helpers/Props';
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
import translate from 'Utilities/String/translate';
import styles from './AuthenticationRequiredModalContent.css';
const oauthData = {
implementation: { value: 'PlexImport' },
configContract: { value: 'PlexListSettings' },
fields: [
{
type: 'textbox',
name: 'accessToken'
},
{
type: 'oAuth',
name: 'signIn',
value: 'startAuth'
}
]
};
function onModalClose() {
// No-op
}
@@ -22,6 +41,7 @@ function onModalClose() {
function AuthenticationRequiredModalContent(props) {
const {
isPopulated,
plexServersPopulated,
error,
isSaving,
settings,
@@ -34,10 +54,18 @@ function AuthenticationRequiredModalContent(props) {
authenticationMethod,
authenticationRequired,
username,
password
password,
plexAuthServer,
plexRequireOwner,
oidcClientId,
oidcClientSecret,
oidcAuthority
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
const showUserPass = authenticationMethod && ['basic', 'forms'].includes(authenticationMethod.value);
const plexEnabled = authenticationMethod && authenticationMethod.value === 'plex';
const oidcEnabled = authenticationMethod && authenticationMethod.value === 'oidc';
const didMount = useRef(false);
@@ -63,20 +91,22 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{translate('AuthenticationRequiredWarning')}
{translate('AuthenticationRequiredWarning', { appName: 'Radarr' })}
</Alert>
{
isPopulated && !error ?
<div>
<FormGroup>
<FormLabel>{translate('Authentication')}</FormLabel>
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Radarr' })}
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
onChange={onInputChange}
{...authenticationMethod}
/>
@@ -95,27 +125,111 @@ function AuthenticationRequiredModalContent(props) {
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
{
showUserPass &&
<>
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
{...username}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
{...username}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
{...password}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
{...password}
/>
</FormGroup>
</>
}
{
plexEnabled &&
<>
<FormGroup>
<FormLabel>Plex Server</FormLabel>
<FormInputGroup
type={inputTypes.PLEX_MACHINE_SELECT}
name="plexAuthServer"
buttons={[
<FormInputButton
key="auth"
ButtonComponent={OAuthInputConnector}
label={plexServersPopulated ? <Icon name={icons.REFRESH} /> : 'Fetch'}
name="plexAuth"
provider="importList"
providerData={oauthData}
section="settings.importLists"
onChange={onInputChange}
/>
]}
onChange={onInputChange}
{...plexAuthServer}
/>
</FormGroup>
<FormGroup>
<FormLabel>Restrict Access to Server Owner</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="plexRequireOwner"
onChange={onInputChange}
{...plexRequireOwner}
/>
</FormGroup>
</>
}
{
oidcEnabled &&
<>
<FormGroup>
<FormLabel>Authority</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="oidcAuthority"
onChange={onInputChange}
{...oidcAuthority}
/>
</FormGroup>
<FormGroup>
<FormLabel>ClientId</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="oidcClientId"
onChange={onInputChange}
{...oidcClientId}
/>
</FormGroup>
<FormGroup>
<FormLabel>ClientSecret</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="oidcClientSecret"
onChange={onInputChange}
{...oidcClientSecret}
/>
</FormGroup>
</>
}
</div> :
null
}
@@ -141,6 +255,7 @@ function AuthenticationRequiredModalContent(props) {
AuthenticationRequiredModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired,
plexServersPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
@@ -13,9 +13,11 @@ const SECTION = 'general';
function createMapStateToProps() {
return createSelector(
createSettingsSectionSelector(SECTION),
(sectionSettings) => {
(state) => state.settings.plex,
(sectionSettings, plex) => {
return {
...sectionSettings
...sectionSettings,
plexServersPopulated: plex.isPopulated
};
}
);
+2
View File
@@ -10,6 +10,7 @@ export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
export const PATH = 'path';
export const PLEX_MACHINE_SELECT = 'plexMachineSelect';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
@@ -38,6 +39,7 @@ export const all = [
OAUTH,
PASSWORD,
PATH,
PLEX_MACHINE_SELECT,
QUALITY_PROFILE_SELECT,
INDEXER_SELECT,
DOWNLOAD_CLIENT_SELECT,
@@ -33,7 +33,6 @@ const columns = [
},
{
name: 'rejections',
columnLabel: () => translate('Rejections'),
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
@@ -93,7 +92,6 @@ const columns = [
},
{
name: 'customFormatScore',
columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
@@ -103,7 +101,6 @@ const columns = [
},
{
name: 'indexerFlags',
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
@@ -52,11 +52,7 @@
width: 75px;
}
.title {
composes: cell;
}
.title div {
.titleContent {
overflow-wrap: break-word;
}
@@ -19,7 +19,7 @@ interface CssExports {
'quality': string;
'rejected': string;
'size': string;
'title': string;
'titleContent': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -63,12 +63,12 @@ function getDownloadTooltip(
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddToDownloadQueue');
return translate('AddedToDownloadQueue');
} else if (grabError) {
return grabError;
}
return translate('AddedToDownloadQueue');
return translate('AddToDownloadQueue');
}
interface InteractiveSearchRowProps {
@@ -246,10 +246,12 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
) : null}
</TableRowCell>
<TableRowCell className={styles.title}>
<Link to={infoUrl} title={title}>
<div>{title}</div>
</Link>
<TableRowCell>
<div className={styles.titleContent}>
<Link to={infoUrl} title={title}>
{title}
</Link>
</div>
</TableRowCell>
<TableRowCell className={styles.indexer}>{indexer}</TableRowCell>
@@ -49,20 +49,24 @@ class DeleteMovieModalContent extends Component {
const {
title,
path,
hasFile,
statistics,
deleteOptions,
sizeOnDisk,
onModalClose,
onDeleteOptionChange
} = this.props;
const {
sizeOnDisk,
movieFileCount
} = statistics;
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = deleteOptions.addImportExclusion;
let deleteFilesLabel = hasFile ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [0]);
let deleteFilesLabel = movieFileCount === 1 ? translate('DeleteFileLabel', [1]) : translate('DeleteFilesLabel', [movieFileCount]);
let deleteFilesHelpText = translate('DeleteFilesHelpText');
if (!hasFile) {
if (movieFileCount === 0) {
deleteFilesLabel = translate('DeleteMovieFolderLabel');
deleteFilesHelpText = translate('DeleteMovieFolderHelpText');
}
@@ -121,9 +125,9 @@ class DeleteMovieModalContent extends Component {
</div>
{
!!hasFile &&
movieFileCount > 0 &&
<div>
{hasFile} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
{movieFileCount} {translate('MovieFilesTotaling')} {formatBytes(sizeOnDisk)}
</div>
}
</div>
@@ -151,12 +155,18 @@ class DeleteMovieModalContent extends Component {
DeleteMovieModalContent.propTypes = {
title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
hasFile: PropTypes.bool.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
deleteOptions: PropTypes.object.isRequired,
onDeleteOptionChange: PropTypes.func.isRequired,
statistics: PropTypes.object.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
DeleteMovieModalContent.defaultProps = {
statistics: {
sizeOnDisk: 0,
movieFileCount: 0
}
};
export default DeleteMovieModalContent;
+17 -17
View File
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import TextTruncate from 'react-text-truncate';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import InfoLabel from 'Components/InfoLabel';
@@ -53,11 +54,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) {
const fanartImage = _.find(images, { coverType: 'fanart' });
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
return _.find(images, { coverType: 'fanart' })?.url;
}
function getExpandedState(newState) {
@@ -265,7 +262,7 @@ class MovieDetails extends Component {
ratings,
path,
sizeOnDisk,
qualityProfileId,
qualityProfileIds,
monitored,
studio,
genres,
@@ -560,7 +557,7 @@ class MovieDetails extends Component {
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
qualityProfileId={qualityProfileId}
qualityProfileIds={qualityProfileIds}
/>
}
</span>
@@ -634,24 +631,27 @@ class MovieDetails extends Component {
<div className={styles.contentContainer}>
{
!isFetching && movieFilesError &&
<div>
!isFetching && movieFilesError ?
<Alert kind={kinds.DANGER}>
{translate('LoadingMovieFilesFailed')}
</div>
</Alert> :
null
}
{
!isFetching && movieCreditsError &&
<div>
!isFetching && movieCreditsError ?
<Alert kind={kinds.DANGER}>
{translate('LoadingMovieCreditsFailed')}
</div>
</Alert> :
null
}
{
!isFetching && extraFilesError &&
<div>
!isFetching && extraFilesError ?
<Alert kind={kinds.DANGER}>
{translate('LoadingMovieExtraFilesFailed')}
</div>
</Alert> :
null
}
<Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
@@ -798,7 +798,7 @@ MovieDetails.propTypes = {
ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
qualityProfileId: PropTypes.number.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
studio: PropTypes.string,
@@ -73,7 +73,7 @@ class EditMovieModalContent extends Component {
const {
monitored,
qualityProfileId,
qualityProfileIds,
minimumAvailability,
// Id,
path,
@@ -114,12 +114,12 @@ class EditMovieModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormLabel>{translate('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...qualityProfileId}
name="qualityProfileIds"
{...qualityProfileIds}
onChange={onInputChange}
/>
</FormGroup>
@@ -37,7 +37,7 @@ function createMapStateToProps() {
const movieSettings = {
monitored: movie.monitored,
qualityProfileId: movie.qualityProfileId,
qualityProfileIds: movie.qualityProfileIds,
minimumAvailability: movie.minimumAvailability,
path: movie.path,
tags: movie.tags
@@ -0,0 +1,333 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import translate from 'Utilities/String/translate';
import DeleteMovieModal from './Delete/DeleteMovieModal';
import MovieEditorFooterLabel from './MovieEditorFooterLabel';
import QualityProfilesModal from './QualityProfiles/QualityProfilesModal';
import TagsModal from './Tags/TagsModal';
import styles from './MovieEditorFooter.css';
const NO_CHANGE = 'noChange';
class MovieEditorFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
monitored: NO_CHANGE,
minimumAvailability: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
savingQualityProfiles: false,
isDeleteMovieModalOpen: false,
isTagsModalOpen: false,
isQualityProfilesModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
};
}
componentDidUpdate(prevProps) {
const {
isSaving,
saveError
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
minimumAvailability: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
savingQualityProfiles: false
});
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
if (value === NO_CHANGE) {
return;
}
switch (name) {
case 'rootFolderPath':
this.setState({
isConfirmMoveModalOpen: true,
destinationRootFolder: value
});
break;
case 'monitored':
this.props.onSaveSelected({ [name]: value === 'monitored' });
break;
default:
this.props.onSaveSelected({ [name]: value });
}
};
onApplyTagsPress = (tags, applyTags) => {
this.setState({
savingTags: true,
isTagsModalOpen: false
});
this.props.onSaveSelected({
tags,
applyTags
});
};
onApplyQualityProfilesPress = (qualityProfileIds) => {
this.setState({
savingQualityProfiles: true,
isQualityProfilesModalOpen: false
});
this.props.onSaveSelected({
qualityProfileIds
});
};
onDeleteSelectedPress = () => {
this.setState({ isDeleteMovieModalOpen: true });
};
onDeleteMovieModalClose = () => {
this.setState({ isDeleteMovieModalOpen: false });
};
onTagsPress = () => {
this.setState({ isTagsModalOpen: true });
};
onTagsModalClose = () => {
this.setState({ isTagsModalOpen: false });
};
onQualityProfilesPress = () => {
this.setState({ isQualityProfilesModalOpen: true });
};
onQualityProfilesModalClose = () => {
this.setState({ isQualityProfilesModalOpen: false });
};
onSaveRootFolderPress = () => {
this.setState({
isConfirmMoveModalOpen: false,
destinationRootFolder: null
});
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
};
onMoveMoviePress = () => {
this.setState({
isConfirmMoveModalOpen: false,
destinationRootFolder: null
});
this.props.onSaveSelected({
rootFolderPath: this.state.destinationRootFolder,
moveFiles: true
});
};
//
// Render
render() {
const {
movieIds,
selectedCount,
isSaving,
isDeleting,
isOrganizingMovie,
onOrganizeMoviePress
} = this.props;
const {
monitored,
qualityProfileIds,
minimumAvailability,
rootFolderPath,
savingTags,
savingQualityProfiles,
isTagsModalOpen,
isQualityProfilesModalOpen,
isDeleteMovieModalOpen,
isConfirmMoveModalOpen,
destinationRootFolder
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<MovieEditorFooterLabel
label={translate('MonitorMovie')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<MovieEditorFooterLabel
label={translate('QualityProfiles')}
isSaving={isSaving && qualityProfileIds !== NO_CHANGE}
/>
<SpinnerButton
className={styles.tagsButton}
isSpinning={isSaving && savingTags && savingQualityProfiles}
isDisabled={!selectedCount || isOrganizingMovie}
onPress={this.onQualityProfilesPress}
>
{translate('SetQualityProfiles')}
</SpinnerButton>
</div>
<div className={styles.inputContainer}>
<MovieEditorFooterLabel
label={translate('MinimumAvailability')}
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
/>
<AvailabilitySelectInput
name="minimumAvailability"
value={minimumAvailability}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<MovieEditorFooterLabel
label={translate('RootFolder')}
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<MovieEditorFooterLabel
label={translate('MoviesSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<div>
<SpinnerButton
className={styles.organizeSelectedButton}
kind={kinds.WARNING}
isSpinning={isOrganizingMovie}
isDisabled={!selectedCount || isOrganizingMovie}
onPress={onOrganizeMoviePress}
>
{translate('RenameFiles')}
</SpinnerButton>
<SpinnerButton
className={styles.tagsButton}
isSpinning={isSaving && savingTags && savingQualityProfiles}
isDisabled={!selectedCount || isOrganizingMovie}
onPress={this.onTagsPress}
>
{translate('SetTags')}
</SpinnerButton>
</div>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
{translate('Delete')}
</SpinnerButton>
</div>
</div>
</div>
<TagsModal
isOpen={isTagsModalOpen}
movieIds={movieIds}
onApplyTagsPress={this.onApplyTagsPress}
onModalClose={this.onTagsModalClose}
/>
<QualityProfilesModal
isOpen={isQualityProfilesModalOpen}
movieIds={movieIds}
onApplyQualityProfilesPress={this.onApplyQualityProfilesPress}
onModalClose={this.onQualityProfilesModalClose}
/>
<DeleteMovieModal
isOpen={isDeleteMovieModalOpen}
movieIds={movieIds}
onModalClose={this.onDeleteMovieModalClose}
/>
<MoveMovieModal
destinationRootFolder={destinationRootFolder}
isOpen={isConfirmMoveModalOpen}
onSavePress={this.onSaveRootFolderPress}
onMoveMoviePress={this.onMoveMoviePress}
/>
</PageContentFooter>
);
}
}
MovieEditorFooter.propTypes = {
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isOrganizingMovie: PropTypes.bool.isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeMoviePress: PropTypes.func.isRequired
};
export default MovieEditorFooter;
@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import QualityProfilesModalContent from './QualityProfilesModalContent';
function QualityProfilesModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<QualityProfilesModalContent
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
QualityProfilesModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default QualityProfilesModal;
@@ -0,0 +1,12 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.result {
padding-top: 4px;
}

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