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

Compare commits

...

109 Commits

Author SHA1 Message Date
Mark McDowall 6f23c465ee Don't send session information to Sentry
(cherry picked from commit fae24e98fb9230c2f3701caef457332952c6723f)
2024-12-28 03:32:11 +02:00
Bogdan af60cca9ae Fixed: Advanced settings for Metadata consumers 2024-12-23 12:15:04 +02:00
Mark McDowall d34d23a052 Fixed: Movies updated during Import List Sync not reflected in the UI
(cherry picked from commit 1c30ecd66dd0fd1dafaf9ab0e41a11a54eaac132)

Closes #10794
2024-12-23 12:08:25 +02:00
Bogdan 0a0da42543 Bump version to 5.17.1 2024-12-22 13:23:52 +02:00
Bogdan e5419f6f06 Bump System.Memory
Closes #10791
2024-12-21 11:20:28 +02:00
Bogdan 88d9c08f1a Bump MailKit to 4.8.0 and Microsoft.Data.SqlClient to 2.1.7
Closes #10790
2024-12-21 11:15:14 +02:00
Bogdan 6b4259757c Add test for do not prefer repacks/propers 2024-12-20 20:33:18 +02:00
Mark McDowall f1d7c56d94 Fixed: Custom Format score bypassing upgrades not being allowed
(cherry picked from commit ebe23104d4b29a3c900a982fb84e75c27ed531ab)

Co-authored-by: CeruleanRed <toni.suta@gmail.com>
2024-12-20 20:33:18 +02:00
Mark McDowall c81b2e80ee Convert MediaInfo to TypeScript
(cherry picked from commit 4e4bf3507f20c0f8581c66804f8ef406c41952d8)

Closes #10753
2024-12-20 15:37:16 +02:00
Mark McDowall 5efefd804b Upgrade @typescript-eslint packages to 8.181.1
(cherry picked from commit ed10b63fa0c161cac7e0a2084e53785ab1798208)
2024-12-17 13:15:06 +02:00
Mark McDowall 38f9543526 Upgrade Font Awesome to 6.7.1
(cherry picked from commit 016b5718386593c030f14fcac307c93ee1ceeca6)
2024-12-17 13:11:07 +02:00
Mark McDowall aae68e681e Upgrade babel to 7.26.0
(cherry picked from commit bfcd017012730c97eb587ae2d2e91f72ee7a1de3)
2024-12-17 13:08:36 +02:00
Bogdan 1d21bbf78f Bump version to 5.17.0 2024-12-16 20:24:54 +02:00
Stevie Robinson 99c3c8ce5b Replace URLs in translations with tokens
(cherry picked from commit 98d60e1a8e9abce6b31b3cdd745eff0fed181458)
2024-12-16 15:40:58 +02:00
Weblate 85171e40a5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-12-16 15:28:28 +02:00
Bogdan 86b656d323 Use minor version for core-js in babel/preset-env 2024-12-16 12:40:37 +02:00
Bogdan 5ae5d1043a Improve opening add movie modal for Discover Overview 2024-12-16 01:25:03 +02:00
Mark McDowall b801aa0935 New: Add metadata links to telegram messages
Co-authored-by: Ivar Stangeby <istangeby@gmail.com>

Fixed errors sending Telegram notifications when links aren't available

(cherry picked from commit 4eab168267db716a9e897a992e3a7f6889571f9f)
(cherry picked from commit 4d7a3d0909437268b4ad0a0dbeb59d45b4435118)

Closes #10242
Closes #10489
2024-12-15 15:20:16 +02:00
Stevie Robinson b2b5aa1f79 New: Optionally as Instance Name to Telegram notifications
(cherry picked from commit 36633b5d08c19158f185c0fa5faabbaec607fcb5)

Closes #10757
2024-12-15 14:43:23 +02:00
Mark McDowall 8c6ba9a543 Fixed: Augmenting languages from indexer for release with stale indexer ID
(cherry picked from commit cb7489ce8fe933920ea04297bd2941496a0c07c6)

Closes #10768
2024-12-15 14:43:23 +02:00
Mark McDowall 4e024c51d3 Fixed: Movies without tags bypassing tags on Download Client
(cherry picked from commit c0e264cfc520ee387bfc882c95a5822c655e0d9b)

Closes #10765
2024-12-15 14:43:23 +02:00
Mark McDowall e4106f0ede Upgrade TypeScript and core-js
(cherry picked from commit 148480909917f69ff3b2ca547ccb4716dd56606e)

Closes #10763
2024-12-15 14:43:20 +02:00
Bogdan 9032ac20ff Bump version to 5.16.3 2024-12-15 10:04:38 +02:00
Bogdan 23fce4bf2e Fixed: Refresh backup list on deletion
(cherry picked from commit 3b00112447361b19c04851a510e63f812597a043)
2024-12-15 05:29:12 +02:00
Mark McDowall 64fd8552f8 Fixed: Error getting processes in some cases
(cherry picked from commit b552d4e9f7ca7388404aa0d52566010a54cb0244)
2024-12-15 05:28:39 +02:00
Weblate e016410c10 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Rodion <rodyon009@gmail.com>
Co-authored-by: Tomer Horowitz <tomerh2001@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: farebyting <farelbyting@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: hhjuhl <hans@kopula.dk>
Co-authored-by: kaisernet <afimark7@gmail.com>
Co-authored-by: keysuck <joshkkim@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-12-14 02:33:47 +02:00
Bogdan bea943adf8 New: Tooltip with extra genres on search and collections 2024-12-13 19:17:59 +02:00
Bogdan 9780d20f8a Improve is visible property check for discover movies 2024-12-13 19:14:41 +02:00
Bogdan 62722d45b0 Fixed: Using all movie genres for collection filters 2024-12-13 19:13:55 +02:00
Bogdan 27dd8e8cd5 New: Tooltip with extra genres on movie details page 2024-12-12 21:59:19 +02:00
Bogdan 6c47ede76b Fixed: Refreshing movie genres 2024-12-12 21:58:54 +02:00
Mark McDowall 7b9562bb38 Update React
(cherry picked from commit 4491df3ae7530f2167beebc3548dd01fd2cc1a12)

Towards #10703
2024-12-12 21:01:19 +02:00
Stevie Robinson 8b0b7c1cb0 New: Reactive search button on Wanted pages
(cherry picked from commit e8c3aa20bd92701a16dcd97c5e103b79b3683105)

Closes #10750
2024-12-12 20:42:55 +02:00
Mark McDowall 7ebd341cd6 Sync TimeSpanConverter with upstream
(cherry picked from commit 1374240321f08d1400faf95e84217e4b7a2d116b)

Closes #10756
2024-12-09 14:03:12 +02:00
Bogdan 6c85f166ff Bump version to 5.16.2 2024-12-08 11:22:50 +02:00
Servarr 45aabce107 Automated API Docs update 2024-12-04 14:09:02 +02:00
soup 0caa793df4 New: Add config file setting for CGNAT authentication bypass
(cherry picked from commit 4c41a4f368046f73f82306bbd73bec992392938b)
2024-12-04 13:30:56 +02:00
Stevie Robinson 9a107cc8d7 New: Add Languages to Webhook Notifications
(cherry picked from commit e039dc45e267cf717e00c0a49ba637012f37e3d7)

Closes #10733
2024-12-02 16:59:45 +02:00
Mark McDowall a6d727fe2a New: Kometa metadata file creation disabled
(cherry picked from commit c62fc9d05bb9e1fe51b454d78e80bd9250e31f89)

Closes #10738
2024-12-02 16:56:12 +02:00
Servarr 01a53d3624 Automated API Docs update 2024-12-02 16:03:25 +02:00
Weblate 348c29c9d7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Robin Dadswell <robin@robindadswell.tech>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mryx007 <mryx@mail.de>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translation: Servarr/Radarr
2024-12-02 15:34:43 +02:00
Bogdan 64739712c6 Add return type for movie lookup and import endpoints
Closes #10737
2024-12-02 15:29:50 +02:00
Bogdan 6ac9cca953 Avoid default category on existing Transmission configurations
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
(cherry picked from commit bd656ae7f66fc9224ef2a57857152ee5d54d54f8)
2024-12-02 15:12:07 +02:00
Bogdan a2b38c5b7d New: Labels support for Transmission 4.0
(cherry picked from commit 675e3cd38a14ea33c27f2d66a4be2bf802e17d88)
2024-12-02 15:12:07 +02:00
Bogdan 3cc4105d71 Bump NLog, Npgsql, Ical.Net, IPAddressRange, ImageSharp and Polly 2024-12-02 14:30:38 +02:00
Mark McDowall 3449a5d3fe Fixed: Don't fail import if symlink target can't be resolved
(cherry picked from commit 8cb58a63d8ec1b290bc57ad2cf1e90809ceebce9)
2024-12-02 02:32:21 +02:00
Gylesie 5bac157d36 Remove unnecessary heap allocations in local IP check
(cherry picked from commit ed536a85ad5f2062bf6f01f80efddb19fa935f63)
2024-12-02 02:31:28 +02:00
Mark McDowall 114d260f42 Deprecate Sizeleft and Timeleft queue item properties
Rename SizeLeft and TimeLeft queue item properties

(cherry picked from commit b51a49097941e5f306cae5785c63985b319784fd)

Fixed: Error loading queue

(cherry picked from commit f9606518eef78117f1e06a8bcc34af57ab0d2454)
2024-12-01 19:25:45 +02:00
Bogdan 617b9c5d35 Console warnings for missing translations on development builds
(cherry picked from commit 67a1ecb0fea4e6c7dfdb68fbe3ef30d4c22398d8)

Closes #10669
2024-12-01 18:54:53 +02:00
Mark McDowall ba4ccbb0bd Deluge communication improvements
(cherry picked from commit 183b8b574a4dd948b5fada94d0e645b87710f223)
2024-12-01 14:01:41 +02:00
Mark McDowall b845268b3d New: Support for new SABnzbd history retention values
Closes #10699

(cherry picked from commit e361f18837d98c089f7dc9c0190221ca8e2cf225)
2024-12-01 14:01:09 +02:00
Bogdan 0fee552074 Bump version to 5.16.1 2024-12-01 14:00:15 +02:00
Mark McDowall 828b994ef4 Support Postgres with non-standard version string
(cherry picked from commit 40f4ef27b22113c1dae0d0cbdee8205132bed68a)
2024-12-01 11:47:19 +00:00
Weblate 7952fd325b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albrt9527 <2563009889@qq.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_HANS/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-11-30 02:30:22 +02:00
Barry Mieny 4b4e598b67 New: Add Afrikaans language 2024-11-30 02:29:04 +02:00
Bogdan 71ccebd0f5 Fix cutoff fixture 2024-11-30 02:12:26 +02:00
Mark McDowall 2607c67912 Fixed: Prevent lack of internet from stopping all health checks from running
(cherry picked from commit dba3a8243988d3e9870b841696303191e1703a0d)

Closes #10694
2024-11-30 02:05:47 +02:00
Bogdan a626b4f3c4 Fixed: Custom Format upgrading not respecting 'Upgrades Allowed'
(cherry picked from commit 91c5e6f12292e522ceb9094825525fb3684b97c6)

Closes #10691
2024-11-30 02:01:46 +02:00
Bogdan 1526bf29f4 Fixed path in downloading to root folder check message 2024-11-30 01:59:16 +02:00
bpoxy 2194772736 New: Add Albanian language (#10663) 2024-11-28 01:00:33 +02:00
Gauthier cd490d6334 New: Add headers setting in webhook connection
(cherry picked from commit 78fb20282de73c0ea47375895a807235385d90e3)
2024-11-28 00:58:27 +02:00
bakerboy448 ff609848d8 New: Replace 'Ben the Man' release group parsing with 'Ben the Men'
Closes #10676

(cherry picked from commit 202190d032257b3cd19e42606385db7052b2aae4)
2024-11-27 15:59:51 -06:00
Weblate 15b6f7212d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mizuyoru_TW <mizuyoru.tw@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: hebian <zyf200913@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-11-27 14:57:43 -06:00
Mark McDowall af06a9f70d Webpack web target
(cherry picked from commit a90866a73e6cff9a286c23e60c74672f4c0d317a)
2024-11-27 11:12:43 +02:00
Servarr c3fa440cf8 Multiple Translations updated by Weblate (#10688)
ignore-downstream


Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
2024-11-19 20:37:07 -06:00
Bogdan 0411d66520 Bump version to 5.16.0 2024-11-19 03:10:17 +02:00
Bogdan 179637fe8b Fixed: Release dates for Discover Movie posters 2024-11-19 00:40:44 +02:00
Weblate 09b4bf15cf Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2024-11-17 14:00:33 +02:00
Mark McDowall ea86d14ca7 Fixed: Normalize unicode characters when comparing paths for equality
(cherry picked from commit ceeec091f85d0094e07537b7f62f18292655a710)
2024-11-17 11:28:51 +02:00
Elias Benbourenane 2429dd91c6 Allow GetFileSize to follow symlinks
(cherry picked from commit ca0bb14027f3409014e7cf9ffa8e04e577001d77)
2024-11-17 11:28:35 +02:00
Mark McDowall a752476cdb Fixed: Allow files to be moved from Torrent Blackhole even when remove is disabled
(cherry picked from commit f739fd0900695e2ff312d13985c87d84ae00ea75)
2024-11-17 11:27:35 +02:00
Bogdan 50ce480abf Pin ReportGenerator in Azure Pipelines for .NET 6 2024-11-15 22:18:40 +02:00
Weblate 0ef6e56e5d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-11-07 17:04:54 +02:00
Bogdan 12d5014125 New: Track Kometa metadata files
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>

Fixes #10059
Fixes #10419
Closes #10311
2024-11-05 14:31:08 +02:00
Bogdan c8301d425c Fix translation token for Mount Health Check 2024-11-04 19:26:46 +02:00
Servarr b1df9b2401 Automated API Docs update 2024-11-04 15:41:58 +02:00
Mark McDowall ff09da3a69 New: Filter queue by status
(cherry picked from commit fb540040ef66e90c55b82539b85df378d6c76bd3)

Closes #10648
2024-11-04 15:05:57 +02:00
Mark McDowall 3b9bd696fb New: Favorite folders in Manual Import
(cherry picked from commit 3ddc6ac6de5c27a9aab915672321c8818dc5da48)

Closes #10630
2024-11-04 14:36:38 +02:00
Weblate 9ab3e6bab7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lars <lars.erik.heloe@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mytelegrambot <lacsonluxur@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-11-04 13:26:17 +02:00
Mark McDowall 86f4f86a0a Fixed: Filtering queue by multiple qualities
(cherry picked from commit b8af3af9f16db96337832c2989f4e7ff3dc2ed30)

Closes #10647
2024-11-04 13:12:31 +02:00
Bogdan 40d95a04e3 Sync coding style with upstream for join methods
Towards #10637
2024-11-04 13:01:40 +02:00
Mark McDowall ca724836ce Rename Manage Custom Formats to Manage Formats
(cherry picked from commit 0f225b05c00add562c9a6aa8cc4cf494e83176c1)

Closes #10629
2024-11-04 12:57:55 +02:00
Mark McDowall 10e3964111 New: Use instance name in PWA manifest
(cherry picked from commit 1fcfb88d2aa0126c4b3c878c8e310311ea57d04d)

Closes #10625
2024-11-04 12:54:59 +02:00
Mark McDowall b22a86e1d7 New: Include source path with Webhook import event movie file
(cherry picked from commit 73208e2f60263b1236f094a2bf6c47ebd5a8a271)

Closes #10635
2024-11-04 12:53:12 +02:00
Mark McDowall 5976d66511 New: Reject files during import that have no audio tracks
(cherry picked from commit 978349e24135572889095c743d0e7fac734ba7e0)

Closes #10643
2024-11-04 12:51:14 +02:00
Bogdan b4eff4d4f9 Show a movie path as example in Mount Health Check
Closes #10649
2024-11-04 12:38:48 +02:00
Mark McDowall 1414a09111 New: Add individual edit to Manage Custom Formats
(cherry picked from commit e006b405323c276eb5b7f2dd97b97c80394a6930)
2024-11-04 12:38:30 +02:00
Mark McDowall b30efd0c62 Use current time for cache break in development
(cherry picked from commit 020ed32fcfab1c6fbe57af5ea650300272c93fd7)
2024-11-04 12:37:12 +02:00
Mark McDowall def6950db4 Fixed: Use download client name for history column
(cherry picked from commit 1df0ba9e5aef2d2745a45c546c869837ac8e68db)
2024-11-04 12:36:55 +02:00
Mark McDowall f23c2dbaba Increase retries for DebouncerFixture
(cherry picked from commit 78cf13d341e6690bf6079dd1819d060d002155a7)
2024-11-04 12:36:26 +02:00
Bogdan 186e9cdd23 Bump version to 5.15.1 2024-11-03 11:41:20 +02:00
Bogdan 394f34eb2a Fixed: Root folder existence for import lists and movie collections 2024-11-02 21:59:32 +02:00
Weblate d9f508280d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Daniel <statoxxl@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: HUi <huynguyeexn@gmail.com>
Co-authored-by: Kuzmich <kuzmich55@gmail.com>
Co-authored-by: Moon55 <dylan.gurdak@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: http://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-11-02 21:12:23 +02:00
Bogdan b5505800de Fix file browser translations 2024-11-02 21:07:10 +02:00
Bogdan 48a79eb7d3 Fixed: Loading queue with pending releases for deleted movies 2024-10-31 18:33:04 +02:00
Bogdan b42f7e09f9 Fixed: Cleaning the French preposition 'à' from titles 2024-10-31 11:33:08 +02:00
Bogdan 8f507ac726 Fixed: Parse "Català" and "Catalán" as Catalan 2024-10-29 20:49:00 +02:00
Bogdan 06d54e0ec2 Update JetBrains logos
Closes #10603
2024-10-29 10:00:57 +02:00
Bogdan 3708d58847 Fixed: Custom filtering movies by year
Fixes #10610
2024-10-28 07:49:55 +02:00
Bogdan 0049ccd39f Inherit trigger from pushed command models
(cherry picked from commit 0bc4903954b955fce0c368ef7fd2a6f3761d6a93)

Closes #10592
2024-10-27 08:20:57 +02:00
Bogdan ab8a2d190e Improve message for grab errors due to no matching tags
Co-authored-by: zakary <zak@ary.dev>
(cherry picked from commit df672487cf1d5f067849367a2bfb0068defc315d)

Closes #10593
2024-10-27 08:10:38 +02:00
Hadrien Patte 25bb52b206 Use OperatingSystem class to get OS information
(cherry picked from commit 135b5c2ddd8f0a274b0d59eb07f75aaf1446b9da)
2024-10-27 08:08:01 +02:00
Bogdan 63c6f70e67 Fixed: Changing movies to another root folder without moving files 2024-10-27 08:07:44 +02:00
Bogdan 79cd6269f4 Fixed: Status check for completed directories in Deluge
(cherry picked from commit 33139d4b53c1adad769c7e2b0510e8990c66b84a)
2024-10-27 08:06:29 +02:00
Bogdan 879c872179 Cleanse exceptions in event logs
(cherry picked from commit 404e6d68ea526ab521cd39ecda1bf3b02285765d)
2024-10-27 08:06:11 +02:00
Bogdan d4993cf69b Bump version to 5.15.0 2024-10-26 17:41:27 +03:00
Bogdan 781e0c9d1c Fixed: Optional square and round brackets for "{Release Year}" 2024-10-26 15:02:46 +03:00
Bogdan c946ed83f9 Fixed: Stopped/Started as initial state for qBittorrent v5.0 2024-10-26 10:32:20 +03:00
Bogdan 9aecf94e8e Bump version to 5.14.0 2024-10-26 09:10:14 +03:00
272 changed files with 5916 additions and 3912 deletions
-33
View File
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1.3318" y1="43.7371" x2="67.0419" y2="26.0967">
<stop offset="0.1237" style="stop-color:#7866FF"/>
<stop offset="0.5376" style="stop-color:#FE2EB6"/>
<stop offset="0.8548" style="stop-color:#FD0486"/>
</linearGradient>
<polygon style="fill:url(#SVGID_1_);" points="67.3,16 43.7,0 0,31.1 11.1,70 58.9,60.3 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="45.9148" y1="38.9098" x2="67.6577" y2="9.0989">
<stop offset="0.1237" style="stop-color:#FF0080"/>
<stop offset="0.2587" style="stop-color:#FE0385"/>
<stop offset="0.4109" style="stop-color:#FA0C92"/>
<stop offset="0.5713" style="stop-color:#F41BA9"/>
<stop offset="0.7363" style="stop-color:#EB2FC8"/>
<stop offset="0.8656" style="stop-color:#E343E6"/>
</linearGradient>
<polygon style="fill:url(#SVGID_2_);" points="67.3,16 43.7,0 38,15.7 38,47.8 70,47.8 "/>
</g>
<g>
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
<rect x="17.4" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
<g>
<path style="fill:#FFFFFF;" d="M17.4,19.1h6.9c5.6,0,9.5,3.8,9.5,8.9V28c0,5-3.9,8.9-9.5,8.9h-6.9V19.1z M21.4,22.7v10.7h3
c3.2,0,5.4-2.2,5.4-5.3V28c0-3.2-2.2-5.4-5.4-5.4H21.4z"/>
<polygon style="fill:#FFFFFF;" points="40.3,22.7 34.9,22.7 34.9,19.1 49.6,19.1 49.6,22.7 44.2,22.7 44.2,37 40.3,37 "/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

-66
View File
@@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve"
>
<g>
<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24">
<stop offset="0" style="stop-color:#FCEE39"/>
<stop offset="1" style="stop-color:#F37B3D"/>
</linearGradient>
<path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9
c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0
c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/>
<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546">
<stop offset="0" style="stop-color:#EF5A6B"/>
<stop offset="0.57" style="stop-color:#F26F4E"/>
<stop offset="1" style="stop-color:#F37B3D"/>
</linearGradient>
<path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0
c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3
c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/>
<linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562">
<stop offset="0" style="stop-color:#7C59A4"/>
<stop offset="0.3852" style="stop-color:#AF4C92"/>
<stop offset="0.7654" style="stop-color:#DC4183"/>
<stop offset="0.957" style="stop-color:#ED3D7D"/>
</linearGradient>
<path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9
c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0
c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/>
<linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971">
<stop offset="0" style="stop-color:#EF5A6B"/>
<stop offset="0.364" style="stop-color:#EE4E72"/>
<stop offset="1" style="stop-color:#ED3D7D"/>
</linearGradient>
<path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0
l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0
c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/>
<g id="XMLID_3008_">
<rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/>
<rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/>
<g id="XMLID_3009_">
<path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0
l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/>
<path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0
L45.3,43.8z"/>
<path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/>
<path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0
c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5
l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/>
<path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0
c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1
l-1.5,0v2H50.6z"/>
<path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z
M58.8,59l-0.9-2.3L57,59L58.8,59z"/>
<path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/>
<path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z"
/>
<path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0
c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6
c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7
C76.1,62.5,74.7,62,73.7,61.1z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

-50
View File
@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<g>
<g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="22.9451" y1="75.7869" x2="74.7868" y2="20.6415">
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
<stop offset="0.4044" style="stop-color:#C41E57"/>
<stop offset="0.4677" style="stop-color:#C41E57"/>
<stop offset="0.6505" style="stop-color:#EB8523"/>
<stop offset="0.9516" style="stop-color:#FEBD11"/>
</linearGradient>
<polygon style="fill:url(#SVGID_1_);" points="49.8,15.2 36,36.7 58.4,70 70,23.1 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.7187" y1="73.2922" x2="69.5556" y2="18.1519">
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
<stop offset="0.4044" style="stop-color:#C41E57"/>
<stop offset="0.4677" style="stop-color:#C41E57"/>
<stop offset="0.7043" style="stop-color:#EB8523"/>
</linearGradient>
<polygon style="fill:url(#SVGID_2_);" points="51.1,15.7 49,0 18.8,33.6 27.6,42.3 20.8,70 58.4,70 "/>
</g>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1.8281" y1="53.4275" x2="48.8245" y2="9.2255">
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
<stop offset="0.6613" style="stop-color:#C41E57"/>
</linearGradient>
<polygon style="fill:url(#SVGID_3_);" points="49,0 11.6,0 0,47.1 55.6,47.1 "/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="49.8935" y1="-11.5569" x2="48.8588" y2="24.0352">
<stop offset="0.5" style="stop-color:#C41E57"/>
<stop offset="0.6668" style="stop-color:#D13F48"/>
<stop offset="0.7952" style="stop-color:#D94F39"/>
<stop offset="0.8656" style="stop-color:#DD5433"/>
</linearGradient>
<polygon style="fill:url(#SVGID_4_);" points="55.3,47.1 51.1,15.7 49,0 41.7,23 "/>
</g>
<g>
<rect x="13.4" y="13.5" transform="matrix(-1 2.577289e-003 -2.577289e-003 -1 70.0288 70.081)" style="fill:#000000;" width="43.2" height="43.2"/>
<rect x="17.6" y="48.6" transform="matrix(1 -2.577289e-003 2.577289e-003 1 -0.1287 6.634109e-002)" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
<path style="fill:#FFFFFF;" d="M17.4,19.1l8.2,0c2.3,0,4,0.6,5.2,1.8c1,1,1.5,2.4,1.5,4.1l0,0.1c0,1.5-0.3,2.6-1.1,3.5
c-0.7,0.9-1.6,1.6-2.8,2l4.4,6.4l-4.6,0l-3.7-5.5l-3.3,0l0,5.5l-3.9,0L17.4,19.1z M25.3,27.8c1,0,1.7-0.2,2.2-0.7
c0.5-0.5,0.8-1.1,0.8-1.8l0-0.1c0-0.9-0.3-1.5-0.8-1.9c-0.5-0.4-1.3-0.6-2.3-0.6l-3.9,0l0,5.1L25.3,27.8z"/>
<path style="fill:#FFFFFF;" d="M36,33.2l-1.9,0l0-3.3l2.5,0l0.6-3.8l-2.3,0l0-3.3l2.8,0l0.6-3.7l3.4,0l-0.6,3.7l3.7,0l0.6-3.7
l3.4,0l-0.6,3.7l1.9,0l0,3.3l-2.5,0L47,29.9l2.3,0l0,3.3l-2.8,0L45.8,37l-3.4,0l0.7-3.8l-3.7,0L38.7,37l-3.4,0L36,33.2z
M43.7,29.9l0.6-3.8l-3.7,0L40,29.9L43.7,29.9z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

-42
View File
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<defs>
<linearGradient id="linear-gradient" x1="70.22612" y1="27.79912" x2="-5.13024" y2="63.12242" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#c90f5e"/>
<stop offset="0.22111" stop-color="#c90f5e"/>
<stop offset="0.2356" stop-color="#c90f5e"/>
<stop offset="0.35559" stop-color="#ca135c"/>
<stop offset="0.46633" stop-color="#ce1e57"/>
<stop offset="0.5735" stop-color="#d4314e"/>
<stop offset="0.67844" stop-color="#dc4b41"/>
<stop offset="0.78179" stop-color="#e66d31"/>
<stop offset="0.88253" stop-color="#f3961d"/>
<stop offset="0.94241" stop-color="#fcb20f"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="24.65904" y1="61.99608" x2="46.04762" y2="2.93445" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
<stop offset="0.04188" stop-color="#077cfb"/>
<stop offset="0.44503" stop-color="#c90f5e"/>
<stop offset="0.95812" stop-color="#077cfb"/>
</linearGradient>
<linearGradient id="linear-gradient-3" x1="17.39552" y1="63.34592" x2="33.19389" y2="7.20092" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
<stop offset="0.27749" stop-color="#c90f5e"/>
<stop offset="0.97382" stop-color="#fcb20f"/>
</linearGradient>
</defs>
<title>rider</title>
<g>
<polygon points="70 27.237 63.391 23.75 20.926 0 3.827 17.921 21.619 41.068 60.537 44.397 70 27.237" fill="url(#linear-gradient)"/>
<polygon points="50.423 16.132 44.271 1.107 27.643 17.471 11.768 50.194 49.411 70 70 57.98 50.423 16.132" fill="url(#linear-gradient-2)"/>
<polygon points="20.926 0 0 14.095 7.779 62.172 27.848 69.889 53.78 48.823 20.926 0" fill="url(#linear-gradient-3)"/>
</g>
<g>
<rect x="13.30219" y="13.19311" width="43.61371" height="43.61371"/>
<g>
<path d="M17.22741,18.86293h8.39564a7.38416,7.38416,0,0,1,5.34268,1.85358,5.86989,5.86989,0,0,1,1.52648,4.1433h0A5.74339,5.74339,0,0,1,28.567,30.5296l4.47041,6.54206H28.34891L24.42368,31.1838h-3.162v5.88785H17.22741V18.86293h0ZM25.296,27.69471c1.96262,0,3.053-1.09034,3.053-2.61682h0c0-1.74455-1.19938-2.61682-3.162-2.61682H21.15265v5.23365H25.296Z" fill="#fff"/>
<path d="M36.09034,18.86293H43.2866c5.77882,0,9.70405,3.92523,9.70405,9.15888h0c0,5.12461-3.92523,9.15888-9.70405,9.15888H36.09034V18.86293Zm4.03427,3.59813V33.47352h3.162a5.23727,5.23727,0,0,0,5.56075-5.45171h0a5.26493,5.26493,0,0,0-5.56075-5.56075h-3.162Z" fill="#fff"/>
</g>
<rect x="17.22741" y="48.62925" width="16.35514" height="2.72586" fill="#fff"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

-36
View File
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
<g>
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="25.0676" y1="1.4599" x2="43.1829" y2="66.675">
<stop offset="0.2849" style="stop-color:#00CDD7"/>
<stop offset="0.9409" style="stop-color:#2086D7"/>
</linearGradient>
<polygon style="fill:url(#SVGID_1_);" points="9.4,63.3 0,7.3 17.5,0.1 28.6,6.7 38.8,1.2 60.1,9.4 48.1,70 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="30.7199" y1="9.7343" x2="61.365" y2="54.6713">
<stop offset="0.1398" style="stop-color:#FFF045"/>
<stop offset="0.3656" style="stop-color:#00CDD7"/>
</linearGradient>
<polygon style="fill:url(#SVGID_2_);" points="70,23.7 61,1.4 44.6,0 19.3,24.3 26.1,55.6 38.8,64.6 70,46 62.3,31.7 "/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="61.0819" y1="15.2899" x2="65.1065" y2="29.5436">
<stop offset="0.2849" style="stop-color:#00CDD7"/>
<stop offset="0.9409" style="stop-color:#2086D7"/>
</linearGradient>
<polygon style="fill:url(#SVGID_3_);" points="56,20.4 62.3,31.7 70,23.7 64.4,9.8 "/>
</g>
<g>
<g>
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
<rect x="17.5" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
<path style="fill:#FFFFFF;" d="M38.7,34.3l2.3-2.8c1.6,1.3,3.3,2.2,5.3,2.2c1.6,0,2.5-0.6,2.5-1.7v-0.1c0-1-0.6-1.5-3.6-2.3
c-3.6-0.9-5.8-1.9-5.8-5.5v-0.1c0-3.3,2.6-5.4,6.2-5.4c2.6,0,4.8,0.8,6.6,2.3l-2,3c-1.6-1.1-3.1-1.8-4.6-1.8
c-1.5,0-2.3,0.7-2.3,1.6v0.1c0,1.2,0.8,1.6,3.8,2.4c3.6,1,5.6,2.3,5.6,5.4v0.1c0,3.6-2.7,5.6-6.5,5.6
C43.5,37.2,40.8,36.2,38.7,34.3"/>
</g>
<polygon style="fill:#FFFFFF;" points="35.2,19 32.5,29.4 29.5,19 26.5,19 23.4,29.4 20.7,19 16.6,19 21.7,36.9 25,36.9 28,26.5
30.9,36.9 34.3,36.9 39.4,19 "/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

+7 -7
View File
@@ -15,7 +15,7 @@ Note that only one type of a given movie is supported. If you want both a 4k ver
* Adding new movies with lots of information, such as trailers, ratings, etc. * Adding new movies with lots of information, such as trailers, ratings, etc.
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc. * Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray* * Can watch for better quality of the movies you have and do an automatic upgrade. _eg. from DVD to Blu-Ray_
* Automatic failed download handling will try another release if one fails * Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically * Manual search so you can pick any release or to see why a release was not downloaded automatically
* Full integration with SABnzbd and NZBGet * Full integration with SABnzbd and NZBGet
@@ -68,12 +68,12 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
## JetBrains ## JetBrains
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. Thank you to [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains" width="96">](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/) * [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/ReSharper_icon.png" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/) * [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/WebStorm_icon.png" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/) * [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/Rider_icon.png" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/) * [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/dotTrace_icon.png" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
## DigitalOcean ## DigitalOcean
@@ -87,4 +87,4 @@ This project is also supported by DigitalOcean
### License ### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2022 * Copyright 2010-2024
+2 -2
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.13.1' majorVersion: '5.17.1'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -1226,7 +1226,7 @@ stages:
- task: SonarCloudAnalyze@2 - task: SonarCloudAnalyze@2
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results displayName: Publish SonarCloud Results
- task: reportgenerator@5 - task: reportgenerator@5.3.11
displayName: Generate Coverage Report displayName: Generate Coverage Report
inputs: inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml' reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
-1
View File
@@ -210,7 +210,6 @@ module.exports = {
'no-undef-init': 'off', 'no-undef-init': 'off',
'no-undefined': 'off', 'no-undefined': 'off',
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }], 'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
'no-use-before-define': 'error',
// Node.js and CommonJS // Node.js and CommonJS
+2 -1
View File
@@ -26,6 +26,7 @@ module.exports = (env) => {
const config = { const config = {
mode: isProduction ? 'production' : 'development', mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map', devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: { stats: {
children: false children: false
@@ -186,7 +187,7 @@ module.exports = (env) => {
loose: true, loose: true,
debug: false, debug: false,
useBuiltIns: 'entry', useBuiltIns: 'entry',
corejs: 3 corejs: '3.39'
} }
] ]
] ]
+6 -1
View File
@@ -154,9 +154,14 @@ function HistoryRow(props: HistoryRowProps) {
} }
if (name === 'downloadClient') { if (name === 'downloadClient') {
const downloadClientName =
'downloadClientName' in data ? data.downloadClientName : null;
const downloadClient =
'downloadClient' in data ? data.downloadClient : null;
return ( return (
<TableRowCell key={name} className={styles.downloadClient}> <TableRowCell key={name} className={styles.downloadClient}>
{'downloadClient' in data ? data.downloadClient : ''} {downloadClientName ?? downloadClient ?? ''}
</TableRowCell> </TableRowCell>
); );
} }
@@ -91,6 +91,10 @@
margin-left: 5px; margin-left: 5px;
} }
.genres {
pointer-events: all;
}
.links { .links {
margin-left: 5px; margin-left: 5px;
pointer-events: all; pointer-events: all;
@@ -10,6 +10,7 @@ import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import MovieStatusLabel from 'Movie/Details/MovieStatusLabel'; import MovieStatusLabel from 'Movie/Details/MovieStatusLabel';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar'; import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieGenres from 'Movie/MovieGenres';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import formatRuntime from 'Utilities/Date/formatRuntime'; import formatRuntime from 'Utilities/Date/formatRuntime';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
@@ -249,9 +250,7 @@ class AddNewMovieSearchResult extends Component {
name={icons.GENRE} name={icons.GENRE}
size={13} size={13}
/> />
<span className={styles.genres}> <MovieGenres className={styles.genres} genres={genres} />
{genres.slice(0, 3).join(', ')}
</span>
</Label> : </Label> :
null null
} }
@@ -280,7 +279,7 @@ class AddNewMovieSearchResult extends Component {
} }
canFlip={true} canFlip={true}
kind={kinds.INVERSE} kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM} position={tooltipPositions.TOP}
/> />
{ {
+18 -3
View File
@@ -1,11 +1,16 @@
import Column from 'Components/Table/Column'; import Column from 'Components/Table/Column';
import { SortDirection } from 'Helpers/Props/sortDirections'; import { SortDirection } from 'Helpers/Props/sortDirections';
import { ValidationFailure } from 'typings/pending';
import { FilterBuilderProp, PropertyFilter } from './AppState'; import { FilterBuilderProp, PropertyFilter } from './AppState';
export interface Error { export interface Error {
responseJSON: { status?: number;
message: string; responseJSON:
}; | {
message: string | undefined;
}
| ValidationFailure[]
| undefined;
} }
export interface AppSectionDeleteState { export interface AppSectionDeleteState {
@@ -51,6 +56,16 @@ export interface AppSectionItemState<T> {
item: T; item: T;
} }
export interface AppSectionProviderState<T>
extends AppSectionDeleteState,
AppSectionSaveState {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: T[];
pendingChanges: Partial<T>;
}
interface AppSectionState<T> { interface AppSectionState<T> {
isFetching: boolean; isFetching: boolean;
isPopulated: boolean; isPopulated: boolean;
@@ -1,11 +1,20 @@
import AppSectionState from 'App/State/AppSectionState'; import AppSectionState from 'App/State/AppSectionState';
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
import ImportMode from 'InteractiveImport/ImportMode'; import ImportMode from 'InteractiveImport/ImportMode';
import InteractiveImport from 'InteractiveImport/InteractiveImport'; import InteractiveImport from 'InteractiveImport/InteractiveImport';
interface FavoriteFolder {
folder: string;
}
interface RecentFolder {
folder: string;
lastUsed: string;
}
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> { interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
originalItems: InteractiveImport[]; originalItems: InteractiveImport[];
importMode: ImportMode; importMode: ImportMode;
favoriteFolders: FavoriteFolder[];
recentFolders: RecentFolder[]; recentFolders: RecentFolder[];
} }
@@ -0,0 +1,6 @@
import { AppSectionProviderState } from 'App/State/AppSectionState';
import Metadata from 'typings/Metadata';
type MetadataAppState = AppSectionProviderState<Metadata>;
export default MetadataAppState;
@@ -1,6 +1,6 @@
import AppSectionState from 'App/State/AppSectionState'; import AppSectionState from 'App/State/AppSectionState';
import MovieCredit from 'typings/MovieCredit'; import MovieCredit from 'typings/MovieCredit';
interface MovieCreditAppState extends AppSectionState<MovieCredit> {} type MovieCreditAppState = AppSectionState<MovieCredit>;
export default MovieCreditAppState; export default MovieCreditAppState;
+3 -2
View File
@@ -20,6 +20,7 @@ import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample'; import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import UiSettings from 'typings/Settings/UiSettings'; import UiSettings from 'typings/Settings/UiSettings';
import MetadataAppState from './MetadataAppState';
export interface DownloadClientAppState export interface DownloadClientAppState
extends AppSectionState<DownloadClient>, extends AppSectionState<DownloadClient>,
@@ -36,8 +37,7 @@ export interface NamingAppState
extends AppSectionItemState<NamingConfig>, extends AppSectionItemState<NamingConfig>,
AppSectionSaveState {} AppSectionSaveState {}
export interface NamingExamplesAppState export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
extends AppSectionItemState<NamingExample> {}
export interface ImportListAppState export interface ImportListAppState
extends AppSectionState<ImportList>, extends AppSectionState<ImportList>,
@@ -97,6 +97,7 @@ interface SettingsAppState {
indexerFlags: IndexerFlagSettingsAppState; indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState; indexers: IndexerAppState;
languages: LanguageSettingsAppState; languages: LanguageSettingsAppState;
metadata: MetadataAppState;
naming: NamingAppState; naming: NamingAppState;
namingExamples: NamingExamplesAppState; namingExamples: NamingExamplesAppState;
notifications: NotificationAppState; notifications: NotificationAppState;
@@ -22,7 +22,7 @@ function createMapStateToProps() {
return { return {
...collection, ...collection,
movies: [...collection.movies].sort((a, b) => b.year - a.year), movies: [...collection.movies].sort((a, b) => b.year - a.year),
genres: Array.from(new Set(allGenres)).slice(0, 3) genres: Array.from(new Set(allGenres))
}; };
} }
); );
@@ -10,6 +10,7 @@ import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import { icons, sizes } from 'Helpers/Props'; import { icons, sizes } from 'Helpers/Props';
import MovieGenres from 'Movie/MovieGenres';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts'; import fonts from 'Styles/Variables/fonts';
@@ -242,12 +243,10 @@ class CollectionOverview extends Component {
size={sizes.MEDIUM} size={sizes.MEDIUM}
> >
<Icon <Icon
name={icons.PROFILE} name={icons.GENRE}
size={13} size={13}
/> />
<span className={styles.genres}> <MovieGenres className={styles.genres} genres={genres} />
{genres.join(', ')}
</span>
</Label> </Label>
} }
@@ -16,6 +16,7 @@ import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue'; import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue';
import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue'; import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
import styles from './FilterBuilderRow.css'; import styles from './FilterBuilderRow.css';
@@ -80,6 +81,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.QUALITY_PROFILE: case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValue; return QualityProfileFilterBuilderRowValue;
case filterBuilderValueTypes.QUEUE_STATUS:
return QueueStatusFilterBuilderRowValue;
case filterBuilderValueTypes.MOVIE: case filterBuilderValueTypes.MOVIE:
return MovieFilterBuilderRowValue; return MovieFilterBuilderRowValue;
@@ -58,7 +58,7 @@ function getValue(input, selectedFilterBuilderProp) {
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) { if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
const { numberFractionDigits = 0 } = selectedFilterBuilderProp; const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
return Number(input).toFixed(numberFractionDigits); return Number(Number(input).toFixed(numberFractionDigits));
} }
return input; return input;
@@ -0,0 +1,67 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
const statusTagList = [
{
id: 'queued',
get name() {
return translate('Queued');
},
},
{
id: 'paused',
get name() {
return translate('Paused');
},
},
{
id: 'downloading',
get name() {
return translate('Downloading');
},
},
{
id: 'completed',
get name() {
return translate('Completed');
},
},
{
id: 'failed',
get name() {
return translate('Failed');
},
},
{
id: 'warning',
get name() {
return translate('Warning');
},
},
{
id: 'delay',
get name() {
return translate('Delay');
},
},
{
id: 'downloadClientUnavailable',
get name() {
return translate('DownloadClientUnavailable');
},
},
{
id: 'fallback',
get name() {
return translate('Fallback');
},
},
];
function QueueStatusFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
return <FilterBuilderRowValue {...props} tagList={statusTagList} />;
}
export default QueueStatusFilterBuilderRowValue;
@@ -2,7 +2,7 @@ import React from 'react';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue'; import FilterBuilderRowValue from './FilterBuilderRowValue';
const protocols = [ const statusTagList = [
{ id: 'tba', name: 'TBA' }, { id: 'tba', name: 'TBA' },
{ {
id: 'announced', id: 'announced',
@@ -33,7 +33,7 @@ const protocols = [
function ReleaseStatusFilterBuilderRowValue(props) { function ReleaseStatusFilterBuilderRowValue(props) {
return ( return (
<FilterBuilderRowValue <FilterBuilderRowValue
tagList={protocols} tagList={statusTagList}
{...props} {...props}
/> />
); );
@@ -1,156 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import KeyValueListInputItem from './KeyValueListInputItem';
import styles from './KeyValueListInput.css';
class KeyValueListInput extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isFocused: false
};
}
//
// Listeners
onItemChange = (index, itemValue) => {
const {
name,
value,
onChange
} = this.props;
const newValue = [...value];
if (index == null) {
newValue.push(itemValue);
} else {
newValue.splice(index, 1, itemValue);
}
onChange({
name,
value: newValue
});
};
onRemoveItem = (index) => {
const {
name,
value,
onChange
} = this.props;
const newValue = [...value];
newValue.splice(index, 1);
onChange({
name,
value: newValue
});
};
onFocus = () => {
this.setState({
isFocused: true
});
};
onBlur = () => {
this.setState({
isFocused: false
});
const {
name,
value,
onChange
} = this.props;
const newValue = value.reduce((acc, v) => {
if (v.key || v.value) {
acc.push(v);
}
return acc;
}, []);
if (newValue.length !== value.length) {
onChange({
name,
value: newValue
});
}
};
//
// Render
render() {
const {
className,
value,
keyPlaceholder,
valuePlaceholder,
hasError,
hasWarning
} = this.props;
const { isFocused } = this.state;
return (
<div className={classNames(
className,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{
[...value, { key: '', value: '' }].map((v, index) => {
return (
<KeyValueListInputItem
key={index}
index={index}
keyValue={v.key}
value={v.value}
keyPlaceholder={keyPlaceholder}
valuePlaceholder={valuePlaceholder}
isNew={index === value.length}
onChange={this.onItemChange}
onRemove={this.onRemoveItem}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
);
})
}
</div>
);
}
}
KeyValueListInput.propTypes = {
className: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.object).isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
keyPlaceholder: PropTypes.string,
valuePlaceholder: PropTypes.string,
onChange: PropTypes.func.isRequired
};
KeyValueListInput.defaultProps = {
className: styles.inputContainer,
value: []
};
export default KeyValueListInput;
@@ -0,0 +1,104 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { InputOnChange } from 'typings/inputs';
import KeyValueListInputItem from './KeyValueListInputItem';
import styles from './KeyValueListInput.css';
interface KeyValue {
key: string;
value: string;
}
export interface KeyValueListInputProps {
className?: string;
name: string;
value: KeyValue[];
hasError?: boolean;
hasWarning?: boolean;
keyPlaceholder?: string;
valuePlaceholder?: string;
onChange: InputOnChange<KeyValue[]>;
}
function KeyValueListInput({
className = styles.inputContainer,
name,
value = [],
hasError = false,
hasWarning = false,
keyPlaceholder,
valuePlaceholder,
onChange,
}: KeyValueListInputProps): JSX.Element {
const [isFocused, setIsFocused] = useState(false);
const handleItemChange = useCallback(
(index: number | null, itemValue: KeyValue) => {
const newValue = [...value];
if (index === null) {
newValue.push(itemValue);
} else {
newValue.splice(index, 1, itemValue);
}
onChange({ name, value: newValue });
},
[value, name, onChange]
);
const handleRemoveItem = useCallback(
(index: number) => {
const newValue = [...value];
newValue.splice(index, 1);
onChange({ name, value: newValue });
},
[value, name, onChange]
);
const onFocus = useCallback(() => setIsFocused(true), []);
const onBlur = useCallback(() => {
setIsFocused(false);
const newValue = value.reduce((acc: KeyValue[], v) => {
if (v.key || v.value) {
acc.push(v);
}
return acc;
}, []);
if (newValue.length !== value.length) {
onChange({ name, value: newValue });
}
}, [value, name, onChange]);
return (
<div
className={classNames(
className,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{[...value, { key: '', value: '' }].map((v, index) => (
<KeyValueListInputItem
key={index}
index={index}
keyValue={v.key}
value={v.value}
keyPlaceholder={keyPlaceholder}
valuePlaceholder={valuePlaceholder}
isNew={index === value.length}
onChange={handleItemChange}
onRemove={handleRemoveItem}
onFocus={onFocus}
onBlur={onBlur}
/>
))}
</div>
);
}
export default KeyValueListInput;
@@ -5,13 +5,19 @@
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
border-bottom: 0;
} }
} }
.inputWrapper { .keyInputWrapper {
flex: 1 0 0; flex: 1 0 0;
} }
.valueInputWrapper {
flex: 1 0 0;
min-width: 40px;
}
.buttonWrapper { .buttonWrapper {
flex: 0 0 22px; flex: 0 0 22px;
} }
@@ -20,4 +26,10 @@
.valueInput { .valueInput {
width: 100%; width: 100%;
border: none; border: none;
background-color: transparent;
color: var(--textColor);
&::placeholder {
color: var(--helpTextColor);
}
} }
@@ -2,10 +2,11 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'buttonWrapper': string; 'buttonWrapper': string;
'inputWrapper': string;
'itemContainer': string; 'itemContainer': string;
'keyInput': string; 'keyInput': string;
'keyInputWrapper': string;
'valueInput': string; 'valueInput': string;
'valueInputWrapper': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -1,124 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import TextInput from './TextInput';
import styles from './KeyValueListInputItem.css';
class KeyValueListInputItem extends Component {
//
// Listeners
onKeyChange = ({ value: keyValue }) => {
const {
index,
value,
onChange
} = this.props;
onChange(index, { key: keyValue, value });
};
onValueChange = ({ value }) => {
// TODO: Validate here or validate at a lower level component
const {
index,
keyValue,
onChange
} = this.props;
onChange(index, { key: keyValue, value });
};
onRemovePress = () => {
const {
index,
onRemove
} = this.props;
onRemove(index);
};
onFocus = () => {
this.props.onFocus();
};
onBlur = () => {
this.props.onBlur();
};
//
// Render
render() {
const {
keyValue,
value,
keyPlaceholder,
valuePlaceholder,
isNew
} = this.props;
return (
<div className={styles.itemContainer}>
<div className={styles.inputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={this.onKeyChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
<div className={styles.inputWrapper}>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={this.onValueChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
<div className={styles.buttonWrapper}>
{
isNew ?
null :
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={this.onRemovePress}
/>
}
</div>
</div>
);
}
}
KeyValueListInputItem.propTypes = {
index: PropTypes.number,
keyValue: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
keyPlaceholder: PropTypes.string.isRequired,
valuePlaceholder: PropTypes.string.isRequired,
isNew: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onBlur: PropTypes.func.isRequired
};
KeyValueListInputItem.defaultProps = {
keyPlaceholder: 'Key',
valuePlaceholder: 'Value'
};
export default KeyValueListInputItem;
@@ -0,0 +1,89 @@
import React, { useCallback } from 'react';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import TextInput from './TextInput';
import styles from './KeyValueListInputItem.css';
interface KeyValueListInputItemProps {
index: number;
keyValue: string;
value: string;
keyPlaceholder?: string;
valuePlaceholder?: string;
isNew: boolean;
onChange: (index: number, itemValue: { key: string; value: string }) => void;
onRemove: (index: number) => void;
onFocus: () => void;
onBlur: () => void;
}
function KeyValueListInputItem({
index,
keyValue,
value,
keyPlaceholder = 'Key',
valuePlaceholder = 'Value',
isNew,
onChange,
onRemove,
onFocus,
onBlur,
}: KeyValueListInputItemProps): JSX.Element {
const handleKeyChange = useCallback(
({ value: keyValue }: { value: string }) => {
onChange(index, { key: keyValue, value });
},
[index, value, onChange]
);
const handleValueChange = useCallback(
({ value }: { value: string }) => {
onChange(index, { key: keyValue, value });
},
[index, keyValue, onChange]
);
const handleRemovePress = useCallback(() => {
onRemove(index);
}, [index, onRemove]);
return (
<div className={styles.itemContainer}>
<div className={styles.keyInputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={handleKeyChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
<div className={styles.valueInputWrapper}>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={handleValueChange}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
<div className={styles.buttonWrapper}>
{isNew ? null : (
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={handleRemovePress}
/>
)}
</div>
</div>
);
}
export default KeyValueListInputItem;
@@ -14,6 +14,8 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.CHECK; return inputTypes.CHECK;
case 'device': case 'device':
return inputTypes.DEVICE; return inputTypes.DEVICE;
case 'keyValueList':
return inputTypes.KEY_VALUE_LIST;
case 'password': case 'password':
return inputTypes.PASSWORD; return inputTypes.PASSWORD;
case 'number': case 'number':
@@ -137,6 +139,8 @@ ProviderFieldFormGroup.propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
advanced: PropTypes.bool.isRequired, advanced: PropTypes.bool.isRequired,
hidden: PropTypes.string, hidden: PropTypes.string,
isDisabled: PropTypes.bool,
provider: PropTypes.string,
pending: PropTypes.bool.isRequired, pending: PropTypes.bool.isRequired,
errors: PropTypes.arrayOf(PropTypes.object).isRequired, errors: PropTypes.arrayOf(PropTypes.object).isRequired,
warnings: PropTypes.arrayOf(PropTypes.object).isRequired, warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -1,7 +1,7 @@
import React, { ComponentPropsWithoutRef } from 'react'; import React, { ComponentPropsWithoutRef } from 'react';
import styles from './TableRowCell.css'; import styles from './TableRowCell.css';
export interface TableRowCellProps extends ComponentPropsWithoutRef<'td'> {} export type TableRowCellProps = ComponentPropsWithoutRef<'td'>;
export default function TableRowCell({ export default function TableRowCell({
className = styles.cell, className = styles.cell,
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "Radarr", "name": "__INSTANCE_NAME__",
"icons": [ "icons": [
{ {
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png", "src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png",
@@ -85,10 +85,16 @@ $hoverScale: 1.05;
flex: 1 0 auto; flex: 1 0 auto;
} }
.overviewContainer {
display: flex;
justify-content: space-between;
flex: 0 1 1000px;
flex-direction: column;
}
.overview { .overview {
composes: link; composes: link;
flex: 0 1 1000px;
overflow: hidden; overflow: hidden;
min-height: 0; min-height: 0;
} }
@@ -11,6 +11,7 @@ interface CssExports {
'link': string; 'link': string;
'lists': string; 'lists': string;
'overview': string; 'overview': string;
'overviewContainer': string;
'poster': string; 'poster': string;
'posterContainer': string; 'posterContainer': string;
'title': string; 'title': string;
@@ -133,14 +133,20 @@ class DiscoverMovieOverview extends Component {
/> />
</div> </div>
<MoviePoster <Link
className={styles.poster} className={styles.link}
style={elementStyle} style={elementStyle}
images={images} {...linkProps}
size={250} >
lazy={false} <MoviePoster
overflow={true} className={styles.poster}
/> style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
/>
</Link>
</div> </div>
</div> </div>
@@ -242,11 +248,13 @@ class DiscoverMovieOverview extends Component {
</div> </div>
<div className={styles.details}> <div className={styles.details}>
<div className={styles.overview}> <div className={styles.overviewContainer}>
<TextTruncate <Link className={styles.overview} {...linkProps}>
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))} <TextTruncate
text={overview} line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
/> text={overview}
/>
</Link>
</div> </div>
<DiscoverMovieOverviewInfo <DiscoverMovieOverviewInfo
@@ -255,7 +263,6 @@ class DiscoverMovieOverview extends Component {
{...overviewOptions} {...overviewOptions}
{...otherProps} {...otherProps}
/> />
</div> </div>
</div> </div>
</div> </div>
@@ -49,7 +49,7 @@ function isVisible(row, props) {
valueProp valueProp
} = row; } = row;
return _.has(props, valueProp) && (_.get(props, showProp) || props.sortKey === name); return _.has(props, valueProp) && _.get(props, valueProp) !== null && (props[showProp] || props.sortKey === name);
} }
function getInfoRowProps(row, props) { function getInfoRowProps(row, props) {
@@ -50,15 +50,13 @@ function DiscoverMoviePosterInfo(props) {
} }
if (sortKey === 'inCinemas' && inCinemas) { if (sortKey === 'inCinemas' && inCinemas) {
const inCinemasDate = getRelativeDate( const inCinemasDate = getRelativeDate({
inCinemas, date: inCinemas,
shortDateFormat, shortDateFormat,
showRelativeDates, showRelativeDates,
{ timeFormat,
timeFormat, timeForToday: false
timeForToday: false });
}
);
return ( return (
<div className={styles.info} title={translate('InCinemas')}> <div className={styles.info} title={translate('InCinemas')}>
@@ -68,15 +66,13 @@ function DiscoverMoviePosterInfo(props) {
} }
if (sortKey === 'digitalRelease' && digitalRelease) { if (sortKey === 'digitalRelease' && digitalRelease) {
const digitalReleaseDate = getRelativeDate( const digitalReleaseDate = getRelativeDate({
digitalRelease, date: digitalRelease,
shortDateFormat, shortDateFormat,
showRelativeDates, showRelativeDates,
{ timeFormat,
timeFormat, timeForToday: false
timeForToday: false });
}
);
return ( return (
<div className={styles.info} title={translate('DigitalRelease')}> <div className={styles.info} title={translate('DigitalRelease')}>
@@ -86,15 +82,13 @@ function DiscoverMoviePosterInfo(props) {
} }
if (sortKey === 'physicalRelease' && physicalRelease) { if (sortKey === 'physicalRelease' && physicalRelease) {
const physicalReleaseDate = getRelativeDate( const physicalReleaseDate = getRelativeDate({
physicalRelease, date: physicalRelease,
shortDateFormat, shortDateFormat,
showRelativeDates, showRelativeDates,
{ timeFormat,
timeFormat, timeForToday: false
timeForToday: false });
}
);
return ( return (
<div className={styles.info} title={translate('PhysicalRelease')}> <div className={styles.info} title={translate('PhysicalRelease')}>
@@ -8,6 +8,7 @@ export const LANGUAGE = 'language';
export const PROTOCOL = 'protocol'; export const PROTOCOL = 'protocol';
export const QUALITY = 'quality'; export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile'; export const QUALITY_PROFILE = 'qualityProfile';
export const QUEUE_STATUS = 'queueStatus';
export const MOVIE = 'movie'; export const MOVIE = 'movie';
export const RELEASE_STATUS = 'releaseStatus'; export const RELEASE_STATUS = 'releaseStatus';
export const MINIMUM_AVAILABILITY = 'minimumAvailability'; export const MINIMUM_AVAILABILITY = 'minimumAvailability';
+2
View File
@@ -15,6 +15,7 @@ import {
faFileVideo as farFileVideo, faFileVideo as farFileVideo,
faFolder as farFolder, faFolder as farFolder,
faHdd as farHdd, faHdd as farHdd,
faHeart as farHeart,
faKeyboard as farKeyboard, faKeyboard as farKeyboard,
faObjectGroup as farObjectGroup, faObjectGroup as farObjectGroup,
faObjectUngroup as farObjectUngroup, faObjectUngroup as farObjectUngroup,
@@ -174,6 +175,7 @@ export const GENRE = fasTheaterMasks;
export const GROUP = farObjectGroup; export const GROUP = farObjectGroup;
export const HEALTH = fasMedkit; export const HEALTH = fasMedkit;
export const HEART = fasHeart; export const HEART = fasHeart;
export const HEART_OUTLINE = farHeart;
export const HISTORY = fasHistory; export const HISTORY = fasHistory;
export const HOUSEKEEPING = fasHome; export const HOUSEKEEPING = fasHome;
export const IGNORE = fasTimesCircle; export const IGNORE = fasTimesCircle;
@@ -0,0 +1,5 @@
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'actions': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,48 @@
import React, { SyntheticEvent, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import { removeFavoriteFolder } from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import styles from './FavoriteFolderRow.css';
interface FavoriteFolderRowProps {
folder: string;
onPress: (folder: string) => unknown;
}
function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
onPress(folder);
}, [folder, onPress]);
const handleRemoveFavoritePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
dispatch(removeFavoriteFolder({ folder }));
},
[folder, dispatch]
);
return (
<TableRowButton onPress={handlePress}>
<TableRowCell>{folder}</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('FavoriteFolderRemove')}
kind="danger"
name={icons.HEART}
onPress={handleRemoveFavoritePress}
/>
</TableRowCell>
</TableRowButton>
);
}
export default FavoriteFolderRow;
@@ -1,7 +1,12 @@
.recentFoldersContainer { .foldersContainer {
margin-top: 15px; margin-top: 15px;
} }
.foldersTitle {
border-bottom: 1px solid var(--borderColor);
font-size: 21px;
}
.buttonsContainer { .buttonsContainer {
margin-top: 30px; margin-top: 30px;
} }
@@ -5,7 +5,8 @@ interface CssExports {
'buttonContainer': string; 'buttonContainer': string;
'buttonIcon': string; 'buttonIcon': string;
'buttonsContainer': string; 'buttonsContainer': string;
'recentFoldersContainer': string; 'foldersContainer': string;
'foldersTitle': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState'; import AppState from 'App/State/AppState';
@@ -14,14 +14,23 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sizes } from 'Helpers/Props'; import { icons, kinds, sizes } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
addRecentFolder,
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import FavoriteFolderRow from './FavoriteFolderRow';
import RecentFolderRow from './RecentFolderRow'; import RecentFolderRow from './RecentFolderRow';
import styles from './InteractiveImportSelectFolderModalContent.css'; import styles from './InteractiveImportSelectFolderModalContent.css';
const favoriteFoldersColumns = [
{
name: 'folder',
label: () => translate('Folder'),
},
{
name: 'actions',
label: '',
},
];
const recentFoldersColumns = [ const recentFoldersColumns = [
{ {
name: 'folder', name: 'folder',
@@ -49,15 +58,22 @@ function InteractiveImportSelectFolderModalContent(
const { modalTitle, onFolderSelect, onModalClose } = props; const { modalTitle, onFolderSelect, onModalClose } = props;
const [folder, setFolder] = useState(''); const [folder, setFolder] = useState('');
const dispatch = useDispatch(); const dispatch = useDispatch();
const recentFolders = useSelector( const { favoriteFolders, recentFolders } = useSelector(
createSelector( createSelector(
(state: AppState) => state.interactiveImport.recentFolders, (state: AppState) => state.interactiveImport,
(recentFolders) => { (interactiveImport) => {
return recentFolders; return {
favoriteFolders: interactiveImport.favoriteFolders,
recentFolders: interactiveImport.recentFolders,
};
} }
) )
); );
const favoriteFolderMap = useMemo(() => {
return new Map(favoriteFolders.map((f) => [f.folder, f]));
}, [favoriteFolders]);
const onPathChange = useCallback( const onPathChange = useCallback(
({ value }: { value: string }) => { ({ value }: { value: string }) => {
setFolder(value); setFolder(value);
@@ -90,13 +106,6 @@ function InteractiveImportSelectFolderModalContent(
onFolderSelect(folder); onFolderSelect(folder);
}, [folder, onFolderSelect, dispatch]); }, [folder, onFolderSelect, dispatch]);
const onRemoveRecentFolderPress = useCallback(
(folderToRemove: string) => {
dispatch(removeRecentFolder({ folder: folderToRemove }));
},
[dispatch]
);
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
@@ -110,8 +119,34 @@ function InteractiveImportSelectFolderModalContent(
onChange={onPathChange} onChange={onPathChange}
/> />
{favoriteFolders.length ? (
<div className={styles.foldersContainer}>
<div className={styles.foldersTitle}>
{translate('FavoriteFolders')}
</div>
<Table columns={favoriteFoldersColumns}>
<TableBody>
{favoriteFolders.map((favoriteFolder) => {
return (
<FavoriteFolderRow
key={favoriteFolder.folder}
folder={favoriteFolder.folder}
onPress={onRecentPathPress}
/>
);
})}
</TableBody>
</Table>
</div>
) : null}
{recentFolders.length ? ( {recentFolders.length ? (
<div className={styles.recentFoldersContainer}> <div className={styles.foldersContainer}>
<div className={styles.foldersTitle}>
{translate('RecentFolders')}
</div>
<Table columns={recentFoldersColumns}> <Table columns={recentFoldersColumns}>
<TableBody> <TableBody>
{recentFolders {recentFolders
@@ -123,8 +158,8 @@ function InteractiveImportSelectFolderModalContent(
key={recentFolder.folder} key={recentFolder.folder}
folder={recentFolder.folder} folder={recentFolder.folder}
lastUsed={recentFolder.lastUsed} lastUsed={recentFolder.lastUsed}
isFavorite={favoriteFolderMap.has(recentFolder.folder)}
onPress={onRecentPathPress} onPress={onRecentPathPress}
onRemoveRecentFolderPress={onRemoveRecentFolderPress}
/> />
); );
})} })}
@@ -1,6 +0,0 @@
interface RecentFolder {
folder: string;
lastUsed: string;
}
export default RecentFolder;
@@ -1,5 +1,5 @@
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 40px; width: 70px;
} }
@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RecentFolderRow.css';
class RecentFolderRow extends Component {
//
// Listeners
onPress = () => {
this.props.onPress(this.props.folder);
};
onRemovePress = (event) => {
event.stopPropagation();
const {
folder,
onRemoveRecentFolderPress
} = this.props;
onRemoveRecentFolderPress(folder);
};
//
// Render
render() {
const {
folder,
lastUsed
} = this.props;
return (
<TableRowButton onPress={this.onPress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCell date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
title={translate('Remove')}
name={icons.REMOVE}
onPress={this.onRemovePress}
/>
</TableRowCell>
</TableRowButton>
);
}
}
RecentFolderRow.propTypes = {
folder: PropTypes.string.isRequired,
lastUsed: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
onRemoveRecentFolderPress: PropTypes.func.isRequired
};
export default RecentFolderRow;
@@ -0,0 +1,85 @@
import React, { SyntheticEvent, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import {
addFavoriteFolder,
removeFavoriteFolder,
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import styles from './RecentFolderRow.css';
interface RecentFolderRowProps {
folder: string;
lastUsed: string;
isFavorite: boolean;
onPress: (folder: string) => unknown;
}
function RecentFolderRow({
folder,
lastUsed,
isFavorite,
onPress,
}: RecentFolderRowProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
onPress(folder);
}, [folder, onPress]);
const handleFavoritePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
if (isFavorite) {
dispatch(removeFavoriteFolder({ folder }));
} else {
dispatch(addFavoriteFolder({ folder }));
}
},
[folder, isFavorite, dispatch]
);
const handleRemovePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
dispatch(removeRecentFolder({ folder }));
},
[folder, dispatch]
);
return (
<TableRowButton onPress={handlePress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCell date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
title={
isFavorite
? translate('FavoriteFolderRemove')
: translate('FavoriteFolderAdd')
}
kind={isFavorite ? 'danger' : 'default'}
name={isFavorite ? icons.HEART : icons.HEART_OUTLINE}
onPress={handleFavoritePress}
/>
<IconButton
title={translate('Remove')}
name={icons.REMOVE}
onPress={handleRemovePress}
/>
</TableRowCell>
</TableRowButton>
);
}
export default RecentFolderRow;
+2 -3
View File
@@ -28,6 +28,7 @@ import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import getMovieStatusDetails from 'Movie/getMovieStatusDetails'; import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
import MovieHistoryModal from 'Movie/History/MovieHistoryModal'; import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector'; import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
import MovieGenres from 'Movie/MovieGenres';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal'; import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable'; import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
@@ -651,9 +652,7 @@ class MovieDetails extends Component {
name={translate('Genres')} name={translate('Genres')}
size={sizes.LARGE} size={sizes.LARGE}
> >
<span className={styles.genres}> <MovieGenres className={styles.genres} genres={genres} />
{genres.join(', ')}
</span>
</InfoLabel> : </InfoLabel> :
null null
} }
+39
View File
@@ -0,0 +1,39 @@
import React from 'react';
import Label from 'Components/Label';
import Tooltip from 'Components/Tooltip/Tooltip';
import { kinds, sizes, tooltipPositions } from 'Helpers/Props';
interface MovieGenresProps {
className?: string;
genres: string[];
}
function MovieGenres({ className, genres }: MovieGenresProps) {
const firstGenres = genres.slice(0, 3);
const otherGenres = genres.slice(3);
if (otherGenres.length) {
return (
<Tooltip
anchor={<span className={className}>{firstGenres.join(', ')}</span>}
tooltip={
<div>
{otherGenres.map((tag) => {
return (
<Label key={tag} kind={kinds.INFO} size={sizes.LARGE}>
{tag}
</Label>
);
})}
</div>
}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
);
}
return <span className={className}>{firstGenres.join(', ')}</span>;
}
export default MovieGenres;
@@ -0,0 +1,27 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import MediaInfoProps from 'typings/MediaInfo';
import getEntries from 'Utilities/Object/getEntries';
function MediaInfo(props: MediaInfoProps) {
return (
<DescriptionList>
{getEntries(props).map(([key, value]) => {
const title = key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
if (!value) {
return null;
}
return (
<DescriptionListItem key={key} title={title} data={props[key]} />
);
})}
</DescriptionList>
);
}
export default MediaInfo;
@@ -1,33 +0,0 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
function MediaInfoPopover(props) {
return (
<DescriptionList>
{
Object.keys(props).map((key) => {
const title = key
.replace(/([A-Z])/g, ' $1')
.replace(/^./, (str) => str.toUpperCase());
const value = props[key];
if (!value) {
return null;
}
return (
<DescriptionListItem
key={key}
title={title}
data={props[key]}
/>
);
})
}
</DescriptionList>
);
}
export default MediaInfoPopover;
@@ -14,7 +14,7 @@ import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages'; import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
import FileEditModal from 'MovieFile/Edit/FileEditModal'; import FileEditModal from 'MovieFile/Edit/FileEditModal';
import MediaInfoConnector from 'MovieFile/MediaInfoConnector'; import MediaInfo from 'MovieFile/MediaInfo';
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes'; import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
@@ -224,7 +224,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.audio} className={styles.audio}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.AUDIO} type={mediaInfoTypes.AUDIO}
movieFileId={id} movieFileId={id}
/> />
@@ -238,7 +238,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.audioLanguages} className={styles.audioLanguages}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.AUDIO_LANGUAGES} type={mediaInfoTypes.AUDIO_LANGUAGES}
movieFileId={id} movieFileId={id}
/> />
@@ -252,7 +252,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.subtitles} className={styles.subtitles}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.SUBTITLES} type={mediaInfoTypes.SUBTITLES}
movieFileId={id} movieFileId={id}
/> />
@@ -266,7 +266,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.video} className={styles.video}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.VIDEO} type={mediaInfoTypes.VIDEO}
movieFileId={id} movieFileId={id}
/> />
@@ -280,7 +280,7 @@ class MovieFileEditorRow extends Component {
key={name} key={name}
className={styles.videoDynamicRangeType} className={styles.videoDynamicRangeType}
> >
<MediaInfoConnector <MediaInfo
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE} type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
movieFileId={id} movieFileId={id}
/> />
+2 -2
View File
@@ -8,7 +8,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import MediaInfoPopover from './Editor/MediaInfoPopover'; import MediaInfo from './Editor/MediaInfo';
function FileDetailsModal(props) { function FileDetailsModal(props) {
const { const {
@@ -31,7 +31,7 @@ function FileDetailsModal(props) {
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<MediaInfoPopover {...mediaInfo} /> <MediaInfo {...mediaInfo} />
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
-104
View File
@@ -1,104 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import getLanguageName from 'Utilities/String/getLanguageName';
import translate from 'Utilities/String/translate';
import * as mediaInfoTypes from './mediaInfoTypes';
function formatLanguages(languages) {
if (!languages) {
return null;
}
const splitLanguages = _.uniq(languages.split('/')).map((l) => {
const simpleLanguage = l.split('_')[0];
if (simpleLanguage === 'und') {
return translate('Unknown');
}
return getLanguageName(simpleLanguage);
});
if (splitLanguages.length > 3) {
return (
<span title={splitLanguages.join(', ')}>
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more
</span>
);
}
return (
<span>
{splitLanguages.join(', ')}
</span>
);
}
function MediaInfo(props) {
const {
type,
audioChannels,
audioCodec,
audioLanguages,
subtitles,
videoCodec,
videoDynamicRangeType
} = props;
if (type === mediaInfoTypes.AUDIO) {
return (
<span>
{
audioCodec ? audioCodec : ''
}
{
audioCodec && audioChannels ? ' - ' : ''
}
{
audioChannels ? audioChannels.toFixed(1) : ''
}
</span>
);
}
if (type === mediaInfoTypes.AUDIO_LANGUAGES) {
return formatLanguages(audioLanguages);
}
if (type === mediaInfoTypes.SUBTITLES) {
return formatLanguages(subtitles);
}
if (type === mediaInfoTypes.VIDEO) {
return (
<span>
{videoCodec}
</span>
);
}
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
return (
<span>
{videoDynamicRangeType}
</span>
);
}
return null;
}
MediaInfo.propTypes = {
type: PropTypes.string.isRequired,
audioChannels: PropTypes.number,
audioCodec: PropTypes.string,
audioLanguages: PropTypes.string,
subtitles: PropTypes.string,
videoCodec: PropTypes.string,
videoDynamicRangeType: PropTypes.string
};
export default MediaInfo;
+92
View File
@@ -0,0 +1,92 @@
import React from 'react';
import getLanguageName from 'Utilities/String/getLanguageName';
import translate from 'Utilities/String/translate';
import useMovieFile from './useMovieFile';
function formatLanguages(languages: string | undefined) {
if (!languages) {
return null;
}
const splitLanguages = [...new Set(languages.split('/'))].map((l) => {
const simpleLanguage = l.split('_')[0];
if (simpleLanguage === 'und') {
return translate('Unknown');
}
return getLanguageName(simpleLanguage);
});
if (splitLanguages.length > 3) {
return (
<span title={splitLanguages.join(', ')}>
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2}{' '}
more
</span>
);
}
return <span>{splitLanguages.join(', ')}</span>;
}
export type MediaInfoType =
| 'audio'
| 'audioLanguages'
| 'subtitles'
| 'video'
| 'videoDynamicRangeType';
interface MediaInfoProps {
movieFileId?: number;
type: MediaInfoType;
}
function MediaInfo({ movieFileId, type }: MediaInfoProps) {
const movieFile = useMovieFile(movieFileId);
if (!movieFile?.mediaInfo) {
return null;
}
const {
audioChannels,
audioCodec,
audioLanguages,
subtitles,
videoCodec,
videoDynamicRangeType,
} = movieFile.mediaInfo;
if (type === 'audio') {
return (
<span>
{audioCodec ? audioCodec : ''}
{audioCodec && audioChannels ? ' - ' : ''}
{audioChannels ? audioChannels.toFixed(1) : ''}
</span>
);
}
if (type === 'audioLanguages') {
return formatLanguages(audioLanguages);
}
if (type === 'subtitles') {
return formatLanguages(subtitles);
}
if (type === 'video') {
return <span>{videoCodec}</span>;
}
if (type === 'videoDynamicRangeType') {
return <span>{videoDynamicRangeType}</span>;
}
return null;
}
export default MediaInfo;
@@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
import MediaInfo from './MediaInfo';
function createMapStateToProps() {
return createSelector(
createMovieFileSelector(),
(movieFile) => {
if (movieFile) {
return {
...movieFile.mediaInfo
};
}
return {};
}
);
}
export default connect(createMapStateToProps)(MediaInfo);
@@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieLanguages from 'Movie/MovieLanguages';
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
function createMapStateToProps() {
return createSelector(
createMovieFileSelector(),
(movieFile) => {
return {
languages: movieFile ? movieFile.languages : undefined
};
}
);
}
export default connect(createMapStateToProps)(MovieLanguages);
@@ -0,0 +1,15 @@
import React from 'react';
import MovieLanguages from 'Movie/MovieLanguages';
import useMovieFile from './useMovieFile';
interface MovieFileLanguagesProps {
movieFileId: number;
}
function MovieFileLanguages({ movieFileId }: MovieFileLanguagesProps) {
const movieFile = useMovieFile(movieFileId);
return <MovieLanguages languages={movieFile?.languages ?? []} />;
}
export default MovieFileLanguages;
+18
View File
@@ -0,0 +1,18 @@
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createMovieFileSelector(movieFileId?: number) {
return createSelector(
(state: AppState) => state.movieFiles.items,
(movieFiles) => {
return movieFiles.find(({ id }) => id === movieFileId);
}
);
}
function useMovieFile(movieFileId: number | undefined) {
return useSelector(createMovieFileSelector(movieFileId));
}
export default useMovieFile;
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditCustomFormatModal from './EditCustomFormatModal'; import EditCustomFormatModal from './EditCustomFormatModal';
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
function mapStateToProps() { function mapStateToProps() {
return {}; return {};
@@ -36,6 +37,7 @@ class EditCustomFormatModalConnector extends Component {
} }
EditCustomFormatModalConnector.propTypes = { EditCustomFormatModalConnector.propTypes = {
...EditCustomFormatModalContentConnector.propTypes,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired clearPendingChanges: PropTypes.func.isRequired
}; };
@@ -47,6 +47,11 @@ const COLUMNS = [
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{
name: 'actions',
label: '',
isVisible: true,
},
]; ];
interface ManageCustomFormatsModalContentProps { interface ManageCustomFormatsModalContentProps {
@@ -4,3 +4,9 @@
word-break: break-all; word-break: break-all;
} }
.actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 40px;
}
@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string;
'includeCustomFormatWhenRenaming': string; 'includeCustomFormatWhenRenaming': string;
'name': string; 'name': string;
} }
@@ -1,10 +1,18 @@
import React, { useCallback } from 'react'; import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column'; import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import { deleteCustomFormat } from 'Store/Actions/settingsActions';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import EditCustomFormatModalConnector from '../EditCustomFormatModalConnector';
import styles from './ManageCustomFormatsModalRow.css'; import styles from './ManageCustomFormatsModalRow.css';
interface ManageCustomFormatsModalRowProps { interface ManageCustomFormatsModalRowProps {
@@ -16,6 +24,15 @@ interface ManageCustomFormatsModalRowProps {
onSelectedChange(result: SelectStateInputProps): void; onSelectedChange(result: SelectStateInputProps): void;
} }
function isDeletingSelector() {
return createSelector(
(state: AppState) => state.settings.customFormats.isDeleting,
(isDeleting) => {
return isDeleting;
}
);
}
function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
const { const {
id, id,
@@ -25,7 +42,16 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
onSelectedChange, onSelectedChange,
} = props; } = props;
const onSelectedChangeWrapper = useCallback( const dispatch = useDispatch();
const isDeleting = useSelector(isDeletingSelector());
const [isEditCustomFormatModalOpen, setIsEditCustomFormatModalOpen] =
useState(false);
const [isDeleteCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen] =
useState(false);
const handlelectedChange = useCallback(
(result: SelectStateInputProps) => { (result: SelectStateInputProps) => {
onSelectedChange({ onSelectedChange({
...result, ...result,
@@ -34,12 +60,33 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
[onSelectedChange] [onSelectedChange]
); );
const handleEditCustomFormatModalOpen = useCallback(() => {
setIsEditCustomFormatModalOpen(true);
}, [setIsEditCustomFormatModalOpen]);
const handleEditCustomFormatModalClose = useCallback(() => {
setIsEditCustomFormatModalOpen(false);
}, [setIsEditCustomFormatModalOpen]);
const handleDeleteCustomFormatPress = useCallback(() => {
setIsEditCustomFormatModalOpen(false);
setIsDeleteCustomFormatModalOpen(true);
}, [setIsEditCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen]);
const handleDeleteCustomFormatModalClose = useCallback(() => {
setIsDeleteCustomFormatModalOpen(false);
}, [setIsDeleteCustomFormatModalOpen]);
const handleConfirmDeleteCustomFormat = useCallback(() => {
dispatch(deleteCustomFormat({ id }));
}, [id, dispatch]);
return ( return (
<TableRow> <TableRow>
<TableSelectCell <TableSelectCell
id={id} id={id}
isSelected={isSelected} isSelected={isSelected}
onSelectedChange={onSelectedChangeWrapper} onSelectedChange={handlelectedChange}
/> />
<TableRowCell className={styles.name}>{name}</TableRowCell> <TableRowCell className={styles.name}>{name}</TableRowCell>
@@ -47,6 +94,31 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
<TableRowCell className={styles.includeCustomFormatWhenRenaming}> <TableRowCell className={styles.includeCustomFormatWhenRenaming}>
{includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')} {includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
name={icons.EDIT}
onPress={handleEditCustomFormatModalOpen}
/>
</TableRowCell>
<EditCustomFormatModalConnector
id={id}
isOpen={isEditCustomFormatModalOpen}
onModalClose={handleEditCustomFormatModalClose}
onDeleteCustomFormatPress={handleDeleteCustomFormatPress}
/>
<ConfirmModal
isOpen={isDeleteCustomFormatModalOpen}
kind="danger"
title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={handleConfirmDeleteCustomFormat}
onCancel={handleDeleteCustomFormatModalClose}
/>
</TableRow> </TableRow>
); );
} }
@@ -12,7 +12,7 @@ function ManageCustomFormatsToolbarButton() {
return ( return (
<> <>
<PageToolbarButton <PageToolbarButton
label={translate('ManageCustomFormats')} label={translate('ManageFormats')}
iconName={icons.MANAGE} iconName={icons.MANAGE}
onPress={openManageModal} onPress={openManageModal}
/> />
@@ -55,10 +55,10 @@ function EditSpecificationModalContent(props) {
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} /> <InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} /> <InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} /> <InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
</div> </div>
</Alert> </Alert>
} }
@@ -40,7 +40,7 @@ function createImportListExclusionSelector(id?: number) {
importListExclusions; importListExclusions;
const mapping = id const mapping = id
? items.find((i) => i.id === id) ? items.find((i) => i.id === id)!
: newImportListExclusion; : newImportListExclusion;
const settings = selectSettings(mapping, pendingChanges, saveError); const settings = selectSettings(mapping, pendingChanges, saveError);
@@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import EditMetadataModalContentConnector from './EditMetadataModalContentConnector';
function EditMetadataModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditMetadataModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditMetadataModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditMetadataModal;
@@ -0,0 +1,43 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMetadataModalContent, {
EditMetadataModalContentProps,
} from './EditMetadataModalContent';
interface EditMetadataModalProps
extends Omit<EditMetadataModalContentProps, 'advancedSettings'> {
isOpen: boolean;
}
function EditMetadataModal({
isOpen,
onModalClose,
...otherProps
}: EditMetadataModalProps) {
const dispatch = useDispatch();
const advancedSettings = useSelector(
(state: AppState) => state.settings.advancedSettings
);
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section: 'metadata' }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditMetadataModalContent
{...otherProps}
advancedSettings={advancedSettings}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditMetadataModal;
@@ -1,44 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMetadataModal from './EditMetadataModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.metadata';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
}
};
}
class EditMetadataModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges({ section: 'metadata' });
this.props.onModalClose();
};
//
// Render
render() {
return (
<EditMetadataModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditMetadataModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditMetadataModalConnector);
@@ -0,0 +1,5 @@
.message {
composes: alert from '~Components/Alert.css';
margin-bottom: 30px;
}
@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,105 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function EditMetadataModalContent(props) {
const {
advancedSettings,
isSaving,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
...otherProps
} = props;
const {
name,
enable,
fields
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('EditMetadata', { metadataType: name.value })}
</ModalHeader>
<ModalBody>
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enable"
helpText={translate('EnableMetadataHelpText')}
{...enable}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="metadata"
{...field}
isDisabled={!enable.value}
onChange={onFieldChange}
/>
);
})
}
</Form>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditMetadataModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onDeleteMetadataPress: PropTypes.func
};
export default EditMetadataModalContent;
@@ -0,0 +1,128 @@
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import {
saveMetadata,
setMetadataFieldValue,
setMetadataValue,
} from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './EditMetadataModalContent.css';
export interface EditMetadataModalContentProps {
id: number;
advancedSettings: boolean;
onModalClose: () => void;
}
function EditMetadataModalContent({
id,
advancedSettings,
onModalClose,
}: EditMetadataModalContentProps) {
const dispatch = useDispatch();
const { isSaving, saveError, pendingChanges, items } = useSelector(
(state: AppState) => state.settings.metadata
);
const { settings, ...otherSettings } = useMemo(() => {
const item = items.find((item) => item.id === id)!;
return selectSettings(item, pendingChanges, saveError);
}, [id, items, pendingChanges, saveError]);
const { name, enable, fields, message } = settings;
const handleInputChange = useCallback(
({ name, value }: InputChanged) => {
// @ts-expect-error not typed
dispatch(setMetadataValue({ name, value }));
},
[dispatch]
);
const handleFieldChange = useCallback(
({ name, value }: InputChanged) => {
// @ts-expect-error not typed
dispatch(setMetadataFieldValue({ name, value }));
},
[dispatch]
);
const handleSavePress = useCallback(() => {
dispatch(saveMetadata({ id }));
}, [id, dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('EditMetadata', { metadataType: name.value })}
</ModalHeader>
<ModalBody>
<Form {...otherSettings}>
{message ? (
<Alert className={styles.message} kind={message.value.type}>
{message.value.message}
</Alert>
) : null}
<FormGroup>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enable"
helpText={translate('EnableMetadataHelpText')}
{...enable}
onChange={handleInputChange}
/>
</FormGroup>
{fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="metadata"
{...field}
isDisabled={!enable.value}
onChange={handleFieldChange}
/>
);
})}
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
export default EditMetadataModalContent;
@@ -1,95 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveMetadata, setMetadataFieldValue, setMetadataValue } from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings';
import EditMetadataModalContent from './EditMetadataModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
(state, { id }) => id,
(state) => state.settings.metadata,
(advancedSettings, id, metadata) => {
const {
isSaving,
saveError,
pendingChanges,
items
} = metadata;
const settings = selectSettings(_.find(items, { id }), pendingChanges, saveError);
return {
advancedSettings,
id,
isSaving,
saveError,
item: settings.settings,
...settings
};
}
);
}
const mapDispatchToProps = {
setMetadataValue,
setMetadataFieldValue,
saveMetadata
};
class EditMetadataModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setMetadataValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setMetadataFieldValue({ name, value });
};
onSavePress = () => {
this.props.saveMetadata({ id: this.props.id });
};
//
// Render
render() {
return (
<EditMetadataModalContent
{...this.props}
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditMetadataModalContentConnector.propTypes = {
id: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setMetadataValue: PropTypes.func.isRequired,
setMetadataFieldValue: PropTypes.func.isRequired,
saveMetadata: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditMetadataModalContentConnector);
@@ -1,150 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditMetadataModalConnector from './EditMetadataModalConnector';
import styles from './Metadata.css';
class Metadata extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditMetadataModalOpen: false
};
}
//
// Listeners
onEditMetadataPress = () => {
this.setState({ isEditMetadataModalOpen: true });
};
onEditMetadataModalClose = () => {
this.setState({ isEditMetadataModalOpen: false });
};
//
// Render
render() {
const {
id,
name,
enable,
fields
} = this.props;
const metadataFields = [];
const imageFields = [];
fields.forEach((field) => {
if (field.section === 'metadata') {
metadataFields.push(field);
} else {
imageFields.push(field);
}
});
return (
<Card
className={styles.metadata}
overlayContent={true}
onPress={this.onEditMetadataPress}
>
<div className={styles.name}>
{name}
</div>
<div>
{
enable ?
<Label kind={kinds.SUCCESS}>
{translate('Enabled')}
</Label> :
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
</div>
{
enable && !!metadataFields.length &&
<div>
<div className={styles.section}>
{translate('Metadata')}
</div>
{
metadataFields.map((field) => {
if (!field.value) {
return null;
}
return (
<Label
key={field.label}
kind={kinds.SUCCESS}
>
{field.label}
</Label>
);
})
}
</div>
}
{
enable && !!imageFields.length &&
<div>
<div className={styles.section}>
{translate('Images')}
</div>
{
imageFields.map((field) => {
if (!field.value) {
return null;
}
return (
<Label
key={field.label}
kind={kinds.SUCCESS}
>
{field.label}
</Label>
);
})
}
</div>
}
<EditMetadataModalConnector
id={id}
isOpen={this.state.isEditMetadataModalOpen}
onModalClose={this.onEditMetadataModalClose}
/>
</Card>
);
}
}
Metadata.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enable: PropTypes.bool.isRequired,
fields: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default Metadata;
@@ -0,0 +1,106 @@
import React, { useCallback, useMemo, useState } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import Field from 'typings/Field';
import translate from 'Utilities/String/translate';
import EditMetadataModal from './EditMetadataModal';
import styles from './Metadata.css';
interface MetadataProps {
id: number;
name: string;
enable: boolean;
fields: Field[];
}
function Metadata({ id, name, enable, fields }: MetadataProps) {
const [isEditMetadataModalOpen, setIsEditMetadataModalOpen] = useState(false);
const { metadataFields, imageFields } = useMemo(() => {
return fields.reduce<{ metadataFields: Field[]; imageFields: Field[] }>(
(acc, field) => {
if (field.section === 'metadata') {
acc.metadataFields.push(field);
} else {
acc.imageFields.push(field);
}
return acc;
},
{ metadataFields: [], imageFields: [] }
);
}, [fields]);
const handleOpenPress = useCallback(() => {
setIsEditMetadataModalOpen(true);
}, []);
const handleModalClose = useCallback(() => {
setIsEditMetadataModalOpen(false);
}, []);
return (
<Card
className={styles.metadata}
overlayContent={true}
onPress={handleOpenPress}
>
<div className={styles.name}>{name}</div>
<div>
{enable ? (
<Label kind={kinds.SUCCESS}>{translate('Enabled')}</Label>
) : (
<Label kind={kinds.DISABLED} outline={true}>
{translate('Disabled')}
</Label>
)}
</div>
{enable && metadataFields.length ? (
<div>
<div className={styles.section}>{translate('Metadata')}</div>
{metadataFields.map((field) => {
if (!field.value) {
return null;
}
return (
<Label key={field.label} kind={kinds.SUCCESS}>
{field.label}
</Label>
);
})}
</div>
) : null}
{enable && imageFields.length ? (
<div>
<div className={styles.section}>{translate('Images')}</div>
{imageFields.map((field) => {
if (!field.value) {
return null;
}
return (
<Label key={field.label} kind={kinds.SUCCESS}>
{field.label}
</Label>
);
})}
</div>
) : null}
<EditMetadataModal
id={id}
isOpen={isEditMetadataModalOpen}
onModalClose={handleModalClose}
/>
</Card>
);
}
export default Metadata;
@@ -1,44 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import FieldSet from 'Components/FieldSet';
import PageSectionContent from 'Components/Page/PageSectionContent';
import translate from 'Utilities/String/translate';
import Metadata from './Metadata';
import styles from './Metadatas.css';
function Metadatas(props) {
const {
items,
...otherProps
} = props;
return (
<FieldSet legend={translate('Metadata')}>
<PageSectionContent
errorMessage={translate('MetadataLoadError')}
{...otherProps}
>
<div className={styles.metadatas}>
{
items.map((item) => {
return (
<Metadata
key={item.id}
{...item}
/>
);
})
}
</div>
</PageSectionContent>
</FieldSet>
);
}
Metadatas.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default Metadatas;
@@ -0,0 +1,52 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import MetadataAppState from 'App/State/MetadataAppState';
import FieldSet from 'Components/FieldSet';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { fetchMetadata } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import MetadataType from 'typings/Metadata';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import Metadata from './Metadata';
import styles from './Metadatas.css';
function createMetadatasSelector() {
return createSelector(
createSortedSectionSelector<MetadataType>(
'settings.metadata',
sortByProp('name')
),
(metadata: MetadataAppState) => metadata
);
}
function Metadatas() {
const dispatch = useDispatch();
const { isFetching, error, items, ...otherProps } = useSelector(
createMetadatasSelector()
);
useEffect(() => {
dispatch(fetchMetadata());
}, [dispatch]);
return (
<FieldSet legend={translate('Metadata')}>
<PageSectionContent
isFetching={isFetching}
errorMessage={translate('MetadataLoadError')}
{...otherProps}
>
<div className={styles.metadatas}>
{items.map((item) => {
return <Metadata key={item.id} {...item} />;
})}
</div>
</PageSectionContent>
</FieldSet>
);
}
export default Metadatas;
@@ -1,47 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchMetadata } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import Metadatas from './Metadatas';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.metadata', sortByProp('name')),
(metadata) => metadata
);
}
const mapDispatchToProps = {
fetchMetadata
};
class MetadatasConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchMetadata();
}
//
// Render
render() {
return (
<Metadatas
{...this.props}
onConfirmDeleteMetadata={this.onConfirmDeleteMetadata}
/>
);
}
}
MetadatasConnector.propTypes = {
fetchMetadata: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
@@ -3,7 +3,7 @@ import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import MetadatasConnector from './Metadata/MetadatasConnector'; import Metadatas from './Metadata/Metadatas';
import MetadataOptionsConnector from './Options/MetadataOptionsConnector'; import MetadataOptionsConnector from './Options/MetadataOptionsConnector';
class MetadataSettings extends Component { class MetadataSettings extends Component {
@@ -62,7 +62,7 @@ class MetadataSettings extends Component {
onChildStateChange={this.onChildStateChange} onChildStateChange={this.onChildStateChange}
/> />
<MetadatasConnector /> <Metadatas />
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );
@@ -19,14 +19,15 @@ import {
setReleaseProfileValue, setReleaseProfileValue,
} from 'Store/Actions/Settings/releaseProfiles'; } from 'Store/Actions/Settings/releaseProfiles';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
import { PendingSection } from 'typings/pending';
import ReleaseProfile from 'typings/Settings/ReleaseProfile'; import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditReleaseProfileModalContent.css'; import styles from './EditReleaseProfileModalContent.css';
const tagInputDelimiters = ['Tab', 'Enter']; const tagInputDelimiters = ['Tab', 'Enter'];
const newReleaseProfile = { const newReleaseProfile: ReleaseProfile = {
id: 0,
name: '',
enabled: true, enabled: true,
required: [], required: [],
ignored: [], ignored: [],
@@ -41,8 +42,12 @@ function createReleaseProfileSelector(id?: number) {
const { items, isFetching, error, isSaving, saveError, pendingChanges } = const { items, isFetching, error, isSaving, saveError, pendingChanges } =
releaseProfiles; releaseProfiles;
const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile; const mapping = id ? items.find((i) => i.id === id)! : newReleaseProfile;
const settings = selectSettings(mapping, pendingChanges, saveError); const settings = selectSettings<ReleaseProfile>(
mapping,
pendingChanges,
saveError
);
return { return {
id, id,
@@ -50,7 +55,7 @@ function createReleaseProfileSelector(id?: number) {
error, error,
isSaving, isSaving,
saveError, saveError,
item: settings.settings as PendingSection<ReleaseProfile>, item: settings.settings,
...settings, ...settings,
}; };
} }
@@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} /> <InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} /> <InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
</div> </div>
<div> <div>
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} /> <InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
</div> </div>
</Alert> </Alert>
} }
@@ -3,6 +3,7 @@ import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import sortByProp from 'Utilities/Array/sortByProp';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import naturalExpansion from 'Utilities/String/naturalExpansion'; import naturalExpansion from 'Utilities/String/naturalExpansion';
import { set, update, updateItem } from './baseActions'; import { set, update, updateItem } from './baseActions';
@@ -30,6 +31,7 @@ export const defaultState = {
items: [], items: [],
sortKey: 'relativePath', sortKey: 'relativePath',
sortDirection: sortDirections.ASCENDING, sortDirection: sortDirections.ASCENDING,
favoriteFolders: [],
recentFolders: [], recentFolders: [],
importMode: 'chooseImportMode', importMode: 'chooseImportMode',
sortPredicates: { sortPredicates: {
@@ -58,6 +60,7 @@ export const defaultState = {
export const persistState = [ export const persistState = [
'interactiveImport.sortKey', 'interactiveImport.sortKey',
'interactiveImport.sortDirection', 'interactiveImport.sortDirection',
'interactiveImport.favoriteFolders',
'interactiveImport.recentFolders', 'interactiveImport.recentFolders',
'interactiveImport.importMode' 'interactiveImport.importMode'
]; ];
@@ -73,6 +76,8 @@ export const UPDATE_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/updateInteract
export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport'; export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport';
export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder'; export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder'; export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
export const ADD_FAVORITE_FOLDER = 'interactiveImport/addFavoriteFolder';
export const REMOVE_FAVORITE_FOLDER = 'interactiveImport/removeFavoriteFolder';
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode'; export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
// //
@@ -86,6 +91,8 @@ export const updateInteractiveImportItems = createAction(UPDATE_INTERACTIVE_IMPO
export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT); export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT);
export const addRecentFolder = createAction(ADD_RECENT_FOLDER); export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER); export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
export const addFavoriteFolder = createAction(ADD_FAVORITE_FOLDER);
export const removeFavoriteFolder = createAction(REMOVE_FAVORITE_FOLDER);
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE); export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
// //
@@ -264,9 +271,31 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { recentFolders }); return Object.assign({}, state, { recentFolders });
}, },
[ADD_FAVORITE_FOLDER]: function(state, { payload }) {
const folder = payload.folder;
const favoriteFolder = { folder };
const favoriteFolders = [...state.favoriteFolders, favoriteFolder].sort(sortByProp('folder'));
return Object.assign({}, state, { favoriteFolders });
},
[REMOVE_FAVORITE_FOLDER]: function(state, { payload }) {
const folder = payload.folder;
const favoriteFolders = state.favoriteFolders.reduce((acc, item) => {
if (item.folder !== folder) {
acc.push(item);
}
return acc;
}, []);
return Object.assign({}, state, { favoriteFolders });
},
[CLEAR_INTERACTIVE_IMPORT]: function(state) { [CLEAR_INTERACTIVE_IMPORT]: function(state) {
const newState = { const newState = {
...defaultState, ...defaultState,
favoriteFolders: state.favoriteFolders,
recentFolders: state.recentFolders, recentFolders: state.recentFolders,
importMode: state.importMode importMode: state.importMode
}; };
@@ -91,12 +91,8 @@ export const defaultState = {
genres: function(item, filterValue, type) { genres: function(item, filterValue, type) {
const predicate = filterTypePredicates[type]; const predicate = filterTypePredicates[type];
let allGenres = []; const allGenres = item.movies.flatMap(({ genres }) => genres);
item.movies.forEach((movie) => { const genres = Array.from(new Set(allGenres));
allGenres = allGenres.concat(movie.genres);
});
const genres = Array.from(new Set(allGenres)).slice(0, 3);
return predicate(genres, filterValue); return predicate(genres, filterValue);
}, },
@@ -138,12 +134,8 @@ export const defaultState = {
type: filterBuilderTypes.ARRAY, type: filterBuilderTypes.ARRAY,
optionsSelector: function(items) { optionsSelector: function(items) {
const genreList = items.reduce((acc, collection) => { const genreList = items.reduce((acc, collection) => {
let collectionGenres = []; const collectionGenres = collection.movies.flatMap(({ genres }) => genres);
collection.movies.forEach((movie) => { const genres = Array.from(new Set(collectionGenres));
collectionGenres = collectionGenres.concat(movie.genres);
});
const genres = Array.from(new Set(collectionGenres)).slice(0, 3);
genres.forEach((genre) => { genres.forEach((genre) => {
acc.push({ acc.push({
@@ -201,6 +201,12 @@ export const defaultState = {
label: () => translate('Protocol'), label: () => translate('Protocol'),
type: filterBuilderTypes.EQUAL, type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.PROTOCOL valueType: filterBuilderValueTypes.PROTOCOL
},
{
name: 'status',
label: () => translate('Status'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.QUEUE_STATUS
} }
] ]
}, },
@@ -1,13 +1,14 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import AppState from 'App/State/AppState'; import AppState from 'App/State/AppState';
import Movie from 'Movie/Movie'; import Movie from 'Movie/Movie';
import QualityProfile from 'typings/QualityProfile';
import { createMovieSelectorForHook } from './createMovieSelector'; import { createMovieSelectorForHook } from './createMovieSelector';
function createMovieQualityProfileSelector(movieId: number) { function createMovieQualityProfileSelector(movieId: number) {
return createSelector( return createSelector(
(state: AppState) => state.settings.qualityProfiles.items, (state: AppState) => state.settings.qualityProfiles.items,
createMovieSelectorForHook(movieId), createMovieSelectorForHook(movieId),
(qualityProfiles, movie = {} as Movie) => { (qualityProfiles: QualityProfile[], movie = {} as Movie) => {
return qualityProfiles.find( return qualityProfiles.find(
(profile) => profile.id === movie.qualityProfileId (profile) => profile.id === movie.qualityProfileId
); );
@@ -1,104 +0,0 @@
import _ from 'lodash';
function getValidationFailures(saveError) {
if (!saveError || saveError.status !== 400) {
return [];
}
return _.cloneDeep(saveError.responseJSON);
}
function mapFailure(failure) {
return {
message: failure.errorMessage,
link: failure.infoLink,
detailedMessage: failure.detailedDescription
};
}
function selectSettings(item, pendingChanges, saveError) {
const validationFailures = getValidationFailures(saveError);
// Merge all settings from the item along with pending
// changes to ensure any settings that were not included
// with the item are included.
const allSettings = Object.assign({}, item, pendingChanges);
const settings = _.reduce(allSettings, (result, value, key) => {
if (key === 'fields') {
return result;
}
// Return a flattened value
if (key === 'implementationName') {
result.implementationName = item[key];
return result;
}
const setting = {
value: item[key],
errors: _.map(_.remove(validationFailures, (failure) => {
return failure.propertyName.toLowerCase() === key.toLowerCase() && !failure.isWarning;
}), mapFailure),
warnings: _.map(_.remove(validationFailures, (failure) => {
return failure.propertyName.toLowerCase() === key.toLowerCase() && failure.isWarning;
}), mapFailure)
};
if (pendingChanges.hasOwnProperty(key)) {
setting.previousValue = setting.value;
setting.value = pendingChanges[key];
setting.pending = true;
}
result[key] = setting;
return result;
}, {});
const fields = _.reduce(item.fields, (result, f) => {
const field = Object.assign({ pending: false }, f);
const hasPendingFieldChange = pendingChanges.fields && pendingChanges.fields.hasOwnProperty(field.name);
if (hasPendingFieldChange) {
field.previousValue = field.value;
field.value = pendingChanges.fields[field.name];
field.pending = true;
}
field.errors = _.map(_.remove(validationFailures, (failure) => {
return failure.propertyName.toLowerCase() === field.name.toLowerCase() && !failure.isWarning;
}), mapFailure);
field.warnings = _.map(_.remove(validationFailures, (failure) => {
return failure.propertyName.toLowerCase() === field.name.toLowerCase() && failure.isWarning;
}), mapFailure);
result.push(field);
return result;
}, []);
if (fields.length) {
settings.fields = fields;
}
const validationErrors = _.filter(validationFailures, (failure) => {
return !failure.isWarning;
});
const validationWarnings = _.filter(validationFailures, (failure) => {
return failure.isWarning;
});
return {
settings,
validationErrors,
validationWarnings,
hasPendingChanges: !_.isEmpty(pendingChanges),
hasSettings: !_.isEmpty(settings),
pendingChanges
};
}
export default selectSettings;
@@ -0,0 +1,168 @@
import { cloneDeep, isEmpty } from 'lodash';
import { Error } from 'App/State/AppSectionState';
import Field from 'typings/Field';
import {
Failure,
Pending,
PendingField,
PendingSection,
ValidationError,
ValidationFailure,
ValidationWarning,
} from 'typings/pending';
interface ValidationFailures {
errors: ValidationError[];
warnings: ValidationWarning[];
}
function getValidationFailures(saveError?: Error): ValidationFailures {
if (!saveError || saveError.status !== 400) {
return {
errors: [],
warnings: [],
};
}
return cloneDeep(saveError.responseJSON as ValidationFailure[]).reduce(
(acc: ValidationFailures, failure: ValidationFailure) => {
if (failure.isWarning) {
acc.warnings.push(failure as ValidationWarning);
} else {
acc.errors.push(failure as ValidationError);
}
return acc;
},
{
errors: [],
warnings: [],
}
);
}
function getFailures(failures: ValidationFailure[], key: string) {
const result = [];
for (let i = failures.length - 1; i >= 0; i--) {
if (failures[i].propertyName.toLowerCase() === key.toLowerCase()) {
result.unshift(mapFailure(failures[i]));
failures.splice(i, 1);
}
}
return result;
}
function mapFailure(failure: ValidationFailure): Failure {
return {
errorMessage: failure.errorMessage,
infoLink: failure.infoLink,
detailedDescription: failure.detailedDescription,
// TODO: Remove these renamed properties
message: failure.errorMessage,
link: failure.infoLink,
detailedMessage: failure.detailedDescription,
};
}
interface ModelBaseSetting {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[id: string]: any;
}
function selectSettings<T extends ModelBaseSetting>(
item: T,
pendingChanges: Partial<ModelBaseSetting>,
saveError?: Error
) {
const { errors, warnings } = getValidationFailures(saveError);
// Merge all settings from the item along with pending
// changes to ensure any settings that were not included
// with the item are included.
const allSettings = Object.assign({}, item, pendingChanges);
const settings = Object.keys(allSettings).reduce(
(acc: PendingSection<T>, key) => {
if (key === 'fields') {
return acc;
}
// Return a flattened value
if (key === 'implementationName') {
acc.implementationName = item[key];
return acc;
}
const setting: Pending<T> = {
value: item[key],
pending: false,
errors: getFailures(errors, key),
warnings: getFailures(warnings, key),
};
if (pendingChanges.hasOwnProperty(key)) {
setting.previousValue = setting.value;
setting.value = pendingChanges[key];
setting.pending = true;
}
// @ts-expect-error - This is a valid key
acc[key] = setting;
return acc;
},
{} as PendingSection<T>
);
if ('fields' in item) {
const fields =
(item.fields as Field[]).reduce((acc: PendingField<T>[], f) => {
const field: PendingField<T> = Object.assign(
{ pending: false, errors: [], warnings: [] },
f
);
if ('fields' in pendingChanges) {
const pendingChangesFields = pendingChanges.fields as Record<
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any
>;
if (pendingChangesFields.hasOwnProperty(field.name)) {
field.previousValue = field.value;
field.value = pendingChangesFields[field.name];
field.pending = true;
}
}
field.errors = getFailures(errors, field.name);
field.warnings = getFailures(warnings, field.name);
acc.push(field);
return acc;
}, []) ?? [];
if (fields.length) {
settings.fields = fields;
}
}
const validationErrors = errors;
const validationWarnings = warnings;
return {
settings,
validationErrors,
validationWarnings,
hasPendingChanges: !isEmpty(pendingChanges),
hasSettings: !isEmpty(settings),
pendingChanges,
};
}
export default selectSettings;
@@ -0,0 +1,9 @@
export type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
function getEntries<T extends object>(obj: T): Entries<T> {
return Object.entries(obj) as Entries<T>;
}
export default getEntries;
@@ -35,7 +35,7 @@ export default function getLanguageName(code: string) {
try { try {
return languageNames.of(code) ?? code; return languageNames.of(code) ?? code;
} catch (error) { } catch {
return code; return code;
} }
} }
+7 -1
View File
@@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise<boolean> {
translations = data.Strings; translations = data.Strings;
resolve(true); resolve(true);
} catch (error) { } catch {
resolve(false); resolve(false);
} }
}); });
@@ -27,6 +27,12 @@ export default function translate(
key: string, key: string,
tokens: Record<string, string | number | boolean> = {} tokens: Record<string, string | number | boolean> = {}
) { ) {
const { isProduction = true } = window.Radarr;
if (!isProduction && !(key in translations)) {
console.warn(`Missing translation for key: ${key}`);
}
const translation = translations[key] || key; const translation = translations[key] || key;
tokens.appName = 'Radarr'; tokens.appName = 'Radarr';
+6 -15
View File
@@ -153,12 +153,15 @@ class CutoffUnmet extends Component {
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton <PageToolbarButton
label={translate('SearchSelected')} label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
iconName={icons.SEARCH} iconName={icons.SEARCH}
isDisabled={!itemsSelected || isSearchingForCutoffUnmetMovies} isDisabled={isSearchingForCutoffUnmetMovies}
onPress={this.onSearchSelectedPress} isSpinning={isSearchingForCutoffUnmetMovies}
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllCutoffUnmetPress}
/> />
<PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')} label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
iconName={icons.MONITORED} iconName={icons.MONITORED}
@@ -166,18 +169,6 @@ class CutoffUnmet extends Component {
isSpinning={isSaving} isSpinning={isSaving}
onPress={this.onToggleSelectedPress} onPress={this.onToggleSelectedPress}
/> />
<PageToolbarSeparator />
<PageToolbarButton
label={translate('SearchAll')}
iconName={icons.SEARCH}
isDisabled={!items.length}
isSpinning={isSearchingForCutoffUnmetMovies}
onPress={this.onSearchAllCutoffUnmetPress}
/>
<PageToolbarSeparator />
</PageToolbarSection> </PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}> <PageToolbarSection alignContent={align.RIGHT}>
@@ -18,9 +18,10 @@ function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.wanted.cutoffUnmet, (state) => state.wanted.cutoffUnmet,
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH), createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH),
(cutoffUnmet, isSearchingForCutoffUnmetMovies) => { createCommandExecutingSelector(commandNames.MOVIE_SEARCH),
(cutoffUnmet, isSearchingForCutoffUnmetMovies, isSearchingForSelectedCutoffUnmetMovies) => {
return { return {
isSearchingForCutoffUnmetMovies, isSearchingForCutoffUnmetMovies: isSearchingForCutoffUnmetMovies || isSearchingForSelectedCutoffUnmetMovies,
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1, isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
...cutoffUnmet ...cutoffUnmet
}; };
@@ -8,7 +8,7 @@ import movieEntities from 'Movie/movieEntities';
import MovieSearchCell from 'Movie/MovieSearchCell'; import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatusConnector from 'Movie/MovieStatusConnector'; import MovieStatusConnector from 'Movie/MovieStatusConnector';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import MovieFileLanguageConnector from 'MovieFile/MovieFileLanguageConnector'; import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
import styles from './CutoffUnmetRow.css'; import styles from './CutoffUnmetRow.css';
function CutoffUnmetRow(props) { function CutoffUnmetRow(props) {
@@ -104,7 +104,7 @@ function CutoffUnmetRow(props) {
key={name} key={name}
className={styles.languages} className={styles.languages}
> >
<MovieFileLanguageConnector <MovieFileLanguages
movieFileId={movieFileId} movieFileId={movieFileId}
/> />
</TableRowCell> </TableRowCell>
+6 -13
View File
@@ -159,12 +159,15 @@ class Missing extends Component {
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton <PageToolbarButton
label={translate('SearchSelected')} label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
iconName={icons.SEARCH} iconName={icons.SEARCH}
isDisabled={!itemsSelected || isSearchingForMissingMovies} isSpinning={isSearchingForMissingMovies}
onPress={this.onSearchSelectedPress} isDisabled={isSearchingForMissingMovies}
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllMissingPress}
/> />
<PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')} label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
iconName={icons.MONITORED} iconName={icons.MONITORED}
@@ -175,16 +178,6 @@ class Missing extends Component {
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton
label={translate('SearchAll')}
iconName={icons.SEARCH}
isDisabled={!items.length}
isSpinning={isSearchingForMissingMovies}
onPress={this.onSearchAllMissingPress}
/>
<PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
label={translate('ManualImport')} label={translate('ManualImport')}
iconName={icons.INTERACTIVE} iconName={icons.INTERACTIVE}
@@ -17,9 +17,10 @@ function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.wanted.missing, (state) => state.wanted.missing,
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH), createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH),
(missing, isSearchingForMissingMovies) => { createCommandExecutingSelector(commandNames.MOVIE_SEARCH),
(missing, isSearchingForMissingMovies, isSearchingForSelectedMissingMovies) => {
return { return {
isSearchingForMissingMovies, isSearchingForMissingMovies: isSearchingForMissingMovies || isSearchingForSelectedMissingMovies,
isSaving: missing.items.filter((m) => m.isSaving).length > 1, isSaving: missing.items.filter((m) => m.isSaving).length > 1,
...missing ...missing
}; };
+4 -5
View File
@@ -1,6 +1,6 @@
import { createBrowserHistory } from 'history'; import { createBrowserHistory } from 'history';
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { createRoot } from 'react-dom/client';
import createAppStore from 'Store/createAppStore'; import createAppStore from 'Store/createAppStore';
import App from './App/App'; import App from './App/App';
@@ -9,9 +9,8 @@ import 'Diag/ConsoleApi';
export async function bootstrap() { export async function bootstrap() {
const history = createBrowserHistory(); const history = createBrowserHistory();
const store = createAppStore(history); const store = createAppStore(history);
const container = document.getElementById('root');
render( const root = createRoot(container!); // createRoot(container!) if you use TypeScript
<App store={store} history={history} />, root.render(<App store={store} history={history} />);
document.getElementById('root')
);
} }

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