1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-16 21:15:33 -04:00

Compare commits

..

182 Commits

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

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

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

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

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

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

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

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

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

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

(cherry picked from commit 997aabbc3cfc3c9c5c220786d1d08cfceec5e2f2)

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

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

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

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

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

Rename LocalizationLanguageResource to avoid collision with LanguageResource

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: Albert <zuozl1992@foxmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ivan Mazzoli <dreadtank27@gmail.com>
Co-authored-by: TrojanHorsePower <alaa_alahmad@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: byakurau <byakurau1@gmail.com>
Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-08-09 18:30:53 +03:00
Mark McDowall
e0d70dc341 Fixed: Allow Original Language in Custom Format
(cherry picked from commit 6103c023de0d5b28d96931663ef2185dbd4c5491)
2023-08-09 18:30:29 +03:00
Bogdan
aa98b2bac9 Fixed border for actions in health status 2023-08-09 16:14:50 +03:00
Bogdan
145f67d14b Fixed: Detect Docker when using control group v2 2023-08-09 10:47:48 +03:00
Mark McDowall
caea810908 Fixed: Allow Unknown Language in Custom Format
(cherry picked from commit 65323d5e872cb87b1f3d16c520aef373f4447915)
2023-08-08 11:59:27 +03:00
Qstick
9a567b93d0 New: Performance tweaks to MovieLookup endpoint 2023-08-06 21:38:18 -05:00
Qstick
6ecd41bc5a Fixed: Error trying to notify user when process not UserInteractive
Closes #8927
2023-08-06 21:24:04 -05:00
Qstick
d5b4f0efa9 Cleanup MovieService 2023-08-06 21:17:51 -05:00
Bogdan
b337f62a34 Fixed: Add translations for import list movies in Discover 2023-08-06 22:57:51 +03:00
Bogdan
c42fc6094d Bump version to 4.7.5 2023-08-06 19:29:18 +03:00
Qstick
a6f61b2722 Filter user issues from Sentry (#5859)
(cherry picked from commit 03d361f5537bfc0caba1b86085f974570942fdbc)
2023-08-05 13:49:36 -05:00
Mark McDowall
54bb267e17 New: Ignore inaccessible files with getting files
(cherry picked from commit e5aa8584100d96a2077c57f74ae5b2ceab63de19)
2023-08-04 12:32:33 +03:00
Mark McDowall
00e2933052 Fix GetBestRootFolderPath tests
(cherry picked from commit 63a911a9a549749b5460c2b9fea48a25e78c52a4)
2023-08-04 12:32:33 +03:00
Mark McDowall
b8f06eb97d Fixed: UI loading when movie or root folder path is for wrong OS
(cherry picked from commit 5f7217844533907d7fc6287a48efb31987736c4c)
2023-08-04 12:32:33 +03:00
Bogdan
bd49a4ee8b New: Health check for indexers with invalid download client
(cherry picked from commit 377fce6fe15c0875c4bd33f1371a31af79c9310c)

Closes #8931
2023-08-04 07:21:17 +03:00
Bogdan
247ca9b22a Move RootFolderAppState to root AppState
(cherry picked from commit 5e1b46d2aa8d6621db38e6ddb077a8dd591a9aca)

Close #8928
2023-08-04 07:19:20 +03:00
Weblate
779b65fa2e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albert <zuozl1992@foxmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magnus <magnus.fladvad@gmail.com>
Co-authored-by: Stjepan <stjepstjepanovic@gmail.com>
Co-authored-by: Thirrian <matthiaslantermann@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: stormaac <yxc.frank@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-08-03 22:57:35 +03:00
Weblate
002cbdb864 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albert <zuozl1992@foxmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magnus <magnus.fladvad@gmail.com>
Co-authored-by: Thirrian <matthiaslantermann@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-08-03 22:56:44 +03:00
Bogdan
e36715d359 Simplify page sidebar translations 2023-08-03 20:56:21 +03:00
Bogdan
69b621b13a Simplify label translations in columns
Closes #8922
2023-08-02 12:22:24 +03:00
Bogdan
385c7971bb Inherit default props in MoviePoster 2023-08-02 11:54:14 +03:00
bakerboy448
1129d3901c Update bug_report.yml - no logs; no bug
[common]
2023-08-01 13:10:25 +03:00
Bogdan
d057d15ac7 Ensure yarn packages are installed when running only LintUI 2023-07-31 08:34:25 +03:00
Mark McDowall
722c20a5dc Re-order frontend build steps
(cherry picked from commit 97ad6682f7d54af8886144bc5a179fa7242f1f1f)
2023-07-31 07:59:35 +03:00
Bogdan
43a0e75acf Convert store selectors to Typescript
(cherry picked from commit a57b35a1966266b49d1241474fe3b69523f70474)
2023-07-31 07:03:38 +03:00
Bogdan
abad6a9f18 Convert Root Folders to Typescript 2023-07-31 06:02:42 +03:00
Michon van Dooren
835a539275 Fixed: Include preferred size in quality definition reset
(cherry picked from commit 8e925ac76d2f46cf5fef1ea62a20ae5e85d3000e)
2023-07-30 21:45:15 +03:00
Bogdan
cd2d81a5aa Fixed: Release note text
(cherry picked from commit 94c6b0fde39d838f7becc16eccd5fe05183117ed)
2023-07-30 21:16:52 +03:00
Stevie Robinson
5aee804bc0 Extend InlineMarkdown to handle code blocks in backticks
(cherry picked from commit e1c5533efa397632becc606c17232f97055e371b)
2023-07-30 21:16:41 +03:00
Bogdan
12fcd3f9b9 Bump version to 4.7.4 2023-07-30 09:11:20 +03:00
Bogdan
47360d4d38 Ensure movies without year are first in descending order 2023-07-29 18:06:07 +03:00
Bogdan
788782d009 Fixed: Ensure failing indexers are marked as failed when testing all
(cherry picked from commit b407eba61284d5fb855df6a2868805853aa6f448)
2023-07-29 09:34:12 +03:00
Bogdan
847d6244aa Convert formatCustomFormatScore to Typescript 2023-07-29 04:28:03 +03:00
Bogdan
8fd8128641 Add indexer default priority as constant 2023-07-29 04:23:26 +03:00
Bogdan
136075d233 Fixed: Check only enabled indexers in IndexerJackettAll health check 2023-07-29 04:22:01 +03:00
Servarr
02cec5312c Automated API Docs update 2023-07-28 13:01:41 +03:00
Weblate
e5f728352c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translation: Servarr/Radarr
2023-07-28 12:54:33 +03:00
Weblate
2cc1333e5c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: aguillement <adrien.guillement@gmail.com>
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-07-27 12:42:33 +03:00
Bogdan
a79980aae5 New: Add Monitored specification to Auto Tagging 2023-07-27 07:51:38 +03:00
Bogdan
f2bbef75dd New: Add Year specification to Auto Tagging 2023-07-27 07:51:38 +03:00
Bogdan
d5c1f58839 Fixed: Ensure validation for Auto Tagging specifications 2023-07-27 07:51:38 +03:00
Bogdan
430ea81937 Add translations to Auto Tagging 2023-07-27 07:51:38 +03:00
Mark McDowall
80099dcacb New: Auto tagging of movies
(cherry picked from commit 335fc05dd1595b6db912ebdde51ef4667963b37d)
2023-07-27 07:51:38 +03:00
Bogdan
938b69b240 Fixed: Add dedupe releases rule based on indexer priority 2023-07-26 11:36:59 +03:00
Bogdan
9839b482b2 New: Support for specific locale in Movie TitleFirstCharacter naming token
Fixes #8044
2023-07-26 03:28:55 +03:00
Servarr
4dbd962fca Automated API Docs update 2023-07-26 00:06:51 +03:00
Hayden
856c4fa4bb Fixed: Limit Discord embed title length to 256 characters
Co-authored-by: HeyBanditoz <7574664+HeyBanditoz@users.noreply.github.com>
(cherry picked from commit a6a61a016be777972f60f76a63d8e828f96a27cd)

Closes #8690
2023-07-25 23:55:18 +03:00
Mark McDowall
45f5ce5f29 Fixed: Prevent loss of restrictions when attempting to edit multiple restrictions at once
(cherry picked from commit b16094a9e3153f2ac39800475c1ddb1dafb6ab34)

Closes #8231
2023-07-25 23:31:46 +03:00
Mark McDowall
9d3e7f62ca Fixed: Overflowing release profile terms
(cherry picked from commit b90e25f652dcee3b9510a6f148a8d7a32a1ebe58)
2023-07-25 23:31:42 +03:00
Mark McDowall
594ed666e1 New: Ability to edit restriction terms
(cherry picked from commit dab6242ff28a603f2f15818c1c86567137ed0089)
2023-07-25 23:11:40 +03:00
jack-mil
36338310df New: Show Custom Format score in Manual Import
(cherry picked from commit 972e1408993fc4656196087c6646f23d222e41f5)

Closes #8839
2023-07-25 08:26:59 +03:00
Bogdan
ffde07e4d6 Fix custom format translations 2023-07-25 07:59:41 +03:00
Weblate
90a1e1dbb3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: SHUAI.W <x@ousui.org>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-07-25 07:35:00 +03:00
Qstick
8b5f305462 Remove old test 2023-07-24 23:09:58 -05:00
Qstick
2fe6847eb3 Fixed: False positives on selective Kodi library updates
Fixes #8879
2023-07-24 21:08:40 -05:00
Bogdan
bf0f681d46 Fix children with the same key and make scrollTop optional 2023-07-24 11:40:50 +03:00
Bogdan
f9cb4c1abd Fixed: More translations for columns 2023-07-24 11:40:50 +03:00
Bogdan
1190bf791c Fixed translations 2023-07-24 11:40:50 +03:00
Mark McDowall
53eb88d9a9 Fixed: Translations for columns
(cherry picked from commit 6d53d2a153a98070c42d0619c15902b6bd5dfab4)
2023-07-24 11:40:50 +03:00
Mark McDowall
ed5c063127 Fixed: Improve translation loading
(cherry picked from commit 73c5ec1da4dd00301e1b0dddbcea37590a99b045)
2023-07-24 11:40:50 +03:00
Mark McDowall
e691253419 UI loading improvements
Fixed: Caching for dynamically loaded JS files
Fixed: Incorrect caching of initialize.js
(cherry picked from commit f0cb5b81f140c67fa84162e094cc4e0f3476f5da)
2023-07-24 11:40:50 +03:00
Servarr
2959f72a10 Automated API Docs update 2023-07-24 04:51:11 +03:00
Mark McDowall
78ae059f3d Sort available filters options in custom filters
(cherry picked from commit 9e694c7b06c6d54fd652792fa1d81cc27ec1f311)
2023-07-24 04:43:42 +03:00
Qstick
7226cab3d8 Don't generate API docs for InitializeJson
Closes #8840

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2023-07-23 12:43:41 -05:00
Qstick
622162c5f6 Fixed: Default empty Tags for Collections
Fixes #8872
2023-07-23 12:33:04 -05:00
Bogdan
e612d8c485 Update webpack, eslint and core-js 2023-07-23 11:45:14 +03:00
Bogdan
b20e15855c Bump version to 4.7.3 2023-07-23 07:09:23 +03:00
738 changed files with 14950 additions and 6443 deletions

View File

@@ -1,5 +1,5 @@
name: Bug Report
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
- type: checkboxes
@@ -65,18 +65,18 @@ body:
required: true
- type: textarea
attributes:
label: Trace Logs?
label: Trace Logs? **Not Optional**
description: |
Trace Logs (https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files)
***Generally speaking, all bug reports must have trace logs provided.***
***Generally speaking, all bug reports MUST have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations:
required: true
- type: checkboxes
attributes:
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
label: Trace Logs have been provided as applicable. Reports will be closed if the required logs are not provided.
description: Trace logs are **generally required** and are not optional for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
options:
- label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
required: true

View File

@@ -3,6 +3,3 @@ contact_links:
- name: Support via Discord
url: https://radarr.video/discord
about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/radarr
about: Discuss and search thru support topics.

View File

@@ -19,9 +19,9 @@ jobs:
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)
or [Subreddit](https://reddit.com/r/radarr)
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:

View File

@@ -35,7 +35,6 @@ Note that only one type of a given movie is supported. If you want both an 4k ve
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Radarr)
Note: GitHub Issues are for Bugs and Feature Requests Only

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '4.7.2'
majorVersion: '5.0.0'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -27,6 +27,10 @@ trigger:
include:
- develop
- master
paths:
exclude:
- .github
- src/Radarr.Api.*/openapi.json
pr:
branches:
@@ -34,6 +38,7 @@ pr:
- develop
paths:
exclude:
- .github
- src/NzbDrone.Core/Localization/Core
- src/Radarr.Api.*/openapi.json
@@ -536,8 +541,8 @@ stages:
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
- job: Unit_LinuxCore_Postgres14
displayName: Unit Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
@@ -589,7 +594,63 @@ stages:
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres15
displayName: Unit Native LinuxCore with Postgres15 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Radarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests
Radarr__Postgres__Host: 'localhost'
Radarr__Postgres__Port: '5432'
Radarr__Postgres__User: 'radarr'
Radarr__Postgres__Password: 'radarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: |
chmod a+x _tests/ffprobe
displayName: Make ffprobe Executable
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=radarr \
-e POSTGRES_USER=radarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
failTaskOnFailedTests: true
- stage: Integration
@@ -675,8 +736,8 @@ stages:
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
- job: Integration_LinuxCore_Postgres14
displayName: Integration Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
@@ -733,7 +794,70 @@ stages:
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres15
displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Radarr.*.linux-core-x64.tar.gz'
Radarr__Postgres__Host: 'localhost'
Radarr__Postgres__Port: '5432'
Radarr__Postgres__User: 'radarr'
Radarr__Postgres__Password: 'radarr'
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=radarr \
-e POSTGRES_USER=radarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results

View File

@@ -392,22 +392,21 @@ then
fi
fi
if [ "$FRONTEND" = "YES" ];
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
then
YarnInstall
RunWebpack
fi
if [ "$LINT" = "YES" ];
then
if [ -z "$FRONTEND" ];
then
YarnInstall
fi
LintUI
fi
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ];
then
UpdateVersionNumber

View File

@@ -36,7 +36,7 @@ module.exports = (env) => {
},
entry: {
index: 'index.js'
index: 'index.ts'
},
resolve: {
@@ -97,7 +97,8 @@ module.exports = (env) => {
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
filename: 'index.html',
publicPath: '/'
publicPath: '/',
inject: false
}),
new FileManagerPlugin({

View File

@@ -35,7 +35,7 @@ function QueueDetails(props) {
<Icon
name={icons.DOWNLOAD}
kind={kinds.DANGER}
title={translate('ImportFailedInterp', [errorMessage])}
title={translate('ImportFailedInterp', { errorMessage })}
/>
);
}
@@ -76,7 +76,7 @@ function QueueDetails(props) {
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={translate('DownloadFailedInterp', [errorMessage])}
title={translate('DownloadFailedInterp', { errorMessage })}
/>
);
}

View File

@@ -107,14 +107,14 @@ function QueueStatusCell(props) {
iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING;
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
title = translate('DownloadWarning', [warningMessage]);
title = translate('DownloadWarning', { warningMessage });
}
if (hasError) {
if (status === 'completed') {
iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER;
title = translate('ImportFailed', [sourceTitle]);
title = translate('ImportFailed', { sourceTitle });
} else {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;

View File

@@ -88,12 +88,12 @@ class RemoveQueueItemModal extends Component {
onModalClose={this.onModalClose}
>
<ModalHeader>
{translate('Remove')} - {sourceTitle}
{translate('RemoveQueueItem', { sourceTitle })}
</ModalHeader>
<ModalBody>
<div>
{translate('RemoveFromQueueText', [sourceTitle])}
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
</div>
{
@@ -106,7 +106,7 @@ class RemoveQueueItemModal extends Component {
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>

View File

@@ -94,7 +94,7 @@ class RemoveQueueItemsModal extends Component {
<ModalBody>
<div className={styles.message}>
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
</div>
{

View File

@@ -155,7 +155,7 @@ class AddNewMovie extends Component {
!isFetching && !error && !items.length && !!term &&
<div className={styles.message}>
<div className={styles.noResults}>
{translate('CouldNotFindResults', [term])}
{translate('CouldNotFindResults', { term })}
</div>
<div>
{translate('YouCanAlsoSearch')}

View File

@@ -119,7 +119,7 @@ class ImportMovie extends Component {
rootFoldersPopulated &&
!unmappedFolders.length ?
<Alert kind={kinds.INFO}>
{translate('AllMoviesInPathHaveBeenImported', [path])}
{translate('AllMoviesInPathHaveBeenImported', { path })}
</Alert> :
null
}

View File

@@ -18,17 +18,17 @@ import styles from './ImportMovieSelectFolder.css';
const rootFolderColumns = [
{
name: 'path',
label: translate('Path'),
label: () => translate('Path'),
isVisible: true
},
{
name: 'freeSpace',
label: translate('FreeSpace'),
label: () => translate('FreeSpace'),
isVisible: true
},
{
name: 'unmappedFolders',
label: translate('UnmappedFolders'),
label: () => translate('UnmappedFolders'),
isVisible: true
},
{

View File

@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes';
function App({ store, history, hasTranslationsError }) {
function App({ store, history }) {
return (
<DocumentTitle title={window.Radarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector hasTranslationsError={hasTranslationsError}>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
@@ -25,8 +25,7 @@ function App({ store, history, hasTranslationsError }) {
App.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
hasTranslationsError: PropTypes.bool.isRequired
history: PropTypes.object.isRequired
};
export default App;

View File

@@ -1,9 +1,14 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import MovieCollectionAppState from './MovieCollectionAppState';
import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
import ParseAppState from './ParseAppState';
import QueueAppState from './QueueAppState';
import RootFolderAppState from './RootFolderAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState';
interface FilterBuilderPropOption {
@@ -39,14 +44,19 @@ export interface CustomFilter {
}
interface AppState {
movieFiles: MovieFilesAppState;
calendar: CalendarAppState;
commands: CommandAppState;
interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState;
movieFiles: MovieFilesAppState;
movieIndex: MovieIndexAppState;
parse: ParseAppState;
settings: SettingsAppState;
movies: MoviesAppState;
tags: TagsAppState;
parse: ParseAppState;
queue: QueueAppState;
rootFolders: RootFolderAppState;
settings: SettingsAppState;
system: SystemAppState;
tags: TagsAppState;
}
export default AppState;

View File

@@ -0,0 +1,9 @@
import AppSectionState from 'App/State/AppSectionState';
import Movie from 'Movie/Movie';
import { FilterBuilderProp } from './AppState';
interface CalendarAppState extends AppSectionState<Movie> {
filterBuilderProps: FilterBuilderProp<Movie>[];
}
export default CalendarAppState;

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import MovieCollection from 'typings/MovieCollection';
type MovieCollectionAppState = AppSectionState<MovieCollection>;
export default MovieCollectionAppState;

View File

@@ -22,6 +22,9 @@ export interface MovieIndexAppState {
showQualityProfile: boolean;
showReleaseDate: boolean;
showCinemaRelease: boolean;
showTmdbRating: boolean;
showImdbRating: boolean;
showRottenTomatoesRating: boolean;
showSearchAction: boolean;
};

View File

@@ -0,0 +1,12 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import RootFolder from 'typings/RootFolder';
interface RootFolderAppState
extends AppSectionState<RootFolder>,
AppSectionDeleteState,
AppSectionSaveState {}
export default RootFolderAppState;

View File

@@ -1,5 +1,6 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemState,
AppSectionSaveState,
AppSectionSchemaState,
} from 'App/State/AppSectionState';
@@ -35,16 +36,16 @@ export interface QualityProfilesAppState
AppSectionSchemaState<QualityProfile> {}
export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionState<UiSettings>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
downloadClients: DownloadClientAppState;
importLists: ImportListAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
notifications: NotificationAppState;
language: LanguageSettingsAppState;
uiSettings: UiSettingsAppState;
qualityProfiles: QualityProfilesAppState;
ui: UiSettingsAppState;
}
export default SettingsAppState;

View File

@@ -0,0 +1,10 @@
import SystemStatus from 'typings/SystemStatus';
import { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
interface SystemAppState {
status: SystemStatusAppState;
}
export default SystemAppState;

View File

@@ -1,12 +1,32 @@
import ModelBase from 'App/ModelBase';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
export interface Tag extends ModelBase {
label: string;
}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
export interface TagDetail extends ModelBase {
label: string;
autoTagIds: number[];
delayProfileIds: number[];
downloadClientIds: number[];
importListIds: number[];
indexerIds: number[];
movieIds: number[];
notificationIds: number[];
restrictionIds: number[];
}
export interface TagDetailAppState
extends AppSectionState<TagDetail>,
AppSectionDeleteState,
AppSectionSaveState {}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
details: TagDetailAppState;
}
export default TagsAppState;

View File

@@ -0,0 +1,56 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setCalendarFilter } from 'Store/Actions/calendarActions';
function createCalendarSelector() {
return createSelector(
(state: AppState) => state.calendar.items,
(calendar) => {
return calendar;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.calendar.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface SeriesIndexFilterModalProps {
isOpen: boolean;
}
export default function CalendarFilterModal(
props: SeriesIndexFilterModalProps
) {
const sectionItems = useSelector(createCalendarSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'calendar';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setCalendarFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -14,6 +14,7 @@ import NoMovie from 'Movie/NoMovie';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import CalendarConnector from './CalendarConnector';
import CalendarFilterModal from './CalendarFilterModal';
import CalendarLinkModal from './iCal/CalendarLinkModal';
import LegendConnector from './Legend/LegendConnector';
import CalendarOptionsModal from './Options/CalendarOptionsModal';
@@ -83,6 +84,7 @@ class CalendarPage extends Component {
movieIsFetching,
movieIsPopulated,
missingMovieIds,
customFilters,
isRssSyncExecuting,
isSearchingForMissing,
useCurrentPage,
@@ -137,7 +139,8 @@ class CalendarPage extends Component {
isDisabled={!hasMovie}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
customFilters={customFilters}
filterModalConnectorComponent={CalendarFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
@@ -208,6 +211,7 @@ CalendarPage.propTypes = {
movieIsFetching: PropTypes.bool.isRequired,
movieIsPopulated: PropTypes.bool.isRequired,
missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired,
isSearchingForMissing: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,

View File

@@ -5,6 +5,7 @@ import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
@@ -59,6 +60,7 @@ function createMapStateToProps() {
return createSelector(
(state) => state.calendar.selectedFilterKey,
(state) => state.calendar.filters,
createCustomFiltersSelector('calendar'),
createMovieCountSelector(),
createUISettingsSelector(),
createMissingMovieIdsSelector(),
@@ -67,6 +69,7 @@ function createMapStateToProps() {
(
selectedFilterKey,
filters,
customFilters,
movieCount,
uiSettings,
missingMovieIds,
@@ -76,6 +79,7 @@ function createMapStateToProps() {
return {
selectedFilterKey,
filters,
customFilters,
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasMovie: !!movieCount.count,
movieError: movieCount.error,

View File

@@ -258,7 +258,7 @@ CollectionOverviews.propTypes = {
sortKey: PropTypes.string,
overviewOptions: PropTypes.object.isRequired,
jumpToCharacter: PropTypes.string,
scrollTop: PropTypes.number.isRequired,
scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,

View File

@@ -14,9 +14,24 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const posterSizeOptions = [
{ key: 'small', value: translate('Small') },
{ key: 'medium', value: translate('Medium') },
{ key: 'large', value: translate('Large') }
{
key: 'small',
get value() {
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
];
class CollectionOverviewOptionsModalContent extends Component {

View File

@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
render() {
const {
className,
titleClassName,
descriptionClassName,
title,
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
} = this.props;
return (
<div>
<div className={className}>
<DescriptionListItemTitle
className={titleClassName}
>
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
}
DescriptionListItem.propTypes = {
className: PropTypes.string,
titleClassName: PropTypes.string,
descriptionClassName: PropTypes.string,
title: PropTypes.string,

View File

@@ -20,12 +20,12 @@ import styles from './FileBrowserModalContent.css';
const columns = [
{
name: 'type',
label: translate('Type'),
label: () => translate('Type'),
isVisible: true
},
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isVisible: true
}
];

View File

@@ -10,12 +10,42 @@ import { NAME } from './FilterBuilderRowValue';
import styles from './DateFilterBuilderRowValue.css';
const timeOptions = [
{ key: 'seconds', value: translate('Seconds') },
{ key: 'minutes', value: translate('Minutes') },
{ key: 'hours', value: translate('Hours') },
{ key: 'days', value: translate('Days') },
{ key: 'weeks', value: translate('Weeks') },
{ key: 'months', value: translate('Months') }
{
key: 'seconds',
get value() {
return translate('Seconds');
}
},
{
key: 'minutes',
get value() {
return translate('Minutes');
}
},
{
key: 'hours',
get value() {
return translate('Hours');
}
},
{
key: 'days',
get value() {
return translate('Days');
}
},
{
key: 'weeks',
get value() {
return translate('Weeks');
}
},
{
key: 'months',
get value() {
return translate('Months');
}
}
];
function isInFilter(filterType) {

View File

@@ -210,11 +210,13 @@ class FilterBuilderRow extends Component {
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
const { name, label } = availablePropFilter;
return {
key: availablePropFilter.name,
value: availablePropFilter.label
key: name,
value: typeof label === 'function' ? label() : label
};
});
}).sort((a, b) => a.value.localeCompare(b.value));
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);

View File

@@ -3,9 +3,24 @@ import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const protocols = [
{ id: 'announced', name: translate('Announced') },
{ id: 'inCinemas', name: translate('InCinemas') },
{ id: 'released', name: translate('Released') }
{
id: 'announced',
get name() {
return translate('Announced');
}
},
{
id: 'inCinemas',
get name() {
return translate('InCinemas');
}
},
{
id: 'released',
get name() {
return translate('Released');
}
}
];
function MinimumAvailabilityFilterBuilderRowValue(props) {

View File

@@ -4,10 +4,30 @@ import FilterBuilderRowValue from './FilterBuilderRowValue';
const protocols = [
{ id: 'tba', name: 'TBA' },
{ id: 'announced', name: translate('Announced') },
{ id: 'inCinemas', name: translate('InCinemas') },
{ id: 'released', name: translate('Released') },
{ id: 'deleted', name: translate('Deleted') }
{
id: 'announced',
get name() {
return translate('Announced');
}
},
{
id: 'inCinemas',
get name() {
return translate('InCinemas');
}
},
{
id: 'released',
get name() {
return translate('Released');
}
},
{
id: 'deleted',
get name() {
return translate('Deleted');
}
}
];
function ReleaseStatusFilterBuilderRowValue(props) {

View File

@@ -4,9 +4,24 @@ import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
const availabilityOptions = [
{ key: 'announced', value: translate('Announced') },
{ key: 'inCinemas', value: translate('InCinemas') },
{ key: 'released', value: translate('Released') }
{
key: 'announced',
get value() {
return translate('Announced');
}
},
{
key: 'inCinemas',
get value() {
return translate('InCinemas');
}
},
{
key: 'released',
get value() {
return translate('Released');
}
}
];
function AvailabilitySelectInput(props) {
@@ -20,7 +35,7 @@ function AvailabilitySelectInput(props) {
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
value: translate('NoChange'),
disabled: true
});
}

View File

@@ -13,6 +13,7 @@ import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
@@ -65,6 +66,9 @@ function getComponent(type) {
case inputTypes.QUALITY_PROFILE_SELECT:
return QualityProfileSelectInputConnector;
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.MOVIE_MONITORED_SELECT:
return MovieMonitoredSelectInput;

View File

@@ -0,0 +1,93 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexers } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers,
(state, { includeAny }) => includeAny,
(indexers, includeAny) => {
const {
isFetching,
isPopulated,
error,
items
} = indexers;
const values = items.sort(sortByName).map((indexer) => ({
key: indexer.id,
value: indexer.name
}));
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexers: fetchIndexers
};
class IndexerSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchIndexers();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
};
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
IndexerSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired
};
IndexerSelectInputConnector.defaultProps = {
includeAny: false
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import monitorOptions from 'Utilities/Movie/monitorOptions';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput';
function MovieMonitoredSelectInput(props) {
@@ -14,7 +15,7 @@ function MovieMonitoredSelectInput(props) {
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
value: translate('NoChange'),
disabled: true
});
}

View File

@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) {
const { value } = this.props;
if (value !== prevProps.value && !this.state.isFocused) {
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
this.setState({
value: value == null ? '' : value.toString()
});

View File

@@ -35,6 +35,8 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.TEXT;
case 'oAuth':
return inputTypes.OAUTH;
case 'rootFolder':
return inputTypes.ROOT_FOLDER_SELECT;
default:
return inputTypes.TEXT;
}

View File

@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
@@ -24,7 +25,7 @@ function createMapStateToProps() {
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
value: translate('NoChange'),
disabled: includeNoChangeDisabled
});
}

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRootFolder } from 'Store/Actions/rootFolderActions';
import translate from 'Utilities/String/translate';
import RootFolderSelectInput from './RootFolderSelectInput';
const ADD_NEW_KEY = 'addNew';
@@ -27,7 +28,7 @@ function createMapStateToProps() {
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
value: translate('NoChange'),
isDisabled: includeNoChangeDisabled,
isMissing: false
});

View File

@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key}
{...otherOptionProps}
>
{optionValue}
{typeof optionValue === 'function' ? optionValue() : optionValue}
</option>
);
})
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string,
disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,

View File

@@ -75,6 +75,18 @@ class TagInput extends Component {
//
// Listeners
onTagEdit = ({ value, ...otherProps }) => {
const currentValue = this.state.value;
if (currentValue && this.props.onTagReplace) {
this.props.onTagReplace(otherProps, { name: currentValue });
} else {
this.props.onTagDelete(otherProps);
}
this.setState({ value });
};
onInputContainerPress = () => {
this._autosuggestRef.input.focus();
};
@@ -188,6 +200,7 @@ class TagInput extends Component {
const {
tags,
kind,
canEdit,
tagComponent,
onTagDelete
} = this.props;
@@ -199,8 +212,10 @@ class TagInput extends Component {
kind={kind}
inputProps={inputProps}
isFocused={this.state.isFocused}
canEdit={canEdit}
tagComponent={tagComponent}
onTagDelete={onTagDelete}
onTagEdit={this.onTagEdit}
onInputContainerPress={this.onInputContainerPress}
/>
);
@@ -223,7 +238,7 @@ class TagInput extends Component {
<AutoSuggestInput
{...otherProps}
forwardedRef={this._setAutosuggestRef}
className={styles.internalInput}
className={className}
inputContainerClassName={classNames(
inputContainerClassName,
isFocused && styles.isFocused
@@ -258,11 +273,13 @@ TagInput.propTypes = {
placeholder: PropTypes.string.isRequired,
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
minQueryLength: PropTypes.number.isRequired,
canEdit: PropTypes.bool,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
tagComponent: PropTypes.elementType.isRequired,
onTagAdd: PropTypes.func.isRequired,
onTagDelete: PropTypes.func.isRequired
onTagDelete: PropTypes.func.isRequired,
onTagReplace: PropTypes.func
};
TagInput.defaultProps = {
@@ -273,6 +290,7 @@ TagInput.defaultProps = {
placeholder: '',
delimiters: ['Tab', 'Enter', ' ', ','],
minQueryLength: 1,
canEdit: false,
tagComponent: TagInputTag
};

View File

@@ -138,6 +138,7 @@ class TagInputConnector extends Component {
<TagInput
onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete}
onTagReplace={this.onTagReplace}
{...this.props}
/>
);

View File

@@ -28,8 +28,10 @@ class TagInputInput extends Component {
tags,
inputProps,
kind,
canEdit,
tagComponent: TagComponent,
onTagDelete
onTagDelete,
onTagEdit
} = this.props;
return (
@@ -46,8 +48,10 @@ class TagInputInput extends Component {
index={index}
tag={tag}
kind={kind}
canEdit={canEdit}
isLastTag={index === tags.length - 1}
onDelete={onTagDelete}
onEdit={onTagEdit}
/>
);
})
@@ -66,8 +70,10 @@ TagInputInput.propTypes = {
inputProps: PropTypes.object.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired,
isFocused: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
tagComponent: PropTypes.elementType.isRequired,
onTagDelete: PropTypes.func.isRequired,
onTagEdit: PropTypes.func.isRequired,
onInputContainerPress: PropTypes.func.isRequired
};

View File

@@ -1,5 +1,40 @@
.tag {
composes: link from '~Components/Link/Link.css';
display: flex;
justify-content: center;
flex-direction: column;
max-width: 100%;
height: 31px;
}
.link {
composes: link from '~Components/Link/Link.css';
max-width: 100%;
line-height: 1px;
}
.linkWithEdit {
composes: link from '~Components/Link/Link.css';
max-width: calc(100% - 9px - 4px - 2px);
line-height: 1px;
}
.editContainer {
display: inline-block;
margin-left: 4px;
padding-left: 2px;
border-left: 1px solid #eee;
}
.editButton {
composes: button from '~Components/Link/IconButton.css';
width: 9px;
}
.label {
composes: label from '~Components/Label.css';
max-width: 100%;
}

View File

@@ -1,6 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'editButton': string;
'editContainer': string;
'label': string;
'link': string;
'linkWithEdit': string;
'tag': string;
}
export const cssExports: CssExports;

View File

@@ -1,8 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import { kinds } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import styles from './TagInputTag.css';
@@ -24,24 +25,61 @@ class TagInputTag extends Component {
});
};
onEdit = () => {
const {
index,
tag,
onEdit
} = this.props;
onEdit({
index,
id: tag.id,
value: tag.name
});
};
//
// Render
render() {
const {
tag,
kind
kind,
canEdit
} = this.props;
return (
<Link
<div
className={styles.tag}
tabIndex={-1}
onPress={this.onDelete}
>
<Label kind={kind}>
{tag.name}
<Label
className={styles.label}
kind={kind}
>
<Link
className={canEdit ? styles.linkWithEdit : styles.link}
tabIndex={-1}
onPress={this.onDelete}
>
{tag.name}
</Link>
{
canEdit ?
<div className={styles.editContainer}>
<IconButton
className={styles.editButton}
name={icons.EDIT}
size={9}
onPress={this.onEdit}
/>
</div> :
null
}
</Label>
</Link>
</div>
);
}
}
@@ -50,7 +88,9 @@ TagInputTag.propTypes = {
index: PropTypes.number.isRequired,
tag: PropTypes.shape(tagShape),
kind: PropTypes.oneOf(kinds.all).isRequired,
onDelete: PropTypes.func.isRequired
canEdit: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired
};
export default TagInputTag;

View File

@@ -46,13 +46,13 @@ class TextTagInputConnector extends Component {
// to oddities with restrictions (as an example).
const newValue = [...valueArray];
const newTags = split(tag.name);
const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
newTags.forEach((newTag) => {
newValue.push(newTag.trim());
});
onChange({ name, value: newValue.join(',') });
onChange({ name, value: newValue });
};
onTagDelete = ({ index }) => {
@@ -67,10 +67,24 @@ class TextTagInputConnector extends Component {
onChange({
name,
value: newValue.join(',')
value: newValue
});
};
onTagReplace = (tagToReplace, newTag) => {
const {
name,
valueArray,
onChange
} = this.props;
const newValue = [...valueArray];
newValue.splice(tagToReplace.index, 1);
newValue.push(newTag.name.trim());
onChange({ name, value: newValue });
};
//
// Render
@@ -80,6 +94,7 @@ class TextTagInputConnector extends Component {
tagList={[]}
onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete}
onTagReplace={this.onTagReplace}
{...this.props}
/>
);

View File

@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return (
<span
className={containerClassName}
title={title}
title={typeof title === 'function' ? title() : title}
>
{icon}
</span>
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};

View File

@@ -2,6 +2,7 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import translate from 'Utilities/String/translate';
import Link from './Link';
import styles from './IconButton.css';
@@ -23,7 +24,7 @@ function IconButton(props) {
className,
isDisabled && styles.isDisabled
)}
aria-label="Table Options Button"
aria-label={translate('TableOptionsButton')}
isDisabled={isDisabled}
{...otherProps}
>

View File

@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
render() {
const {
kind,
isSpinning,
error,
children,
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK;
let iconKind = kinds.SUCCESS;
let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
if (hasWarning) {
iconName = icons.WARNING;
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
return (
<SpinnerButton
kind={kind}
isSpinning={isSpinning}
{...otherProps}
>
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
}
SpinnerErrorButton.propTypes = {
kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object,
children: PropTypes.node.isRequired

View File

@@ -13,24 +13,51 @@ class InlineMarkdown extends Component {
data
} = this.props;
// For now only replace links
// For now only replace links or code blocks (not both)
const markdownBlocks = [];
if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0;
let match = null;
while ((match = regex.exec(data)) !== null) {
while ((match = linkRegex.exec(data)) !== null) {
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length) {
if (endIndex !== data.length && markdownBlocks.length > 0) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
endIndex = 0;
match = null;
let matchedCode = false;
while ((match = codeRegex.exec(data)) !== null) {
matchedCode = true;
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<code key={`code-${match.index}`}>{match[0].substring(1, match[0].length - 1)}</code>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
if (markdownBlocks.length === 0) {
markdownBlocks.push(data);
}
}
return <span className={className}>{markdownBlocks}</span>;

View File

@@ -33,7 +33,7 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
{typeof filter.label === 'function' ? filter.label() : filter.label}
</FilterMenuItem>
);
})

View File

@@ -7,7 +7,7 @@ function ErrorPage(props) {
const {
version,
isLocalStorageSupported,
hasTranslationsError,
translationsError,
moviesError,
customFiltersError,
tagsError,
@@ -21,8 +21,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (hasTranslationsError) {
errorMessage = 'Failed to load translations from API';
} else if (translationsError) {
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
} else if (moviesError) {
errorMessage = getErrorMessage(moviesError, 'Failed to load movie from API');
} else if (customFiltersError) {
@@ -55,7 +55,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = {
version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired,
hasTranslationsError: PropTypes.bool.isRequired,
translationsError: PropTypes.object,
moviesError: PropTypes.object,
customFiltersError: PropTypes.object,
tagsError: PropTypes.object,

View File

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

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchMovies } from 'Store/Actions/movieActions';
import { fetchMovieCollections } from 'Store/Actions/movieCollectionActions';
@@ -11,6 +11,7 @@ import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfil
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ErrorPage from './ErrorPage';
import LoadingPage from './LoadingPage';
import Page from './Page';
@@ -53,6 +54,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.movieCollections.isPopulated,
(state) => state.app.translations.isPopulated,
(
customFiltersIsPopulated,
tagsIsPopulated,
@@ -62,7 +64,8 @@ const selectIsPopulated = createSelector(
indexerFlagsIsPopulated,
importListsIsPopulated,
systemStatusIsPopulated,
movieCollectionsIsPopulated
movieCollectionsIsPopulated,
translationsIsPopulated
) => {
return (
customFiltersIsPopulated &&
@@ -73,7 +76,8 @@ const selectIsPopulated = createSelector(
indexerFlagsIsPopulated &&
importListsIsPopulated &&
systemStatusIsPopulated &&
movieCollectionsIsPopulated
movieCollectionsIsPopulated &&
translationsIsPopulated
);
}
);
@@ -88,6 +92,7 @@ const selectErrors = createSelector(
(state) => state.settings.importLists.error,
(state) => state.system.status.error,
(state) => state.movieCollections.error,
(state) => state.app.translations.error,
(
customFiltersError,
tagsError,
@@ -97,7 +102,8 @@ const selectErrors = createSelector(
indexerFlagsError,
importListsError,
systemStatusError,
movieCollectionsError
movieCollectionsError,
translationsError
) => {
const hasError = !!(
customFiltersError ||
@@ -108,7 +114,8 @@ const selectErrors = createSelector(
indexerFlagsError ||
importListsError ||
systemStatusError ||
movieCollectionsError
movieCollectionsError ||
translationsError
);
return {
@@ -121,7 +128,8 @@ const selectErrors = createSelector(
indexerFlagsError,
importListsError,
systemStatusError,
movieCollectionsError
movieCollectionsError,
translationsError
};
}
);
@@ -133,18 +141,21 @@ function createMapStateToProps() {
selectErrors,
selectAppProps,
createDimensionsSelector(),
createSystemStatusSelector(),
(
enableColorImpairedMode,
isPopulated,
errors,
app,
dimensions
dimensions,
systemStatus
) => {
return {
...app,
...errors,
isPopulated,
isSmallScreen: dimensions.isSmallScreen,
authenticationEnabled: systemStatus.authentication !== 'none',
enableColorImpairedMode
};
}
@@ -183,6 +194,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() {
dispatch(fetchStatus());
},
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) {
dispatch(saveDimensions(dimensions));
},
@@ -217,6 +231,7 @@ class PageConnector extends Component {
this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
}
}
@@ -232,7 +247,6 @@ class PageConnector extends Component {
render() {
const {
hasTranslationsError,
isPopulated,
hasError,
dispatchFetchMovies,
@@ -244,15 +258,15 @@ class PageConnector extends Component {
dispatchFetchImportLists,
dispatchFetchUISettings,
dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps
} = this.props;
if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) {
if (hasError || !this.state.isLocalStorageSupported) {
return (
<ErrorPage
{...this.state}
{...otherProps}
hasTranslationsError={hasTranslationsError}
/>
);
}
@@ -273,7 +287,6 @@ class PageConnector extends Component {
}
PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired,
@@ -287,6 +300,7 @@ PageConnector.propTypes = {
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired
};

View File

@@ -21,24 +21,24 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.MOVIE_CONTINUING,
title: translate('Movies'),
title: () => translate('Movies'),
to: '/',
alias: '/movies',
children: [
{
title: translate('AddNew'),
title: () => translate('AddNew'),
to: '/add/new'
},
{
title: translate('ImportLibrary'),
title: () => translate('ImportLibrary'),
to: '/add/import'
},
{
title: translate('Collections'),
title: () => translate('Collections'),
to: '/collections'
},
{
title: translate('Discover'),
title: () => translate('Discover'),
to: '/add/discover'
}
]
@@ -46,26 +46,26 @@ const links = [
{
iconName: icons.CALENDAR,
title: translate('Calendar'),
title: () => translate('Calendar'),
to: '/calendar'
},
{
iconName: icons.ACTIVITY,
title: translate('Activity'),
title: () => translate('Activity'),
to: '/activity/queue',
children: [
{
title: translate('Queue'),
title: () => translate('Queue'),
to: '/activity/queue',
statusComponent: QueueStatusConnector
},
{
title: translate('History'),
title: () => translate('History'),
to: '/activity/history'
},
{
title: translate('Blocklist'),
title: () => translate('Blocklist'),
to: '/activity/blocklist'
}
]
@@ -73,55 +73,55 @@ const links = [
{
iconName: icons.SETTINGS,
title: translate('Settings'),
title: () => translate('Settings'),
to: '/settings',
children: [
{
title: translate('MediaManagement'),
title: () => translate('MediaManagement'),
to: '/settings/mediamanagement'
},
{
title: translate('Profiles'),
title: () => translate('Profiles'),
to: '/settings/profiles'
},
{
title: translate('Quality'),
title: () => translate('Quality'),
to: '/settings/quality'
},
{
title: translate('CustomFormats'),
title: () => translate('CustomFormats'),
to: '/settings/customformats'
},
{
title: translate('Indexers'),
title: () => translate('Indexers'),
to: '/settings/indexers'
},
{
title: translate('DownloadClients'),
title: () => translate('DownloadClients'),
to: '/settings/downloadclients'
},
{
title: translate('Lists'),
title: () => translate('Lists'),
to: '/settings/importlists'
},
{
title: translate('Connect'),
title: () => translate('Connect'),
to: '/settings/connect'
},
{
title: translate('Metadata'),
title: () => translate('Metadata'),
to: '/settings/metadata'
},
{
title: translate('Tags'),
title: () => translate('Tags'),
to: '/settings/tags'
},
{
title: translate('General'),
title: () => translate('General'),
to: '/settings/general'
},
{
title: translate('UI'),
title: () => translate('UI'),
to: '/settings/ui'
}
]
@@ -129,32 +129,32 @@ const links = [
{
iconName: icons.SYSTEM,
title: translate('System'),
title: () => translate('System'),
to: '/system/status',
children: [
{
title: translate('Status'),
title: () => translate('Status'),
to: '/system/status',
statusComponent: HealthStatusConnector
},
{
title: translate('Tasks'),
title: () => translate('Tasks'),
to: '/system/tasks'
},
{
title: translate('Backup'),
title: () => translate('Backup'),
to: '/system/backup'
},
{
title: translate('Updates'),
title: () => translate('Updates'),
to: '/system/updates'
},
{
title: translate('Events'),
title: () => translate('Events'),
to: '/system/events'
},
{
title: translate('LogFiles'),
title: () => translate('LogFiles'),
to: '/system/logs/files'
}
]

View File

@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
}
<span className={isChildItem ? styles.noIcon : null}>
{title}
{typeof title === 'function' ? title() : title}
</span>
{
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = {
iconName: PropTypes.object,
title: PropTypes.string.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
to: PropTypes.string.isRequired,
isActive: PropTypes.bool,
isActiveParent: PropTypes.bool,

View File

@@ -1,8 +1,10 @@
import React from 'react';
type PropertyFunction<T> = () => T;
interface Column {
name: string;
label: string | React.ReactNode;
label: string | PropertyFunction<string> | React.ReactNode;
columnLabel?: string;
isSortable?: boolean;
isVisible: boolean;

View File

@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)}
{...column}
>
{column.label}
{typeof column.label === 'function' ? column.label() : column.label}
</TableHeaderCell>
);
})

View File

@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
const {
className,
name,
label,
columnLabel,
isSortable,
isVisible,
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
{...otherProps}
component="th"
className={className}
title={columnLabel}
label={typeof label === 'function' ? label() : label}
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress}
>
{children}
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = {
className: PropTypes.string,
name: PropTypes.string.isRequired,
columnLabel: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSortable: PropTypes.bool,
isVisible: PropTypes.bool,
isModifiable: PropTypes.bool,

View File

@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false}
onChange={onVisibleChange}
/>
{label}
{typeof label === 'function' ? label() : label}
</label>
{
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,

View File

@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
<TableOptionsColumn
name={name}
label={label}
label={typeof label === 'function' ? label() : label}
isVisible={isVisible}
isModifiable={isModifiable}
index={index}
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,

View File

@@ -6,47 +6,65 @@ import translate from 'Utilities/String/translate';
export const shortcuts = {
OPEN_KEYBOARD_SHORTCUTS_MODAL: {
key: '?',
name: translate('OpenThisModal')
get name() {
return translate('OpenThisModal');
}
},
CLOSE_MODAL: {
key: 'Esc',
name: translate('CloseCurrentModal')
get name() {
return translate('CloseCurrentModal');
}
},
ACCEPT_CONFIRM_MODAL: {
key: 'Enter',
name: translate('AcceptConfirmationModal')
get name() {
return translate('AcceptConfirmationModal');
}
},
MOVIE_SEARCH_INPUT: {
key: 's',
name: translate('FocusSearchBox')
get name() {
return translate('FocusSearchBox');
}
},
SAVE_SETTINGS: {
key: 'mod+s',
name: translate('SaveSettings')
get name() {
return translate('SaveSettings');
}
},
SCROLL_TOP: {
key: 'mod+home',
name: translate('MovieIndexScrollTop')
get name() {
return translate('MovieIndexScrollTop');
}
},
SCROLL_BOTTOM: {
key: 'mod+end',
name: translate('MovieIndexScrollBottom')
get name() {
return translate('MovieIndexScrollBottom');
}
},
DETAILS_NEXT: {
key: '→',
name: translate('MovieDetailsNextMovie')
get name() {
return translate('MovieDetailsNextMovie');
}
},
DETAILS_PREVIOUS: {
key: '←',
name: translate('MovieDetailsPreviousMovie')
get name() {
return translate('MovieDetailsPreviousMovie');
}
}
};

View File

@@ -215,7 +215,7 @@ class DiscoverMovieFooter extends Component {
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<DiscoverMovieFooterLabel
label={translate('MoviesSelectedInterp', [selectedCount])}
label={translate('MoviesSelectedInterp', { count: selectedCount })}
isSaving={false}
/>

View File

@@ -47,7 +47,7 @@ function NoDiscoverMovie(props) {
to="/settings/importlists"
kind={kinds.PRIMARY}
>
{translate('AddList')}
{translate('AddImportList')}
</Button>
</div>
</div>

View File

@@ -14,9 +14,24 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const posterSizeOptions = [
{ key: 'small', value: translate('Small') },
{ key: 'medium', value: translate('Medium') },
{ key: 'large', value: translate('Large') }
{
key: 'small',
get value() {
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
];
class DiscoverMovieOverviewOptionsModalContent extends Component {

View File

@@ -14,9 +14,24 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const posterSizeOptions = [
{ key: 'small', value: translate('Small') },
{ key: 'medium', value: translate('Medium') },
{ key: 'large', value: translate('Large') }
{
key: 'small',
get value() {
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
];
class DiscoverMoviePosterOptionsModalContent extends Component {

View File

@@ -111,7 +111,7 @@ class DiscoverMovieHeader extends Component {
isSortable={isSortable}
{...otherProps}
>
{label}
{typeof label === 'function' ? label() : label}
</VirtualTableHeaderCell>
);
})

View File

@@ -0,0 +1,7 @@
enum DownloadProtocol {
Unknown = 'unknown',
Usenet = 'usenet',
Torrent = 'torrent',
}
export default DownloadProtocol;

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'addCustomFormatMessage': string;
'authRequiredAlert': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,153 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import Alert from 'Components/Alert';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
import translate from 'Utilities/String/translate';
import styles from './AuthenticationRequiredModalContent.css';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModalContent(props) {
const {
isPopulated,
error,
isSaving,
settings,
onInputChange,
onSavePress,
dispatchFetchStatus
} = props;
const {
authenticationMethod,
authenticationRequired,
username,
password
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
const didMount = useRef(false);
useEffect(() => {
if (!isSaving && didMount.current) {
dispatchFetchStatus();
}
didMount.current = true;
}, [isSaving, dispatchFetchStatus]);
return (
<ModalContent
showCloseButton={false}
onModalClose={onModalClose}
>
<ModalHeader>
{translate('AuthenticationRequired')}
</ModalHeader>
<ModalBody>
<Alert
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{translate('AuthenticationRequiredWarning')}
</Alert>
{
isPopulated && !error ?
<div>
<FormGroup>
<FormLabel>{translate('Authentication')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
onChange={onInputChange}
{...authenticationMethod}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
{...username}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
{...password}
/>
</FormGroup>
</div> :
null
}
{
!isPopulated && !error ? <LoadingIndicator /> : null
}
</ModalBody>
<ModalFooter>
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isSaving}
isDisabled={!authenticationEnabled}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
AuthenticationRequiredModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
settings: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired
};
export default AuthenticationRequiredModalContent;

View File

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

View File

@@ -1,14 +1,18 @@
import * as filterTypes from './filterTypes';
export const ARRAY = 'array';
export const CONTAINS = 'contains';
export const DATE = 'date';
export const EQUAL = 'equal';
export const EXACT = 'exact';
export const NUMBER = 'number';
export const STRING = 'string';
export const all = [
ARRAY,
CONTAINS,
DATE,
EQUAL,
EXACT,
NUMBER,
STRING
@@ -20,6 +24,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
],
[CONTAINS]: [
{ key: filterTypes.CONTAINS, value: 'contains' }
],
[DATE]: [
{ key: filterTypes.LESS_THAN, value: 'is before' },
{ key: filterTypes.GREATER_THAN, value: 'is after' },
@@ -29,6 +37,10 @@ export const possibleFilterTypes = {
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
],
[EQUAL]: [
{ key: filterTypes.EQUAL, value: 'is' }
],
[EXACT]: [
{ key: filterTypes.EQUAL, value: 'is' },
{ key: filterTypes.NOT_EQUAL, value: 'is not' }

View File

@@ -43,6 +43,7 @@ import {
faChevronCircleRight as fasChevronCircleRight,
faChevronCircleUp as fasChevronCircleUp,
faCircle as fasCircle,
faCircleDown as fasCircleDown,
faCloud as fasCloud,
faCloudDownloadAlt as fasCloudDownloadAlt,
faCog as fasCog,
@@ -135,6 +136,7 @@ export const CHECK_INDETERMINATE = fasMinus;
export const CHECK_CIRCLE = fasCheckCircle;
export const CHECK_SQUARE = fasSquareCheck;
export const CIRCLE = fasCircle;
export const CIRCLE_DOWN = fasCircleDown;
export const CIRCLE_OUTLINE = farCircle;
export const CLEAR = fasTrashAlt;
export const CLIPBOARD = fasCopy;

View File

@@ -5,11 +5,13 @@ export const CHECK = 'check';
export const DEVICE = 'device';
export const KEY_VALUE_LIST = 'keyValueList';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const FLOAT = 'float';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const LANGUAGE_SELECT = 'languageSelect';
@@ -31,11 +33,13 @@ export const all = [
DEVICE,
KEY_VALUE_LIST,
MOVIE_MONITORED_SELECT,
FLOAT,
NUMBER,
OAUTH,
PASSWORD,
PATH,
QUALITY_PROFILE_SELECT,
INDEXER_SELECT,
DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT,
INDEXER_FLAGS_SELECT,

View File

@@ -25,11 +25,11 @@ import styles from './InteractiveImportSelectFolderModalContent.css';
const recentFoldersColumns = [
{
name: 'folder',
label: translate('Folder'),
label: () => translate('Folder'),
},
{
name: 'lastUsed',
label: translate('LastUsed'),
label: () => translate('LastUsed'),
},
{
name: 'actions',
@@ -100,7 +100,7 @@ function InteractiveImportSelectFolderModalContent(
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {translate('SelectFolder')}
{translate('SelectFolderModalTitle', { modalTitle })}
</ModalHeader>
<ModalBody>

View File

@@ -71,36 +71,36 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [
{
name: 'relativePath',
label: translate('RelativePath'),
label: () => translate('RelativePath'),
isSortable: true,
isVisible: true,
},
{
name: 'movie',
label: translate('Movie'),
label: () => translate('Movie'),
isSortable: true,
isVisible: true,
},
{
name: 'releaseGroup',
label: translate('ReleaseGroup'),
label: () => translate('ReleaseGroup'),
isVisible: true,
},
{
name: 'quality',
label: translate('Quality'),
label: () => translate('Quality'),
isSortable: true,
isVisible: true,
},
{
name: 'languages',
label: translate('Languages'),
label: () => translate('Languages'),
isSortable: true,
isVisible: true,
},
{
name: 'size',
label: translate('Size'),
label: () => translate('Size'),
isSortable: true,
isVisible: true,
},
@@ -108,7 +108,7 @@ const COLUMNS = [
name: 'customFormats',
label: React.createElement(Icon, {
name: icons.INTERACTIVE,
title: translate('CustomFormat'),
title: () => translate('CustomFormat'),
}),
isSortable: true,
isVisible: true,
@@ -118,6 +118,7 @@ const COLUMNS = [
label: React.createElement(Icon, {
name: icons.DANGER,
kind: kinds.DANGER,
title: () => translate('Rejections'),
}),
isSortable: true,
isVisible: true,
@@ -127,11 +128,17 @@ const COLUMNS = [
const importModeOptions = [
{
key: 'chooseImportMode',
value: translate('ChooseImportMode'),
value: () => translate('ChooseImportMode'),
disabled: true,
},
{ key: 'move', value: translate('MoveFiles') },
{ key: 'copy', value: translate('HardlinkCopyFiles') },
{
key: 'move',
value: () => translate('MoveFiles'),
},
{
key: 'copy',
value: () => translate('HardlinkCopyFiles'),
},
];
function isSameMovieFile(
@@ -236,10 +243,23 @@ function InteractiveImportModalContent(
useState<string | null>(null);
const [selectState, setSelectState] = useSelectState();
const [bulkSelectOptions, setBulkSelectOptions] = useState([
{ key: 'select', value: translate('SelectDotDot'), disabled: true },
{ key: 'quality', value: translate('SelectQuality') },
{ key: 'releaseGroup', value: translate('SelectReleaseGroup') },
{ key: 'language', value: translate('SelectLanguage') },
{
key: 'select',
value: translate('SelectDropdown'),
disabled: true,
},
{
key: 'quality',
value: translate('SelectQuality'),
},
{
key: 'releaseGroup',
value: translate('SelectReleaseGroup'),
},
{
key: 'language',
value: translate('SelectLanguage'),
},
]);
const { allSelected, allUnselected, selectedState } = selectState;
const previousIsDeleting = usePrevious(isDeleting);
@@ -384,7 +404,9 @@ function InteractiveImportModalContent(
const files: InteractiveImportCommandOptions[] = [];
if (finalImportMode === 'chooseImportMode') {
setInteractiveImportErrorMessage('An import mode must be selected');
setInteractiveImportErrorMessage(
translate('InteractiveImportNoImportMode')
);
return;
}
@@ -397,21 +419,21 @@ function InteractiveImportModalContent(
if (!movie) {
setInteractiveImportErrorMessage(
translate('InteractiveImportErrMovie')
translate('InteractiveImportNoMovie')
);
return;
}
if (!quality) {
setInteractiveImportErrorMessage(
translate('InteractiveImportErrQuality')
translate('InteractiveImportNoQuality')
);
return;
}
if (!languages) {
setInteractiveImportErrorMessage(
translate('InteractiveImportErrLanguage')
translate('InteractiveImportNoLanguage')
);
return;
}
@@ -599,7 +621,7 @@ function InteractiveImportModalContent(
const errorMessage = getErrorMessage(
error,
translate('UnableToLoadManualImportItems')
translate('InteractiveImportLoadError')
);
return (
@@ -679,7 +701,7 @@ function InteractiveImportModalContent(
) : null}
{isPopulated && !items.length && !isFetching
? translate('NoVideoFilesFoundSelectedFolder')
? translate('InteractiveImportNoFilesFound')
: null}
</ModalBody>
@@ -775,8 +797,8 @@ function InteractiveImportModalContent(
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelectedMovieFiles')}
message={translate('DeleteSelectedMovieFilesMessage')}
confirmLabel="Delete"
message={translate('DeleteSelectedMovieFilesHelpText')}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onConfirmDeleteModalClose}
/>

View File

@@ -25,6 +25,7 @@ import {
import { SelectStateInputProps } from 'typings/props';
import Rejection from 'typings/Rejection';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
@@ -45,6 +46,7 @@ interface InteractiveImportRowProps {
languages?: Language[];
size: number;
customFormats?: object[];
customFormatScore?: number;
rejections: Rejection[];
columns: Column[];
movieFileId?: number;
@@ -66,6 +68,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
releaseGroup,
size,
customFormats,
customFormatScore,
rejections,
isSelected,
modalTitle,
@@ -293,8 +296,11 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
<TableRowCell>
{customFormats?.length ? (
<Popover
anchor={<Icon name={icons.INTERACTIVE} />}
title={translate('Formats')}
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
title={translate('CustomFormats')}
body={
<div className={styles.customFormatTooltip}>
<MovieFormats formats={customFormats} />

View File

@@ -80,16 +80,14 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {translate('SelectLanguage')}
{translate('SelectLanguageModalTitle', { modalTitle })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadLanguages')}
</Alert>
<Alert kind={kinds.DANGER}>{translate('LanguagesLoadError')}</Alert>
) : null}
{isPopulated && !error ? (

View File

@@ -29,22 +29,22 @@ import styles from './SelectMovieModalContent.css';
const columns = [
{
name: 'title',
label: translate('Title'),
label: () => translate('Title'),
isVisible: true,
},
{
name: 'year',
label: translate('Year'),
label: () => translate('Year'),
isVisible: true,
},
{
name: 'imdbId',
label: translate('ImdbId'),
label: () => translate('IMDbId'),
isVisible: true,
},
{
name: 'tmdbId',
label: translate('TmdbId'),
label: () => translate('TMDBId'),
isVisible: true,
},
];
@@ -166,8 +166,11 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) {
a.sortTitle.localeCompare(b.sortTitle)
);
return sorted.filter((item) =>
item.title.toLowerCase().includes(filter.toLowerCase())
return sorted.filter(
(item) =>
item.title.toLowerCase().includes(filter.toLowerCase()) ||
item.tmdbId.toString().includes(filter) ||
item.imdbId?.includes(filter)
);
}, [allMovies, filter]);

View File

@@ -30,7 +30,7 @@ function SelectMovieModalTableHeader(props: SelectMovieModalTableHeaderProps) {
}
name={name}
>
{label}
{typeof label === 'function' ? label() : label}
</VirtualTableHeaderCell>
);
})}

View File

@@ -28,7 +28,11 @@ class SelectMovieRow extends Component {
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.imdbId}>
<Label>{this.props.imdbId}</Label>
{
this.props.imdbId ?
<Label>{this.props.imdbId}</Label> :
null
}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.tmdbId}>
@@ -43,7 +47,7 @@ SelectMovieRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
tmdbId: PropTypes.number.isRequired,
imdbId: PropTypes.string.isRequired,
imdbId: PropTypes.string,
year: PropTypes.number.isRequired,
onMovieSelect: PropTypes.func.isRequired
};

View File

@@ -131,9 +131,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
{isFetching && <LoadingIndicator />}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadQualities')}
</Alert>
<Alert kind={kinds.DANGER}>{translate('QualitiesLoadError')}</Alert>
) : null}
{isPopulated && !error ? (

View File

@@ -39,7 +39,7 @@ function SelectReleaseGroupModalContent(
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {translate('SetReleaseGroup')}
{translate('SetReleaseGroupModalTitle', { modalTitle })}
</ModalHeader>
<ModalBody
@@ -62,7 +62,7 @@ function SelectReleaseGroupModalContent(
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.SUCCESS} onPress={onReleaseGroupSelectWrapper}>
{translate('SetReleaseGroup')}

View File

@@ -1,11 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import React, { Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearchContent.css';
@@ -13,13 +14,13 @@ import styles from './InteractiveSearchContent.css';
const columns = [
{
name: 'protocol',
label: translate('Source'),
label: () => translate('Source'),
isSortable: true,
isVisible: true
},
{
name: 'age',
label: translate('Age'),
label: () => translate('Age'),
isSortable: true,
isVisible: true
},
@@ -32,72 +33,81 @@ const columns = [
},
{
name: 'rejections',
label: React.createElement(Icon, { name: icons.DANGER }),
columnLabel: () => translate('Rejections'),
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'title',
label: translate('Title'),
label: () => translate('Title'),
isSortable: true,
isVisible: true
},
{
name: 'indexer',
label: translate('Indexer'),
label: () => translate('Indexer'),
isSortable: true,
isVisible: true
},
{
name: 'history',
label: translate('History'),
label: () => translate('History'),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'size',
label: translate('Size'),
label: () => translate('Size'),
isSortable: true,
isVisible: true
},
{
name: 'peers',
label: translate('Peers'),
label: () => translate('Peers'),
isSortable: true,
isVisible: true
},
{
name: 'languages',
label: translate('Language'),
label: () => translate('Language'),
isSortable: true,
isVisible: true
},
{
name: 'qualityWeight',
label: translate('Quality'),
label: () => translate('Quality'),
isSortable: true,
isVisible: true
},
{
name: 'customFormat',
label: translate('Formats'),
label: () => translate('Formats'),
isSortable: true,
isVisible: true
},
{
name: 'customFormatScore',
columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, {
name: icons.SCORE,
title: translate('CustomFormatScore')
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'indexerFlags',
label: React.createElement(Icon, { name: icons.FLAG }),
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
}
@@ -119,36 +129,46 @@ function InteractiveSearchContent(props) {
onGrabPress
} = props;
const errorMessage = getErrorMessage(error);
return (
<div>
{
isFetching &&
<LoadingIndicator />
isFetching ? <LoadingIndicator /> : null
}
{
!isFetching && !!error &&
!isFetching && error ?
<Alert kind={kinds.DANGER} className={styles.alert}>
{translate('UnableToLoadResultsIntSearch')}
</Alert>
{
errorMessage ?
<Fragment>
{translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
</Fragment> :
translate('MovieSearchResultsLoadError')
}
</Alert> :
null
}
{
!isFetching && isPopulated && !totalReleasesCount &&
!isFetching && isPopulated && !totalReleasesCount ?
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('NoResultsFound')}
</Alert>
</Alert> :
null
}
{
!!totalReleasesCount && isPopulated && !items.length &&
!!totalReleasesCount && isPopulated && !items.length ?
<Alert kind={kinds.WARNING} className={styles.alert}>
{translate('AllResultsHiddenFilter')}
</Alert>
</Alert> :
null
}
{
isPopulated && !!items.length &&
isPopulated && !!items.length ?
<Table
columns={columns}
sortKey={sortKey}
@@ -171,14 +191,16 @@ function InteractiveSearchContent(props) {
})
}
</TableBody>
</Table>
</Table> :
null
}
{
totalReleasesCount !== items.length && !!items.length &&
totalReleasesCount !== items.length && !!items.length ?
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('SomeResultsHiddenFilter')}
</Alert>
</Alert> :
null
}
</div>
);

View File

@@ -16,11 +16,11 @@
.quality,
.customFormat,
.language {
.languages {
composes: cell;
}
.language {
.languages {
width: 100px;
}
@@ -33,8 +33,7 @@
}
.rejected,
.indexerFlags,
.download {
.indexerFlags {
composes: cell;
width: 50px;
@@ -70,3 +69,39 @@
.blocklist {
margin-left: 5px;
}
.download {
composes: cell;
width: 80px;
}
.manualDownloadContent {
position: relative;
display: inline-block;
margin: 0 2px;
width: 22px;
height: 20.39px;
vertical-align: middle;
line-height: 20.39px;
&:hover {
color: var(--iconButtonHoverColor);
}
}
.interactiveIcon {
position: absolute;
top: 4px;
left: 0;
/* width: 100%; */
text-align: center;
}
.downloadIcon {
position: absolute;
top: 7px;
left: 8px;
/* width: 100%; */
text-align: center;
}

View File

@@ -7,10 +7,13 @@ interface CssExports {
'customFormat': string;
'customFormatScore': string;
'download': string;
'downloadIcon': string;
'history': string;
'indexer': string;
'indexerFlags': string;
'language': string;
'interactiveIcon': string;
'languages': string;
'manualDownloadContent': string;
'peers': string;
'protocol': string;
'quality': string;

View File

@@ -1,353 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import Peers from './Peers';
import styles from './InteractiveSearchRow.css';
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddedToDownloadQueue');
} else if (grabError) {
return grabError;
}
return translate('AddToDownloadQueue');
}
class InteractiveSearchRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isConfirmGrabModalOpen: false
};
}
//
// Listeners
onGrabPress = () => {
const {
guid,
indexerId,
onGrabPress
} = this.props;
onGrabPress({
guid,
indexerId
});
};
onConfirmGrabPress = () => {
this.setState({ isConfirmGrabModalOpen: true });
};
onGrabConfirm = () => {
this.setState({ isConfirmGrabModalOpen: false });
const {
guid,
indexerId,
searchPayload,
onGrabPress
} = this.props;
onGrabPress({
guid,
indexerId,
...searchPayload
});
};
onGrabCancel = () => {
this.setState({ isConfirmGrabModalOpen: false });
};
//
// Render
render() {
const {
protocol,
age,
ageHours,
ageMinutes,
publishDate,
title,
infoUrl,
indexer,
size,
seeders,
leechers,
quality,
customFormats,
customFormatScore,
languages,
indexerFlags,
rejections,
downloadAllowed,
isGrabbing,
isGrabbed,
longDateFormat,
timeFormat,
grabError,
historyGrabbedData,
historyFailedData,
blocklistData
} = this.props;
return (
<TableRow>
<TableRowCell className={styles.protocol}>
<ProtocolLabel
protocol={protocol}
/>
</TableRowCell>
<TableRowCell
className={styles.age}
title={formatDateTime(publishDate, longDateFormat, timeFormat, { includeSeconds: true })}
>
{formatAge(age, ageHours, ageMinutes)}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
/>
</TableRowCell>
<TableRowCell className={styles.rejected}>
{
!!rejections.length &&
<Popover
anchor={
<Icon
name={icons.DANGER}
kind={kinds.DANGER}
/>
}
title={translate('ReleaseRejected')}
body={
<ul>
{
rejections.map((rejection, index) => {
return (
<li key={index}>
{rejection}
</li>
);
})
}
</ul>
}
position={tooltipPositions.BOTTOM}
/>
}
</TableRowCell>
<TableRowCell className={styles.title}>
<Link
to={infoUrl}
title={title}
>
<div>
{title}
</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.indexer}>
{indexer}
</TableRowCell>
<TableRowCell className={styles.history}>
{
historyGrabbedData?.date && !historyFailedData?.date &&
<Icon
name={icons.DOWNLOADING}
kind={kinds.DEFAULT}
title={`${translate('Grabbed')}: ${formatDateTime(historyGrabbedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
/>
}
{
historyFailedData?.date &&
<Icon
className={styles.failed}
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={`${translate('Failed')}: ${formatDateTime(historyFailedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
/>
}
{
blocklistData?.date &&
<Icon
className={historyGrabbedData || historyFailedData ? styles.blocklist : ''}
name={icons.BLOCKLIST}
kind={kinds.DANGER}
title={`${translate('Blocklisted')}: ${formatDateTime(blocklistData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
/>
}
</TableRowCell>
<TableRowCell className={styles.size}>
{formatBytes(size)}
</TableRowCell>
<TableRowCell className={styles.peers}>
{
protocol === 'torrent' &&
<Peers
seeders={seeders}
leechers={leechers}
/>
}
</TableRowCell>
<TableRowCell className={styles.language}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
<TableRowCell className={styles.quality}>
<MovieQuality
quality={quality}
/>
</TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
{customFormatScore > 0 && `+${customFormatScore}`}
{customFormatScore < 0 && customFormatScore}
</TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{
!!indexerFlags.length &&
<Popover
anchor={
<Icon
name={icons.FLAG}
kind={kinds.PRIMARY}
/>
}
title={translate('IndexerFlags')}
body={
<ul>
{
indexerFlags.map((flag, index) => {
return (
<li key={index}>
{flag}
</li>
);
})
}
</ul>
}
position={tooltipPositions.BOTTOM}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={this.state.isConfirmGrabModalOpen}
kind={kinds.WARNING}
title={translate('GrabRelease')}
message={translate('GrabReleaseMessageText', [title])}
confirmLabel={translate('Grab')}
onConfirm={this.onGrabConfirm}
onCancel={this.onGrabCancel}
/>
</TableRow>
);
}
}
InteractiveSearchRow.propTypes = {
guid: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
ageHours: PropTypes.number.isRequired,
ageMinutes: PropTypes.number.isRequired,
publishDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
infoUrl: PropTypes.string.isRequired,
indexerId: PropTypes.number.isRequired,
indexer: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
seeders: PropTypes.number,
leechers: PropTypes.number,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
customFormatScore: PropTypes.number.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
downloadAllowed: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
searchPayload: PropTypes.object.isRequired,
onGrabPress: PropTypes.func.isRequired,
historyFailedData: PropTypes.object,
historyGrabbedData: PropTypes.object,
blocklistData: PropTypes.object
};
InteractiveSearchRow.defaultProps = {
rejections: [],
isGrabbing: false,
isGrabbed: false
};
export default InteractiveSearchRow;

View File

@@ -0,0 +1,376 @@
import React, { useCallback, useState } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import MovieBlocklist from 'typings/MovieBlocklist';
import MovieHistory from 'typings/MovieHistory';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
import Peers from './Peers';
import styles from './InteractiveSearchRow.css';
function getDownloadIcon(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddToDownloadQueue');
} else if (grabError) {
return grabError;
}
return translate('AddedToDownloadQueue');
}
interface InteractiveSearchRowProps {
guid: string;
protocol: DownloadProtocol;
age: number;
ageHours: number;
ageMinutes: number;
publishDate: string;
title: string;
infoUrl: string;
indexerId: number;
indexer: string;
size: number;
seeders?: number;
leechers?: number;
quality: QualityModel;
languages: Language[];
customFormats: CustomFormat[];
customFormatScore: number;
mappedMovieId?: number;
rejections: string[];
indexerFlags: string[];
downloadAllowed: boolean;
isGrabbing: boolean;
isGrabbed: boolean;
grabError?: string;
historyFailedData?: MovieHistory;
historyGrabbedData?: MovieHistory;
blocklistData?: MovieBlocklist;
longDateFormat: string;
timeFormat: string;
searchPayload: object;
onGrabPress(...args: unknown[]): void;
}
function InteractiveSearchRow(props: InteractiveSearchRowProps) {
const {
guid,
indexerId,
protocol,
age,
ageHours,
ageMinutes,
publishDate,
title,
infoUrl,
indexer,
size,
seeders,
leechers,
quality,
languages,
customFormatScore,
customFormats,
mappedMovieId,
rejections = [],
indexerFlags = [],
downloadAllowed,
isGrabbing = false,
isGrabbed = false,
longDateFormat,
timeFormat,
grabError,
historyGrabbedData,
historyFailedData,
blocklistData,
searchPayload,
onGrabPress,
} = props;
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
const onGrabPressWrapper = useCallback(() => {
if (downloadAllowed) {
onGrabPress({
guid,
indexerId,
});
return;
}
setIsConfirmGrabModalOpen(true);
}, [
guid,
indexerId,
downloadAllowed,
onGrabPress,
setIsConfirmGrabModalOpen,
]);
const onGrabConfirm = useCallback(() => {
setIsConfirmGrabModalOpen(false);
onGrabPress({
guid,
indexerId,
...searchPayload,
});
}, [guid, indexerId, searchPayload, onGrabPress, setIsConfirmGrabModalOpen]);
const onGrabCancel = useCallback(() => {
setIsConfirmGrabModalOpen(false);
}, [setIsConfirmGrabModalOpen]);
const onOverridePress = useCallback(() => {
setIsOverrideModalOpen(true);
}, [setIsOverrideModalOpen]);
const onOverrideModalClose = useCallback(() => {
setIsOverrideModalOpen(false);
}, [setIsOverrideModalOpen]);
return (
<TableRow>
<TableRowCell className={styles.protocol}>
<ProtocolLabel protocol={protocol} />
</TableRowCell>
<TableRowCell
className={styles.age}
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
includeSeconds: true,
})}
>
{formatAge(age, ageHours, ageMinutes)}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.RIGHT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.title}>
<Link to={infoUrl} title={title}>
<div>{title}</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.indexer}>{indexer}</TableRowCell>
<TableRowCell className={styles.history}>
{historyGrabbedData?.date && !historyFailedData?.date ? (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DEFAULT}
title={`${translate('Grabbed')}: ${formatDateTime(
historyGrabbedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
/>
) : null}
{historyFailedData?.date ? (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={`${translate('Failed')}: ${formatDateTime(
historyFailedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
/>
) : null}
{blocklistData?.date ? (
<Icon
className={
historyGrabbedData || historyFailedData ? styles.blocklist : ''
}
name={icons.BLOCKLIST}
kind={kinds.DANGER}
title={`${translate('Blocklisted')}: ${formatDateTime(
blocklistData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.size}>{formatBytes(size)}</TableRowCell>
<TableRowCell className={styles.peers}>
{protocol === 'torrent' ? (
<Peers seeders={seeders} leechers={leechers} />
) : null}
</TableRowCell>
<TableRowCell className={styles.languages}>
<MovieLanguage languages={languages} />
</TableRowCell>
<TableRowCell className={styles.quality}>
<MovieQuality quality={quality} />
</TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
/>
</TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{indexerFlags.length ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={
<ul>
{indexerFlags.map((flag, index) => {
return <li key={index}>{flag}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<ConfirmModal
isOpen={isConfirmGrabModalOpen}
kind={kinds.WARNING}
title={translate('GrabRelease')}
message={translate('GrabReleaseMessageText', { title })}
confirmLabel={translate('Grab')}
onConfirm={onGrabConfirm}
onCancel={onGrabCancel}
/>
<OverrideMatchModal
isOpen={isOverrideModalOpen}
title={title}
indexerId={indexerId}
guid={guid}
movieId={mappedMovieId}
languages={languages}
quality={quality}
protocol={protocol}
isGrabbing={isGrabbing}
grabError={grabError}
onModalClose={onOverrideModalClose}
/>
</TableRow>
);
}
export default InteractiveSearchRow;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { sizes } from 'Helpers/Props';
import SelectDownloadClientModalContent from './SelectDownloadClientModalContent';
interface SelectDownloadClientModalProps {
isOpen: boolean;
protocol: DownloadProtocol;
modalTitle: string;
onDownloadClientSelect(downloadClientId: number): void;
onModalClose(): void;
}
function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } =
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
<SelectDownloadClientModalContent
protocol={protocol}
modalTitle={modalTitle}
onDownloadClientSelect={onDownloadClientSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectDownloadClientModal;

View File

@@ -0,0 +1,74 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { kinds } from 'Helpers/Props';
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
import translate from 'Utilities/String/translate';
import SelectDownloadClientRow from './SelectDownloadClientRow';
interface SelectDownloadClientModalContentProps {
protocol: DownloadProtocol;
modalTitle: string;
onDownloadClientSelect(downloadClientId: number): void;
onModalClose(): void;
}
function SelectDownloadClientModalContent(
props: SelectDownloadClientModalContentProps
) {
const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props;
const { isFetching, isPopulated, error, items } = useSelector(
createEnabledDownloadClientsSelector(protocol)
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('SelectDownloadClientModalTitle', { modalTitle })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('DownloadClientsLoadError')}
</Alert>
) : null}
{isPopulated && !error ? (
<Form>
{items.map((downloadClient) => {
const { id, name, priority } = downloadClient;
return (
<SelectDownloadClientRow
key={id}
id={id}
name={name}
priority={priority}
onDownloadClientSelect={onDownloadClientSelect}
/>
);
})}
</Form>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectDownloadClientModalContent;

View File

@@ -0,0 +1,6 @@
.downloadClient {
display: flex;
justify-content: space-between;
padding: 8px;
border-bottom: 1px solid var(--borderColor);
}

View File

@@ -1,8 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'enabled': string;
'restriction': string;
'downloadClient': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import Link from 'Components/Link/Link';
import translate from 'Utilities/String/translate';
import styles from './SelectDownloadClientRow.css';
interface SelectSeasonRowProps {
id: number;
name: string;
priority: number;
onDownloadClientSelect(downloadClientId: number): unknown;
}
function SelectDownloadClientRow(props: SelectSeasonRowProps) {
const { id, name, priority, onDownloadClientSelect } = props;
const onSeasonSelectWrapper = useCallback(() => {
onDownloadClientSelect(id);
}, [id, onDownloadClientSelect]);
return (
<Link
className={styles.downloadClient}
component="div"
onPress={onSeasonSelectWrapper}
>
<div>{name}</div>
<div>{translate('PrioritySettings', { priority })}</div>
</Link>
);
}
export default SelectDownloadClientRow;

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