1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-12 15:30:39 -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
.github/label-actions.yml vendored Normal file
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
.github/workflows/label-actions.yml vendored Normal file
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'

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

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

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: {

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}

View File

@@ -82,7 +82,7 @@ class BlocklistRow extends Component {
return null;
}
if (name === 'movies.sortTitle') {
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink

View File

@@ -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
};

View File

@@ -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,

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>
}

View File

@@ -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');
}
}

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,

View File

@@ -233,7 +233,7 @@ class Queue extends Component {
{
!isRefreshing && hasError ?
<Alert kind={kinds.DANGER}>
{translate('FailedToLoadQueue')}
{translate('QueueLoadError')}
</Alert> :
null
}

View File

@@ -0,0 +1,5 @@
.progressBarContainer {
display: flex;
justify-content: center;
width: 100%;
}

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;

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
};

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>

View File

@@ -0,0 +1,3 @@
.noMessages {
margin-bottom: 10px;
}

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;

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;

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>
);

View File

@@ -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>

View File

@@ -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>

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>

View File

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

View File

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

View File

@@ -72,15 +72,19 @@ class AddNewMovieSearchResult extends Component {
colorImpairedMode,
id,
monitored,
hasFile,
isAvailable,
queueStatus,
queueState,
runtime,
movieRuntimeFormat,
certification
certification,
statistics
} = this.props;
const {
movieFileCount
} = statistics;
const {
isNewAddMovieModalOpen
} = this.state;
@@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
.version {
margin: 0 3px;
font-weight: bold;
font-family: var(--defaultFontFamily);
}
.maintenance {

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>
}
{

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>

View File

@@ -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[];

View File

@@ -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;

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;
}

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;

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>
);
}

View File

@@ -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) &&

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;

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;

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);
}
}

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;

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 = {

View File

@@ -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
};

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
};

View File

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

View File

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

View File

@@ -1,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';
}

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;

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
};

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ function createMapStateToProps() {
const collectionDefaults = {
rootFolderPath: collection.rootFolderPath,
monitor: 'movieOnly',
qualityProfileId: collection.qualityProfileId,
qualityProfileIds: collection.qualityProfileIds,
minimumAvailability: collection.minimumAvailability,
searchForMovie: collection.searchOnAdd,
tags: 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,

View File

@@ -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>

View File

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

View File

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

View File

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

View File

@@ -2,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>
);

View File

@@ -9,6 +9,10 @@
&:hover {
background-color: var(--inputHoverBackgroundColor);
}
&.isDisabled {
cursor: not-allowed;
}
}
.optionCheck {

View File

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

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

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

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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;

View File

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

View File

@@ -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>

View File

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

View File

@@ -18,6 +18,7 @@ class Icon extends PureComponent {
kind,
size,
title,
darken,
isSpinning,
...otherProps
} = this.props;
@@ -26,7 +27,8 @@ class Icon extends PureComponent {
<FontAwesomeIcon
className={classNames(
className,
styles[kind]
styles[kind],
darken && 'darken'
)}
icon={name}
spin={isSpinning}
@@ -59,6 +61,7 @@ Icon.propTypes = {
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.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
};

View File

@@ -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;

View File

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

View File

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

View File

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

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;

View File

@@ -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;

View File

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

View File

@@ -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}
/>

View File

@@ -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,

View File

@@ -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
};
}
);

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,

View File

@@ -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')

View File

@@ -52,11 +52,7 @@
width: 75px;
}
.title {
composes: cell;
}
.title div {
.titleContent {
overflow-wrap: break-word;
}

View File

@@ -19,7 +19,7 @@ interface CssExports {
'quality': string;
'rejected': string;
'size': string;
'title': string;
'titleContent': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -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>

View File

@@ -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;

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,

View File

@@ -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>

View File

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

View File

@@ -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;

View File

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

View File

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

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