Compare commits

...

225 Commits

Author SHA1 Message Date
Stevie Robinson
73494c462c Fixed: Listening on all IPv4 Addresses
(cherry picked from commit 035c474f10c257331a5f47e863d24af82537e335)
2025-01-05 14:27:33 +02:00
Bogdan
36f6896f30 Fixed: (PassThePopcorn) Increase rate limit 2025-01-02 23:20:39 +02:00
Bogdan
e01741a69e New: (AnimeBytes) Use error message from response 2024-12-31 16:33:19 +02:00
Bogdan
1dbff1235e Match single digits only in season number regex for AnimeBytes 2024-12-31 16:26:05 +02:00
Bogdan
1a9ad6b363 Suggest adding IP to RPC whitelist for on failed Transmission auth
(cherry picked from commit f05e552e8e6dc02cd26444073ab9a678dcb36492)
2024-12-31 12:23:32 +02:00
Bogdan
c88249300c Check if backup folder is writable on backup
(cherry picked from commit 8aad79fd3e14eb885724a5e5790803c289be2f25)
2024-12-31 12:23:09 +02:00
Qstick
7b8e352d87 Bump SonarCloud azure extension to 3.X 2024-12-30 22:49:24 -06:00
Bogdan
81f7a6cbab Word boundary in season number regex for AnimeBytes 2024-12-31 02:11:14 +02:00
Bogdan
523e46af2a Fixed: (AnimeBytes) Include year in release title for series with year in filenames 2024-12-31 01:55:55 +02:00
Bogdan
2b4a6def2a Fixed privacy level for Nebulance's API key 2024-12-30 00:59:48 +02:00
Bogdan
9097c0ef6d Bump version to 1.29.2 2024-12-30 00:59:35 +02:00
Bogdan
4321c1d40c Catch search engine related error messages for MyAnonaMouse 2024-12-28 23:42:14 +02:00
Mark McDowall
bb2548a08d Don't send session information to Sentry
(cherry picked from commit fae24e98fb9230c2f3701caef457332952c6723f)
2024-12-28 15:22:41 +02:00
Weblate
3a9b841fad Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Tommy Au <smarttommyau@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: marapavelka <mara.pavelka@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-12-27 22:55:30 +02:00
Bogdan
31203d1370 Add more links for info FlareSolverr and category 8000 2024-12-27 22:53:53 +02:00
Bogdan
c8a910eaf4 Fixed: (RuTracker) Update categories
Co-authored-by: garfield69 <garfield69@outlook.com>
2024-12-27 22:53:53 +02:00
Bogdan
9ab3c3e6c7 Update how to get cookies info for Cardigann
Co-authored-by: garfield69 <garfield69@outlook.com>
2024-12-27 22:53:53 +02:00
Bogdan
4659cb706a Fixed: (Knaben) Update base url
Co-authored-by: garfield69 <garfield69@outlook.com>
2024-12-27 22:53:53 +02:00
Bogdan
500759bf1f Bump version to 1.29.1 2024-12-22 13:24:47 +02:00
Bogdan
43c7c43257 Bump Microsoft.Data.SqlClient to 2.1.7 2024-12-18 12:56:44 +02:00
Bogdan
9c2fced391 Bump System.Text.Json to 6.0.10 2024-12-18 02:34:29 +02:00
Bogdan
52ec5b6ff6 Bump MailKit to 4.8.0 2024-12-18 02:34:29 +02:00
zodihax
b46e657976 Fixed: (NorBits) Searching UTF-8 characters with full search disabled (#2305)
Co-authored-by: zodihaxx <parity.umpires-0m@icloud.com>
2024-12-17 23:04:05 +02:00
Bogdan
51fd30ba10 Use message from error response for Gazelle indexers 2024-12-17 15:30:25 +02:00
Mark McDowall
5fbb347108 Upgrade typescript-eslint packages to 8.181.1
(cherry picked from commit ed10b63fa0c161cac7e0a2084e53785ab1798208)
2024-12-17 14:09:24 +02:00
Mark McDowall
54d3d44620 Upgrade Font Awesome to 6.7.1
(cherry picked from commit 016b5718386593c030f14fcac307c93ee1ceeca6)
2024-12-17 14:04:46 +02:00
Mark McDowall
5ca18683ca Upgrade babel to 7.26.0
(cherry picked from commit bfcd017012730c97eb587ae2d2e91f72ee7a1de3)
2024-12-17 13:59:56 +02:00
Bogdan
6bdf5f5d69 Use error message from Redacted response 2024-12-16 22:25:55 +02:00
Bogdan
7cba7152f1 Bump version to 1.29.0 2024-12-16 22:11:18 +02:00
Bogdan
cf012eb001 Use minor version for core-js in babel/preset-env 2024-12-16 13:26:25 +02:00
Weblate
6b8a7993ff Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translation: Servarr/Prowlarr
2024-12-15 18:40:20 +02:00
Mark McDowall
c6440bb21b Upgrade TypeScript and core-js
(cherry picked from commit 148480909917f69ff3b2ca547ccb4716dd56606e)
2024-12-15 15:37:10 +02:00
Mark McDowall
b95eac98b9 Fixed: Error getting processes in some cases
(cherry picked from commit b552d4e9f7ca7388404aa0d52566010a54cb0244)
2024-12-15 15:37:10 +02:00
Bogdan
0eb19ce834 Bump version to 1.28.2 2024-12-15 10:05:45 +02:00
Weblate
4b8016d95d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translation: Servarr/Prowlarr
2024-12-14 02:34:32 +02:00
Bogdan
31d8d2419a Fixed: Refresh backup list on deletion
Fixes #2300
2024-12-11 14:36:59 +02:00
Weblate
d29ccd7749 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Robin Dadswell <robin@robindadswell.tech>
Co-authored-by: Rodion <rodyon009@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: hhjuhl <hans@kopula.dk>
Co-authored-by: keysuck <joshkkim@gmail.com>
Co-authored-by: mryx007 <mryx@mail.de>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_HANS/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_Hans/
Translation: Servarr/Prowlarr
2024-12-10 14:12:15 +02:00
Mark McDowall
e789f4ec54 Sync TimeSpanConverter with upstream
(cherry picked from commit 1374240321f08d1400faf95e84217e4b7a2d116b)
2024-12-09 14:09:23 +02:00
Bogdan
58d495d618 Bump version to 1.28.1 2024-12-08 12:25:51 +02:00
Bogdan
f3328863e1 Fixed: (M-Team) IMDb removed from releases response 2024-12-07 14:08:48 +02:00
Bogdan
a23d792781 Fixed: Syncing Newznab indexers with expired VIP expiration dates to apps 2024-12-07 12:10:45 +02:00
Bogdan
f066cf399d Fixed: Link to TMDb shows in search history
Fixes #2294
2024-12-07 10:37:29 +02:00
Servarr
61e863cb31 Automated API Docs update 2024-12-03 00:26:48 -06:00
soup
b2afbc6872 New: Add config file setting for CGNAT authentication bypass
(cherry picked from commit 4c41a4f368046f73f82306bbd73bec992392938b)
2024-12-03 00:07:27 -06:00
Elias Benbourenane
aace65f88e Allow GetFileSize to follow symlinks
(cherry picked from commit ca0bb14027f3409014e7cf9ffa8e04e577001d77)

Don't fail if symlink target can't be resolved

(cherry picked from commit 8cb58a63d8ec1b290bc57ad2cf1e90809ceebce9)
2024-12-02 03:18:30 +02:00
Bogdan
9ab2d8b444 Bump IPAddressRange, Npgsql and Polly 2024-12-02 03:14:36 +02:00
Mark McDowall
bc314061ef Fixed: Prevent lack of internet from stopping all health checks from running
(cherry picked from commit dba3a8243988d3e9870b841696303191e1703a0d)
2024-12-02 03:10:32 +02:00
Mark McDowall
87b3dcd780 Support Postgres with non-standard version string
(cherry picked from commit 40f4ef27b22113c1dae0d0cbdee8205132bed68a)
2024-12-02 03:06:47 +02:00
Gylesie
f3b99f68f6 Remove unnecessary heap allocations in local IP check
(cherry picked from commit ed536a85ad5f2062bf6f01f80efddb19fa935f63)
2024-12-02 03:05:29 +02:00
Mark McDowall
c4a90e8ba4 Webpack web target
(cherry picked from commit a90866a73e6cff9a286c23e60c74672f4c0d317a)
2024-11-27 12:26:23 +02:00
Bogdan
41320ca2dc Bump version to 1.28.0 2024-11-26 19:27:15 +02:00
Bogdan
b8b32f8708 Fixed: (ImmortalSeed) Update relogin check 2024-11-24 11:45:55 +02:00
Weblate
30c4bb24e8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Mizuyoru_TW <mizuyoru.tw@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-11-24 07:44:46 +02:00
Alexander Bruun
b447db5d08 Fixed: (RED) Update indexer url (#2285)
* Updated RED CNAME record
* Added LegacyUrls
2024-11-23 22:41:21 -06:00
Weblate
299001a513 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: 4kwins <hanszimmerme@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-11-19 20:37:31 -06:00
Bogdan
2871f1f2a2 Bump version to 1.27.0 2024-11-19 03:11:37 +02:00
Bogdan
a9b93df0c9 Pin ReportGenerator in Azure Pipelines for .NET 6
(cherry picked from commit 50ce480abf043140e209d2d2959fbea8dd5dd2ab)
2024-11-15 15:43:29 -06:00
Weblate
2726787ee9 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: My name is Svetoslav Kolev <slubchev@yahoo.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-11-14 20:56:24 -06:00
bakerboy448
b917932f19 Improve No Results Messaging 2024-11-14 19:30:12 -06:00
bakerboy448
06ae85e6d1 Fixed: Updates Page Translations 2024-11-12 21:14:38 -06:00
Weblate
b1c7e98664 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lars <lars.erik.heloe@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mytelegrambot <lacsonluxur@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translation: Servarr/Prowlarr
2024-11-08 13:45:13 +02:00
Bogdan
62479737a7 Fixed: (Torrent RSS) Clear old cookies on edit
Fixes #2275
2024-11-04 14:23:29 +02:00
Bogdan
8e69415d64 Check for disabled till value in filtering blocked providers 2024-11-03 18:16:39 +02:00
Bogdan
222dfb1821 Bump version to 1.26.1 2024-11-03 11:43:41 +02:00
Weblate
94f439e238 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: HUi <huynguyeexn@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Kuzmich <kuzmich55@gmail.com>
Co-authored-by: Moon55 <dylan.gurdak@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-11-02 21:13:55 +02:00
Bogdan
903a88c121 Update timezone offset for FL 2024-11-01 17:56:47 +02:00
Bogdan
9690ab6883 Fixed: (IPTorrents) Search IMDb ID in descriptions 2024-11-01 17:19:28 +02:00
Bogdan
1e1a2b3b4a Fixed: (BeyondHD) Enforce length for API and RSS keys 2024-11-01 12:59:11 +02:00
bakerboy448
9dc2d3669c Fixed: NzbIndex removed, API not supported 2024-10-31 09:31:25 +02:00
Bogdan
511c76e219 Update JetBrains logos 2024-10-29 10:06:00 +02:00
Bogdan
78329b7b92 Improve exception message for invalid torrent files 2024-10-28 17:35:17 +02:00
Bogdan
4240048853 Add Knaben as native indexer 2024-10-28 08:23:42 +02:00
Bogdan
432af42ffd Fixed indexer names for no definitions check 2024-10-27 16:04:17 +02:00
Weblate
0d6c03f8d4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-10-27 16:02:41 +02:00
Bogdan
96830f975e Cleaning paths for top level root folders
(cherry picked from commit a00121695715feb2cf8f04da246dc18262ab3237)
2024-10-27 15:12:00 +02:00
Mark McDowall
13c538ff58 Ignore extra spaces in path when not running on Windows
(cherry picked from commit 6d0f10b877912edef21232c64339cc6548d9690e)
2024-10-27 15:10:34 +02:00
Mark McDowall
14250e9634 Fixed getting parents from different OS paths
(cherry picked from commit e791f4b743d9660b0ad1decc4c5ed0e864f3b243)
2024-10-27 15:08:13 +02:00
Hadrien Patte
e2f7890d76 Use OperatingSystem class to get OS information
(cherry picked from commit 135b5c2ddd8f0a274b0d59eb07f75aaf1446b9da)
2024-10-27 09:06:21 +02:00
Bogdan
257d38de66 Inherit trigger from pushed command models
(cherry picked from commit 0bc4903954b955fce0c368ef7fd2a6f3761d6a93)
2024-10-27 09:05:38 +02:00
Bogdan
fd2a14e01b Fix settings fetching failure for updates 2024-10-27 09:01:07 +02:00
Bogdan
b4d76c7138 Fixed: Initial state for qBittorrent v5.0
(cherry picked from commit ff724b7f4099284b8062f1625cf07b7822782edf)
2024-10-27 08:57:54 +02:00
Bogdan
9655f37fa8 Trim directory separators in GetRelativePath
(cherry picked from commit 240a0339bee7d407e564df6b6575a2ade6ac03cd)
2024-10-27 08:56:57 +02:00
Bogdan
246fb9b855 Update check returns error if build older than 180 days 2024-10-24 08:14:07 +03:00
Bogdan
25afadc9b2 Bump version to 1.26.0 2024-10-22 05:51:30 +03:00
Bogdan
3f547f0856 Cleanse exceptions in event logs 2024-10-20 13:39:43 +03:00
Bogdan
11e322b6d7 Bump version to 1.25.4 2024-10-20 08:05:32 +03:00
Weblate
02ff133a62 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-10-20 04:42:29 +03:00
Bogdan
47268aac87 Fix stable branch label in updates 2024-10-20 04:36:52 +03:00
Bogdan
8aad1ac554 New: Allow major version updates to be installed (#2260)
* New: Allow major version updates to be installed

(cherry picked from commit 0e95ba2021b23cc65bce0a0620dd48e355250dab)

* fixup! New: Allow major version updates to be installed

---------

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-10-19 09:01:23 +03:00
Bogdan
9037cde439 Rename ApplicationCheckUpdate to ApplicationUpdateCheck 2024-10-19 07:13:31 +03:00
Mark McDowall
2afafd79e4 Fixed: Don't block updates under docker unless configured in package_info
(cherry picked from commit 5a7e34e291c2715aa67161e5c455d25e80f498df)
2024-10-19 07:13:31 +03:00
Bogdan
f4fa2517d2 Sort indexers by name when syncing to applications 2024-10-18 23:12:57 +03:00
Stevie Robinson
37bc46c1cd Translate System pages
(cherry picked from commit 93e8ff0ac7610fa8739f2e577ece98c2c06c8881)
2024-10-18 11:44:20 +03:00
Weblate
3e3a7ed4f0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: JoseFilipeFerreira <jose.filipe.matos.ferreira@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translation: Servarr/Prowlarr
2024-10-16 04:09:51 +03:00
Bogdan
04fa7d366d Fixed: (Cardigann) Redirect warnings with "Refresh" header 2024-10-13 20:08:54 +03:00
Bogdan
ed9a3214a2 Fix redirect url in HttpClient development warning message 2024-10-13 19:56:32 +03:00
Bogdan
66a9e1a653 Bump dotnet to 6.0.35 2024-10-13 19:32:37 +03:00
Bogdan
8cb59c35fb New: Sync UI updates for providers 2024-10-13 07:23:10 +03:00
Bogdan
94e9c05d60 Natural sorting for tags list in the UI 2024-10-13 07:21:27 +03:00
Bogdan
8d2c4e1246 Bump version to 1.25.3 2024-10-13 07:20:52 +03:00
Bogdan
c05be39346 Treat unauthorized newbie accounts in AvistaZ parser 2024-10-12 22:21:05 +03:00
Bogdan
951d42a591 Fix indexer url info 2024-10-12 05:30:03 +03:00
Bogdan
dd046d8a68 Fixed: (Cardigann) Validate definition file and setting fields existence
Towards #2245
2024-10-11 19:23:30 +03:00
Bogdan
efa54a4d51 Remove unused gulp packages 2024-10-10 19:19:59 +03:00
Bogdan
3f07c50cc5 Fixed: Copy to clipboard in non-secure contexts
(cherry picked from commit 3828e475cc8860e74cdfd8a70b4f886de7f9c5c3)
2024-10-10 19:19:59 +03:00
Treycos
94cf07ddb4 Convert ClipboardButton to TypeScript
(cherry picked from commit 99fc52039f44264c83d939e5f096d8e16d2f3355)
2024-10-10 19:19:59 +03:00
Bogdan
24063e06ab Convert FormInputButton to TypeScript
(cherry picked from commit 32fa63d24d08d8d8877386a8d2e7065ab5d0ad39)
2024-10-10 19:19:59 +03:00
Treycos
e8ebb87189 Convert Label to TypeScript
(cherry picked from commit 3eca63a67c898256b711d37607f07cbabb9ed323)
2024-10-10 19:19:59 +03:00
Treycos
896e196767 Convert Button to TypeScript
(cherry picked from commit 63b4998c8e51d0d2b8b51133cbb1fd928394a7e6)
2024-10-10 19:19:59 +03:00
Bogdan
9f5be75e6d Link polymorphic static typing
(cherry picked from commit a2e06e9e650642518b926a61f624a2c7a49c0988)
(cherry picked from commit cfa2f4d4c6e35d7b9ddd2e1da2e59f7287859516)
2024-10-10 19:19:59 +03:00
Bogdan
9cc9e720bb Bump frontend packages 2024-10-10 19:19:59 +03:00
Bogdan
a9c2cca66d Bump dotnet packages 2024-10-09 23:56:11 +03:00
Bogdan
9cc3646be5 Fixed: (Cardigann) Using variables in login paths 2024-10-09 00:50:40 +03:00
Bogdan
d6bca449da Cleanse sharewood passkey 2024-10-09 00:26:08 +03:00
Bogdan
cb5764c654 Log exceptions when getting indexer definitions
Closes #2245
2024-10-08 01:44:36 +03:00
Weblate
19a9b56fa4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: Mathias <mathias@rodilbach.dk>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: angelsky11 <angelsky11@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: jsain <josip.sain@gmail.com>
Co-authored-by: liuwqq <843384478@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-10-06 18:19:32 +03:00
Tiago Santos
a2b0f199f1 Fixed: (BeyondHD) Filter freeleech or limited releases when configured 2024-10-06 17:56:21 +03:00
Mark McDowall
59bfad7614 New: Use 307 redirect for requests missing URL Base 2024-10-06 17:48:02 +03:00
Bogdan
aee3f2d12b Fixed: Handle 307 redirects from applications 2024-10-06 17:47:48 +03:00
Bogdan
11d58b4460 Bump macOS runner version to 13 2024-10-06 16:22:06 +03:00
Bogdan
ee4de6c6ca Bump version to 1.25.2 2024-10-06 12:04:50 +03:00
Bogdan
8d16b88185 Return bad request for unprotect download link failures 2024-10-05 17:20:17 +03:00
Bogdan
121ef8e80d Add new category for FL 2024-09-30 17:26:31 +03:00
Bogdan
d53fec7e75 Add newbie warning for AvistaZ's API use 2024-09-30 11:21:36 +03:00
Bogdan
c017a3cd7e New: (PTP) Filter by Golden Popcorn only releases 2024-09-29 12:12:26 +03:00
Bogdan
27ea93090f Use proxied requests for fetching user class for MAM 2024-09-29 10:40:16 +03:00
Bogdan
d79845144e Bump version to 1.25.1 2024-09-29 08:17:56 +03:00
Servarr
3f77900dd0 Automated API Docs update 2024-09-27 15:59:16 +03:00
Bogdan
4e8b9e81cf New: Option to prefer magnet URLs over torrent file links
Co-authored-by: Deathspike <meister.deathspike@outlook.com>

New: Bulk edit Prefer Magnet Url for indexers
2024-09-27 06:42:06 +03:00
Bogdan
a32ab3acfd Fixed: (AnimeBytes) Avoid specials for non-zero season searches 2024-09-27 06:24:04 +03:00
Bogdan
942da3a5c0 Bump version to 1.25.0 2024-09-27 06:23:48 +03:00
Qstick
17e1a72baf Bump webpack to 5.94.0 and regenerate yarn.lock 2024-09-22 22:34:45 -05:00
Bogdan
b454ded00a Bump version to 1.24.3 2024-09-22 07:48:53 +03:00
Servarr
d4512393e2 Automated API Docs update 2024-09-21 22:05:08 +03:00
Bogdan
97d1384726 Guard against using invalid sort keys 2024-09-21 21:35:23 +03:00
Bogdan
ba002a7a4a Add packages needed for RemoveDiacritics 2024-09-21 21:30:34 +03:00
momo
349efab7a8 Fix description for API key as query parameter
(cherry picked from commit 30c36fdc3baa686102ff124833c7963fc786f251)
2024-09-21 21:21:26 +03:00
Mark McDowall
af9a6f42db Fixed: Unable to login when instance name contained brackets 2024-09-21 00:27:15 +03:00
Mark McDowall
6b20fa8abd New: Use instance name in forms authentication cookie name
Closes #2224
2024-09-16 16:47:22 +03:00
Bogdan
029ad3903f Bump version to 1.24.2 2024-09-15 15:56:33 +03:00
Weblate
a23d66930b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: FloatStream <1213193613@qq.com>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: shixiaotongy <shixiaotong2280@sina.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-09-14 17:14:52 +03:00
Bogdan
710ab7ae09 New: (Gazelle/OPS/RED) Prevent downloads without FL tokens 2024-09-08 15:19:25 +03:00
jaype87
434b07ae64 New: Sync seeding limits for LazyLibrarian (#2215)
* add support for seeders, seed_ratio and seed_duration for LazyLibrarian
2024-09-08 11:34:26 +03:00
Bogdan
eee8c95ca6 Fix weblate widget 2024-09-08 11:24:36 +03:00
Bogdan
1f5c514011 Bump version to 1.24.1 2024-09-08 11:10:36 +03:00
Weblate
66d722e097 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: FloatStream <1213193613@qq.com>
Co-authored-by: Gabriel Markowski <gmarkowski62@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Kerk en IT <info@kerkenit.nl>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Nota Inutilis <hugo@notainutilis.fr>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-09-07 13:43:28 -05:00
Bogdan
39befe5aa4 Use error message from Nebulance response
Fixes #2212
2024-09-06 10:41:26 +03:00
Bogdan
ab043e87dc Display grabs, failures and queries stats with values 2024-09-04 16:28:06 +03:00
Bogdan
58ae9c0a13 Fixed: (MyAnonamouse) Avoid using FL wedges for freeleech torrents 2024-09-02 10:37:11 +03:00
Bogdan
44c446943c Fixed: (Gazelle) Allow freeleech torrents with Use Freeleech Tokens 2024-09-02 10:31:56 +03:00
Weblate
8301b669fe Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: Gabriel Markowski <gmarkowski62@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Kerk en IT <info@kerkenit.nl>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translation: Servarr/Prowlarr
2024-09-01 21:21:40 -05:00
Bogdan
6fa0b79c67 Bump version to 1.24.0 2024-09-01 06:43:21 +03:00
Bogdan
32d23d6636 Simplify cookie clearing for MAM 2024-08-28 03:28:49 +03:00
Bogdan
b31b695887 Fixed: Mapping of Cardigann indexers on bulk edit 2024-08-27 07:15:49 +03:00
Bogdan
33de32b138 Simplify app profile validation on indexers 2024-08-27 06:51:45 +03:00
Bogdan
753b53a529 Use UTC for filtering out AB releases 2024-08-27 06:09:35 +03:00
Bogdan
123535b9a5 Fixed: Use renewed mam_id from response to avoid invalid credentials after original one expires 2024-08-27 06:02:58 +03:00
Bogdan
7a5fa452f0 Don't persist value for SslCertHash when checking for existence 2024-08-27 00:27:34 +03:00
Bogdan
281e712542 Fixed: Hide reboot and shutdown UI buttons on docker
(cherry picked from commit 50d7e8fed4f9a43b501551f84471656f8bb19458)
2024-08-26 03:29:31 +03:00
Bogdan
c2c34ecf53 New: Bypass IP addresses ranges in proxies
(cherry picked from commit 402db9128c214d4c5af6583643cb49d3aa7a28b5)

Closes #2203
2024-08-26 03:26:56 +03:00
bakerboy448
615193617c Fixed: Trim spaces and empty values in Proxy Bypass List
(cherry picked from commit 846333ddf0d9da775c80d004fdb9b41e700ef359)
2024-08-26 03:26:27 +03:00
Bogdan
1b58d50b6d Bump version to 1.23.1 2024-08-25 10:13:49 +03:00
Bogdan
99f9a0b4e6 Improve sorting indexer by status 2024-08-23 02:52:59 +03:00
Bogdan
696001a8bb Remove AroLol
Site has shutdown
2024-08-22 20:09:51 +03:00
Bogdan
31f057c097 Hiding "enable" property in API docs for applications 2024-08-20 17:28:11 +03:00
Bogdan
0391537a60 Don't display validation errors as HTML
Display the link to application only if it's enabled

Thanks to higa on discord for pointing this to us.
2024-08-20 17:10:24 +03:00
Servarr
521c1f760c Automated API Docs update 2024-08-20 05:26:16 +03:00
Bogdan
3bf9b4f90f Dedupe titles to avoid similar release names for AB 2024-08-20 05:16:35 +03:00
martylukyy
af86a6d34e New: Configure log file size limit in UI
(cherry picked from commit 35baebaf7280749d5dfe5440e28b425e45a22d21)
2024-08-19 15:54:55 +03:00
Bogdan
3ecf5c6166 Fixed: (AnimeBytes) Improve filtering of old releases 2024-08-19 15:30:45 +03:00
Bogdan
4da3e7b2b3 Fixed: (MyAnonamouse) Sanitise search query and stop search if term is empty 2024-08-19 01:08:15 +03:00
Bogdan
66f38f1566 Bump version to 1.23.0 2024-08-18 15:52:58 +03:00
Weblate
04b513ad14 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-08-18 04:59:35 +03:00
Bogdan
1ce7fda8bb Fixed: Removing invalid statuses on provider deletion 2024-08-17 18:11:43 +03:00
Bogdan
6d09fad675 Bump babel packages 2024-08-16 18:38:54 +03:00
Bogdan
ad061e7ece Fix select background appearance on iOS
And make use of autoprefixer
2024-08-16 18:38:53 +03:00
Bogdan
3155343bcc Fixed: (Uniotaku) Updated categories
Co-authored-by: Garfield69 <garfield69@outlook.com>
2024-08-16 15:03:24 +03:00
Bogdan
f5e91f7bfd Fixed: (RuTracker) Updated categories
Co-authored-by: Garfield69 <garfield69@outlook.com>
2024-08-16 15:01:55 +03:00
Bogdan
1d69f2ed3f Fixed: (GazelleGames) Fixed release titles 2024-08-16 14:41:45 +03:00
Mark McDowall
1d233dbcab New: Configurable log file size limit
(cherry picked from commit 813965e6a20edef2772d68eaa7646af33028425a)
2024-08-15 04:02:33 +03:00
Mark McDowall
1aafb0b201 New: Add Compact Log Event Format option for console logging
(cherry picked from commit 0d914f4c53876540ed2df83ad3d71615c013856f)
2024-08-15 04:00:23 +03:00
Bogdan
d7d5a2dd42 Upgrade nlog to 5.3.3 2024-08-15 03:57:21 +03:00
Bogdan
8060a65ef6 Fixed: Duplicated changelog lines 2024-08-15 03:51:36 +03:00
Bogdan
379071f838 Convert formatBytes to TypeScript 2024-08-15 03:49:37 +03:00
Mark McDowall
5cbbd060a4 Update React Lint rules for TSX
(cherry picked from commit 1299a97579bec52ee3d16ab8d05c9e22edd80330)
2024-08-15 03:31:29 +03:00
Mark McDowall
ef19673a76 Convert System to TypeScript
(cherry picked from commit 72db8099e0f4abc3176e397f8dda3b2b69026daf)
2024-08-15 03:31:29 +03:00
The Dark
c3cf8a6ebb Convert App to TypeScript
(cherry picked from commit d6d90a64a39d3b9d3a95fb6b265517693a70fdd7)
(cherry picked from commit 428569106499b5e3a463f1990ae2996d1ae4ab49)
(cherry picked from commit d0e9504af0d88391a74e04b90638e4b2d99fb476)
(cherry picked from commit ee80564dd427ca1dc14c192955efaa61f386ad44)
(cherry picked from commit 76650af9fdc7ef06d13ce252986d21574903d293)
2024-08-15 03:31:29 +03:00
Bogdan
c22b27525a Include available version in update health check
(cherry picked from commit 15e3c3efb18242caf28b9bfc77a72a78296018bf)
2024-08-15 03:31:29 +03:00
Mark McDowall
eec3b01f5b Don't hash files in development builds
(cherry picked from commit bc7799139e52b92956eb595fb87f44d7dda9a320)
2024-08-15 03:31:29 +03:00
Mark McDowall
e67a127a02 Fixed: Allow leading/trailing spaces on non-Windows
(cherry picked from commit 9127a91dfc460f442498a00faed98737047098cd)
2024-08-15 03:31:29 +03:00
Mark McDowall
a074ebc951 New: Default file log level changed to debug
(cherry picked from commit 9b528eb82914a05cfc3b67d4d6146ce51e86f68d)
2024-08-15 03:31:29 +03:00
Weblate
d1cd814663 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: iMohmmedSA <i.mohmmed.i+1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translation: Servarr/Prowlarr
2024-08-15 03:30:44 +03:00
Bogdan
ac76646a20 Fixed: Fallback to saved capabilities when syncing failed indexers 2024-08-15 03:29:45 +03:00
Bogdan
6549f799f6 Fix parsing imdb ids for native indexers 2024-08-15 03:29:18 +03:00
Bogdan
cca55fd66c Bump version to 1.22.0 2024-07-27 00:57:57 +03:00
Weblate
2f67d2813a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: seidnerj <jonathan.seidner@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-07-27 00:19:19 +03:00
Bogdan
9a7a5fdc38 Remove PropTypes 2024-07-26 06:40:34 +03:00
Bogdan
f1fdec6822 Convert Add Indexer Modal to Typescript 2024-07-26 06:25:37 +03:00
Bogdan
5464b23329 Sort indexer queries stats by a sum of all 3 types 2024-07-26 01:09:04 +03:00
Bogdan
4c99971882 Improve messaging on no results with applied filter 2024-07-25 08:08:48 +03:00
Weblate
cc7769b601 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dream <seth.gecko.rr@gmail.com>
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/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2024-07-24 19:42:16 +03:00
Bogdan
cb2ed7daf9 Fixed: Improve elapsed time collecting for grabs 2024-07-22 19:52:12 +03:00
Bogdan
78508094c8 Bump some frontend libraries 2024-07-22 01:02:25 +03:00
Bogdan
b0f755a30c Fixed: (Nebulance) Searching for daily episodes using ids 2024-07-22 00:38:13 +03:00
Bogdan
9d1384792a Bump version to 1.21.2 2024-07-21 18:06:41 +03:00
Bogdan
ea17116998 Fixed: (Nebulance) Avoid requests for release calls that are 2 characters or fewer 2024-07-21 02:39:02 +03:00
Servarr
2c23681fc5 Automated API Docs update 2024-07-21 01:53:11 +03:00
Bogdan
17aa2832ea New: Split average response time statistics for queries and grabs 2024-07-21 01:01:03 +03:00
Bogdan
5f3a329ef2 Don't show null for non-cached indexer queries 2024-07-20 20:36:01 +03:00
Bogdan
96f49da79e New: Improve history details for release grabs 2024-07-20 19:50:20 +03:00
Bogdan
c7dfde0ce9 Improve messaging for invalid request for M-Team-TP 2024-07-19 19:49:17 +03:00
Bogdan
8cf32020f7 New: Bump dotnet to 6.0.32 2024-07-19 19:39:06 +03:00
Weblate
a5ed5a0e60 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Rauniik <raunerjakub@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-07-19 00:05:13 +03:00
Bogdan
3279936fc9 New: Litestream compatibility for SQLite (#2179) 2024-07-18 23:33:55 +03:00
Bogdan
8abccc709e Use natural sorting for remaining lists of items in the UI 2024-07-18 22:49:43 +03:00
Mark McDowall
76f30e7682 New: Use natural sorting for lists of items in the UI
(cherry picked from commit 1a1c8e6c08a6db5fcd2b5d17e65fa1f943d2e746)
2024-07-18 21:35:06 +03:00
Mark McDowall
ab289b3e42 New: Show update settings on all platforms
(cherry picked from commit c023fc700896c7f0751c4ac63c4e1a89d6e1a9bb)
2024-07-18 21:22:01 +03:00
Bogdan
ef7e04065c Fixed: (BeyondHD) Don't die on invalid TMDb ids 2024-07-17 02:36:58 +03:00
Bogdan
d1084039b3 Bump version to 1.21.1 2024-07-14 12:29:09 +03:00
Bogdan
7bada440d2 Log invalid torrent files contents as debug
Fixes #2169
2024-07-12 13:00:23 +03:00
Bogdan
803c4752db Fixed: Sending health restored notifications with Gotify
Fixed #2176
2024-07-11 13:35:02 +03:00
Bogdan
c0777474c0 Bump Polly 2024-07-09 23:47:23 +03:00
Weblate
66dcea5604 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Serhii Matrunchyk <serhii@digitalidea.studio>
Co-authored-by: Taylan Tatlı <taylantatli90@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: quek76 <quek@libertysurf.fr>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2024-07-09 11:06:17 +03:00
Qstick
a2a12d2450 Update SonarCloud pipeline versions (#2171)
* Update SonarCloud pipeline versions

* Update reportgenerator to remove PublishCodeCoverage dep warnings
2024-07-07 17:55:24 +03:00
Bogdan
39593bd5a8 Bump version to 1.21.0 2024-07-07 10:44:00 +03:00
388 changed files with 9727 additions and 7301 deletions

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

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

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

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

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

View File

@@ -1,7 +1,7 @@
# Prowlarr
[![Build Status](https://dev.azure.com/Prowlarr/Prowlarr/_apis/build/status/Prowlarr.Prowlarr?branchName=develop)](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
[![Translation status](https://translate.servarr.com/widget/servarr/prowlarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/prowlarr.svg)](https://wiki.servarr.com/prowlarr/installation/docker)
![Github Downloads](https://img.shields.io/github/downloads/Prowlarr/Prowlarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Prowlarr/backers/badge.svg)](#backers)
@@ -68,16 +68,16 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
## 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="/Logo/webstorm.svg" 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="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
* [<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="https://resources.jetbrains.com/storage/products/company/brand/logos/WebStorm_icon.png" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<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="https://resources.jetbrains.com/storage/products/company/brand/logos/dotTrace_icon.png" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
### License
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
- Copyright 2010-2022
- Copyright 2010-2024
Icon Credit - [Box vector created by freepik - www.freepik.com](https://www.freepik.com/vectors/box)

View File

@@ -9,18 +9,18 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.20.1'
majorVersion: '1.29.2'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.421'
dotnetVersion: '6.0.427'
nodeVersion: '20.X'
innoVersion: '6.2.2'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-12'
macImage: 'macOS-13'
trigger:
branches:
@@ -1169,12 +1169,12 @@ stages:
submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
- task: SonarCloudPrepare@1
- task: SonarCloudPrepare@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs:
SonarCloud: 'SonarCloud'
organization: 'prowlarr'
scannerMode: 'MSBuild'
scannerMode: 'dotnet'
projectKey: 'Prowlarr_Prowlarr'
projectName: 'Prowlarr'
projectVersion: '$(prowlarrVersion)'
@@ -1187,21 +1187,16 @@ stages:
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@1
- task: SonarCloudAnalyze@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@4
- task: reportgenerator@5.3.11
displayName: Generate Coverage Report
inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
- task: PublishCodeCoverageResults@1
displayName: Publish Coverage Report
inputs:
codeCoverageTool: 'cobertura'
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
reportDirectory: './CoverageResults/combined/'
publishCodeCoverageResults: true
- stage: Report_Out
dependsOn:

View File

@@ -357,11 +357,16 @@ module.exports = {
],
rules: Object.assign(typescriptEslintRecommended.rules, {
'no-shadow': 'off',
// These should be enabled after cleaning things up
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'after-used',
argsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off',
'no-shadow': 'off',
'prettier/prettier': 'error',
'simple-import-sort/imports': [
'error',
@@ -374,7 +379,41 @@ module.exports = {
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
]
}
]
],
// React Hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// React
'react/function-component-definition': 'error',
'react/hook-use-state': 'error',
'react/jsx-boolean-value': ['error', 'always'],
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' }
],
'react/jsx-fragments': 'error',
'react/jsx-handler-names': [
'error',
{
eventHandlerPrefix: 'on',
eventHandlerPropPrefix: 'on'
}
],
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
'react/jsx-sort-props': [
'error',
{
callbacksLast: true,
noSortAlphabetically: true,
reservedFirst: true
}
],
'react/prop-types': 'off',
'react/self-closing-comp': 'error'
})
},
{

View File

@@ -25,6 +25,7 @@ module.exports = (env) => {
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: {
children: false
@@ -65,7 +66,7 @@ module.exports = (env) => {
output: {
path: distFolder,
publicPath: '/',
filename: '[name]-[contenthash].js',
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
sourceMapFilename: '[file].map'
},
@@ -90,7 +91,7 @@ module.exports = (env) => {
new MiniCssExtractPlugin({
filename: 'Content/styles.css',
chunkFilename: 'Content/[id]-[chunkhash].css'
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
}),
new HtmlWebpackPlugin({
@@ -169,7 +170,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
corejs: '3.39'
}
]
]
@@ -190,7 +191,7 @@ module.exports = (env) => {
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
}
}
},

View File

@@ -16,6 +16,7 @@ const mixinsFiles = [
module.exports = {
plugins: [
'autoprefixer',
['postcss-mixins', {
mixinsFiles
}],

View File

@@ -1,20 +1,25 @@
import { ConnectedRouter } from 'connected-react-router';
import PropTypes from 'prop-types';
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
import React from 'react';
import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes';
function App({ store, history }) {
interface AppProps {
store: Store;
history: ConnectedRouterProps['history'];
}
function App({ store, history }: AppProps) {
return (
<DocumentTitle title={window.Prowlarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme />
<PageConnector>
<AppRoutes app={App} />
<AppRoutes />
</PageConnector>
</ConnectedRouter>
</Provider>
@@ -22,9 +27,4 @@ function App({ store, history }) {
);
}
App.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
export default App;

View File

@@ -1,184 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import HistoryConnector from 'History/HistoryConnector';
import IndexerIndex from 'Indexer/Index/IndexerIndex';
import IndexerStats from 'Indexer/Stats/IndexerStats';
import SearchIndexConnector from 'Search/SearchIndexConnector';
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Settings from 'Settings/Settings';
import TagSettings from 'Settings/Tags/TagSettings';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import BackupsConnector from 'System/Backup/BackupsConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
function AppRoutes(props) {
const {
app
} = props;
return (
<Switch>
{/*
Indexers
*/}
<Route
exact={true}
path="/"
component={IndexerIndex}
/>
{
window.Prowlarr.urlBase &&
<Route
exact={true}
path="/"
addUrlBase={false}
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/')}
component={app}
/>
);
}}
/>
}
<Route
path="/indexers/stats"
component={IndexerStats}
/>
{/*
Search
*/}
<Route
path="/search"
component={SearchIndexConnector}
/>
{/*
Activity
*/}
<Route
path="/history"
component={HistoryConnector}
/>
{/*
Settings
*/}
<Route
exact={true}
path="/settings"
component={Settings}
/>
<Route
path="/settings/indexers"
component={IndexerSettings}
/>
<Route
path="/settings/applications"
component={ApplicationSettings}
/>
<Route
path="/settings/downloadclients"
component={DownloadClientSettingsConnector}
/>
<Route
path="/settings/connect"
component={NotificationSettings}
/>
<Route
path="/settings/tags"
component={TagSettings}
/>
<Route
path="/settings/general"
component={GeneralSettingsConnector}
/>
<Route
path="/settings/ui"
component={UISettingsConnector}
/>
<Route
path="/settings/development"
component={DevelopmentSettingsConnector}
/>
{/*
System
*/}
<Route
path="/system/status"
component={Status}
/>
<Route
path="/system/tasks"
component={Tasks}
/>
<Route
path="/system/backup"
component={BackupsConnector}
/>
<Route
path="/system/updates"
component={UpdatesConnector}
/>
<Route
path="/system/events"
component={LogsTableConnector}
/>
<Route
path="/system/logs/files"
component={Logs}
/>
{/*
Not Found
*/}
<Route
path="*"
component={NotFound}
/>
</Switch>
);
}
AppRoutes.propTypes = {
app: PropTypes.func.isRequired
};
export default AppRoutes;

View File

@@ -0,0 +1,117 @@
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import HistoryConnector from 'History/HistoryConnector';
import IndexerIndex from 'Indexer/Index/IndexerIndex';
import IndexerStats from 'Indexer/Stats/IndexerStats';
import SearchIndexConnector from 'Search/SearchIndexConnector';
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Settings from 'Settings/Settings';
import TagSettings from 'Settings/Tags/TagSettings';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import BackupsConnector from 'System/Backup/BackupsConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks';
import Updates from 'System/Updates/Updates';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
function RedirectWithUrlBase() {
return <Redirect to={getPathWithUrlBase('/')} />;
}
function AppRoutes() {
return (
<Switch>
{/*
Indexers
*/}
<Route exact={true} path="/" component={IndexerIndex} />
{window.Prowlarr.urlBase && (
<Route
exact={true}
path="/"
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
addUrlBase={false}
render={RedirectWithUrlBase}
/>
)}
<Route path="/indexers/stats" component={IndexerStats} />
{/*
Search
*/}
<Route path="/search" component={SearchIndexConnector} />
{/*
Activity
*/}
<Route path="/history" component={HistoryConnector} />
{/*
Settings
*/}
<Route exact={true} path="/settings" component={Settings} />
<Route path="/settings/indexers" component={IndexerSettings} />
<Route path="/settings/applications" component={ApplicationSettings} />
<Route
path="/settings/downloadclients"
component={DownloadClientSettingsConnector}
/>
<Route path="/settings/connect" component={NotificationSettings} />
<Route path="/settings/tags" component={TagSettings} />
<Route path="/settings/general" component={GeneralSettingsConnector} />
<Route path="/settings/ui" component={UISettingsConnector} />
<Route
path="/settings/development"
component={DevelopmentSettingsConnector}
/>
{/*
System
*/}
<Route path="/system/status" component={Status} />
<Route path="/system/tasks" component={Tasks} />
<Route path="/system/backup" component={BackupsConnector} />
<Route path="/system/updates" component={Updates} />
<Route path="/system/events" component={LogsTableConnector} />
<Route path="/system/logs/files" component={Logs} />
{/*
Not Found
*/}
<Route path="*" component={NotFound} />
</Switch>
);
}
export default AppRoutes;

View File

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

View File

@@ -0,0 +1,28 @@
import React, { useCallback } from 'react';
import Modal from 'Components/Modal/Modal';
import AppUpdatedModalContent from './AppUpdatedModalContent';
interface AppUpdatedModalProps {
isOpen: boolean;
onModalClose: (...args: unknown[]) => unknown;
}
function AppUpdatedModal(props: AppUpdatedModalProps) {
const { isOpen, onModalClose } = props;
const handleModalClose = useCallback(() => {
location.reload();
}, []);
return (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<AppUpdatedModalContent onModalClose={handleModalClose} />
</Modal>
);
}
export default AppUpdatedModal;

View File

@@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import AppUpdatedModal from './AppUpdatedModal';
function createMapDispatchToProps(dispatch, props) {
return {
onModalClose() {
location.reload();
}
};
}
export default connect(null, createMapDispatchToProps)(AppUpdatedModal);

View File

@@ -1,139 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import UpdateChanges from 'System/Updates/UpdateChanges';
import translate from 'Utilities/String/translate';
import styles from './AppUpdatedModalContent.css';
function mergeUpdates(items, version, prevVersion) {
let installedIndex = items.findIndex((u) => u.version === version);
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
if (installedIndex === -1) {
installedIndex = 0;
}
if (installedPreviouslyIndex === -1) {
installedPreviouslyIndex = items.length;
} else if (installedPreviouslyIndex === installedIndex && items.length) {
installedPreviouslyIndex += 1;
}
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
if (!appliedUpdates.length) {
return null;
}
const appliedChanges = { new: [], fixed: [] };
appliedUpdates.forEach((u) => {
if (u.changes) {
appliedChanges.new.push(... u.changes.new);
appliedChanges.fixed.push(... u.changes.fixed);
}
});
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
mergedUpdate.changes = null;
}
return mergedUpdate;
}
function AppUpdatedModalContent(props) {
const {
version,
prevVersion,
isPopulated,
error,
items,
onSeeChangesPress,
onModalClose
} = props;
const update = mergeUpdates(items, version, prevVersion);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AppUpdated')}
</ModalHeader>
<ModalBody>
<div>
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
</div>
{
isPopulated && !error && !!update &&
<div>
{
!update.changes &&
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
}
{
!!update.changes &&
<div>
<div className={styles.changes}>
{translate('WhatsNew')}
</div>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
}
</div>
}
{
!isPopulated && !error &&
<LoadingIndicator />
}
</ModalBody>
<ModalFooter>
<Button
onPress={onSeeChangesPress}
>
{translate('RecentChanges')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={onModalClose}
>
{translate('Reload')}
</Button>
</ModalFooter>
</ModalContent>
);
}
AppUpdatedModalContent.propTypes = {
version: PropTypes.string.isRequired,
prevVersion: PropTypes.string,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onSeeChangesPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AppUpdatedModalContent;

View File

@@ -0,0 +1,145 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { kinds } from 'Helpers/Props';
import { fetchUpdates } from 'Store/Actions/systemActions';
import UpdateChanges from 'System/Updates/UpdateChanges';
import Update from 'typings/Update';
import translate from 'Utilities/String/translate';
import AppState from './State/AppState';
import styles from './AppUpdatedModalContent.css';
function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
let installedIndex = items.findIndex((u) => u.version === version);
let installedPreviouslyIndex = items.findIndex(
(u) => u.version === prevVersion
);
if (installedIndex === -1) {
installedIndex = 0;
}
if (installedPreviouslyIndex === -1) {
installedPreviouslyIndex = items.length;
} else if (installedPreviouslyIndex === installedIndex && items.length) {
installedPreviouslyIndex += 1;
}
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
if (!appliedUpdates.length) {
return null;
}
const appliedChanges: Update['changes'] = { new: [], fixed: [] };
appliedUpdates.forEach((u: Update) => {
if (u.changes) {
appliedChanges.new.push(...u.changes.new);
appliedChanges.fixed.push(...u.changes.fixed);
}
});
const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], {
changes: appliedChanges,
});
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
mergedUpdate.changes = null;
}
return mergedUpdate;
}
interface AppUpdatedModalContentProps {
onModalClose: () => void;
}
function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
const dispatch = useDispatch();
const { version, prevVersion } = useSelector((state: AppState) => state.app);
const { isPopulated, error, items } = useSelector(
(state: AppState) => state.system.updates
);
const previousVersion = usePrevious(version);
const { onModalClose } = props;
const update = mergeUpdates(items, version, prevVersion);
const handleSeeChangesPress = useCallback(() => {
window.location.href = `${window.Prowlarr.urlBase}/system/updates`;
}, []);
useEffect(() => {
dispatch(fetchUpdates());
}, [dispatch]);
useEffect(() => {
if (version !== previousVersion) {
dispatch(fetchUpdates());
}
}, [version, previousVersion, dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AppUpdated')}</ModalHeader>
<ModalBody>
<div>
<InlineMarkdown
data={translate('AppUpdatedVersion', { version })}
blockClassName={styles.version}
/>
</div>
{isPopulated && !error && !!update ? (
<div>
{update.changes ? (
<div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
) : null}
{update.changes ? (
<div>
<div className={styles.changes}>{translate('WhatsNew')}</div>
<UpdateChanges
title={translate('New')}
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
changes={update.changes.fixed}
/>
</div>
) : null}
</div>
) : null}
{!isPopulated && !error ? <LoadingIndicator /> : null}
</ModalBody>
<ModalFooter>
<Button onPress={handleSeeChangesPress}>
{translate('RecentChanges')}
</Button>
<Button kind={kinds.PRIMARY} onPress={onModalClose}>
{translate('Reload')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default AppUpdatedModalContent;

View File

@@ -1,78 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchUpdates } from 'Store/Actions/systemActions';
import AppUpdatedModalContent from './AppUpdatedModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.app.version,
(state) => state.app.prevVersion,
(state) => state.system.updates,
(version, prevVersion, updates) => {
const {
isPopulated,
error,
items
} = updates;
return {
version,
prevVersion,
isPopulated,
error,
items
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchUpdates() {
dispatch(fetchUpdates());
},
onSeeChangesPress() {
window.location = `${window.Prowlarr.urlBase}/system/updates`;
}
};
}
class AppUpdatedModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchUpdates();
}
componentDidUpdate(prevProps) {
if (prevProps.version !== this.props.version) {
this.props.dispatchFetchUpdates();
}
}
//
// Render
render() {
const {
dispatchFetchUpdates,
...otherProps
} = this.props;
return (
<AppUpdatedModalContent {...otherProps} />
);
}
}
AppUpdatedModalContentConnector.propTypes = {
version: PropTypes.string.isRequired,
dispatchFetchUpdates: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector);

View File

@@ -1,13 +1,9 @@
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
import AppState from './State/AppState';
interface ApplyThemeProps {
children: ReactNode;
}
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Prowlarr.theme,
@@ -17,7 +13,7 @@ function createThemeSelector() {
);
}
function ApplyTheme({ children }: ApplyThemeProps) {
function ApplyTheme() {
const theme = useSelector(createThemeSelector());
const updateCSSVariables = useCallback(() => {
@@ -31,7 +27,7 @@ function ApplyTheme({ children }: ApplyThemeProps) {
updateCSSVariables();
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
return null;
}
export default ApplyTheme;

View File

@@ -1,5 +1,4 @@
import PropTypes from 'prop-types';
import React from 'react';
import React, { useCallback } from 'react';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
@@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ConnectionLostModal.css';
function ConnectionLostModal(props) {
const {
isOpen,
onModalClose
} = props;
interface ConnectionLostModalProps {
isOpen: boolean;
}
function ConnectionLostModal(props: ConnectionLostModalProps) {
const { isOpen } = props;
const handleModalClose = useCallback(() => {
location.reload();
}, []);
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('ConnectionLost')}
</ModalHeader>
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
<ModalContent onModalClose={handleModalClose}>
<ModalHeader>{translate('ConnectionLost')}</ModalHeader>
<ModalBody>
<div>
{translate('ConnectionLostToBackend')}
</div>
<div>{translate('ConnectionLostToBackend')}</div>
<div className={styles.automatic}>
{translate('ConnectionLostReconnect')}
</div>
</ModalBody>
<ModalFooter>
<Button
kind={kinds.PRIMARY}
onPress={onModalClose}
>
<Button kind={kinds.PRIMARY} onPress={handleModalClose}>
{translate('Reload')}
</Button>
</ModalFooter>
@@ -48,9 +42,4 @@ function ConnectionLostModal(props) {
);
}
ConnectionLostModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ConnectionLostModal;

View File

@@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import ConnectionLostModal from './ConnectionLostModal';
function createMapDispatchToProps(dispatch, props) {
return {
onModalClose() {
location.reload();
}
};
}
export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal);

View File

@@ -1,5 +1,6 @@
import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
import { FilterBuilderProp } from './AppState';
import { FilterBuilderProp, PropertyFilter } from './AppState';
export interface Error {
responseJSON: {
@@ -18,10 +19,18 @@ export interface AppSectionSaveState {
}
export interface PagedAppSectionState {
page: number;
pageSize: number;
totalPages: number;
totalRecords?: number;
}
export interface TableAppSectionState {
columns: Column[];
}
export interface AppSectionFilterState<T> {
selectedFilterKey: string;
filters: PropertyFilter[];
filterBuilderProps: FilterBuilderProp<T>[];
}
@@ -38,6 +47,7 @@ export interface AppSectionItemState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
pendingChanges: Partial<T>;
item: T;
}

View File

@@ -43,6 +43,10 @@ export interface CustomFilter {
}
export interface AppSectionState {
isConnected: boolean;
isReconnecting: boolean;
version: string;
prevVersion?: string;
dimensions: {
isSmallScreen: boolean;
width: number;

View File

@@ -31,6 +31,8 @@ interface IndexerAppState
AppSectionDeleteState,
AppSectionSaveState {
itemMap: Record<number, number>;
isTestingAll: boolean;
}
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;

View File

@@ -7,7 +7,8 @@ import { IndexerCategory } from 'Indexer/Indexer';
import Application from 'typings/Application';
import DownloadClient from 'typings/DownloadClient';
import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings';
import General from 'typings/Settings/General';
import UiSettings from 'typings/Settings/UiSettings';
export interface AppProfileAppState
extends AppSectionState<Application>,
@@ -24,6 +25,12 @@ export interface ApplicationAppState
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {
isTestingAll: boolean;
}
export interface GeneralAppState
extends AppSectionItemState<General>,
AppSectionSaveState {}
export interface IndexerCategoryAppState
@@ -41,6 +48,7 @@ interface SettingsAppState {
appProfiles: AppProfileAppState;
applications: ApplicationAppState;
downloadClients: DownloadClientAppState;
general: GeneralAppState;
indexerCategories: IndexerCategoryAppState;
notifications: NotificationAppState;
ui: UiSettingsAppState;

View File

@@ -1,10 +1,19 @@
import Health from 'typings/Health';
import SystemStatus from 'typings/SystemStatus';
import { AppSectionItemState } from './AppSectionState';
import Task from 'typings/Task';
import Update from 'typings/Update';
import AppSectionState, { AppSectionItemState } from './AppSectionState';
export type HealthAppState = AppSectionState<Health>;
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type TaskAppState = AppSectionState<Task>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState {
health: HealthAppState;
status: SystemStatusAppState;
tasks: TaskAppState;
updates: UpdateAppState;
}
export default SystemAppState;

View File

@@ -46,6 +46,10 @@ class StackedBarChart extends Component {
size: 14,
family: defaultFontFamily
}
},
tooltip: {
mode: 'index',
position: 'average'
}
}
},

View File

@@ -63,11 +63,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
<div>{info.componentStack}</div>
)}
{
<div className={styles.version}>
Version: {window.Prowlarr.version}
</div>
}
<div className={styles.version}>Version: {window.Prowlarr.version}</div>
</details>
</div>
);

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import SelectInput from 'Components/Form/SelectInput';
import IconButton from 'Components/Link/IconButton';
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
import sortByProp from 'Utilities/Array/sortByProp';
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
@@ -212,7 +213,7 @@ class FilterBuilderRow extends Component {
key: name,
value: typeof label === 'function' ? label() : label
};
}).sort((a, b) => a.value.localeCompare(b.value));
}).sort(sortByProp('value'));
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { filterBuilderTypes } from 'Helpers/Props';
import * as filterTypes from 'Helpers/Props/filterTypes';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createTagListSelector() {
@@ -38,7 +38,7 @@ function createTagListSelector() {
}
return acc;
}, []).sort(sortByName);
}, []).sort(sortByProp('name'));
}
return _.uniqBy(items, 'id');

View File

@@ -5,6 +5,7 @@ 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 sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import CustomFilter from './CustomFilter';
import styles from './CustomFiltersModalContent.css';
@@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) {
<ModalBody>
{
customFilters
.sort((a, b) => a.label.localeCompare(b.label))
.sort((a, b) => sortByProp(a, b, 'label'))
.map((customFilter) => {
return (
<CustomFilter

View File

@@ -4,13 +4,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.appProfiles', sortByName),
createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
(state, { includeNoChange }) => includeNoChange,
(state, { includeMixed }) => includeMixed,
(appProfiles, includeNoChange, includeMixed) => {

View File

@@ -3,7 +3,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
@@ -21,7 +22,7 @@ function createMapStateToProps() {
const values = items
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
.sort(sortByName)
.sort(sortByProp('name'))
.map((downloadClient) => ({
key: downloadClient.id,
value: downloadClient.name,
@@ -31,7 +32,7 @@ function createMapStateToProps() {
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
value: `(${translate('Any')})`
});
}

View File

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

View File

@@ -0,0 +1,38 @@
import classNames from 'classnames';
import React from 'react';
import Button, { ButtonProps } from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import { kinds } from 'Helpers/Props';
import styles from './FormInputButton.css';
export interface FormInputButtonProps extends ButtonProps {
canSpin?: boolean;
isLastButton?: boolean;
}
function FormInputButton({
className = styles.button,
canSpin = false,
isLastButton = true,
...otherProps
}: FormInputButtonProps) {
if (canSpin) {
return (
<SpinnerButton
className={classNames(className, !isLastButton && styles.middleButton)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
return (
<Button
className={classNames(className, !isLastButton && styles.middleButton)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
export default FormInputButton;

View File

@@ -25,7 +25,7 @@ function FormInputHelpText(props) {
isCheckInput && styles.isCheckInput
)}
>
<div dangerouslySetInnerHTML={{ __html: text }} />
{text}
{
link ?

View File

@@ -4,14 +4,14 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import titleCase from 'Utilities/String/titleCase';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
createSortedSectionSelector('indexers', sortByName),
createSortedSectionSelector('indexers', sortByProp('name')),
(value, indexers) => {
const values = [];
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));

View File

@@ -1,7 +1,14 @@
.select {
@add-mixin truncate;
composes: input from '~Components/Form/Input.css';
padding: 0 11px;
padding: 0 30px 0 11px;
background-image: none, linear-gradient(-135deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(-225deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(var(--inputBackgroundColor) 42%, var(--textColor) 42%);
background-position: right 30px center, right bottom, right bottom, right bottom;
background-size: 1px 100%, 35px 27px, 30px 35px, 30px 100%;
background-repeat: no-repeat;
appearance: none;
}
.hasError {

View File

@@ -1,48 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { kinds, sizes } from 'Helpers/Props';
import styles from './Label.css';
function Label(props) {
const {
className,
kind,
size,
outline,
children,
...otherProps
} = props;
return (
<span
className={classNames(
className,
styles[kind],
styles[size],
outline && styles.outline
)}
{...otherProps}
>
{children}
</span>
);
}
Label.propTypes = {
className: PropTypes.string.isRequired,
title: PropTypes.string,
kind: PropTypes.oneOf(kinds.all).isRequired,
size: PropTypes.oneOf(sizes.all).isRequired,
outline: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired
};
Label.defaultProps = {
className: styles.label,
kind: kinds.DEFAULT,
size: sizes.SMALL,
outline: false
};
export default Label;

View File

@@ -0,0 +1,33 @@
import classNames from 'classnames';
import React, { ComponentProps, ReactNode } from 'react';
import { kinds, sizes } from 'Helpers/Props';
import { Kind } from 'Helpers/Props/kinds';
import { Size } from 'Helpers/Props/sizes';
import styles from './Label.css';
export interface LabelProps extends ComponentProps<'span'> {
kind?: Extract<Kind, keyof typeof styles>;
size?: Extract<Size, keyof typeof styles>;
outline?: boolean;
children: ReactNode;
}
export default function Label({
className = styles.label,
kind = kinds.DEFAULT,
size = sizes.SMALL,
outline = false,
...otherProps
}: LabelProps) {
return (
<span
className={classNames(
className,
styles[kind],
styles[size],
outline && styles.outline
)}
{...otherProps}
/>
);
}

View File

@@ -1,54 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { align, kinds, sizes } from 'Helpers/Props';
import Link from './Link';
import styles from './Button.css';
class Button extends Component {
//
// Render
render() {
const {
className,
buttonGroupPosition,
kind,
size,
children,
...otherProps
} = this.props;
return (
<Link
className={classNames(
className,
styles[kind],
styles[size],
buttonGroupPosition && styles[buttonGroupPosition]
)}
{...otherProps}
>
{children}
</Link>
);
}
}
Button.propTypes = {
className: PropTypes.string.isRequired,
buttonGroupPosition: PropTypes.oneOf(align.all),
kind: PropTypes.oneOf(kinds.all),
size: PropTypes.oneOf(sizes.all),
children: PropTypes.node
};
Button.defaultProps = {
className: styles.button,
kind: kinds.DEFAULT,
size: sizes.MEDIUM
};
export default Button;

View File

@@ -0,0 +1,37 @@
import classNames from 'classnames';
import React from 'react';
import { align, kinds, sizes } from 'Helpers/Props';
import { Kind } from 'Helpers/Props/kinds';
import { Size } from 'Helpers/Props/sizes';
import Link, { LinkProps } from './Link';
import styles from './Button.css';
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
buttonGroupPosition?: Extract<
(typeof align.all)[number],
keyof typeof styles
>;
kind?: Extract<Kind, keyof typeof styles>;
size?: Extract<Size, keyof typeof styles>;
children: Required<LinkProps['children']>;
}
export default function Button({
className = styles.button,
buttonGroupPosition,
kind = kinds.DEFAULT,
size = sizes.MEDIUM,
...otherProps
}: ButtonProps) {
return (
<Link
className={classNames(
className,
styles[kind],
styles[size],
buttonGroupPosition && styles[buttonGroupPosition]
)}
{...otherProps}
/>
);
}

View File

@@ -1,139 +0,0 @@
import Clipboard from 'clipboard';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormInputButton from 'Components/Form/FormInputButton';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import styles from './ClipboardButton.css';
class ClipboardButton extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._id = getUniqueElememtId();
this._successTimeout = null;
this._testResultTimeout = null;
this.state = {
showSuccess: false,
showError: false
};
}
componentDidMount() {
this._clipboard = new Clipboard(`#${this._id}`, {
text: () => this.props.value,
container: document.getElementById(this._id)
});
this._clipboard.on('success', this.onSuccess);
}
componentDidUpdate() {
const {
showSuccess,
showError
} = this.state;
if (showSuccess || showError) {
this._testResultTimeout = setTimeout(this.resetState, 3000);
}
}
componentWillUnmount() {
if (this._clipboard) {
this._clipboard.destroy();
}
if (this._testResultTimeout) {
clearTimeout(this._testResultTimeout);
}
}
//
// Control
resetState = () => {
this.setState({
showSuccess: false,
showError: false
});
};
//
// Listeners
onSuccess = () => {
this.setState({
showSuccess: true
});
};
onError = () => {
this.setState({
showError: true
});
};
//
// Render
render() {
const {
value,
className,
...otherProps
} = this.props;
const {
showSuccess,
showError
} = this.state;
const showStateIcon = showSuccess || showError;
const iconName = showError ? icons.DANGER : icons.CHECK;
const iconKind = showError ? kinds.DANGER : kinds.SUCCESS;
return (
<FormInputButton
id={this._id}
className={className}
{...otherProps}
>
<span className={showStateIcon ? styles.showStateIcon : undefined}>
{
showSuccess &&
<span className={styles.stateIconContainer}>
<Icon
name={iconName}
kind={iconKind}
/>
</span>
}
{
<span className={styles.clipboardIconContainer}>
<Icon name={icons.CLIPBOARD} />
</span>
}
</span>
</FormInputButton>
);
}
}
ClipboardButton.propTypes = {
className: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
};
ClipboardButton.defaultProps = {
className: styles.button
};
export default ClipboardButton;

View File

@@ -0,0 +1,76 @@
import copy from 'copy-to-clipboard';
import React, { useCallback, useEffect, useState } from 'react';
import FormInputButton from 'Components/Form/FormInputButton';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import { ButtonProps } from './Button';
import styles from './ClipboardButton.css';
export interface ClipboardButtonProps extends Omit<ButtonProps, 'children'> {
value: string;
}
export type ClipboardState = 'success' | 'error' | null;
export default function ClipboardButton({
id,
value,
className = styles.button,
...otherProps
}: ClipboardButtonProps) {
const [state, setState] = useState<ClipboardState>(null);
useEffect(() => {
if (!state) {
return;
}
const timeoutId = setTimeout(() => {
setState(null);
}, 3000);
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [state]);
const handleClick = useCallback(async () => {
try {
if ('clipboard' in navigator) {
await navigator.clipboard.writeText(value);
} else {
copy(value);
}
setState('success');
} catch (e) {
setState('error');
console.error(`Failed to copy to clipboard`, e);
}
}, [value]);
return (
<FormInputButton
className={className}
onClick={handleClick}
{...otherProps}
>
<span className={state ? styles.showStateIcon : undefined}>
{state ? (
<span className={styles.stateIconContainer}>
<Icon
name={state === 'error' ? icons.DANGER : icons.CHECK}
kind={state === 'error' ? kinds.DANGER : kinds.SUCCESS}
/>
</span>
) : null}
<span className={styles.clipboardIconContainer}>
<Icon name={icons.CLIPBOARD} />
</span>
</span>
</FormInputButton>
);
}

View File

@@ -1,96 +1,93 @@
import classNames from 'classnames';
import React, {
ComponentClass,
FunctionComponent,
ComponentPropsWithoutRef,
ElementType,
SyntheticEvent,
useCallback,
} from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styles from './Link.css';
interface ReactRouterLinkProps {
to?: string;
}
export type LinkProps<C extends ElementType = 'button'> =
ComponentPropsWithoutRef<C> & {
component?: C;
to?: string;
target?: string;
isDisabled?: LinkProps<C>['disabled'];
noRouter?: boolean;
onPress?(event: SyntheticEvent): void;
};
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
className?: string;
component?:
| string
| FunctionComponent<LinkProps>
| ComponentClass<LinkProps, unknown>;
to?: string;
target?: string;
isDisabled?: boolean;
noRouter?: boolean;
onPress?(event: SyntheticEvent): void;
}
function Link(props: LinkProps) {
const {
className,
component = 'button',
to,
target,
type,
isDisabled,
noRouter = false,
onPress,
...otherProps
} = props;
export default function Link<C extends ElementType = 'button'>({
className,
component,
to,
target,
type,
isDisabled,
noRouter,
onPress,
...otherProps
}: LinkProps<C>) {
const Component = component || 'button';
const onClick = useCallback(
(event: SyntheticEvent) => {
if (!isDisabled && onPress) {
onPress(event);
if (isDisabled) {
return;
}
onPress?.(event);
},
[isDisabled, onPress]
);
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
target,
};
let el = component;
if (to) {
if (/\w+?:\/\//.test(to)) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_blank';
linkProps.rel = 'noreferrer';
} else if (noRouter) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_self';
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
el = RouterLink;
linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.target = target;
}
}
if (el === 'button' || el === 'input') {
linkProps.type = type || 'button';
linkProps.disabled = isDisabled;
}
linkProps.className = classNames(
const linkClass = classNames(
className,
styles.link,
to && styles.to,
isDisabled && 'isDisabled'
);
const elementProps = {
...otherProps,
type,
...linkProps,
};
if (to) {
const toLink = /\w+?:\/\//.test(to);
elementProps.onClick = onClick;
if (toLink || noRouter) {
return (
<a
href={to}
target={target || (toLink ? '_blank' : '_self')}
rel={toLink ? 'noreferrer' : undefined}
className={linkClass}
onClick={onClick}
{...otherProps}
/>
);
}
return React.createElement(el, elementProps);
return (
<RouterLink
to={`${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`}
target={target}
className={linkClass}
onClick={onClick}
{...otherProps}
/>
);
}
return (
<Component
type={
component === 'button' || component === 'input'
? type || 'button'
: type
}
target={target}
className={linkClass}
disabled={isDisabled}
onClick={onClick}
{...otherProps}
/>
);
}
export default Link;

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import FilterMenuItem from './FilterMenuItem';
import MenuContent from './MenuContent';
@@ -47,7 +48,7 @@ class FilterMenuContent extends Component {
{
customFilters
.sort((a, b) => a.label.localeCompare(b.label))
.sort(sortByProp('label'))
.map((filter) => {
return (
<FilterMenuItem

View File

@@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import IndexerSearchInputConnector from './IndexerSearchInputConnector';
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
import styles from './PageHeader.css';
class PageHeader extends Component {
@@ -87,7 +87,8 @@ class PageHeader extends Component {
to="https://translate.servarr.com/projects/servarr/prowlarr/"
size={24}
/>
<PageHeaderActionsMenuConnector
<PageHeaderActionsMenu
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
/>
</div>

View File

@@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem';
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './PageHeaderActionsMenu.css';
function PageHeaderActionsMenu(props) {
const {
formsAuth,
onKeyboardShortcutsPress,
onRestartPress,
onShutdownPress
} = props;
return (
<div>
<Menu alignMenu={align.RIGHT}>
<MenuButton className={styles.menuButton} aria-label="Menu Button">
<Icon
name={icons.INTERACTIVE}
title={translate('Menu')}
/>
</MenuButton>
<MenuContent>
<MenuItem onPress={onKeyboardShortcutsPress}>
<Icon
className={styles.itemIcon}
name={icons.KEYBOARD}
/>
{translate('KeyboardShortcuts')}
</MenuItem>
<MenuItemSeparator />
<MenuItem onPress={onRestartPress}>
<Icon
className={styles.itemIcon}
name={icons.RESTART}
/>
{translate('Restart')}
</MenuItem>
<MenuItem onPress={onShutdownPress}>
<Icon
className={styles.itemIcon}
name={icons.SHUTDOWN}
kind={kinds.DANGER}
/>
{translate('Shutdown')}
</MenuItem>
{
formsAuth &&
<div className={styles.separator} />
}
{
formsAuth &&
<MenuItem
to={`${window.Prowlarr.urlBase}/logout`}
noRouter={true}
>
<Icon
className={styles.itemIcon}
name={icons.LOGOUT}
/>
Logout
</MenuItem>
}
</MenuContent>
</Menu>
</div>
);
}
PageHeaderActionsMenu.propTypes = {
formsAuth: PropTypes.bool.isRequired,
onKeyboardShortcutsPress: PropTypes.func.isRequired,
onRestartPress: PropTypes.func.isRequired,
onShutdownPress: PropTypes.func.isRequired
};
export default PageHeaderActionsMenu;

View File

@@ -0,0 +1,90 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Icon from 'Components/Icon';
import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem';
import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
import { align, icons, kinds } from 'Helpers/Props';
import { restart, shutdown } from 'Store/Actions/systemActions';
import translate from 'Utilities/String/translate';
import styles from './PageHeaderActionsMenu.css';
interface PageHeaderActionsMenuProps {
onKeyboardShortcutsPress(): void;
}
function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
const { onKeyboardShortcutsPress } = props;
const dispatch = useDispatch();
const { authentication, isDocker } = useSelector(
(state: AppState) => state.system.status.item
);
const formsAuth = authentication === 'forms';
const handleRestartPress = useCallback(() => {
dispatch(restart());
}, [dispatch]);
const handleShutdownPress = useCallback(() => {
dispatch(shutdown());
}, [dispatch]);
return (
<div>
<Menu alignMenu={align.RIGHT}>
<MenuButton className={styles.menuButton} aria-label="Menu Button">
<Icon name={icons.INTERACTIVE} title={translate('Menu')} />
</MenuButton>
<MenuContent>
<MenuItem onPress={onKeyboardShortcutsPress}>
<Icon className={styles.itemIcon} name={icons.KEYBOARD} />
{translate('KeyboardShortcuts')}
</MenuItem>
{isDocker ? null : (
<>
<MenuItemSeparator />
<MenuItem onPress={handleRestartPress}>
<Icon className={styles.itemIcon} name={icons.RESTART} />
{translate('Restart')}
</MenuItem>
<MenuItem onPress={handleShutdownPress}>
<Icon
className={styles.itemIcon}
name={icons.SHUTDOWN}
kind={kinds.DANGER}
/>
{translate('Shutdown')}
</MenuItem>
</>
)}
{formsAuth ? (
<>
<MenuItemSeparator />
<MenuItem
to={`${window.Prowlarr.urlBase}/logout`}
noRouter={true}
>
<Icon className={styles.itemIcon} name={icons.LOGOUT} />
{translate('Logout')}
</MenuItem>
</>
) : null}
</MenuContent>
</Menu>
</div>
);
}
export default PageHeaderActionsMenu;

View File

@@ -1,56 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { restart, shutdown } from 'Store/Actions/systemActions';
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
function createMapStateToProps() {
return createSelector(
(state) => state.system.status,
(status) => {
return {
formsAuth: status.item.authentication === 'forms'
};
}
);
}
const mapDispatchToProps = {
restart,
shutdown
};
class PageHeaderActionsMenuConnector extends Component {
//
// Listeners
onRestartPress = () => {
this.props.restart();
};
onShutdownPress = () => {
this.props.shutdown();
};
//
// Render
render() {
return (
<PageHeaderActionsMenu
{...this.props}
onRestartPress={this.onRestartPress}
onShutdownPress={this.onShutdownPress}
/>
);
}
}
PageHeaderActionsMenuConnector.propTypes = {
restart: PropTypes.func.isRequired,
shutdown: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);

View File

@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
import AppUpdatedModal from 'App/AppUpdatedModal';
import ColorImpairedContext from 'App/ColorImpairedContext';
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
import ConnectionLostModal from 'App/ConnectionLostModal';
import SignalRConnector from 'Components/SignalRConnector';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import locationShape from 'Helpers/Props/Shapes/locationShape';
@@ -102,12 +102,12 @@ class Page extends Component {
{children}
</div>
<AppUpdatedModalConnector
<AppUpdatedModal
isOpen={this.state.isUpdatedModalOpen}
onModalClose={this.onUpdatedModalClose}
/>
<ConnectionLostModalConnector
<ConnectionLostModal
isOpen={this.state.isConnectionLostModalOpen}
onModalClose={this.onConnectionLostModalClose}
/>

View File

@@ -8,7 +8,7 @@ import Scroller from 'Components/Scroller/Scroller';
import { icons } from 'Helpers/Props';
import locationShape from 'Helpers/Props/Shapes/locationShape';
import dimensions from 'Styles/Variables/dimensions';
import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector';
import HealthStatus from 'System/Status/Health/HealthStatus';
import translate from 'Utilities/String/translate';
import MessagesConnector from './Messages/MessagesConnector';
import PageSidebarItem from './PageSidebarItem';
@@ -87,7 +87,7 @@ const links = [
{
title: () => translate('Status'),
to: '/system/status',
statusComponent: HealthStatusConnector
statusComponent: HealthStatus
},
{
title: () => translate('Tasks'),

View File

@@ -141,6 +141,16 @@ class SignalRConnector extends Component {
console.error(`signalR: Unable to find handler for ${name}`);
};
handleApplications = ({ action, resource }) => {
const section = 'settings.applications';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleCommand = (body) => {
if (body.action === 'sync') {
this.props.dispatchFetchCommands();
@@ -150,8 +160,8 @@ class SignalRConnector extends Component {
const resource = body.resource;
const status = resource.status;
// Both sucessful and failed commands need to be
// completed, otherwise they spin until they timeout.
// Both successful and failed commands need to be
// completed, otherwise they spin until they time out.
if (status === 'completed' || status === 'failed') {
this.props.dispatchFinishCommand(resource);
@@ -160,6 +170,16 @@ class SignalRConnector extends Component {
}
};
handleDownloadclient = ({ action, resource }) => {
const section = 'settings.downloadClients';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleHealth = () => {
this.props.dispatchFetchHealth();
};
@@ -168,14 +188,33 @@ class SignalRConnector extends Component {
this.props.dispatchFetchIndexerStatus();
};
handleIndexer = (body) => {
const action = body.action;
handleIndexer = ({ action, resource }) => {
const section = 'indexers';
if (action === 'updated') {
this.props.dispatchUpdateItem({ section, ...body.resource });
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleIndexerproxy = ({ action, resource }) => {
const section = 'settings.indexerProxies';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleNotification = ({ action, resource }) => {
const section = 'settings.notifications';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};

View File

@@ -2,9 +2,11 @@ import React from 'react';
type PropertyFunction<T> = () => T;
// TODO: Convert to generic so `name` can be a type
interface Column {
name: string;
label: string | PropertyFunction<string> | React.ReactNode;
className?: string;
columnLabel?: string;
isSortable?: boolean;
isVisible: boolean;

View File

@@ -0,0 +1,54 @@
import { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
interface PagingOptions {
page: number;
totalPages: number;
gotoPage: ({ page }: { page: number }) => void;
}
function usePaging(options: PagingOptions) {
const { page, totalPages, gotoPage } = options;
const dispatch = useDispatch();
const handleFirstPagePress = useCallback(() => {
dispatch(gotoPage({ page: 1 }));
}, [dispatch, gotoPage]);
const handlePreviousPagePress = useCallback(() => {
dispatch(gotoPage({ page: Math.max(page - 1, 1) }));
}, [page, dispatch, gotoPage]);
const handleNextPagePress = useCallback(() => {
dispatch(gotoPage({ page: Math.min(page + 1, totalPages) }));
}, [page, totalPages, dispatch, gotoPage]);
const handleLastPagePress = useCallback(() => {
dispatch(gotoPage({ page: totalPages }));
}, [totalPages, dispatch, gotoPage]);
const handlePageSelect = useCallback(
(page: number) => {
dispatch(gotoPage({ page }));
},
[dispatch, gotoPage]
);
return useMemo(() => {
return {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
};
}, [
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
]);
}
export default usePaging;

View File

@@ -1,14 +1,15 @@
import PropTypes from 'prop-types';
import React from 'react';
import { kinds } from 'Helpers/Props';
import sortByProp from 'Utilities/Array/sortByProp';
import Label from './Label';
import styles from './TagList.css';
function TagList({ tags, tagList }) {
const sortedTags = tags
.map((tagId) => tagList.find((tag) => tag.id === tagId))
.filter((t) => t !== undefined)
.sort((a, b) => a.label.localeCompare(b.label));
.filter((tag) => !!tag)
.sort(sortByProp('label'));
return (
<div className={styles.tags}>

View File

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

View File

@@ -0,0 +1,9 @@
import { useHistory } from 'react-router-dom';
function useCurrentPage() {
const history = useHistory();
return history.action === 'POP';
}
export default useCurrentPage;

View File

@@ -3,15 +3,15 @@ import { useCallback, useState } from 'react';
export default function useModalOpenState(
initialState: boolean
): [boolean, () => void, () => void] {
const [isOpen, setOpen] = useState(initialState);
const [isOpen, setIsOpen] = useState(initialState);
const setModalOpen = useCallback(() => {
setOpen(true);
}, [setOpen]);
setIsOpen(true);
}, [setIsOpen]);
const setModalClosed = useCallback(() => {
setOpen(false);
}, [setOpen]);
setIsOpen(false);
}, [setIsOpen]);
return [isOpen, setModalOpen, setModalClosed];
}

View File

@@ -0,0 +1,3 @@
type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';
export default TooltipPosition;

View File

@@ -7,7 +7,6 @@ export const PRIMARY = 'primary';
export const PURPLE = 'purple';
export const SUCCESS = 'success';
export const WARNING = 'warning';
export const QUEUE = 'queue';
export const all = [
DANGER,
@@ -19,5 +18,15 @@ export const all = [
PURPLE,
SUCCESS,
WARNING,
QUEUE
];
] as const;
export type Kind =
| 'danger'
| 'default'
| 'disabled'
| 'info'
| 'inverse'
| 'primary'
| 'purple'
| 'success'
| 'warning';

View File

@@ -4,4 +4,6 @@ export const MEDIUM = 'medium';
export const LARGE = 'large';
export const EXTRA_LARGE = 'extraLarge';
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const;
export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge';

View File

@@ -3,6 +3,7 @@ import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Link from 'Components/Link/Link';
import formatDateTime from 'Utilities/Date/formatDateTime';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
@@ -10,7 +11,10 @@ function HistoryDetails(props) {
const {
indexer,
eventType,
data
date,
data,
shortDateFormat,
timeFormat
} = props;
if (eventType === 'indexerQuery' || eventType === 'indexerRss') {
@@ -22,7 +26,9 @@ function HistoryDetails(props) {
offset,
source,
host,
url
url,
elapsedTime,
cached
} = data;
return (
@@ -104,6 +110,24 @@ function HistoryDetails(props) {
/> :
null
}
{
elapsedTime ?
<DescriptionListItem
title={translate('ElapsedTime')}
data={`${elapsedTime}ms${cached === '1' ? ' (cached)' : ''}`}
/> :
null
}
{
date ?
<DescriptionListItem
title={translate('Date')}
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
/> :
null
}
</DescriptionList>
);
}
@@ -111,10 +135,19 @@ function HistoryDetails(props) {
if (eventType === 'releaseGrabbed') {
const {
source,
host,
grabTitle,
url
url,
publishedDate,
infoUrl,
downloadClient,
downloadClientName,
elapsedTime,
grabMethod
} = data;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
return (
<DescriptionList>
{
@@ -135,6 +168,15 @@ function HistoryDetails(props) {
null
}
{
data ?
<DescriptionListItem
title={translate('Host')}
data={host}
/> :
null
}
{
data ?
<DescriptionListItem
@@ -144,6 +186,33 @@ function HistoryDetails(props) {
null
}
{
infoUrl ?
<DescriptionListItem
title={translate('InfoUrl')}
data={<Link to={infoUrl}>{infoUrl}</Link>}
/> :
null
}
{
publishedDate ?
<DescriptionListItem
title={translate('PublishedDate')}
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/> :
null
}
{
downloadClientNameInfo ?
<DescriptionListItem
title={translate('DownloadClient')}
data={downloadClientNameInfo}
/> :
null
}
{
data ?
<DescriptionListItem
@@ -152,11 +221,40 @@ function HistoryDetails(props) {
/> :
null
}
{
elapsedTime ?
<DescriptionListItem
title={translate('ElapsedTime')}
data={`${elapsedTime}ms`}
/> :
null
}
{
grabMethod ?
<DescriptionListItem
title={translate('Redirected')}
data={grabMethod.toLowerCase() === 'redirect' ? translate('Yes') : translate('No')}
/> :
null
}
{
date ?
<DescriptionListItem
title={translate('Date')}
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
/> :
null
}
</DescriptionList>
);
}
if (eventType === 'indexerAuth') {
const { elapsedTime } = data;
return (
<DescriptionList
descriptionClassName={styles.description}
@@ -170,6 +268,24 @@ function HistoryDetails(props) {
/> :
null
}
{
elapsedTime ?
<DescriptionListItem
title={translate('ElapsedTime')}
data={`${elapsedTime}ms`}
/> :
null
}
{
date ?
<DescriptionListItem
title={translate('Date')}
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
/> :
null
}
</DescriptionList>
);
}
@@ -181,6 +297,15 @@ function HistoryDetails(props) {
title={translate('Name')}
data={data.query}
/>
{
date ?
<DescriptionListItem
title={translate('Date')}
data={formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
/> :
null
}
</DescriptionList>
);
}
@@ -188,6 +313,7 @@ function HistoryDetails(props) {
HistoryDetails.propTypes = {
indexer: PropTypes.object.isRequired,
eventType: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired

View File

@@ -29,6 +29,7 @@ function HistoryDetailsModal(props) {
isOpen,
eventType,
indexer,
date,
data,
shortDateFormat,
timeFormat,
@@ -49,6 +50,7 @@ function HistoryDetailsModal(props) {
<HistoryDetails
eventType={eventType}
indexer={indexer}
date={date}
data={data}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
@@ -71,6 +73,7 @@ HistoryDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired,
indexer: PropTypes.object.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,

View File

@@ -20,21 +20,22 @@ function getIconName(eventType) {
}
}
function getIconKind(successful) {
switch (successful) {
case false:
return kinds.DANGER;
default:
return kinds.DEFAULT;
function getIconKind(successful, redirect) {
if (redirect) {
return kinds.INFO;
} else if (!successful) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getTooltip(eventType, data, indexer) {
function getTooltip(eventType, data, indexer, redirect) {
switch (eventType) {
case 'indexerQuery':
return `Query "${data.query}" sent to ${indexer.name}`;
case 'releaseGrabbed':
return `Release grabbed from ${indexer.name}`;
return redirect ? `Release grabbed via redirect from ${indexer.name}` : `Release grabbed from ${indexer.name}`;
case 'indexerAuth':
return `Auth attempted for ${indexer.name}`;
case 'indexerRss':
@@ -45,9 +46,12 @@ function getTooltip(eventType, data, indexer) {
}
function HistoryEventTypeCell({ eventType, successful, data, indexer }) {
const { grabMethod } = data;
const redirect = grabMethod && grabMethod.toLowerCase() === 'redirect';
const iconName = getIconName(eventType);
const iconKind = getIconKind(successful);
const tooltip = getTooltip(eventType, data, indexer);
const iconKind = getIconKind(successful, redirect);
const tooltip = getTooltip(eventType, data, indexer, redirect);
return (
<TableRowCell

View File

@@ -257,6 +257,7 @@ class HistoryRow extends Component {
key={parameter.key}
title={parameter.title}
value={data[parameter.key]}
queryType={data.queryType}
/>
);
}
@@ -370,8 +371,9 @@ class HistoryRow extends Component {
return (
<RelativeDateCell
key={name}
date={date}
className={styles.date}
date={date}
includeSeconds={true}
/>
);
}
@@ -408,6 +410,7 @@ class HistoryRow extends Component {
<HistoryDetailsModal
isOpen={this.state.isDetailsModalOpen}
eventType={eventType}
date={date}
data={data}
indexer={indexer}
isMarkingAsFailed={isMarkingAsFailed}

View File

@@ -1,14 +1,16 @@
import React from 'react';
import Link from 'Components/Link/Link';
import { HistoryQueryType } from 'typings/History';
import styles from './HistoryRowParameter.css';
interface HistoryRowParameterProps {
title: string;
value: string;
queryType: HistoryQueryType;
}
function HistoryRowParameter(props: HistoryRowParameterProps) {
const { title, value } = props;
const { title, value, queryType } = props;
const type = title.toLowerCase();
@@ -18,7 +20,13 @@ function HistoryRowParameter(props: HistoryRowParameterProps) {
link = <Link to={`https://imdb.com/title/${value}/`}>{value}</Link>;
} else if (type === 'tmdb') {
link = (
<Link to={`https://www.themoviedb.org/movie/${value}`}>{value}</Link>
<Link
to={`https://www.themoviedb.org/${
queryType === 'tvsearch' ? 'tv' : 'movie'
}/${value}`}
>
{value}
</Link>
);
} else if (type === 'tvdb') {
link = (

View File

@@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
import styles from './AddIndexerModal.css';
function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
onModalClose={onModalClose}
className={styles.modal}
>
<AddIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
onSelectIndexer={onSelectIndexer}
/>
</Modal>
);
}
AddIndexerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired,
onSelectIndexer: PropTypes.func.isRequired
};
export default AddIndexerModal;

View File

@@ -0,0 +1,44 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import { clearIndexerSchema } from 'Store/Actions/indexerActions';
import AddIndexerModalContent from './AddIndexerModalContent';
import styles from './AddIndexerModal.css';
interface AddIndexerModalProps {
isOpen: boolean;
onSelectIndexer(): void;
onModalClose(): void;
}
function AddIndexerModal({
isOpen,
onSelectIndexer,
onModalClose,
...otherProps
}: AddIndexerModalProps) {
const dispatch = useDispatch();
const onModalClosePress = useCallback(() => {
dispatch(clearIndexerSchema());
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
className={styles.modal}
onModalClose={onModalClosePress}
>
<AddIndexerModalContent
{...otherProps}
onSelectIndexer={onSelectIndexer}
onModalClose={onModalClosePress}
/>
</Modal>
);
}
export default AddIndexerModal;

View File

@@ -19,12 +19,18 @@
margin-bottom: 16px;
}
.alert {
.notice {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}
.alert {
composes: alert from '~Components/Alert.css';
text-align: center;
}
.scroller {
flex: 1 1 auto;
}

View File

@@ -10,6 +10,7 @@ interface CssExports {
'indexers': string;
'modalBody': string;
'modalFooter': string;
'notice': string;
'scroller': string;
}
export const cssExports: CssExports;

View File

@@ -1,324 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Scroller from 'Components/Scroller/Scroller';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds, scrollDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import SelectIndexerRow from './SelectIndexerRow';
import styles from './AddIndexerModalContent.css';
const columns = [
{
name: 'protocol',
label: () => translate('Protocol'),
isSortable: true,
isVisible: true
},
{
name: 'sortName',
label: () => translate('Name'),
isSortable: true,
isVisible: true
},
{
name: 'language',
label: () => translate('Language'),
isSortable: true,
isVisible: true
},
{
name: 'description',
label: () => translate('Description'),
isSortable: false,
isVisible: true
},
{
name: 'privacy',
label: () => translate('Privacy'),
isSortable: true,
isVisible: true
},
{
name: 'categories',
label: () => translate('Categories'),
isSortable: false,
isVisible: true
}
];
const protocols = [
{
key: 'torrent',
value: 'torrent'
},
{
key: 'usenet',
value: 'nzb'
}
];
const privacyLevels = [
{
key: 'private',
get value() {
return translate('Private');
}
},
{
key: 'semiPrivate',
get value() {
return translate('SemiPrivate');
}
},
{
key: 'public',
get value() {
return translate('Public');
}
}
];
class AddIndexerModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
filter: '',
filterProtocols: [],
filterLanguages: [],
filterPrivacyLevels: [],
filterCategories: []
};
}
//
// Listeners
onFilterChange = ({ value }) => {
this.setState({ filter: value });
};
//
// Render
render() {
const {
indexers,
onIndexerSelect,
sortKey,
sortDirection,
isFetching,
isPopulated,
error,
onSortPress,
onModalClose
} = this.props;
const languages = Array.from(new Set(indexers.map(({ language }) => language)))
.sort((a, b) => a.localeCompare(b))
.map((language) => ({ key: language, value: language }));
const filteredIndexers = indexers.filter((indexer) => {
const {
filter,
filterProtocols,
filterLanguages,
filterPrivacyLevels,
filterCategories
} = this.state;
if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
return false;
}
if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) {
return false;
}
if (filterLanguages.length && !filterLanguages.includes(indexer.language)) {
return false;
}
if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) {
return false;
}
if (filterCategories.length) {
const { categories = [] } = indexer.capabilities || {};
const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)];
const flatCategories = categories
.filter((item) => item.id < 100000)
.flatMap(flat);
if (!filterCategories.every((item) => flatCategories.includes(item))) {
return false;
}
}
return true;
});
const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddIndexer')}
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<TextInput
className={styles.filterInput}
placeholder={translate('FilterPlaceHolder')}
name="filter"
value={this.state.filter}
autoFocus={true}
onChange={this.onFilterChange}
/>
<div className={styles.filterRow}>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>{translate('Protocol')}</label>
<EnhancedSelectInput
name="indexerProtocols"
value={this.state.filterProtocols}
values={protocols}
onChange={({ value }) => this.setState({ filterProtocols: value })}
/>
</div>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>{translate('Language')}</label>
<EnhancedSelectInput
name="indexerLanguages"
value={this.state.filterLanguages}
values={languages}
onChange={({ value }) => this.setState({ filterLanguages: value })}
/>
</div>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>{translate('Privacy')}</label>
<EnhancedSelectInput
name="indexerPrivacyLevels"
value={this.state.filterPrivacyLevels}
values={privacyLevels}
onChange={({ value }) => this.setState({ filterPrivacyLevels: value })}
/>
</div>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>{translate('Categories')}</label>
<NewznabCategorySelectInputConnector
name="indexerCategories"
value={this.state.filterCategories}
onChange={({ value }) => this.setState({ filterCategories: value })}
/>
</div>
</div>
<Alert
kind={kinds.INFO}
className={styles.alert}
>
<div>
{translate('ProwlarrSupportsAnyIndexer')}
</div>
</Alert>
<Scroller
className={styles.scroller}
autoFocus={false}
>
{
isFetching ? <LoadingIndicator /> : null
}
{
error ? <Alert kind={kinds.DANGER}>{errorMessage}</Alert> : null
}
{
isPopulated && !!indexers.length ?
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{
filteredIndexers.map((indexer) => (
<SelectIndexerRow
key={`${indexer.implementation}-${indexer.name}`}
implementation={indexer.implementation}
implementationName={indexer.implementationName}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
))
}
</TableBody>
</Table> :
null
}
{
isPopulated && !!indexers.length && !filteredIndexers.length ?
<Alert
kind={kinds.WARNING}
>
{translate('NoIndexersFound')}
</Alert> :
null
}
</Scroller>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.available}>
{
isPopulated ?
translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
null
}
</div>
<div>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
}
AddIndexerModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
onSortPress: PropTypes.func.isRequired,
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
onIndexerSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerModalContent;

View File

@@ -0,0 +1,434 @@
import { some } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import IndexerAppState from 'App/State/IndexerAppState';
import Alert from 'Components/Alert';
import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Scroller from 'Components/Scroller/Scroller';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds, scrollDirections } from 'Helpers/Props';
import Indexer, { IndexerCategory } from 'Indexer/Indexer';
import {
fetchIndexerSchema,
selectIndexerSchema,
setIndexerSchemaSort,
} from 'Store/Actions/indexerActions';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SortCallback } from 'typings/callbacks';
import sortByProp from 'Utilities/Array/sortByProp';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import SelectIndexerRow from './SelectIndexerRow';
import styles from './AddIndexerModalContent.css';
const COLUMNS = [
{
name: 'protocol',
label: () => translate('Protocol'),
isSortable: true,
isVisible: true,
},
{
name: 'sortName',
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'language',
label: () => translate('Language'),
isSortable: true,
isVisible: true,
},
{
name: 'description',
label: () => translate('Description'),
isSortable: false,
isVisible: true,
},
{
name: 'privacy',
label: () => translate('Privacy'),
isSortable: true,
isVisible: true,
},
{
name: 'categories',
label: () => translate('Categories'),
isSortable: false,
isVisible: true,
},
];
const PROTOCOLS = [
{
key: 'torrent',
value: 'torrent',
},
{
key: 'usenet',
value: 'nzb',
},
];
const PRIVACY_LEVELS = [
{
key: 'private',
get value() {
return translate('Private');
},
},
{
key: 'semiPrivate',
get value() {
return translate('SemiPrivate');
},
},
{
key: 'public',
get value() {
return translate('Public');
},
},
];
interface IndexerSchema extends Indexer {
isExistingIndexer: boolean;
}
function createAddIndexersSelector() {
return createSelector(
createClientSideCollectionSelector('indexers.schema'),
createAllIndexersSelector(),
(indexers: IndexerAppState, allIndexers) => {
const { isFetching, isPopulated, error, items, sortDirection, sortKey } =
indexers;
const indexerList: IndexerSchema[] = items.map((item) => {
const { definitionName } = item;
return {
...item,
isExistingIndexer: some(allIndexers, { definitionName }),
};
});
return {
isFetching,
isPopulated,
error,
indexers: indexerList,
sortKey,
sortDirection,
};
}
);
}
interface AddIndexerModalContentProps {
onSelectIndexer(): void;
onModalClose(): void;
}
function AddIndexerModalContent(props: AddIndexerModalContentProps) {
const { onSelectIndexer, onModalClose } = props;
const { isFetching, isPopulated, error, indexers, sortKey, sortDirection } =
useSelector(createAddIndexersSelector());
const dispatch = useDispatch();
const [filter, setFilter] = useState('');
const [filterProtocols, setFilterProtocols] = useState<string[]>([]);
const [filterLanguages, setFilterLanguages] = useState<string[]>([]);
const [filterPrivacyLevels, setFilterPrivacyLevels] = useState<string[]>([]);
const [filterCategories, setFilterCategories] = useState<number[]>([]);
useEffect(
() => {
dispatch(fetchIndexerSchema());
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const onFilterChange = useCallback(
({ value }: { value: string }) => {
setFilter(value);
},
[setFilter]
);
const onFilterProtocolsChange = useCallback(
({ value }: { value: string[] }) => {
setFilterProtocols(value);
},
[setFilterProtocols]
);
const onFilterLanguagesChange = useCallback(
({ value }: { value: string[] }) => {
setFilterLanguages(value);
},
[setFilterLanguages]
);
const onFilterPrivacyLevelsChange = useCallback(
({ value }: { value: string[] }) => {
setFilterPrivacyLevels(value);
},
[setFilterPrivacyLevels]
);
const onFilterCategoriesChange = useCallback(
({ value }: { value: number[] }) => {
setFilterCategories(value);
},
[setFilterCategories]
);
const onIndexerSelect = useCallback(
({
implementation,
implementationName,
name,
}: {
implementation: string;
implementationName: string;
name: string;
}) => {
dispatch(
selectIndexerSchema({
implementation,
implementationName,
name,
})
);
onSelectIndexer();
},
[dispatch, onSelectIndexer]
);
const onSortPress = useCallback<SortCallback>(
(sortKey, sortDirection) => {
dispatch(setIndexerSchemaSort({ sortKey, sortDirection }));
},
[dispatch]
);
const languages = useMemo(
() =>
Array.from(new Set(indexers.map(({ language }) => language)))
.map((language) => ({ key: language, value: language }))
.sort(sortByProp('value')),
[indexers]
);
const filteredIndexers = useMemo(() => {
const flat = ({
id,
subCategories = [],
}: {
id: number;
subCategories: IndexerCategory[];
}): number[] => [id, ...subCategories.flatMap(flat)];
return indexers.filter((indexer) => {
if (
filter.length &&
!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) &&
!indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())
) {
return false;
}
if (
filterProtocols.length &&
!filterProtocols.includes(indexer.protocol)
) {
return false;
}
if (
filterLanguages.length &&
!filterLanguages.includes(indexer.language)
) {
return false;
}
if (
filterPrivacyLevels.length &&
!filterPrivacyLevels.includes(indexer.privacy)
) {
return false;
}
if (filterCategories.length) {
const { categories = [] } = indexer.capabilities || {};
const flatCategories = categories
.filter((item) => item.id < 100000)
.flatMap(flat);
if (
!filterCategories.every((categoryId) =>
flatCategories.includes(categoryId)
)
) {
return false;
}
}
return true;
});
}, [
indexers,
filter,
filterProtocols,
filterLanguages,
filterPrivacyLevels,
filterCategories,
]);
const errorMessage = getErrorMessage(
error,
translate('UnableToLoadIndexers')
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<TextInput
className={styles.filterInput}
placeholder={translate('FilterPlaceHolder')}
name="filter"
value={filter}
autoFocus={true}
onChange={onFilterChange}
/>
<div className={styles.filterRow}>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>
{translate('Protocol')}
</label>
<EnhancedSelectInput
name="indexerProtocols"
value={filterProtocols}
values={PROTOCOLS}
onChange={onFilterProtocolsChange}
/>
</div>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>
{translate('Language')}
</label>
<EnhancedSelectInput
name="indexerLanguages"
value={filterLanguages}
values={languages}
onChange={onFilterLanguagesChange}
/>
</div>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>{translate('Privacy')}</label>
<EnhancedSelectInput
name="indexerPrivacyLevels"
value={filterPrivacyLevels}
values={PRIVACY_LEVELS}
onChange={onFilterPrivacyLevelsChange}
/>
</div>
<div className={styles.filterContainer}>
<label className={styles.filterLabel}>
{translate('Categories')}
</label>
<NewznabCategorySelectInputConnector
name="indexerCategories"
value={filterCategories}
onChange={onFilterCategoriesChange}
/>
</div>
</div>
<Alert kind={kinds.INFO} className={styles.notice}>
<div>{translate('ProwlarrSupportsAnyIndexer')}</div>
</Alert>
<Scroller className={styles.scroller} autoFocus={false}>
{isFetching ? <LoadingIndicator /> : null}
{error ? (
<Alert kind={kinds.DANGER} className={styles.alert}>
{errorMessage}
</Alert>
) : null}
{isPopulated && !!indexers.length ? (
<Table
columns={COLUMNS}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{filteredIndexers.map((indexer) => (
<SelectIndexerRow
{...indexer}
key={`${indexer.implementation}-${indexer.name}`}
implementation={indexer.implementation}
implementationName={indexer.implementationName}
onIndexerSelect={onIndexerSelect}
/>
))}
</TableBody>
</Table>
) : null}
{isPopulated && !!indexers.length && !filteredIndexers.length ? (
<Alert kind={kinds.WARNING} className={styles.alert}>
{translate('NoIndexersFound')}
</Alert>
) : null}
</Scroller>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.available}>
{isPopulated
? translate('CountIndexersAvailable', {
count: filteredIndexers.length,
})
: null}
</div>
<div>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
export default AddIndexerModalContent;

View File

@@ -1,94 +0,0 @@
import { some } from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import AddIndexerModalContent from './AddIndexerModalContent';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('indexers.schema'),
createAllIndexersSelector(),
(indexers, allIndexers) => {
const {
isFetching,
isPopulated,
error,
items,
sortDirection,
sortKey
} = indexers;
const indexerList = items.map((item) => {
const { definitionName } = item;
return {
...item,
isExistingIndexer: some(allIndexers, { definitionName })
};
});
return {
isFetching,
isPopulated,
error,
indexers: indexerList,
sortKey,
sortDirection
};
}
);
}
const mapDispatchToProps = {
fetchIndexerSchema,
selectIndexerSchema,
setIndexerSchemaSort
};
class AddIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexerSchema();
}
//
// Listeners
onIndexerSelect = ({ implementation, implementationName, name }) => {
this.props.selectIndexerSchema({ implementation, implementationName, name });
this.props.onSelectIndexer();
};
onSortPress = (sortKey, sortDirection) => {
this.props.setIndexerSchemaSort({ sortKey, sortDirection });
};
//
// Render
render() {
return (
<AddIndexerModalContent
{...this.props}
onSortPress={this.onSortPress}
onIndexerSelect={this.onIndexerSelect}
/>
);
}
}
AddIndexerModalContentConnector.propTypes = {
fetchIndexerSchema: PropTypes.func.isRequired,
selectIndexerSchema: PropTypes.func.isRequired,
setIndexerSchemaSort: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSelectIndexer: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);

View File

@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons } from 'Helpers/Props';
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
@@ -12,7 +13,7 @@ import styles from './SelectIndexerRow.css';
interface SelectIndexerRowProps {
name: string;
protocol: string;
protocol: DownloadProtocol;
privacy: IndexerPrivacy;
language: string;
description: string;

View File

@@ -19,6 +19,7 @@ interface SavePayload {
seedRatio?: number;
seedTime?: number;
packSeedTime?: number;
preferMagnetUrl?: boolean;
}
interface EditIndexerModalContentProps {
@@ -65,6 +66,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
null
);
const [preferMagnetUrl, setPreferMagnetUrl] = useState<
null | string | boolean
>(null);
const save = useCallback(() => {
let hasChanges = false;
@@ -105,6 +109,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
payload.packSeedTime = packSeedTime as number;
}
if (preferMagnetUrl !== null) {
hasChanges = true;
payload.preferMagnetUrl = preferMagnetUrl === 'true';
}
if (hasChanges) {
onSavePress(payload);
}
@@ -118,6 +127,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
seedRatio,
seedTime,
packSeedTime,
preferMagnetUrl,
onSavePress,
onModalClose,
]);
@@ -146,6 +156,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
case 'packSeedTime':
setPackSeedTime(value);
break;
case 'preferMagnetUrl':
setPreferMagnetUrl(value);
break;
default:
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
}
@@ -254,6 +267,18 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
onChange={onInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('PreferMagnetUrl')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="preferMagnetUrl"
value={preferMagnetUrl}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter className={styles.modalFooter}>

View File

@@ -2,6 +2,7 @@ import { uniqBy } from 'lodash';
import React from 'react';
import Label from 'Components/Label';
import { IndexerCapabilities } from 'Indexer/Indexer';
import translate from 'Utilities/String/translate';
interface CapabilitiesLabelProps {
capabilities: IndexerCapabilities;
@@ -38,7 +39,7 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) {
);
})}
{filteredList.length === 0 ? <Label>{'None'}</Label> : null}
{filteredList.length === 0 ? <Label>{translate('None')}</Label> : null}
</span>
);
}

View File

@@ -29,7 +29,8 @@
.minimumSeeders,
.seedRatio,
.seedTime,
.packSeedTime {
.packSeedTime,
.preferMagnetUrl {
composes: cell;
flex: 0 0 90px;

View File

@@ -11,6 +11,7 @@ interface CssExports {
'id': string;
'minimumSeeders': string;
'packSeedTime': string;
'preferMagnetUrl': string;
'priority': string;
'privacy': string;
'protocol': string;

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import CheckInput from 'Components/Form/CheckInput';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
@@ -74,6 +75,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')
?.value ?? undefined;
const preferMagnetUrl =
fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')
?.value ?? undefined;
const rssUrl = `${window.location.origin}${
window.Prowlarr.urlBase
}/${id}/api?apikey=${encodeURIComponent(
@@ -102,6 +107,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
setIsDeleteIndexerModalOpen(false);
}, [setIsDeleteIndexerModalOpen]);
const checkInputCallback = useCallback(() => {
// Mock handler to satisfy `onChange` being required for `CheckInput`.
}, []);
const onSelectedChange = useCallback(
({ id, value, shiftKey }: SelectStateInputProps) => {
selectDispatch({
@@ -277,6 +286,21 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
);
}
if (name === 'preferMagnetUrl') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{preferMagnetUrl === undefined ? null : (
<CheckInput
name="preferMagnetUrl"
value={preferMagnetUrl}
isDisabled={true}
onChange={checkInputCallback}
/>
)}
</VirtualTableRowCell>
);
}
if (name === 'actions') {
return (
<VirtualTableRowCell

View File

@@ -46,11 +46,7 @@ const columnsSelector = createSelector(
(columns) => columns
);
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
index,
style,
data,
}) => {
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data;
if (index >= items.length) {
@@ -77,7 +73,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
/>
</div>
);
};
}
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;

View File

@@ -22,7 +22,8 @@
.minimumSeeders,
.seedRatio,
.seedTime,
.packSeedTime {
.packSeedTime,
.preferMagnetUrl {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 90px;

View File

@@ -8,6 +8,7 @@ interface CssExports {
'id': string;
'minimumSeeders': string;
'packSeedTime': string;
'preferMagnetUrl': string;
'priority': string;
'privacy': string;
'protocol': string;

View File

@@ -1,4 +1,4 @@
import React, { Fragment, useCallback } from 'react';
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -32,19 +32,17 @@ function IndexerIndexTableOptions(props: IndexerIndexTableOptionsProps) {
);
return (
<Fragment>
<FormGroup>
<FormLabel>{translate('ShowSearch')}</FormLabel>
<FormGroup>
<FormLabel>{translate('ShowSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showSearchAction"
value={showSearchAction}
helpText={translate('ShowSearchHelpText')}
onChange={onTableOptionChangeWrapper}
/>
</FormGroup>
</Fragment>
<FormInputGroup
type={inputTypes.CHECK}
name="showSearchAction"
value={showSearchAction}
helpText={translate('ShowSearchHelpText')}
onChange={onTableOptionChangeWrapper}
/>
</FormGroup>
);
}

View File

@@ -8,6 +8,30 @@ import translate from 'Utilities/String/translate';
import DisabledIndexerInfo from './DisabledIndexerInfo';
import styles from './IndexerStatusCell.css';
function getIconKind(enabled: boolean, redirect: boolean) {
if (enabled) {
return redirect ? kinds.INFO : kinds.SUCCESS;
}
return kinds.DEFAULT;
}
function getIconName(enabled: boolean, redirect: boolean) {
if (enabled) {
return redirect ? icons.REDIRECT : icons.CHECK;
}
return icons.BLOCKLIST;
}
function getIconTooltip(enabled: boolean, redirect: boolean) {
if (enabled) {
return redirect ? translate('EnabledRedirected') : translate('Enabled');
}
return translate('Disabled');
}
interface IndexerStatusCellProps {
className: string;
enabled: boolean;
@@ -30,22 +54,14 @@ function IndexerStatusCell(props: IndexerStatusCellProps) {
...otherProps
} = props;
const enableKind = redirect ? kinds.INFO : kinds.SUCCESS;
const enableIcon = redirect ? icons.REDIRECT : icons.CHECK;
const enableTitle = redirect
? translate('EnabledRedirected')
: translate('Enabled');
return (
<Component className={className} {...otherProps}>
{
<Icon
className={styles.statusIcon}
kind={enabled ? enableKind : kinds.DEFAULT}
name={enabled ? enableIcon : icons.BLOCKLIST}
title={enabled ? enableTitle : translate('Disabled')}
/>
}
<Icon
className={styles.statusIcon}
kind={getIconKind(enabled, redirect)}
name={getIconName(enabled, redirect)}
title={getIconTooltip(enabled, redirect)}
/>
{status ? (
<Popover
className={styles.indexerStatusTooltip}

View File

@@ -11,3 +11,7 @@
border-color: var(--usenetColor);
background-color: var(--usenetColor);
}
.unknown {
composes: label from '~Components/Label.css';
}

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'torrent': string;
'unknown': string;
'usenet': string;
}
export const cssExports: CssExports;

View File

@@ -1,18 +1,15 @@
import React from 'react';
import Label from 'Components/Label';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import styles from './ProtocolLabel.css';
interface ProtocolLabelProps {
protocol: string;
protocol: DownloadProtocol;
}
function ProtocolLabel(props: ProtocolLabelProps) {
const { protocol } = props;
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(7053)
return <Label className={styles[protocol]}>{protocolName}</Label>;
}

View File

@@ -1,4 +1,5 @@
import ModelBase from 'App/ModelBase';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
export interface IndexerStatus extends ModelBase {
disabledTill: Date;
@@ -38,6 +39,7 @@ export interface IndexerField extends ModelBase {
interface Indexer extends ModelBase {
name: string;
definitionName: string;
description: string;
encoding: string;
language: string;
@@ -48,7 +50,7 @@ interface Indexer extends ModelBase {
supportsSearch: boolean;
supportsRedirect: boolean;
supportsPagination: boolean;
protocol: string;
protocol: DownloadProtocol;
privacy: IndexerPrivacy;
priority: number;
fields: IndexerField[];

View File

@@ -68,13 +68,14 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) {
key={parameter.key}
title={parameter.title}
value={data[parameter.key as keyof HistoryData].toString()}
queryType={data.queryType}
/>
);
})}
</div>
</TableRowCell>
<RelativeDateCell date={date} />
<RelativeDateCell date={date} includeSeconds={true} />
<TableRowCell className={styles.source}>
{data.source ? data.source : null}
@@ -83,14 +84,15 @@ function IndexerHistoryRow(props: IndexerHistoryRowProps) {
<TableRowCell className={styles.details}>
<IconButton
name={icons.INFO}
onPress={onDetailsModalPress}
title={translate('HistoryDetails')}
onPress={onDetailsModalPress}
/>
</TableRowCell>
<HistoryDetailsModal
isOpen={isDetailsModalOpen}
eventType={eventType}
date={date}
data={data}
indexer={indexer}
shortDateFormat={shortDateFormat}

View File

@@ -1,8 +1,6 @@
import { uniqBy } from 'lodash';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { createSelector } from 'reselect';
import Alert from 'Components/Alert';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
@@ -26,23 +24,12 @@ import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
import Indexer, { IndexerCapabilities } from 'Indexer/Indexer';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import useIndexer from 'Indexer/useIndexer';
import translate from 'Utilities/String/translate';
import IndexerHistory from './History/IndexerHistory';
import styles from './IndexerInfoModalContent.css';
function createIndexerInfoItemSelector(indexerId: number) {
return createSelector(
createIndexerSelectorForHook(indexerId),
(indexer?: Indexer) => {
return {
indexer,
};
}
);
}
const tabs = ['details', 'categories', 'history', 'stats'];
const TABS = ['details', 'categories', 'history', 'stats'];
interface IndexerInfoModalContentProps {
indexerId: number;
@@ -51,9 +38,7 @@ interface IndexerInfoModalContentProps {
}
function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
const { indexerId, onCloneIndexerPress } = props;
const { indexer } = useSelector(createIndexerInfoItemSelector(indexerId));
const { indexerId, onModalClose, onCloneIndexerPress } = props;
const {
id,
@@ -67,53 +52,53 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
protocol,
privacy,
capabilities = {} as IndexerCapabilities,
} = indexer as Indexer;
} = useIndexer(indexerId) as Indexer;
const { onModalClose } = props;
const baseUrl =
fields.find((field) => field.name === 'baseUrl')?.value ??
(Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
const vipExpiration =
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
const [selectedTab, setSelectedTab] = useState(tabs[0]);
const [selectedTab, setSelectedTab] = useState(TABS[0]);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
useState(false);
const onTabSelect = useCallback(
(index: number) => {
const selectedTab = tabs[index];
const handleTabSelect = useCallback(
(selectedIndex: number) => {
const selectedTab = TABS[selectedIndex];
setSelectedTab(selectedTab);
},
[setSelectedTab]
);
const onEditIndexerPress = useCallback(() => {
const handleEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true);
}, [setIsEditIndexerModalOpen]);
const onEditIndexerModalClose = useCallback(() => {
const handleEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, [setIsEditIndexerModalOpen]);
const onDeleteIndexerPress = useCallback(() => {
const handleDeleteIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(false);
setIsDeleteIndexerModalOpen(true);
}, [setIsDeleteIndexerModalOpen]);
const onDeleteIndexerModalClose = useCallback(() => {
const handleDeleteIndexerModalClose = useCallback(() => {
setIsDeleteIndexerModalOpen(false);
onModalClose();
}, [setIsDeleteIndexerModalOpen, onModalClose]);
const onCloneIndexerPressWrapper = useCallback(() => {
const handleCloneIndexerPressWrapper = useCallback(() => {
onCloneIndexerPress(id);
onModalClose();
}, [id, onCloneIndexerPress, onModalClose]);
const baseUrl =
fields.find((field) => field.name === 'baseUrl')?.value ??
(Array.isArray(indexerUrls) ? indexerUrls[0] : undefined);
const indexerUrl = baseUrl?.replace(/(:\/\/)api\./, '$1');
const vipExpiration =
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{`${name}`}</ModalHeader>
@@ -121,8 +106,8 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
<ModalBody>
<Tabs
className={styles.tabs}
selectedIndex={tabs.indexOf(selectedTab)}
onSelect={onTabSelect}
selectedIndex={TABS.indexOf(selectedTab)}
onSelect={handleTabSelect}
>
<TabList className={styles.tabList}>
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
@@ -178,10 +163,8 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
{translate('IndexerSite')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
{baseUrl ? (
<Link to={baseUrl}>
{baseUrl.replace(/(:\/\/)api\./, '$1')}
</Link>
{indexerUrl ? (
<Link to={indexerUrl}>{indexerUrl}</Link>
) : (
'-'
)}
@@ -365,16 +348,16 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
onPress={handleDeleteIndexerPress}
>
{translate('Delete')}
</Button>
<Button onPress={onCloneIndexerPressWrapper}>
<Button onPress={handleCloneIndexerPressWrapper}>
{translate('Clone')}
</Button>
</div>
<div>
<Button onPress={onEditIndexerPress}>{translate('Edit')}</Button>
<Button onPress={handleEditIndexerPress}>{translate('Edit')}</Button>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</div>
</ModalFooter>
@@ -382,14 +365,14 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
id={id}
onModalClose={onEditIndexerModalClose}
onDeleteIndexerPress={onDeleteIndexerPress}
onModalClose={handleEditIndexerModalClose}
onDeleteIndexerPress={handleDeleteIndexerPress}
/>
<DeleteIndexerModal
isOpen={isDeleteIndexerModalOpen}
indexerId={id}
onModalClose={onDeleteIndexerModalClose}
onModalClose={handleDeleteIndexerModalClose}
/>
</ModalContent>
);

View File

@@ -1,4 +1,6 @@
.message {
composes: alert from '~Components/Alert.css';
margin-top: 10px;
margin-bottom: 30px;
text-align: center;

View File

@@ -1,4 +1,5 @@
import React from 'react';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
@@ -14,11 +15,9 @@ function NoIndexer(props: NoIndexerProps) {
if (totalItems > 0) {
return (
<div>
<div className={styles.message}>
{translate('AllIndexersHiddenDueToFilter')}
</div>
</div>
<Alert kind={kinds.WARNING} className={styles.message}>
{translate('AllIndexersHiddenDueToFilter')}
</Alert>
);
}
@@ -29,7 +28,7 @@ function NoIndexer(props: NoIndexerProps) {
</div>
<div className={styles.buttonContainer}>
<Button onPress={onAddIndexerPress} kind={kinds.PRIMARY}>
<Button kind={kinds.PRIMARY} onPress={onAddIndexerPress}>
{translate('AddNewIndexer')}
</Button>
</div>

View File

@@ -32,23 +32,30 @@ import IndexerStatsFilterModal from './IndexerStatsFilterModal';
import styles from './IndexerStats.css';
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.averageResponseTime,
};
});
const statistics = [...indexerStats].sort((a, b) =>
a.averageResponseTime === b.averageResponseTime
? b.averageGrabResponseTime - a.averageGrabResponseTime
: b.averageResponseTime - a.averageResponseTime
);
data.sort((a, b) => {
return b.value - a.value;
});
return data;
return {
labels: statistics.map((indexer) => indexer.indexerName),
datasets: [
{
label: translate('AverageQueries'),
data: statistics.map((indexer) => indexer.averageResponseTime),
},
{
label: translate('AverageGrabs'),
data: statistics.map((indexer) => indexer.averageGrabResponseTime),
},
],
};
}
function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
const data = [...indexerStats]
.map((indexer) => ({
label: indexer.indexerName,
value:
(indexer.numberOfFailedQueries +
@@ -59,109 +66,102 @@ function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
indexer.numberOfRssQueries +
indexer.numberOfAuthQueries +
indexer.numberOfGrabs),
};
});
}))
.filter((s) => s.value > 0);
data.sort((a, b) => {
return b.value - a.value;
});
data.sort((a, b) => b.value - a.value);
return data;
}
function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) {
const data = {
labels: indexerStats.map((indexer) => indexer.indexerName),
const statistics = [...indexerStats]
.filter(
(s) =>
s.numberOfQueries > 0 ||
s.numberOfRssQueries > 0 ||
s.numberOfAuthQueries > 0
)
.sort(
(a, b) =>
b.numberOfQueries +
b.numberOfRssQueries +
b.numberOfAuthQueries -
(a.numberOfQueries + a.numberOfRssQueries + a.numberOfAuthQueries)
);
return {
labels: statistics.map((indexer) => indexer.indexerName),
datasets: [
{
label: translate('SearchQueries'),
data: indexerStats.map((indexer) => indexer.numberOfQueries),
data: statistics.map((indexer) => indexer.numberOfQueries),
},
{
label: translate('RssQueries'),
data: indexerStats.map((indexer) => indexer.numberOfRssQueries),
data: statistics.map((indexer) => indexer.numberOfRssQueries),
},
{
label: translate('AuthQueries'),
data: indexerStats.map((indexer) => indexer.numberOfAuthQueries),
data: statistics.map((indexer) => indexer.numberOfAuthQueries),
},
],
};
return data;
}
function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
const data = [...indexerStats]
.map((indexer) => ({
label: indexer.indexerName,
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
};
});
}))
.filter((s) => s.value > 0);
data.sort((a, b) => {
return b.value - a.value;
});
data.sort((a, b) => b.value - a.value);
return data;
}
function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfGrabs,
};
});
const data = indexerStats.map((indexer) => ({
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfGrabs,
}));
data.sort((a, b) => {
return b.value - a.value;
});
data.sort((a, b) => b.value - a.value);
return data;
}
function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfQueries,
};
});
const data = indexerStats.map((indexer) => ({
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfQueries,
}));
data.sort((a, b) => {
return b.value - a.value;
});
data.sort((a, b) => b.value - a.value);
return data;
}
function getHostGrabsData(indexerStats: IndexerStatsHost[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfGrabs,
};
});
const data = indexerStats.map((indexer) => ({
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfGrabs,
}));
data.sort((a, b) => {
return b.value - a.value;
});
data.sort((a, b) => b.value - a.value);
return data;
}
function getHostQueryData(indexerStats: IndexerStatsHost[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfQueries,
};
});
const data = indexerStats.map((indexer) => ({
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfQueries,
}));
data.sort((a, b) => {
return b.value - a.value;
});
data.sort((a, b) => b.value - a.value);
return data;
}
@@ -241,9 +241,9 @@ function IndexerStats() {
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
onFilterSelect={onFilterSelect}
filterModalConnectorComponent={IndexerStatsFilterModal}
isDisabled={false}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
@@ -294,7 +294,7 @@ function IndexerStats() {
</div>
<div className={styles.fullWidthChart}>
<div className={styles.chartContainer}>
<BarChart
<StackedBarChart
data={getAverageResponseTimeData(item.indexers)}
title={translate('AverageResponseTimesMs')}
stepSize={100}

View File

@@ -0,0 +1,19 @@
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export function createIndexerSelector(indexerId?: number) {
return createSelector(
(state: AppState) => state.indexers.itemMap,
(state: AppState) => state.indexers.items,
(itemMap, allIndexers) => {
return indexerId ? allIndexers[itemMap[indexerId]] : undefined;
}
);
}
function useIndexer(indexerId?: number) {
return useSelector(createIndexerSelector(indexerId));
}
export default useIndexer;

View File

@@ -231,7 +231,9 @@ function SearchIndexOverview(props: SearchIndexOverviewProps) {
{indexerFlags.length
? indexerFlags
.sort((a, b) => a.localeCompare(b))
.sort((a, b) =>
a.localeCompare(b, undefined, { numeric: true })
)
.map((flag, index) => {
return (
<Label key={index} kind={kinds.INFO}>

View File

@@ -1,4 +1,6 @@
.message {
composes: alert from '~Components/Alert.css';
margin-top: 10px;
margin-bottom: 30px;
text-align: center;

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