1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-05 13:21:25 -05:00

Compare commits

...

308 Commits

Author SHA1 Message Date
Bogdan
8853dced9f Fixed: Health warning for downloading inside root folders
(cherry picked from commit 1e9fd02e9d2bf57247adcac5728e2a0d2b084b86)
2025-02-01 23:36:10 +02:00
Mark McDowall
c7aa1bae5e Fixed: Ignore special folders inside Blackhole watch folders
(cherry picked from commit e79dd6f8e689617b1fd9f96c639ac300669112c5)
2025-02-01 23:35:45 +02:00
Bogdan
405ae77070 New: Prefer newer Usenet releases
(cherry picked from commit 6a439f03273b376feda713ef04a6912fc3af9d0a)
2025-02-01 23:35:31 +02:00
Weblate
6236bc9b4f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Mailme Dashite <mailmedashite@protonmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translation: Servarr/Radarr
2025-01-31 23:30:40 +02:00
Bogdan
743c977e5b New: Refresh cache for tracked queue on movies update 2025-01-31 23:29:41 +02:00
Bogdan
c0e5646f07 Bump Polly and NLog.Layouts.ClefJsonLayout 2025-01-26 15:42:43 +02:00
Weblate
10094b4e66 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Craze <christian.strey@gmail.com>
Co-authored-by: Dimitar \"Topper\" Maznekov <d.maznekov@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ηg <jonas.konrath@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fa/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2025-01-26 15:00:33 +02:00
Bogdan
d923406f08 Bump version to 5.18.3 2025-01-26 14:48:34 +02:00
Bogdan
69a9c72286 Fixed: Loading movies with duplicated translations 2025-01-26 14:47:13 +02:00
Bogdan
55b9477a01 Fixed: Cleanup duplicated movie translations 2025-01-26 14:47:13 +02:00
Bogdan
6b81f92137 Fixed: Import Movies page crashing on console.error with non-string values 2025-01-21 16:11:22 +02:00
Bogdan
3ceda1bcda New: Parse releases with JPN as Japanese and KOR as Korean 2025-01-20 03:59:38 +02:00
Luke Anderson
f1f1921517 Update Trakt ratings logo (#10822)
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2025-01-19 17:05:55 +02:00
Weblate
af0c96538a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitar \"Topper\" Maznekov <d.maznekov@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ahgharaghani <ah.gharaghani@gmail.com>
Co-authored-by: keysuck <joshkkim@gmail.com>
Co-authored-by: warkurre86 <tom.novo.86@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fa/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-01-19 16:56:42 +02:00
jcassette
3d52f45b6a New: reflink support for ZFS
(cherry picked from commit a840bb542362d58006b6cc27affd58ee6b965b80)
2025-01-19 16:55:06 +02:00
Bogdan
d4715f119d Bump version to 5.18.2 2025-01-19 16:54:39 +02:00
kephasdev
d58135bf17 Fixed: Augmenting languages for releases with MULTI and other languages (#10842) 2025-01-17 20:32:09 +02:00
Bogdan
b452c10da3 Bump SonarCloud azure extension for UI analysis to 3.X
(cherry picked from commit 396b2ae7c10c7df749ea23ea93608b56482175a1)
2025-01-14 11:43:51 +02:00
Stevie Robinson
f6b364725d Additional logging for delay profile decisions
(cherry picked from commit fa0f77659cbd3e9efdae55bbedb30fd8288622a6)

Closes #10831
2025-01-12 20:40:59 +02:00
Stevie Robinson
99f6be3f3d New: Show release source in history grab details
(cherry picked from commit 1609f0c9647b89bf55b8c043eeffc8a61653a1e5)

Closes #10830
2025-01-12 20:40:59 +02:00
Stevie Robinson
c2ac49a873 Additional logging for custom format score
(cherry picked from commit 3c8268c428688cc703af76b648c9b3385858274f)

Closes #10828
2025-01-12 20:40:58 +02:00
Weblate
0e24a3e8bc Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <Ano10@users.noreply.translate.servarr.com>
Co-authored-by: Dimitar \"Topper\" Maznekov <d.maznekov@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mickaël O <mickael.ouillon@ac-bordeaux.fr>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: xumei51201314 <xumei51201314@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2025-01-12 15:19:47 +02:00
Weblate
18032cc83b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <Ano10@users.noreply.translate.servarr.com>
Co-authored-by: Dimitar \"Topper\" Maznekov <d.maznekov@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Mickaël O <mickael.ouillon@ac-bordeaux.fr>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2025-01-12 15:19:09 +02:00
Bogdan
927eb38945 Bump version to 5.18.1 2025-01-12 15:16:00 +02:00
Qstick
5fac348613 Bump SonarCloud azure extension to 3.X
(cherry picked from commit 7b8e352d876cd8f8e5b6296f0c3938bed4db8bb8)
2025-01-11 17:44:00 -06:00
Bogdan
7ba9603449 Fixed: Sending Discord notifications with images without absolute links 2025-01-06 13:27:14 +02:00
Bogdan
e36de8ab8d New: Auto tag based on movie status 2025-01-06 04:24:29 +02:00
Stevie Robinson
f8704a1655 Translate backend: Autotagging + CF specs
Signed-off-by: Stevie Robinson <stevie.robinson@gmail.com>
(cherry picked from commit de1cc25c903924fecbca79fedb458d729ae584fd)

Towards #9647
2025-01-06 04:06:34 +02:00
Stevie Robinson
f507d5154e Fixed: Listening on all IPv4 Addresses
(cherry picked from commit 035c474f10c257331a5f47e863d24af82537e335)
2025-01-05 13:53:29 +02:00
Stevie Robinson
5f03e7142a Fixed: qBittorrent Ratio Limit Check
(cherry picked from commit 4dcc015fb19ceb57d2e8f4985c5137e765829d1c)
2025-01-05 13:53:17 +02:00
Bogdan
c0ebbee7c9 Bump version to 5.18.0 2025-01-05 13:52:57 +02:00
Bogdan
4051cf3d80 Fixed: Increase rate limit for PassThePopcorn 2025-01-02 23:16:48 +02:00
Weblate
9876ed64e2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: 1 <1228553526@qq.com>
Co-authored-by: Alexander Balya <alexander.balya@gmail.com>
Co-authored-by: Ano10 <Ano10@users.noreply.translate.servarr.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Matti Meikäläinen <diefor-93@hotmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Tommy Au <smarttommyau@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: marapavelka <mara.pavelka@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2025-01-02 23:13:41 +02:00
Bogdan
2f26974ecc New: Add Tagalog language 2025-01-02 21:16:30 +02:00
Siddhant Naik
25f66a3029 New: Add Marathi language 2025-01-02 19:41:08 +02:00
Bogdan
0e25b2708c Fixed: Sending Manual Interaction Required notifications to Discord for unknown movies 2024-12-31 18:20:16 +02:00
Bogdan
410870d21e Check if backup folder is writable on backup
(cherry picked from commit 8aad79fd3e14eb885724a5e5790803c289be2f25)

Closes #10806
2024-12-31 12:16:28 +02:00
Bogdan
a64d931904 Suggest adding IP to RPC whitelist for on failed Transmission auth
(cherry picked from commit f05e552e8e6dc02cd26444073ab9a678dcb36492)
2024-12-31 12:12:52 +02:00
Bogdan
f0a9e76cfc Bump version to 5.17.2 2024-12-30 00:58:56 +02:00
Mark McDowall
6f23c465ee Don't send session information to Sentry
(cherry picked from commit fae24e98fb9230c2f3701caef457332952c6723f)
2024-12-28 03:32:11 +02:00
Bogdan
af60cca9ae Fixed: Advanced settings for Metadata consumers 2024-12-23 12:15:04 +02:00
Mark McDowall
d34d23a052 Fixed: Movies updated during Import List Sync not reflected in the UI
(cherry picked from commit 1c30ecd66dd0fd1dafaf9ab0e41a11a54eaac132)

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

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

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

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

Fixed errors sending Telegram notifications when links aren't available

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

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

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

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

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

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

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

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

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

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

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

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

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

(cherry picked from commit b51a49097941e5f306cae5785c63985b319784fd)

Fixed: Error loading queue

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #10593
2024-10-27 08:10:38 +02:00
Hadrien Patte
25bb52b206 Use OperatingSystem class to get OS information
(cherry picked from commit 135b5c2ddd8f0a274b0d59eb07f75aaf1446b9da)
2024-10-27 08:08:01 +02:00
Bogdan
63c6f70e67 Fixed: Changing movies to another root folder without moving files 2024-10-27 08:07:44 +02:00
Bogdan
79cd6269f4 Fixed: Status check for completed directories in Deluge
(cherry picked from commit 33139d4b53c1adad769c7e2b0510e8990c66b84a)
2024-10-27 08:06:29 +02:00
Bogdan
879c872179 Cleanse exceptions in event logs
(cherry picked from commit 404e6d68ea526ab521cd39ecda1bf3b02285765d)
2024-10-27 08:06:11 +02:00
Bogdan
d4993cf69b Bump version to 5.15.0 2024-10-26 17:41:27 +03:00
Bogdan
781e0c9d1c Fixed: Optional square and round brackets for "{Release Year}" 2024-10-26 15:02:46 +03:00
Bogdan
c946ed83f9 Fixed: Stopped/Started as initial state for qBittorrent v5.0 2024-10-26 10:32:20 +03:00
Bogdan
9aecf94e8e Bump version to 5.14.0 2024-10-26 09:10:14 +03:00
Weblate
234e23eb47 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-10-26 07:45:49 +03:00
Bogdan
748d888520 Sync metadata changes to UI 2024-10-24 11:15:32 +03:00
Bogdan
00d50a030c Translate metadata settings 2024-10-24 11:15:31 +03:00
Mark McDowall
017fa5ad80 New: Group updates for the same movie for Kodi and Emby / Jellyfin
(cherry picked from commit 46c7de379c872f757847a311b21714e905466360)

Closes #10150
2024-10-24 09:00:32 +03:00
Weblate
d5fb1c55c6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2024-10-23 14:19:02 +03:00
Bogdan
1be8385c41 Fix paths mapping for movie specific translations 2024-10-23 14:15:05 +03:00
Weblate
6f26c55a1b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2024-10-23 12:00:38 +01:00
Bogdan
2d2de7f76b Parse "tmdbid" and "imdb" attributes in Newznab and Torznab parsers 2024-10-23 13:53:14 +03:00
Bogdan
2c9292c249 Save ImdbId for grabbed movie events 2024-10-23 12:21:26 +03:00
Mark McDowall
6747b74271 Convert History to TypeScript
(cherry picked from commit 824ed0a36931ce7aae9aa544a7baf0738dae568c)

Closes #10230
Closes #10390
Closes #10247
2024-10-23 12:18:17 +03:00
Mark McDowall
13f10906f1 Fixed: PWA Manifest with URL base
(cherry picked from commit aedcd046fc4fc621dae4b231cc80d4b269a69177)

Fixed: PWA Manifest images

(cherry picked from commit da7d17f5e826d5273dba0b4f73227ffc8ed8a6c7)

Closes #10317
Closes #10329
2024-10-23 09:51:32 +03:00
Weblate
25d08a67e4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: DNArjen <dna.visser@gmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Kuzmich <kuzmich55@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2024-10-22 11:40:24 +03:00
Bogdan
aadefbe3b0 Rename 'On Import' to 'On File Import' and 'On Upgrade' to 'On File Upgrade'
Closes #10164
2024-10-22 09:51:40 +03:00
Mark McDowall
d99a7e9b8a Convert Components to TypeScript
(cherry picked from commit e1cbc4a78249881de96160739a50c0a399ea4313)

Closes #10378

Fixed: Links tooltip closing too quickly

(cherry picked from commit 0b9a212f33381d07ff67e2453753aaab64cc8041)

Closes #10400

Fixed: Movie links not opening on iOS

(cherry picked from commit f20ac9dc348e1f5ded635f12ab925d982b1b8957)

Closes #10425
2024-10-22 09:18:08 +03:00
Mark McDowall
dc29526961 Convert App to TypeScript
(cherry picked from commit d6d90a64a39d3b9d3a95fb6b265517693a70fdd7)

Closes #10233
2024-10-22 06:30:52 +03:00
Bogdan
f8e47fbdc7 Tooltips for certification and runtime on details page 2024-10-22 05:57:21 +03:00
Bogdan
56a7725e52 Improve warning for missing release dates on movie details page when year is not set
Towards #10569
2024-10-21 09:55:28 +03:00
Bogdan
abf1b9d6cf Fallback to text searches for FL only if year is defined 2024-10-21 09:52:55 +03:00
Bogdan
dd90bf53dd Bump version to 5.13.1 2024-10-20 08:04:41 +03:00
Bogdan
cd29c0c9c8 Fix stable branch label in update 2024-10-20 04:41:20 +03:00
Servarr
9986d04d36 Automated API Docs update 2024-10-19 09:48:14 +03:00
Mark McDowall
f900d623dc New: Allow major version updates to be installed
(cherry picked from commit 0e95ba2021b23cc65bce0a0620dd48e355250dab)
2024-10-19 09:01:56 +03:00
Bogdan
84b507faf3 New: Romania and India added to list of Certification Countries 2024-10-19 08:59:29 +03:00
Bogdan
adb27123df Natural sorting for tags list in the UI
(cherry picked from commit 09d3ae969281715a26ffd374c148cafe17a8f438)
2024-10-18 23:04:31 +03:00
Bogdan
a06792b923 New: Sync updates to UI for providers (#10550) 2024-10-18 23:02:42 +03:00
Bogdan
d90ee3ae11 Fixed: Release Year mandatory to generate valid file formats 2024-10-18 12:48:51 +03:00
Bogdan
ff38afd198 Fixed: Add only movies with release dates from monitored collections 2024-10-18 12:09:37 +03:00
Weblate
db70c06b8b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Buloni <ershov.artjom@yandex.ru>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: JoseFilipeFerreira <jose.filipe.matos.ferreira@gmail.com>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translation: Servarr/Radarr
2024-10-16 04:07:30 +03:00
costaht
fb7656be56 New: Parse pt-BR releases as Brazilian Portuguese (#10554)
* New: Match releases with pt-BR (BPC-47) with Portuguese Brazilian
2024-10-15 05:59:42 +03:00
Bogdan
3287e7cdec Bump dotnet to 6.0.35 2024-10-13 19:42:07 +03:00
Bogdan
0761e27cfa New: Parse ES as Spanish 2024-10-12 19:54:54 +03:00
Bogdan
4f47bb39ac Bump version to 5.13.0 2024-10-12 18:58:59 +03:00
Bogdan
889d071004 New: Display items tags on import lists index 2024-10-10 22:51:32 +03:00
Bogdan
0049922ab6 Include exception message in SkyHook failure message 2024-10-10 21:17:20 +03:00
Bogdan
3c995a0fff Bump babel packages 2024-10-10 19:01:53 +03:00
Bogdan
430719baac Remove unused gulp packages 2024-10-10 18:55:46 +03:00
Bogdan
9928d711a3 Trim multiple occurrences of ending separators in filename 2024-10-10 15:26:00 +03:00
Bogdan
f90b43b3e1 Simplify parsing IMDb and TMDb urls as search terms 2024-10-10 03:25:10 +03:00
Steel City Phantom
64122b4cfb Auto-detect building on macOS ARM (#10539) 2024-10-10 02:41:07 +03:00
Bogdan
7912a942f7 Bump frontend packages 2024-10-10 02:40:02 +03:00
Bogdan
0a7607bb62 Bump dotnet packages 2024-10-10 02:40:02 +03:00
Vincent Caron
beeb5204b8 New: Parse IMDB and TMDB URLs as search terms 2024-10-10 00:32:39 +01:00
Bogdan
ab13fb6e99 Fix index variable in fuse worker 2024-10-09 01:26:26 +03:00
Weblate
2a3d595a66 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: angelsky11 <angelsky11@gmail.com>
Co-authored-by: anne <gagatebis@hotmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: jsain <josip.sain@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-10-08 13:26:52 +03:00
Jared Ledvina
958a863d8f Recompare file size after import file if necessary
(cherry picked from commit 6660db22ecf53d7747e3abc400529669ea779fa1)
2024-10-08 13:25:43 +03:00
Servarr
8b7884deb0 Automated API Docs update 2024-10-08 13:25:15 +03:00
Bogdan
9a22e1c791 Bump browserslist-db 2024-10-08 02:15:07 +03:00
Bogdan
f0f828491b Fixed: Copy to clipboard in non-secure contexts
(cherry picked from commit 3828e475cc8860e74cdfd8a70b4f886de7f9c5c3)

Closes #10525
2024-10-08 02:11:35 +03:00
Treycos
7f3d107eda Convert ClipboardButton to TypeScript
(cherry picked from commit 99fc52039f44264c83d939e5f096d8e16d2f3355)

Closes #10452
2024-10-08 02:09:10 +03:00
Bogdan
ce4477eeac Improve filename examples for movies naming 2024-10-08 01:54:46 +03:00
Bogdan
8b64f873f4 Convert Naming options to TypeScript 2024-10-08 01:54:46 +03:00
Bogdan
38bd060960 Convert FormInputButton to TypeScript 2024-10-08 01:54:46 +03:00
Bogdan
7c243cb6e8 Fixed: Error updating providers with ID missing from JSON
(cherry picked from commit c435fcd685cc97e98d14f747227eefd39e4d1164)
2024-10-08 01:52:26 +03:00
Bogdan
b29dee63f4 Use the first allowed quality for cutoff met rejection message with disabled upgrades 2024-10-07 22:26:55 +03:00
Mark McDowall
f6542bab0a New: Use 307 redirect for requests missing URL Base 2024-10-06 17:22:32 +03:00
Bogdan
da1b53b7e2 Bump macOS runner version to 13 2024-10-06 16:21:38 +03:00
Bogdan
0deae95782 Bump version to 5.12.2 2024-10-06 12:03:04 +03:00
Mark McDowall
75c7a3cfc6 Fixed: Ignore free space check before grabbing if directory is missing 2024-10-06 00:15:40 +03:00
Bogdan
cfdb7a15de Simplify defaults set when adding release profiles and list exclusions 2024-10-05 13:02:36 +03:00
Bogdan
63a7d33e7e Fixed: Cleaning the path for movie collections with top level folders 2024-10-05 12:01:13 +03:00
Bogdan
c9836f997c Fixed: Clean paths for top level root folders 2024-10-05 12:01:13 +03:00
Bogdan
d37e71415f Convert Release Profiles to TypeScript 2024-10-04 17:28:04 +03:00
Bogdan
9a5f4bef63 Check if root folder is not empty on files import 2024-10-04 12:04:09 +03:00
Bogdan
40551ba5a3 Fixed: Custom filters with release date filter
Fixes #10508
2024-10-02 22:32:33 +03:00
Bogdan
6e04dc894b Fixed: Validate path on movie update 2024-10-02 19:27:07 +03:00
Bogdan
ac767ed386 New: Add 'Movie CleanTitleThe' token
First attempt to fix movie folder validation by ignoring invalid tokens mixed from 'Original' and 'The'

Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
2024-10-02 19:23:57 +03:00
Mark McDowall
42fbb79017 New: Parse 'BEN THE MAN' release group
(cherry picked from commit da610a1f409c9c03cbed1c27ccaedc32f42e636c)
2024-10-02 15:39:41 +03:00
Bogdan
c43bd77dae Display long date tooltips for release dates 2024-10-02 10:12:07 +03:00
Lorenzo Lewis
68dfa55b35 Fix typo README.md (#10502) 2024-10-01 12:04:50 -05:00
Bogdan
fa190c85a3 Add new category for FL 2024-09-30 17:14:18 +03:00
Bogdan
172dcf6f8d Bump version to 5.12.1 2024-09-29 08:16:01 +03:00
Weblate
0736fc955f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Jhonata da Rocha <jhonata182@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-09-28 04:22:34 +03:00
Bogdan
9d0b8d974d Fixed: Parsing of Hybrid-Remux 2024-09-28 04:21:08 +03:00
Bogdan
9a3e89f283 Fixed: Ignore '.DS_Store' and '.unmanic' when scanning for files 2024-09-28 04:20:22 +03:00
Mark McDowall
e33e45ec73 Fixed: Don't reject revision upgrades if profile doesn't allow upgrades
(cherry picked from commit 4f0e1c54c167f5123a33d19b76653450401adb6d)
2024-09-28 04:19:42 +03:00
Mark McDowall
5893d88058 Fixed: Ignore extra spaces in path when not running on Windows
(cherry picked from commit 6d0f10b877912edef21232c64339cc6548d9690e)
2024-09-28 04:18:47 +03:00
Servarr
a81d27acda Automated API Docs update 2024-09-26 11:43:29 +03:00
Mark McDowall
d2b279a6be Fixed: Replace illegal characters even when renaming is disabled
(cherry picked from commit 4d8a4436810828494e99f0854cf6de3269668fe4)
2024-09-26 10:22:40 +03:00
Bogdan
6686fa0600 New: Smart as default Colon Replacement
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-09-26 10:22:40 +03:00
Bogdan
1d286df85d Display naming example errors when all fields are empty 2024-09-26 10:22:40 +03:00
yammes08
be2e1e4fdb Fixed: SDR files being parsed as HLG
(cherry picked from commit 11e5c5a11b171138c235224c1aa9a258f0a4ec4d)
2024-09-25 09:41:21 +03:00
Bogdan
08868e5d01 Bump version to 5.12.0 2024-09-25 09:39:42 +03:00
Mark McDowall
7b43c2e345 Fixed: Loading movie images after placeholder in Safari
Closes #10474
2024-09-25 06:48:30 +03:00
Bogdan
dc599b6531 Sort allowed sorting keys 2024-09-25 06:47:37 +03:00
Weblate
1421179654 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: FloatStream <1213193613@qq.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>
Co-authored-by: liuwqq <843384478@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-09-25 02:35:29 +03:00
Bogdan
41dcf32e24 Fix translations for MovieMonitoredSelectInput 2024-09-25 02:33:02 +03:00
Bogdan
7a813a44b6 Use UTC to calculate movie status 2024-09-25 02:33:02 +03:00
Bogdan
54a5059080 Convert AvailabilitySelectInput to TypeScript 2024-09-25 02:33:02 +03:00
Bogdan
adaf7444d3 Add movie availability descriptions 2024-09-25 02:33:02 +03:00
Robin Dadswell
49d11e59b3 Fixed: Telegram Log Message 2024-09-24 16:59:30 +01:00
Servarr
a7eb4a4a04 Automated API Docs update 2024-09-24 12:10:31 +03:00
Bogdan
66a6a663ba Prevent line wraps on mobile for ratings 2024-09-24 03:31:43 +03:00
Bogdan
f735e31835 New: Trakt ratings 2024-09-24 03:31:43 +03:00
Bogdan
b8f1286abb Fixed: Sorting queue by columns 2024-09-22 07:30:46 +03:00
Mark McDowall
9df45199d0 Reprocessing manual import items unable to detect sample
(cherry picked from commit 27da0413882dc87e1617a5d091ac5111589e61a6)

Closes #10463
2024-09-22 05:21:38 +03:00
Servarr
a692c35b03 Automated API Docs update 2024-09-21 23:42:37 +03:00
momo
ddcad270c3 Fix description for API key as query parameter
(cherry picked from commit 30c36fdc3baa686102ff124833c7963fc786f251)
2024-09-21 21:18:15 +03:00
Bogdan
b06f1d7c12 Bump version to 5.11.0 2024-09-21 03:57:59 +03:00
Mark McDowall
480bb50b85 Fixed: Rejections for Custom Format score increment 2024-09-21 02:17:18 +03:00
Bogdan
dbc94dbe4e Simplify fallback to default for allowed sort keys
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-09-21 01:35:47 +03:00
Mark McDowall
b89271fc01 Fixed: Unable to login when instance name contained brackets 2024-09-21 01:23:25 +03:00
Bogdan
66fcde7325 Include current quality in rejection message for not an upgrade 2024-09-21 00:41:26 +03:00
Bogdan
463741da1f New: Fetch up to 1000 movies from Plex Watchlist 2024-09-18 03:49:49 +03:00
Bogdan
3388fae1a5 Fix translation key for Skip Free Space Check help text 2024-09-17 17:46:33 +03:00
bakerboy448
72b2cfe8be Fixed: Parse TELESYNCH as TELESYNC (#10445)
Fixes #10414
2024-09-17 02:34:12 +03:00
Servarr
d5dd5e08ca Automated API Docs update 2024-09-17 01:22:25 +03:00
Bogdan
fabd40cbae New: Allowed sort keys for paginated resources 2024-09-16 20:27:34 +03:00
Servarr
3ca327f611 Multiple Translations updated by Weblate (#10418)
ignore-downstream






Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: genoher <genoher@gmail.com>
Co-authored-by: rookie7420 <yuanchong2001@qq.com>
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-09-16 18:34:20 +03:00
Bogdan
c804140896 Fix use of min score increment in rejection message 2024-09-16 18:19:02 +03:00
somniumV
bb43d0c796 New: Minimum Upgrade Score for Custom Formats
(cherry picked from commit 8b20a9449c1ae5ffd1e8d12f1ca771727b8c52a5)
2024-09-16 18:19:02 +03:00
Mark McDowall
5757fa797f New: Use instance name in forms authentication cookie name
Closes #10416
2024-09-16 01:29:10 +03:00
Bogdan
2fc32189d8 Convert Movie Titles to TypeScript 2024-09-16 01:18:23 +03:00
Bogdan
5975be3690 Fixed: Removing import lists for cast and crew from movie details
Convert movie credits to TypeScript

Switching to metadata based order for crew
2024-09-16 01:18:23 +03:00
amdavie
6095819005 New: Scene and Nuked IndexerFlags for Newznab indexers
(cherry picked from commit 278c7891a3add639b4ff5bc1f4f5e8912dabc897)
2024-09-15 23:08:20 +03:00
Bogdan
7528882adf Gotify notification updates
New: Option to include links for Gotify notifications
New: Include images and links for Android

(cherry picked from commit 3c857135c59029635b0972f959f9a8255bcff21f)

Closes #10433
Fixes #10410
2024-09-15 22:42:59 +03:00
Mark McDowall
c1f1307345 New: Add exception to SSL Certificate validation message
(cherry picked from commit d84c4500949a530fac92d73f7f2f8e8462b37244)

Closes #10437
2024-09-15 22:42:59 +03:00
Mark McDowall
348060351a New: Check for available space before grabbing
(cherry picked from commit 4b5ff3927d3c123f9e3a2bc74328323fab1b0745)

Closes #10429
2024-09-15 22:42:59 +03:00
Mark McDowall
ca31cdd33a New: Add additional archive exentions
(cherry picked from commit 750a9353f82da4e016bee25e0c625cd6d8613b57)
2024-09-15 20:41:48 +03:00
Bogdan
36e278aa82 Bump version to 5.10.4 2024-09-15 15:52:54 +03:00
Bogdan
927e84654f Fixed: Filtering by IMDb decimal ratings 2024-09-13 01:25:37 +03:00
Bogdan
96e60906c5 Fixed: Empty or private MDBList lists shown as valid on save 2024-09-12 19:36:04 +03:00
Bogdan
7a55b563c0 New: Importing sup files as subtitles
Towards #10412
2024-09-11 20:44:38 +03:00
Servarr
b4bbb71a9b Automated API Docs update 2024-09-09 20:53:17 +03:00
ManiMatter
0361299a73 New: Last Searched column on Wanted screens (#10392)
* Adding lastSearchTime to API and "Last Searched" to Frontend (cutoff unmet & missing)
Picking lastSearchTime from movie instead of movieMetaData

---------

Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-09-08 20:56:11 +03:00
Bogdan
e11339fb83 Fix weblate widget 2024-09-08 11:26:02 +03:00
Bogdan
fbdd3129f5 Bump version to 5.10.3 2024-09-08 11:12:25 +03:00
Weblate
2843647e23 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: FloatStream <1213193613@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-09-07 13:43:51 -05:00
Jendrik Weise
b6b7d30fc1 Fix subtitle aggregation for not populated movie (#10406) 2024-09-06 16:28:42 +03:00
Bogdan
e688dfadf7 Fixed: Refresh tags after updating autotags 2024-09-05 16:16:01 +03:00
Bogdan
f3ce0ac620 Fixed: Linking autotags with tag specification to all tags 2024-09-05 16:16:01 +03:00
Jendrik Weise
08c250dfe3 Fix OriginalFilePath for imported movies (#10398) 2024-09-04 14:29:20 +03:00
Mark McDowall
a5c1025efd New: Add MDBList link to movie details
(cherry picked from commit 55aaaa5c406a71199152a24f5efb4cf16dbe10fd)

Closes #10403
2024-09-04 13:53:50 +03:00
Bogdan
f0019d622a Convert Progress Bars to TypeScript
(cherry picked from commit a9072ac460f971d3da737de6446153d8cbf1e1c2)

Closes #10404
2024-09-04 13:46:41 +03:00
Mark McDowall
032e8aa920 Convert movie images to TypeScript
(cherry picked from commit ee99c3895de497bb1c99193ba16c56393b8ff593)

Closes #10402
2024-09-04 13:37:29 +03:00
Mark McDowall
51cd7c70ba Convert NoMovie to TypeScript
(cherry picked from commit e1e10e195c09ea78179d92ae11c385403096d966)

Closes #10401
2024-09-04 13:18:37 +03:00
Bogdan
12c814ed78 Improve root folder missing check for movie collections 2024-09-04 13:09:16 +03:00
Bogdan
d7bdc2c46c Convert Donations to TypeScript 2024-09-04 13:09:16 +03:00
Mark McDowall
6b4c0bd24c Convert System to TypeScript
(cherry picked from commit 72db8099e0f4abc3176e397f8dda3b2b69026daf)

Closes #10234
2024-09-04 13:09:14 +03:00
Weblate
c332c38890 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: FloatStream <1213193613@qq.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-09-04 11:33:14 +03:00
ManiMatter
d910611630 Add '_temp*' to .gitignore (#10397)
_temp folder is created automatically when running radarr locally and should not be pushed to git
2024-09-04 00:09:58 +03:00
Treycos
c6feb27962 Convert SpinnerIcon to TypeScript
(cherry picked from commit 25d9f09a43ded9dd07c6777390fc541ca5f89eeb)

Closes #10368
2024-09-03 16:13:43 +03:00
Treycos
0f7721ef11 Convert TableRowCell to Typescript
(cherry picked from commit 7ea1301221793ded0f64258d48294d20451422f2)

Closes #10366
2024-09-03 16:13:43 +03:00
Treycos
89731bdc41 Convert IconButton to Typescript
(cherry picked from commit f033799d7a257b0554c877b4ae6dcc129ccd7fe1)

Closes #10364
2024-09-03 16:13:43 +03:00
Treycos
126b6eba00 Convert Icon to Typescript
(cherry picked from commit ae7b187e412a080e30fc6826564ce9197ed2f329)

Closes #10347
2024-09-03 16:13:43 +03:00
Treycos
56fece293c Convert Label to TypeScript
(cherry picked from commit 3eca63a67c898256b711d37607f07cbabb9ed323)

Closes #10308
2024-09-03 16:13:43 +03:00
Treycos
4503c3d36e Convert Button to TypeScript
(cherry picked from commit 63b4998c8e51d0d2b8b51133cbb1fd928394a7e6)

Closes #10345
2024-09-03 16:13:43 +03:00
Bogdan
0d6ce5ea49 Link polymorphic static typing
(cherry picked from commit a2e06e9e650642518b926a61f624a2c7a49c0988)
(cherry picked from commit cfa2f4d4c6e35d7b9ddd2e1da2e59f7287859516)

Closes #10363
Closes #10348
2024-09-03 16:13:43 +03:00
Weblate
b4b5ad9567 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: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Nota Inutilis <hugo@notainutilis.fr>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: 极染 <poledye@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-09-03 13:56:02 +03:00
Mark McDowall
3ae6347532 Convert Queue to TypeScript
(cherry picked from commit 76650af9fdc7ef06d13ce252986d21574903d293)

Closes #10231
2024-09-03 13:19:48 +03:00
Mark McDowall
a826ffdbc9 Convert Blocklist to TypeScript
(cherry picked from commit ee80564dd427ca1dc14c192955efaa61f386ad44)

Closes #10229
2024-09-03 12:17:58 +03:00
Servarr
dc3bf9acb0 Automated API Docs update 2024-09-03 12:17:51 +03:00
Mark McDowall
eca95826c2 Fixed: Respect Quality cutoff if Custom Format cutoff isn't met
(cherry picked from commit 6f51e72d0073444b441bee5508322cc9e52e98e4)
2024-09-03 11:17:14 +03:00
Mark McDowall
593b943cb0 New: Except language option for Language Custom Formats
(cherry picked from commit 1584311914eed697fdd0f143951f4adfe3403351)

Closes #10388
2024-09-03 08:13:30 +03:00
Mark McDowall
a3faa9ed5f Fixed: Failing to import any file for movie if one has bad encoding
(cherry picked from commit 7f0696c57497ada845a40f7b5f80ace31ec1527f)

Closes #10389
2024-09-03 08:06:06 +03:00
Jendrik Weise
229e87f398 Fix ignoring title based on pre-rename moviefile
(cherry picked from commit af5a681ab7edf7f72544647c490216907853d77d)

Fixed: Renaming movie file for a movie

(cherry picked from commit fc06e5135213f218648c8b36747d3bdf361f08b4)
2024-09-01 15:08:40 +03:00
Jendrik Weise
fcb758bf67 Reimport files imported prematurely during script import
(cherry picked from commit 7776ec995571a6bc3ff1a35bbede02c05b943063)
2024-09-01 15:08:40 +03:00
Jendrik Weise
0a9ae45ed1 Do not remove all extras when script importing
(cherry picked from commit 0a7f3a12c2be783c6374864e1e8f49ff5e969166)
2024-09-01 15:08:40 +03:00
Jendrik Weise
b062a46cbd New: Expose subtitle title and disposition in UI 2024-09-01 15:08:40 +03:00
Jendrik Weise
ac4669dfc1 New: Parse subtitle titles
(cherry picked from commit 69f99373e56a2fca49a2be645e6640624cf12339)

Includes:
* Fixed: Subtitle title migration when original title is null
  (cherry picked from commit 8921c5d7a079c58b0c74713355bf82846abc43ab)
* Fixed: Migrating subtitle files with unexpectedly large number at end
  (cherry picked from commit f95dd00b51e61a96a0e6c094ec922c8f5cbc5334)
* Fixed: Parsing of subtitle languages separated by dash
  (cherry picked from commit cb72e752f9e24e5691292d86ac1f46c8fa35a844)
* Fix incorrect subtitle copy regex
  (cherry picked from commit 2ef46e5b902b42cdbafbd80b3154a5d22b090f29)
2024-09-01 15:08:40 +03:00
Bogdan
36d80387c6 Bump version to 5.10.2 2024-09-01 08:06:34 +03:00
784 changed files with 24274 additions and 19591 deletions

1
.gitignore vendored
View File

@@ -118,6 +118,7 @@ src/UI/.idea/*
node_modules/
_output*
_artifacts
_temp*
_rawPackage/
_dotTrace*
_tests/

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 @@
# Radarr
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Translation status](https://translate.servarr.com/widget/servarr/radarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation/docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
@@ -9,13 +9,13 @@
[![Mega Sponsors on Open Collective](https://opencollective.com/Radarr/megasponsors/badge.svg)](#mega-sponsors)
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
Note that only one type of a given movie is supported. If you want both a 4k version and 1080p version of a given movie you will need multiple instances.
## Major Features Include
* Adding new movies with lots of information, such as trailers, ratings, etc.
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray*
* Can watch for better quality of the movies you have and do an automatic upgrade. _eg. from DVD to Blu-Ray_
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Full integration with SABnzbd and NZBGet
@@ -68,12 +68,12 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
## JetBrains
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/)
## DigitalOcean
@@ -87,4 +87,4 @@ This project is also supported by DigitalOcean
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2022
* Copyright 2010-2024

View File

@@ -9,18 +9,18 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.10.1'
majorVersion: '5.18.3'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.424'
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:
@@ -1116,20 +1116,20 @@ stages:
vmImage: ${{ variables.windowsImage }}
steps:
- checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@2
- task: SonarCloudPrepare@3
env:
SONAR_SCANNER_OPTS: ''
inputs:
SonarCloud: 'SonarCloud'
organization: 'radarr'
scannerMode: 'CLI'
scannerMode: 'cli'
configMode: 'manual'
cliProjectKey: 'Radarr_Radarr.UI'
cliProjectName: 'RadarrUI'
cliProjectVersion: '$(radarrVersion)'
cliSources: './frontend'
- task: SonarCloudAnalyze@2
- task: SonarCloudAnalyze@3
- job: Api_Docs
displayName: API Docs
dependsOn: Prepare
@@ -1205,12 +1205,12 @@ stages:
submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
- task: SonarCloudPrepare@2
- task: SonarCloudPrepare@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs:
SonarCloud: 'SonarCloud'
organization: 'radarr'
scannerMode: 'MSBuild'
scannerMode: 'dotnet'
projectKey: 'Radarr_Radarr'
projectName: 'Radarr'
projectVersion: '$(radarrVersion)'
@@ -1223,10 +1223,10 @@ 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@2
- task: SonarCloudAnalyze@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@5
- task: reportgenerator@5.3.11
displayName: Generate Coverage Report
inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'

View File

@@ -210,7 +210,6 @@ module.exports = {
'no-undef-init': 'off',
'no-undefined': 'off',
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
'no-use-before-define': 'error',
// Node.js and CommonJS

View File

@@ -26,6 +26,7 @@ module.exports = (env) => {
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
target: 'web',
stats: {
children: false
@@ -133,6 +134,12 @@ module.exports = (env) => {
{
source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt')
},
// manifest.json and browserconfig.xml
{
source: 'frontend/src/Content/*.(json|xml)',
destination: path.join(distFolder, 'Content')
}
]
}
@@ -180,7 +187,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
corejs: '3.39'
}
]
]

View File

@@ -1,284 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BlocklistFilterModal from './BlocklistFilterModal';
import BlocklistRowConnector from './BlocklistRowConnector';
class Blocklist extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
isConfirmClearModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
};
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
};
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
};
onClearBlocklistPress = () => {
this.setState({ isConfirmClearModalOpen: true });
};
onClearBlocklistConfirmed = () => {
this.props.onClearBlocklistPress();
this.setState({ isConfirmClearModalOpen: false });
};
onConfirmClearModalClose = () => {
this.setState({ isConfirmClearModalOpen: false });
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onFilterSelect,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen,
isConfirmClearModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return (
<PageContent title={translate('Blocklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isDisabled={!items.length}
isSpinning={isClearingBlocklistExecuting}
onPress={this.onClearBlocklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={BlocklistFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('BlocklistLoadError')}
</Alert>
}
{
isPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}>
{
selectedFilterKey === 'all' ?
translate('NoHistoryBlocklist') :
translate('BlocklistFilterHasNoItems')
}
</Alert>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<BlocklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title={translate('RemoveSelected')}
message={translate('RemoveSelectedBlocklistMessageText')}
confirmLabel={translate('RemoveSelected')}
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
<ConfirmModal
isOpen={isConfirmClearModalOpen}
kind={kinds.DANGER}
title={translate('ClearBlocklist')}
message={translate('ClearBlocklistMessageText')}
confirmLabel={translate('Clear')}
onConfirm={this.onClearBlocklistConfirmed}
onCancel={this.onConfirmClearModalClose}
/>
</PageContent>
);
}
}
Blocklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
export default Blocklist;

View File

@@ -0,0 +1,329 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds } from 'Helpers/Props';
import {
clearBlocklist,
fetchBlocklist,
gotoBlocklistPage,
removeBlocklistItems,
setBlocklistFilter,
setBlocklistSort,
setBlocklistTableOption,
} from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { TableOptionsChangePayload } from 'typings/Table';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import BlocklistFilterModal from './BlocklistFilterModal';
import BlocklistRow from './BlocklistRow';
function Blocklist() {
const requestCurrentPage = useCurrentPage();
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
isRemoving,
} = useSelector((state: AppState) => state.blocklist);
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
const isClearingBlocklistExecuting = useSelector(
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
);
const dispatch = useDispatch();
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
useState(false);
const [isConfirmClearModalOpen, setIsConfirmClearModalOpen] = useState(false);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const wasClearingBlocklistExecuting = usePrevious(
isClearingBlocklistExecuting
);
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const handleSelectedChange = useCallback(
({ id, value, shiftKey = false }: SelectStateInputProps) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const handleRemoveSelectedPress = useCallback(() => {
setIsConfirmRemoveModalOpen(true);
}, [setIsConfirmRemoveModalOpen]);
const handleRemoveSelectedConfirmed = useCallback(() => {
dispatch(removeBlocklistItems({ ids: selectedIds }));
setIsConfirmRemoveModalOpen(false);
}, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]);
const handleConfirmRemoveModalClose = useCallback(() => {
setIsConfirmRemoveModalOpen(false);
}, [setIsConfirmRemoveModalOpen]);
const handleClearBlocklistPress = useCallback(() => {
setIsConfirmClearModalOpen(true);
}, [setIsConfirmClearModalOpen]);
const handleClearBlocklistConfirmed = useCallback(() => {
dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST }));
setIsConfirmClearModalOpen(false);
}, [setIsConfirmClearModalOpen, dispatch]);
const handleConfirmClearModalClose = useCallback(() => {
setIsConfirmClearModalOpen(false);
}, [setIsConfirmClearModalOpen]);
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoBlocklistPage,
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
dispatch(setBlocklistFilter({ selectedFilterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string) => {
dispatch(setBlocklistSort({ sortKey }));
},
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setBlocklistTableOption(payload));
if (payload.pageSize) {
dispatch(gotoBlocklistPage({ page: 1 }));
}
},
[dispatch]
);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchBlocklist());
} else {
dispatch(gotoBlocklistPage({ page: 1 }));
}
return () => {
dispatch(clearBlocklist());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
dispatch(fetchBlocklist());
};
registerPagePopulator(repopulate);
return () => {
unregisterPagePopulator(repopulate);
};
}, [dispatch]);
useEffect(() => {
if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) {
dispatch(gotoBlocklistPage({ page: 1 }));
}
}, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]);
return (
<SelectProvider items={items}>
<PageContent title={translate('Blocklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={handleRemoveSelectedPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isDisabled={!items.length}
isSpinning={isClearingBlocklistExecuting}
onPress={handleClearBlocklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={BlocklistFilterModal}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert>
) : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>
{selectedFilterKey === 'all'
? translate('NoBlocklistItems')
: translate('BlocklistFilterHasNoItems')}
</Alert>
) : null}
{isPopulated && !error && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<BlocklistRow
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
/>
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
</div>
) : null}
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title={translate('RemoveSelected')}
message={translate('RemoveSelectedBlocklistMessageText')}
confirmLabel={translate('RemoveSelected')}
onConfirm={handleRemoveSelectedConfirmed}
onCancel={handleConfirmRemoveModalClose}
/>
<ConfirmModal
isOpen={isConfirmClearModalOpen}
kind={kinds.DANGER}
title={translate('ClearBlocklist')}
message={translate('ClearBlocklistMessageText')}
confirmLabel={translate('Clear')}
onConfirm={handleClearBlocklistConfirmed}
onCancel={handleConfirmClearModalClose}
/>
</PageContent>
</SelectProvider>
);
}
export default Blocklist;

View File

@@ -1,161 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import * as blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blocklist,
createCustomFiltersSelector('blocklist'),
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, customFilters, isClearingBlocklistExecuting) => {
return {
isClearingBlocklistExecuting,
customFilters,
...blocklist
};
}
);
}
const mapDispatchToProps = {
...blocklistActions,
executeCommand
};
class BlocklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlocklist,
gotoBlocklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlocklist();
} else {
gotoBlocklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
this.props.gotoBlocklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlocklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlocklist();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlocklistFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoBlocklistPreviousPage();
};
onNextPagePress = () => {
this.props.gotoBlocklistNextPage();
};
onLastPagePress = () => {
this.props.gotoBlocklistLastPage();
};
onPageSelect = (page) => {
this.props.gotoBlocklistPage({ page });
};
onRemoveSelected = (ids) => {
this.props.removeBlocklistItems({ ids });
};
onSortPress = (sortKey) => {
this.props.setBlocklistSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setBlocklistFilter({ selectedFilterKey });
};
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
};
onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlocklistFirstPage();
}
};
//
// Render
render() {
return (
<Blocklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
/>
);
}
}
BlocklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlocklist: PropTypes.func.isRequired,
gotoBlocklistFirstPage: PropTypes.func.isRequired,
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
gotoBlocklistNextPage: PropTypes.func.isRequired,
gotoBlocklistLastPage: PropTypes.func.isRequired,
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistFilter: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
);

View File

@@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
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 translate from 'Utilities/String/translate';
class BlocklistDetailsModal extends Component {
//
// Render
render() {
const {
isOpen,
sourceTitle,
protocol,
indexer,
message,
onModalClose
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
Details
</ModalHeader>
<ModalBody>
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
data={sourceTitle}
/>
<DescriptionListItem
title={translate('Protocol')}
data={protocol}
/>
{
!!message &&
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/>
}
{
!!message &&
<DescriptionListItem
title={translate('Message')}
data={message}
/>
}
</DescriptionList>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
BlocklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
onModalClose: PropTypes.func.isRequired
};
export default BlocklistDetailsModal;

View File

@@ -0,0 +1,64 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import translate from 'Utilities/String/translate';
interface BlocklistDetailsModalProps {
isOpen: boolean;
sourceTitle: string;
protocol: DownloadProtocol;
indexer?: string;
message?: string;
onModalClose: () => void;
}
function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } =
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Details</ModalHeader>
<ModalBody>
<DescriptionList>
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
<DescriptionListItem
title={translate('Protocol')}
data={protocol}
/>
{message ? (
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/>
) : null}
{message ? (
<DescriptionListItem
title={translate('Message')}
data={message}
/>
) : null}
</DescriptionList>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default BlocklistDetailsModal;

View File

@@ -1,4 +1,4 @@
.language,
.languages,
.quality {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -3,7 +3,7 @@
interface CssExports {
'actions': string;
'indexer': string;
'language': string;
'languages': string;
'quality': string;
}
export const cssExports: CssExports;

View File

@@ -1,213 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import translate from 'Utilities/String/translate';
import BlocklistDetailsModal from './BlocklistDetailsModal';
import styles from './BlocklistRow.css';
class BlocklistRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDetailsModalOpen: false
};
}
//
// Listeners
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
//
// Render
render() {
const {
id,
movie,
sourceTitle,
quality,
customFormats,
languages,
date,
protocol,
indexer,
message,
isSelected,
columns,
onSelectedChange,
onRemovePress
} = this.props;
if (!movie) {
return null;
}
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/>
</TableRowCell>
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell key={name}>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell
key={name}
className={styles.quality}
>
<MovieQuality
quality={quality}
/>
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'date') {
return (
<RelativeDateCell
key={name}
date={date}
/>
);
}
if (name === 'indexer') {
return (
<TableRowCell
key={name}
className={styles.indexer}
>
{indexer}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell
key={name}
className={styles.actions}
>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
<IconButton
title={translate('RemoveFromBlocklist')}
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
/>
</TableRowCell>
);
}
return null;
})
}
<BlocklistDetailsModal
isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
indexer={indexer}
message={message}
onModalClose={this.onDetailsModalClose}
/>
</TableRow>
);
}
}
BlocklistRow.propTypes = {
id: PropTypes.number.isRequired,
movie: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
date: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired
};
export default BlocklistRow;

View File

@@ -0,0 +1,160 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import useMovie from 'Movie/useMovie';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
import Blocklist from 'typings/Blocklist';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import BlocklistDetailsModal from './BlocklistDetailsModal';
import styles from './BlocklistRow.css';
interface BlocklistRowProps extends Blocklist {
isSelected: boolean;
columns: Column[];
onSelectedChange: (options: SelectStateInputProps) => void;
}
function BlocklistRow(props: BlocklistRowProps) {
const {
id,
movieId,
sourceTitle,
languages,
quality,
customFormats,
date,
protocol,
indexer,
message,
isSelected,
columns,
onSelectedChange,
} = props;
const movie = useMovie(movieId);
const dispatch = useDispatch();
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
const handleDetailsPress = useCallback(() => {
setIsDetailsModalOpen(true);
}, [setIsDetailsModalOpen]);
const handleDetailsModalClose = useCallback(() => {
setIsDetailsModalOpen(false);
}, [setIsDetailsModalOpen]);
const handleRemovePress = useCallback(() => {
dispatch(removeBlocklistItem({ id }));
}, [id, dispatch]);
if (!movie) {
return null;
}
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
</TableRowCell>
);
}
if (name === 'sourceTitle') {
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
}
if (name === 'languages') {
return (
<TableRowCell key={name} className={styles.languages}>
<MovieLanguages languages={languages} />
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name} className={styles.quality}>
<MovieQuality quality={quality} />
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats formats={customFormats} />
</TableRowCell>
);
}
if (name === 'date') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
return <RelativeDateCell key={name} date={date} />;
}
if (name === 'indexer') {
return (
<TableRowCell key={name} className={styles.indexer}>
{indexer}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell key={name} className={styles.actions}>
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
<IconButton
title={translate('RemoveFromBlocklist')}
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={handleRemovePress}
/>
</TableRowCell>
);
}
return null;
})}
<BlocklistDetailsModal
isOpen={isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
indexer={indexer}
message={message}
onModalClose={handleDetailsModalClose}
/>
</TableRow>
);
}
export default BlocklistRow;

View File

@@ -1,26 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import BlocklistRow from './BlocklistRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
(movie) => {
return {
movie
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeBlocklistItem({ id: props.id }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);

View File

@@ -1,354 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import Link from 'Components/Link/Link';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
function HistoryDetails(props) {
const {
eventType,
sourceTitle,
data,
downloadId,
shortDateFormat,
timeFormat
} = props;
if (eventType === 'grabbed') {
const {
indexer,
releaseGroup,
movieMatchType,
customFormatScore,
nzbInfoUrl,
downloadClient,
downloadClientName,
age,
ageHours,
ageMinutes,
publishedDate
} = data;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
indexer ?
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/> :
null
}
{
releaseGroup ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseGroup')}
data={releaseGroup}
/> :
null
}
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
{
movieMatchType ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieMatchType')}
data={movieMatchType}
/> :
null
}
{
nzbInfoUrl ?
<span>
<DescriptionListItemTitle>
{translate('InfoUrl')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span> :
null
}
{
downloadClientNameInfo ?
<DescriptionListItem
title={translate('DownloadClient')}
data={downloadClientNameInfo}
/> :
null
}
{
downloadId ?
<DescriptionListItem
title={translate('GrabId')}
data={downloadId}
/> :
null
}
{
indexer ?
<DescriptionListItem
title={translate('AgeWhenGrabbed')}
data={formatAge(age, ageHours, ageMinutes)}
/> :
null
}
{
publishedDate ?
<DescriptionListItem
title={translate('PublishedDate')}
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/> :
null
}
</DescriptionList>
);
}
if (eventType === 'downloadFailed') {
const {
message
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
downloadId ?
<DescriptionListItem
title={translate('GrabId')}
data={downloadId}
/> :
null
}
{
message ?
<DescriptionListItem
title={translate('Message')}
data={message}
/> :
null
}
</DescriptionList>
);
}
if (eventType === 'downloadFolderImported') {
const {
customFormatScore,
droppedPath,
importedPath
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
droppedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Source')}
data={droppedPath}
/> :
null
}
{
importedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ImportedTo')}
data={importedPath}
/> :
null
}
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
</DescriptionList>
);
}
if (eventType === 'movieFileDeleted') {
const {
reason,
customFormatScore
} = data;
let reasonMessage = '';
switch (reason) {
case 'Manual':
reasonMessage = translate('DeletedReasonManual');
break;
case 'MissingFromDisk':
reasonMessage = translate('DeletedReasonMissingFromDisk');
break;
case 'Upgrade':
reasonMessage = translate('DeletedReasonUpgrade');
break;
default:
reasonMessage = '';
}
return (
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
data={sourceTitle}
/>
<DescriptionListItem
title={translate('Reason')}
data={reasonMessage}
/>
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
</DescriptionList>
);
}
if (eventType === 'movieFileRenamed') {
const {
sourcePath,
sourceRelativePath,
path,
relativePath
} = data;
return (
<DescriptionList>
<DescriptionListItem
title={translate('SourcePath')}
data={sourcePath}
/>
<DescriptionListItem
title={translate('SourceRelativePath')}
data={sourceRelativePath}
/>
<DescriptionListItem
title={translate('DestinationPath')}
data={path}
/>
<DescriptionListItem
title={translate('DestinationRelativePath')}
data={relativePath}
/>
</DescriptionList>
);
}
if (eventType === 'downloadIgnored') {
const {
message
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{
downloadId ?
<DescriptionListItem
title={translate('GrabId')}
data={downloadId}
/> :
null
}
{
message ?
<DescriptionListItem
title={translate('Message')}
data={message}
/> :
null
}
</DescriptionList>
);
}
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
</DescriptionList>
);
}
HistoryDetails.propTypes = {
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
export default HistoryDetails;

View File

@@ -0,0 +1,321 @@
import React from 'react';
import { useSelector } from 'react-redux';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import Link from 'Components/Link/Link';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import {
DownloadFailedHistory,
DownloadFolderImportedHistory,
DownloadIgnoredHistory,
GrabbedHistoryData,
HistoryData,
HistoryEventType,
MovieFileDeletedHistory,
MovieFileRenamedHistory,
} from 'typings/History';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
interface HistoryDetailsProps {
eventType: HistoryEventType;
sourceTitle: string;
data: HistoryData;
downloadId?: string;
}
function HistoryDetails(props: HistoryDetailsProps) {
const { eventType, sourceTitle, data, downloadId } = props;
const { shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
if (eventType === 'grabbed') {
const {
indexer,
releaseGroup,
movieMatchType,
releaseSource,
customFormatScore,
nzbInfoUrl,
downloadClient,
downloadClientName,
age,
ageHours,
ageMinutes,
publishedDate,
} = data as GrabbedHistoryData;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
let releaseSourceMessage = '';
switch (releaseSource) {
case 'Unknown':
releaseSourceMessage = translate('Unknown');
break;
case 'Rss':
releaseSourceMessage = translate('Rss');
break;
case 'Search':
releaseSourceMessage = translate('Search');
break;
case 'UserInvokedSearch':
releaseSourceMessage = translate('UserInvokedSearch');
break;
case 'InteractiveSearch':
releaseSourceMessage = translate('InteractiveSearch');
break;
case 'ReleasePush':
releaseSourceMessage = translate('ReleasePush');
break;
default:
releaseSourceMessage = '';
}
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{indexer ? (
<DescriptionListItem title={translate('Indexer')} data={indexer} />
) : null}
{releaseGroup ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseGroup')}
data={releaseGroup}
/>
) : null}
{customFormatScore && customFormatScore !== '0' ? (
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(parseInt(customFormatScore))}
/>
) : null}
{movieMatchType ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieMatchType')}
data={movieMatchType}
/>
) : null}
{releaseSource ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseSource')}
data={releaseSourceMessage}
/>
) : null}
{nzbInfoUrl ? (
<span>
<DescriptionListItemTitle>
{translate('InfoUrl')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span>
) : null}
{downloadClientNameInfo ? (
<DescriptionListItem
title={translate('DownloadClient')}
data={downloadClientNameInfo}
/>
) : null}
{downloadId ? (
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
) : null}
{age || ageHours || ageMinutes ? (
<DescriptionListItem
title={translate('AgeWhenGrabbed')}
data={formatAge(age, ageHours, ageMinutes)}
/>
) : null}
{publishedDate ? (
<DescriptionListItem
title={translate('PublishedDate')}
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, {
includeSeconds: true,
})}
/>
) : null}
</DescriptionList>
);
}
if (eventType === 'downloadFailed') {
const { message } = data as DownloadFailedHistory;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{downloadId ? (
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
) : null}
{message ? (
<DescriptionListItem title={translate('Message')} data={message} />
) : null}
</DescriptionList>
);
}
if (eventType === 'downloadFolderImported') {
const { customFormatScore, droppedPath, importedPath } =
data as DownloadFolderImportedHistory;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{droppedPath ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Source')}
data={droppedPath}
/>
) : null}
{importedPath ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ImportedTo')}
data={importedPath}
/>
) : null}
{customFormatScore && customFormatScore !== '0' ? (
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(parseInt(customFormatScore))}
/>
) : null}
</DescriptionList>
);
}
if (eventType === 'movieFileDeleted') {
const { reason, customFormatScore } = data as MovieFileDeletedHistory;
let reasonMessage = '';
switch (reason) {
case 'Manual':
reasonMessage = translate('DeletedReasonManual');
break;
case 'MissingFromDisk':
reasonMessage = translate('DeletedReasonMovieMissingFromDisk');
break;
case 'Upgrade':
reasonMessage = translate('DeletedReasonUpgrade');
break;
default:
reasonMessage = '';
}
return (
<DescriptionList>
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
<DescriptionListItem title={translate('Reason')} data={reasonMessage} />
{customFormatScore && customFormatScore !== '0' ? (
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatCustomFormatScore(parseInt(customFormatScore))}
/>
) : null}
</DescriptionList>
);
}
if (eventType === 'movieFileRenamed') {
const { sourcePath, sourceRelativePath, path, relativePath } =
data as MovieFileRenamedHistory;
return (
<DescriptionList>
<DescriptionListItem
title={translate('SourcePath')}
data={sourcePath}
/>
<DescriptionListItem
title={translate('SourceRelativePath')}
data={sourceRelativePath}
/>
<DescriptionListItem title={translate('DestinationPath')} data={path} />
<DescriptionListItem
title={translate('DestinationRelativePath')}
data={relativePath}
/>
</DescriptionList>
);
}
if (eventType === 'downloadIgnored') {
const { message } = data as DownloadIgnoredHistory;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
{downloadId ? (
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
) : null}
{message ? (
<DescriptionListItem title={translate('Message')} data={message} />
) : null}
</DescriptionList>
);
}
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
data={sourceTitle}
/>
</DescriptionList>
);
}
export default HistoryDetails;

View File

@@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import HistoryDetails from './HistoryDetails';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return {
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
export default connect(createMapStateToProps)(HistoryDetails);

View File

@@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
@@ -8,11 +7,12 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import { HistoryData, HistoryEventType } from 'typings/History';
import translate from 'Utilities/String/translate';
import HistoryDetails from './HistoryDetails';
import styles from './HistoryDetailsModal.css';
function getHeaderTitle(eventType) {
function getHeaderTitle(eventType: HistoryEventType) {
switch (eventType) {
case 'grabbed':
return translate('Grabbed');
@@ -31,29 +31,33 @@ function getHeaderTitle(eventType) {
}
}
function HistoryDetailsModal(props) {
interface HistoryDetailsModalProps {
isOpen: boolean;
eventType: HistoryEventType;
sourceTitle: string;
data: HistoryData;
downloadId?: string;
isMarkingAsFailed: boolean;
onMarkAsFailedPress: () => void;
onModalClose: () => void;
}
function HistoryDetailsModal(props: HistoryDetailsModalProps) {
const {
isOpen,
eventType,
sourceTitle,
data,
downloadId,
isMarkingAsFailed,
shortDateFormat,
timeFormat,
isMarkingAsFailed = false,
onMarkAsFailedPress,
onModalClose
onModalClose,
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{getHeaderTitle(eventType)}
</ModalHeader>
<ModalHeader>{getHeaderTitle(eventType)}</ModalHeader>
<ModalBody>
<HistoryDetails
@@ -61,50 +65,26 @@ function HistoryDetailsModal(props) {
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
/>
</ModalBody>
<ModalFooter>
{
eventType === 'grabbed' &&
<SpinnerButton
className={styles.markAsFailedButton}
kind={kinds.DANGER}
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
{translate('MarkAsFailed')}
</SpinnerButton>
}
{eventType === 'grabbed' && (
<SpinnerButton
className={styles.markAsFailedButton}
kind={kinds.DANGER}
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
{translate('MarkAsFailed')}
</SpinnerButton>
)}
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
HistoryDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
isMarkingAsFailed: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
HistoryDetailsModal.defaultProps = {
isMarkingAsFailed: false
};
export default HistoryDetailsModal;

View File

@@ -1,158 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
isMoviesFetching,
isMoviesPopulated,
moviesError,
items,
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
onFilterSelect,
onFirstPagePress,
...otherProps
} = this.props;
const isFetchingAny = isFetching || isMoviesFetching;
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
const hasError = error || moviesError;
return (
<PageContent title={translate('History')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isFetching}
onPress={onFirstPagePress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetchingAny && !isAllPopulated &&
<LoadingIndicator />
}
{
!isFetchingAny && hasError &&
<Alert kind={kinds.DANGER}>
{translate('HistoryLoadError')}
</Alert>
}
{
// If history isPopulated and it's empty show no history found and don't
// wait for the episodes to populate because they are never coming.
isPopulated && !hasError && !items.length &&
<Alert kind={kinds.INFO}>
{translate('NoHistoryFound')}
</Alert>
}
{
isAllPopulated && !hasError && !!items.length &&
<div>
<Table
columns={columns}
{...otherProps}
>
<TableBody>
{
items.map((item) => {
return (
<HistoryRowConnector
key={item.id}
columns={columns}
{...item}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={onFirstPagePress}
{...otherProps}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
}
History.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isMoviesFetching: PropTypes.bool.isRequired,
isMoviesPopulated: PropTypes.bool.isRequired,
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
onFilterSelect: PropTypes.func.isRequired,
onFirstPagePress: PropTypes.func.isRequired
};
export default History;

View File

@@ -0,0 +1,216 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import { align, icons, kinds } from 'Helpers/Props';
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
import {
clearHistory,
fetchHistory,
gotoHistoryPage,
setHistoryFilter,
setHistorySort,
setHistoryTableOption,
} from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { TableOptionsChangePayload } from 'typings/Table';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRow from './HistoryRow';
function History() {
const requestCurrentPage = useCurrentPage();
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
} = useSelector((state: AppState) => state.history);
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
createMoviesFetchingSelector()
);
const customFilters = useSelector(createCustomFiltersSelector('history'));
const dispatch = useDispatch();
const isFetchingAny = isFetching || isMoviesFetching;
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
const hasError = error || moviesError;
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoHistoryPage,
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
dispatch(setHistoryFilter({ selectedFilterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string) => {
dispatch(setHistorySort({ sortKey }));
},
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setHistoryTableOption(payload));
if (payload.pageSize) {
dispatch(gotoHistoryPage({ page: 1 }));
}
},
[dispatch]
);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchHistory());
} else {
dispatch(gotoHistoryPage({ page: 1 }));
}
return () => {
dispatch(clearHistory());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
dispatch(fetchHistory());
};
registerPagePopulator(repopulate);
return () => {
unregisterPagePopulator(repopulate);
};
}, [dispatch]);
return (
<PageContent title={translate('History')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isFetching}
onPress={handleFirstPagePress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetchingAny && !isAllPopulated ? <LoadingIndicator /> : null}
{!isFetchingAny && hasError ? (
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
) : null}
{
// If history isPopulated and it's empty show no history found and don't
// wait for the movies to populate because they are never coming.
isPopulated && !hasError && !items.length ? (
<Alert kind={kinds.INFO}>{translate('NoHistoryFound')}</Alert>
) : null
}
{isAllPopulated && !hasError && items.length ? (
<div>
<Table
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<HistoryRow key={item.id} columns={columns} {...item} />
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
</div>
) : null}
</PageContentBody>
</PageContent>
);
}
export default History;

View File

@@ -1,141 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import withCurrentPage from 'Components/withCurrentPage';
import * as historyActions from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History';
function createMapStateToProps() {
return createSelector(
(state) => state.history,
(state) => state.movies,
createCustomFiltersSelector('history'),
(history, movies, customFilters) => {
return {
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
customFilters,
...history
};
}
);
}
const mapDispatchToProps = {
...historyActions
};
class HistoryConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchHistory,
gotoHistoryFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchHistory();
} else {
gotoHistoryFirstPage();
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearHistory();
}
//
// Control
repopulate = () => {
this.props.fetchHistory();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoHistoryFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoHistoryPreviousPage();
};
onNextPagePress = () => {
this.props.gotoHistoryNextPage();
};
onLastPagePress = () => {
this.props.gotoHistoryLastPage();
};
onPageSelect = (page) => {
this.props.gotoHistoryPage({ page });
};
onSortPress = (sortKey) => {
this.props.setHistorySort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setHistoryFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => {
this.props.setHistoryTableOption(payload);
if (payload.pageSize) {
this.props.gotoHistoryFirstPage();
}
};
//
// Render
render() {
return (
<History
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
{...this.props}
/>
);
}
}
HistoryConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchHistory: PropTypes.func.isRequired,
gotoHistoryFirstPage: PropTypes.func.isRequired,
gotoHistoryPreviousPage: PropTypes.func.isRequired,
gotoHistoryNextPage: PropTypes.func.isRequired,
gotoHistoryLastPage: PropTypes.func.isRequired,
gotoHistoryPage: PropTypes.func.isRequired,
setHistorySort: PropTypes.func.isRequired,
setHistoryFilter: PropTypes.func.isRequired,
setHistoryTableOption: PropTypes.func.isRequired,
clearHistory: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
);

View File

@@ -1,12 +1,17 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
import {
GrabbedHistoryData,
HistoryData,
HistoryEventType,
MovieFileDeletedHistory,
} from 'typings/History';
import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css';
function getIconName(eventType, data) {
function getIconName(eventType: HistoryEventType, data: HistoryData) {
switch (eventType) {
case 'grabbed':
return icons.DOWNLOADING;
@@ -17,7 +22,9 @@ function getIconName(eventType, data) {
case 'downloadFailed':
return icons.DOWNLOADING;
case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
? icons.FILE_MISSING
: icons.DELETE;
case 'movieFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
@@ -27,7 +34,7 @@ function getIconName(eventType, data) {
}
}
function getIconKind(eventType) {
function getIconKind(eventType: HistoryEventType) {
switch (eventType) {
case 'downloadFailed':
return kinds.DANGER;
@@ -36,52 +43,47 @@ function getIconKind(eventType) {
}
}
function getTooltip(eventType, data) {
function getTooltip(eventType: HistoryEventType, data: HistoryData) {
switch (eventType) {
case 'grabbed':
return translate('MovieGrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
return translate('MovieGrabbedTooltip', {
indexer: (data as GrabbedHistoryData).indexer,
downloadClient: (data as GrabbedHistoryData).downloadClient,
});
case 'movieFolderImported':
return translate('MovieFolderImportedTooltip');
case 'downloadFolderImported':
return translate('MovieImportedTooltip');
case 'downloadFailed':
return translate('MovieDownloadFailedTooltip');
return translate('DownloadFailedMovieTooltip');
case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
? translate('MovieFileMissingTooltip')
: translate('MovieFileDeletedTooltip');
case 'movieFileRenamed':
return translate('MovieFileRenamedTooltip');
case 'downloadIgnored':
return translate('MovieDownloadIgnoredTooltip');
return translate('DownloadIgnoredMovieTooltip');
default:
return translate('UnknownEventTooltip');
}
}
function HistoryEventTypeCell({ eventType, data }) {
interface HistoryEventTypeCellProps {
eventType: HistoryEventType;
data: HistoryData;
}
function HistoryEventTypeCell({ eventType, data }: HistoryEventTypeCellProps) {
const iconName = getIconName(eventType, data);
const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data);
return (
<TableRowCell
className={styles.cell}
title={tooltip}
>
<Icon
name={iconName}
kind={iconKind}
/>
<TableRowCell className={styles.cell} title={tooltip}>
<Icon name={iconName} kind={iconKind} />
</TableRowCell>
);
}
HistoryEventTypeCell.propTypes = {
eventType: PropTypes.string.isRequired,
data: PropTypes.object
};
HistoryEventTypeCell.defaultProps = {
data: {}
};
export default HistoryEventTypeCell;

View File

@@ -1,277 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css';
class HistoryRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDetailsModalOpen: false
};
}
componentDidUpdate(prevProps) {
if (
prevProps.isMarkingAsFailed &&
!this.props.isMarkingAsFailed &&
!this.props.markAsFailedError
) {
this.setState({ isDetailsModalOpen: false });
}
}
//
// Listeners
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
//
// Render
render() {
const {
movie,
quality,
customFormats,
customFormatScore,
languages,
qualityCutoffNotMet,
eventType,
sourceTitle,
date,
data,
downloadId,
isMarkingAsFailed,
columns,
shortDateFormat,
timeFormat,
onMarkAsFailedPress
} = this.props;
if (!movie) {
return null;
}
return (
<TableRow>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'eventType') {
return (
<HistoryEventTypeCell
key={name}
eventType={eventType}
data={data}
/>
);
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/>
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
<MovieQuality
quality={quality}
isCutoffMet={qualityCutoffNotMet}
/>
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'date') {
return (
<RelativeDateCell
key={name}
date={date}
/>
);
}
if (name === 'downloadClient') {
return (
<TableRowCell
key={name}
className={styles.downloadClient}
>
{data.downloadClient}
</TableRowCell>
);
}
if (name === 'indexer') {
return (
<TableRowCell
key={name}
className={styles.indexer}
>
{data.indexer}
</TableRowCell>
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell
key={name}
className={styles.releaseGroup}
>
{data.releaseGroup}
</TableRowCell>
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') {
return (
<TableRowCell
key={name}
className={styles.details}
>
<div className={styles.actionContents}>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
</div>
</TableRowCell>
);
}
return null;
})
}
<HistoryDetailsModal
isOpen={this.state.isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
isMarkingAsFailed={isMarkingAsFailed}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
onMarkAsFailedPress={onMarkAsFailedPress}
onModalClose={this.onDetailsModalClose}
/>
</TableRow>
);
}
}
HistoryRow.propTypes = {
movieId: PropTypes.number,
movie: PropTypes.object.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
isMarkingAsFailed: PropTypes.bool,
markAsFailedError: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
HistoryRow.defaultProps = {
customFormats: []
};
export default HistoryRow;

View File

@@ -0,0 +1,229 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import useMovie from 'Movie/useMovie';
import { QualityModel } from 'Quality/Quality';
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
import CustomFormat from 'typings/CustomFormat';
import { HistoryData, HistoryEventType } from 'typings/History';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css';
interface HistoryRowProps {
id: number;
movieId: number;
languages: Language[];
quality: QualityModel;
customFormats?: CustomFormat[];
customFormatScore: number;
qualityCutoffNotMet: boolean;
eventType: HistoryEventType;
sourceTitle: string;
date: string;
data: HistoryData;
downloadId?: string;
isMarkingAsFailed?: boolean;
markAsFailedError?: object;
columns: Column[];
}
function HistoryRow(props: HistoryRowProps) {
const {
id,
movieId,
languages,
quality,
customFormats = [],
customFormatScore,
qualityCutoffNotMet,
eventType,
sourceTitle,
date,
data,
downloadId,
isMarkingAsFailed = false,
markAsFailedError,
columns,
} = props;
const wasMarkingAsFailed = usePrevious(isMarkingAsFailed);
const dispatch = useDispatch();
const movie = useMovie(movieId);
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
const handleDetailsPress = useCallback(() => {
setIsDetailsModalOpen(true);
}, [setIsDetailsModalOpen]);
const handleDetailsModalClose = useCallback(() => {
setIsDetailsModalOpen(false);
}, [setIsDetailsModalOpen]);
const handleMarkAsFailedPress = useCallback(() => {
dispatch(markAsFailed({ id }));
}, [id, dispatch]);
useEffect(() => {
if (wasMarkingAsFailed && !isMarkingAsFailed && !markAsFailedError) {
setIsDetailsModalOpen(false);
dispatch(fetchHistory());
}
}, [
wasMarkingAsFailed,
isMarkingAsFailed,
markAsFailedError,
setIsDetailsModalOpen,
dispatch,
]);
if (!movie) {
return null;
}
return (
<TableRow>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'eventType') {
return (
<HistoryEventTypeCell
key={name}
eventType={eventType}
data={data}
/>
);
}
if (name === 'movieMetadata.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages languages={languages} />
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
<MovieQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats formats={customFormats} />
</TableRowCell>
);
}
if (name === 'date') {
return <RelativeDateCell key={name} date={date} />;
}
if (name === 'downloadClient') {
const downloadClientName =
'downloadClientName' in data ? data.downloadClientName : null;
const downloadClient =
'downloadClient' in data ? data.downloadClient : null;
return (
<TableRowCell key={name} className={styles.downloadClient}>
{downloadClientName ?? downloadClient ?? ''}
</TableRowCell>
);
}
if (name === 'indexer') {
return (
<TableRowCell key={name} className={styles.indexer}>
{'indexer' in data ? data.indexer : ''}
</TableRowCell>
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell key={name} className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell key={name} className={styles.releaseGroup}>
{'releaseGroup' in data ? data.releaseGroup : ''}
</TableRowCell>
);
}
if (name === 'sourceTitle') {
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
}
if (name === 'details') {
return (
<TableRowCell key={name} className={styles.details}>
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
</TableRowCell>
);
}
return null;
})}
<HistoryDetailsModal
isOpen={isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
isMarkingAsFailed={isMarkingAsFailed}
onMarkAsFailedPress={handleMarkAsFailedPress}
onModalClose={handleDetailsModalClose}
/>
</TableRow>
);
}
export default HistoryRow;

View File

@@ -1,73 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import HistoryRow from './HistoryRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
return {
movie,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
const mapDispatchToProps = {
fetchHistory,
markAsFailed
};
class HistoryRowConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps) {
if (
prevProps.isMarkingAsFailed &&
!this.props.isMarkingAsFailed &&
!this.props.markAsFailedError
) {
this.props.fetchHistory();
}
}
//
// Listeners
onMarkAsFailedPress = () => {
this.props.markAsFailed({ id: this.props.id });
};
//
// Render
render() {
return (
<HistoryRow
{...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress}
/>
);
}
}
HistoryRowConnector.propTypes = {
id: PropTypes.number.isRequired,
isMarkingAsFailed: PropTypes.bool,
markAsFailedError: PropTypes.object,
fetchHistory: PropTypes.func.isRequired,
markAsFailed: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector);

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,20 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import styles from './ProtocolLabel.css';
function ProtocolLabel({ protocol }) {
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
return (
<Label className={styles[protocol]}>
{protocolName}
</Label>
);
}
ProtocolLabel.propTypes = {
protocol: PropTypes.string.isRequired
};
export default ProtocolLabel;

View File

@@ -0,0 +1,16 @@
import React from 'react';
import Label from 'Components/Label';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import styles from './ProtocolLabel.css';
interface ProtocolLabelProps {
protocol: DownloadProtocol;
}
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
return <Label className={styles[protocol]}>{protocolName}</Label>;
}
export default ProtocolLabel;

View File

@@ -1,374 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import QueueFilterModal from './QueueFilterModal';
import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemModal from './RemoveQueueItemModal';
class Queue extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._shouldBlockRefresh = false;
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isPendingSelected: false,
isConfirmRemoveModalOpen: false,
items: props.items
};
}
shouldComponentUpdate() {
if (this._shouldBlockRefresh) {
return false;
}
return true;
}
componentDidUpdate(prevProps) {
const {
items,
isFetching,
isMoviesFetching
} = this.props;
if (
(!isMoviesFetching && prevProps.isMoviesFetching) ||
(!isFetching && prevProps.isFetching) ||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.movieId))
) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
const nextState = {};
if (prevProps.items !== items) {
nextState.items = items;
}
const selectedIds = this.getSelectedIds();
const isPendingSelected = _.some(this.props.items, (item) => {
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
});
if (isPendingSelected !== this.state.isPendingSelected) {
nextState.isPendingSelected = isPendingSelected;
}
if (!_.isEmpty(nextState)) {
this.setState(nextState);
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
//
// Listeners
onQueueRowModalOpenOrClose = (isOpen) => {
this._shouldBlockRefresh = isOpen;
};
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
onGrabSelectedPress = () => {
this.props.onGrabSelectedPress(this.getSelectedIds());
};
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true }, () => {
this._shouldBlockRefresh = true;
});
};
onRemoveSelectedConfirmed = (payload) => {
this._shouldBlockRefresh = false;
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false });
};
onConfirmRemoveModalClose = () => {
this._shouldBlockRefresh = false;
this.setState({ isConfirmRemoveModalOpen: false });
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
isMoviesFetching,
isMoviesPopulated,
moviesError,
columns,
selectedFilterKey,
filters,
customFilters,
count,
totalRecords,
isGrabbing,
isRemoving,
isRefreshMonitoredDownloadsExecuting,
onRefreshPress,
onFilterSelect,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen,
isPendingSelected,
items
} = this.state;
const isRefreshing = isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId));
const hasError = error || moviesError;
const selectedIds = this.getSelectedIds();
const selectedCount = selectedIds.length;
const disableSelectedActions = selectedCount === 0;
return (
<PageContent title={translate('Queue')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isRefreshing}
onPress={onRefreshPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('GrabSelected')}
iconName={icons.DOWNLOAD}
isDisabled={disableSelectedActions || !isPendingSelected}
isSpinning={isGrabbing}
onPress={this.onGrabSelectedPress}
/>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={disableSelectedActions}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
>
<TableOptionsModalWrapper
columns={columns}
maxPageSize={200}
{...otherProps}
optionsComponent={QueueOptionsConnector}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={QueueFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isRefreshing && !isAllPopulated ?
<LoadingIndicator /> :
null
}
{
!isRefreshing && hasError ?
<Alert kind={kinds.DANGER}>
{translate('QueueLoadError')}
</Alert> :
null
}
{
isAllPopulated && !hasError && !items.length ?
<Alert kind={kinds.INFO}>
{
selectedFilterKey !== 'all' && count > 0 ?
translate('QueueFilterHasNoItems') :
translate('QueueIsEmpty')
}
</Alert> :
null
}
{
isAllPopulated && !hasError && !!items.length ?
<div>
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
{...otherProps}
optionsComponent={QueueOptionsConnector}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<QueueRowConnector
key={item.id}
movieId={item.movieId}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isRefreshing}
{...otherProps}
/>
</div> :
null
}
</PageContentBody>
<RemoveQueueItemModal
isOpen={isConfirmRemoveModalOpen}
selectedCount={selectedCount}
canChangeCategory={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.downloadClientHasPostImportCategory);
})
)}
canIgnore={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.movieId);
})
)}
pending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
}
Queue.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isMoviesFetching: PropTypes.bool.isRequired,
isMoviesPopulated: PropTypes.bool.isRequired,
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
count: PropTypes.number.isRequired,
totalRecords: PropTypes.number,
isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired,
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onGrabSelectedPress: PropTypes.func.isRequired,
onRemoveSelectedPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
Queue.defaultProps = {
count: 0
};
export default Queue;

View File

@@ -0,0 +1,400 @@
import React, {
ReactElement,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds } from 'Helpers/Props';
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
import { executeCommand } from 'Store/Actions/commandActions';
import {
clearQueue,
fetchQueue,
gotoQueuePage,
grabQueueItems,
removeQueueItems,
setQueueFilter,
setQueueSort,
setQueueTableOption,
} from 'Store/Actions/queueActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { TableOptionsChangePayload } from 'typings/Table';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import QueueFilterModal from './QueueFilterModal';
import QueueOptions from './QueueOptions';
import QueueRow from './QueueRow';
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
import createQueueStatusSelector from './Status/createQueueStatusSelector';
function Queue() {
const requestCurrentPage = useCurrentPage();
const dispatch = useDispatch();
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
isGrabbing,
isRemoving,
} = useSelector((state: AppState) => state.queue.paged);
const { count } = useSelector(createQueueStatusSelector());
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
createMoviesFetchingSelector()
);
const customFilters = useSelector(createCustomFiltersSelector('queue'));
const isRefreshMonitoredDownloadsExecuting = useSelector(
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)
);
const shouldBlockRefresh = useRef(false);
const currentQueue = useRef<ReactElement | null>(null);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const isPendingSelected = useMemo(() => {
return items.some((item) => {
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
});
}, [items, selectedIds]);
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
useState(false);
const isRefreshing =
isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
const isAllPopulated =
isPopulated &&
(isMoviesPopulated || !items.length || items.every((m) => !m.movieId));
const hasError = error || moviesError;
const selectedCount = selectedIds.length;
const disableSelectedActions = selectedCount === 0;
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const handleSelectedChange = useCallback(
({ id, value, shiftKey = false }: SelectStateInputProps) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const handleRefreshPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.REFRESH_MONITORED_DOWNLOADS,
})
);
}, [dispatch]);
const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => {
shouldBlockRefresh.current = isOpen;
}, []);
const handleGrabSelectedPress = useCallback(() => {
dispatch(grabQueueItems({ ids: selectedIds }));
}, [selectedIds, dispatch]);
const handleRemoveSelectedPress = useCallback(() => {
shouldBlockRefresh.current = true;
setIsConfirmRemoveModalOpen(true);
}, [setIsConfirmRemoveModalOpen]);
const handleRemoveSelectedConfirmed = useCallback(
(payload: RemovePressProps) => {
shouldBlockRefresh.current = false;
dispatch(removeQueueItems({ ids: selectedIds, ...payload }));
setIsConfirmRemoveModalOpen(false);
},
[selectedIds, setIsConfirmRemoveModalOpen, dispatch]
);
const handleConfirmRemoveModalClose = useCallback(() => {
shouldBlockRefresh.current = false;
setIsConfirmRemoveModalOpen(false);
}, [setIsConfirmRemoveModalOpen]);
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoQueuePage,
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
dispatch(setQueueFilter({ selectedFilterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string) => {
dispatch(setQueueSort({ sortKey }));
},
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setQueueTableOption(payload));
if (payload.pageSize) {
dispatch(gotoQueuePage({ page: 1 }));
}
},
[dispatch]
);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchQueue());
} else {
dispatch(gotoQueuePage({ page: 1 }));
}
return () => {
dispatch(clearQueue());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
dispatch(fetchQueue());
};
registerPagePopulator(repopulate);
return () => {
unregisterPagePopulator(repopulate);
};
}, [dispatch]);
if (!shouldBlockRefresh.current) {
currentQueue.current = (
<PageContentBody>
{isRefreshing && !isAllPopulated ? <LoadingIndicator /> : null}
{!isRefreshing && hasError ? (
<Alert kind={kinds.DANGER}>{translate('QueueLoadError')}</Alert>
) : null}
{isAllPopulated && !hasError && !items.length ? (
<Alert kind={kinds.INFO}>
{selectedFilterKey !== 'all' && count > 0
? translate('QueueFilterHasNoItems')
: translate('QueueIsEmpty')}
</Alert>
) : null}
{isAllPopulated && !hasError && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
optionsComponent={QueueOptions}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<QueueRow
key={item.id}
movieId={item.movieId}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
onQueueRowModalOpenOrClose={
handleQueueRowModalOpenOrClose
}
/>
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
</div>
) : null}
</PageContentBody>
);
}
return (
<PageContent title={translate('Queue')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Refresh"
iconName={icons.REFRESH}
isSpinning={isRefreshing}
onPress={handleRefreshPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('GrabSelected')}
iconName={icons.DOWNLOAD}
isDisabled={disableSelectedActions || !isPendingSelected}
isSpinning={isGrabbing}
onPress={handleGrabSelectedPress}
/>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={disableSelectedActions}
isSpinning={isRemoving}
onPress={handleRemoveSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
maxPageSize={200}
optionsComponent={QueueOptions}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={QueueFilterModal}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
{currentQueue.current}
<RemoveQueueItemModal
isOpen={isConfirmRemoveModalOpen}
selectedCount={selectedCount}
canChangeCategory={
isConfirmRemoveModalOpen &&
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.downloadClientHasPostImportCategory);
})
}
canIgnore={
isConfirmRemoveModalOpen &&
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.movieId);
})
}
isPending={
isConfirmRemoveModalOpen &&
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return (
item.status === 'delay' ||
item.status === 'downloadClientUnavailable'
);
})
}
onRemovePress={handleRemoveSelectedConfirmed}
onModalClose={handleConfirmRemoveModalClose}
/>
</PageContent>
);
}
export default Queue;

View File

@@ -1,185 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import * as queueActions from 'Store/Actions/queueActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Queue from './Queue';
function createMapStateToProps() {
return createSelector(
(state) => state.movies,
(state) => state.queue.options,
(state) => state.queue.paged,
(state) => state.queue.status.item,
createCustomFiltersSelector('queue'),
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
return {
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
customFilters,
isRefreshMonitoredDownloadsExecuting,
...options,
...queue
};
}
);
}
const mapDispatchToProps = {
...queueActions,
executeCommand
};
class QueueConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchQueue,
fetchQueueStatus,
gotoQueueFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchQueue();
} else {
gotoQueueFirstPage();
}
fetchQueueStatus();
}
componentDidUpdate(prevProps) {
if (
this.props.includeUnknownMovieItems !==
prevProps.includeUnknownMovieItems
) {
this.repopulate();
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearQueue();
}
//
// Control
repopulate = () => {
this.props.fetchQueue();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoQueueFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoQueuePreviousPage();
};
onNextPagePress = () => {
this.props.gotoQueueNextPage();
};
onLastPagePress = () => {
this.props.gotoQueueLastPage();
};
onPageSelect = (page) => {
this.props.gotoQueuePage({ page });
};
onSortPress = (sortKey) => {
this.props.setQueueSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setQueueFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => {
this.props.setQueueTableOption(payload);
if (payload.pageSize) {
this.props.gotoQueueFirstPage();
}
};
onRefreshPress = () => {
this.props.executeCommand({
name: commandNames.REFRESH_MONITORED_DOWNLOADS
});
};
onGrabSelectedPress = (ids) => {
this.props.grabQueueItems({ ids });
};
onRemoveSelectedPress = (payload) => {
this.props.removeQueueItems(payload);
};
//
// Render
render() {
return (
<Queue
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onRefreshPress={this.onRefreshPress}
onGrabSelectedPress={this.onGrabSelectedPress}
onRemoveSelectedPress={this.onRemoveSelectedPress}
{...this.props}
/>
);
}
}
QueueConnector.propTypes = {
includeUnknownMovieItems: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQueue: PropTypes.func.isRequired,
fetchQueueStatus: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired,
gotoQueuePreviousPage: PropTypes.func.isRequired,
gotoQueueNextPage: PropTypes.func.isRequired,
gotoQueueLastPage: PropTypes.func.isRequired,
gotoQueuePage: PropTypes.func.isRequired,
setQueueSort: PropTypes.func.isRequired,
setQueueFilter: PropTypes.func.isRequired,
setQueueTableOption: PropTypes.func.isRequired,
clearQueue: PropTypes.func.isRequired,
grabQueueItems: PropTypes.func.isRequired,
removeQueueItems: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
);

View File

@@ -1,36 +1,49 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, tooltipPositions } from 'Helpers/Props';
import {
QueueTrackedDownloadState,
QueueTrackedDownloadStatus,
StatusMessage,
} from 'typings/Queue';
import translate from 'Utilities/String/translate';
import QueueStatus from './QueueStatus';
import styles from './QueueDetails.css';
function QueueDetails(props) {
interface QueueDetailsProps {
title: string;
size: number;
sizeleft: number;
estimatedCompletionTime?: string;
status: string;
trackedDownloadState?: QueueTrackedDownloadState;
trackedDownloadStatus?: QueueTrackedDownloadStatus;
statusMessages?: StatusMessage[];
errorMessage?: string;
progressBar: React.ReactNode;
}
function QueueDetails(props: QueueDetailsProps) {
const {
title,
size,
sizeleft,
status,
trackedDownloadState,
trackedDownloadStatus,
trackedDownloadState = 'downloading',
trackedDownloadStatus = 'ok',
statusMessages,
errorMessage,
progressBar
progressBar,
} = props;
const progress = size ? (100 - sizeleft / size * 100) : 0;
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
const isDownloading = status === 'downloading';
const isPaused = status === 'paused';
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
if (
(isDownloading || isPaused) &&
!hasWarning &&
!hasError
) {
if ((isDownloading || isPaused) && !hasWarning && !hasError) {
const state = isPaused ? translate('Paused') : translate('Downloading');
if (progress < 5) {
@@ -45,11 +58,9 @@ function QueueDetails(props) {
return (
<Popover
className={styles.progressBarContainer}
anchor={progressBar}
anchor={progressBar!}
title={`${state} - ${progress.toFixed(1)}%`}
body={
<div>{title}</div>
}
body={<div>{title}</div>}
position={tooltipPositions.LEFT}
/>
);
@@ -68,22 +79,4 @@ function QueueDetails(props) {
);
}
QueueDetails.propTypes = {
title: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired
};
QueueDetails.defaultProps = {
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading'
};
export default QueueDetails;

View File

@@ -1,78 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class QueueOptions extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
includeUnknownMovieItems: props.includeUnknownMovieItems
};
}
componentDidUpdate(prevProps) {
const {
includeUnknownMovieItems
} = this.props;
if (includeUnknownMovieItems !== prevProps.includeUnknownMovieItems) {
this.setState({
includeUnknownMovieItems
});
}
}
//
// Listeners
onOptionChange = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onOptionChange({
[name]: value
});
});
};
//
// Render
render() {
const {
includeUnknownMovieItems
} = this.state;
return (
<Fragment>
<FormGroup>
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownMovieItems"
value={includeUnknownMovieItems}
helpText={translate('ShowUnknownMovieItemsHelpText')}
onChange={this.onOptionChange}
/>
</FormGroup>
</Fragment>
);
}
}
QueueOptions.propTypes = {
includeUnknownMovieItems: PropTypes.bool.isRequired,
onOptionChange: PropTypes.func.isRequired
};
export default QueueOptions;

View File

@@ -0,0 +1,48 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
function QueueOptions() {
const dispatch = useDispatch();
const { includeUnknownMovieItems } = useSelector(
(state: AppState) => state.queue.options
);
const handleOptionChange = useCallback(
({ name, value }: CheckInputChanged) => {
dispatch(
setQueueOption({
[name]: value,
})
);
if (name === 'includeUnknownMovieItems') {
dispatch(gotoQueuePage({ page: 1 }));
}
},
[dispatch]
);
return (
<FormGroup>
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownMovieItems"
value={includeUnknownMovieItems}
helpText={translate('ShowUnknownMovieItemsHelpText')}
onChange={handleOptionChange}
/>
</FormGroup>
);
}
export default QueueOptions;

View File

@@ -1,19 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setQueueOption } from 'Store/Actions/queueActions';
import QueueOptions from './QueueOptions';
function createMapStateToProps() {
return createSelector(
(state) => state.queue.options,
(options) => {
return options;
}
);
}
const mapDispatchToProps = {
onOptionChange: setQueueOption
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);

View File

@@ -1,434 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
import TimeleftCell from './TimeleftCell';
import styles from './QueueRow.css';
class QueueRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isRemoveQueueItemModalOpen: false,
isInteractiveImportModalOpen: false
};
}
//
// Listeners
onRemoveQueueItemPress = () => {
this.setState({ isRemoveQueueItemModalOpen: true });
};
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist, skipRedownload);
this.setState({ isRemoveQueueItemModalOpen: false });
};
onRemoveQueueItemModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isRemoveQueueItemModalOpen: false });
};
onInteractiveImportPress = () => {
this.props.onQueueRowModalOpenOrClose(true);
this.setState({ isInteractiveImportModalOpen: true });
};
onInteractiveImportModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isInteractiveImportModalOpen: false });
};
//
// Render
render() {
const {
id,
downloadId,
title,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage,
movie,
quality,
customFormats,
customFormatScore,
languages,
protocol,
indexer,
outputPath,
downloadClient,
downloadClientHasPostImportCategory,
estimatedCompletionTime,
added,
timeleft,
size,
sizeleft,
showRelativeDates,
shortDateFormat,
timeFormat,
isGrabbing,
grabError,
isRemoving,
isSelected,
columns,
onSelectedChange,
onGrabPress
} = this.props;
const {
isRemoveQueueItemModalOpen,
isInteractiveImportModalOpen
} = this.state;
const progress = 100 - (sizeleft / size * 100);
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<QueueStatusCell
key={name}
sourceTitle={title}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
/>
);
}
if (name === 'movies.sortTitle') {
return (
<TableRowCell key={name}>
{
movie ?
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/> :
title
}
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
{
quality ?
<MovieQuality
quality={quality}
/> :
null
}
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'protocol') {
return (
<TableRowCell key={name}>
<ProtocolLabel
protocol={protocol}
/>
</TableRowCell>
);
}
if (name === 'indexer') {
return (
<TableRowCell key={name}>
{indexer}
</TableRowCell>
);
}
if (name === 'downloadClient') {
return (
<TableRowCell key={name}>
{downloadClient}
</TableRowCell>
);
}
if (name === 'size') {
return (
<TableRowCell key={name}>
{formatBytes(size)}
</TableRowCell>
);
}
if (name === 'year') {
return (
<TableRowCell key={name}>
{
movie ? movie.year : ''
}
</TableRowCell>
);
}
if (name === 'title') {
return (
<TableRowCell key={name}>
{title}
</TableRowCell>
);
}
if (name === 'outputPath') {
return (
<TableRowCell key={name}>
{outputPath}
</TableRowCell>
);
}
if (name === 'estimatedCompletionTime') {
return (
<TimeleftCell
key={name}
status={status}
estimatedCompletionTime={estimatedCompletionTime}
timeleft={timeleft}
size={size}
sizeleft={sizeleft}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
/>
);
}
if (name === 'progress') {
return (
<TableRowCell
key={name}
className={styles.progress}
>
{
!!progress &&
<ProgressBar
progress={progress}
title={`${progress.toFixed(1)}%`}
/>
}
</TableRowCell>
);
}
if (name === 'added') {
return (
<RelativeDateCell
key={name}
date={added}
/>
);
}
if (name === 'actions') {
return (
<TableRowCell
key={name}
className={styles.actions}
>
{
showInteractiveImport &&
<IconButton
name={icons.INTERACTIVE}
onPress={this.onInteractiveImportPress}
/>
}
{
isPending &&
<SpinnerIconButton
name={icons.DOWNLOAD}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
isSpinning={isGrabbing}
onPress={onGrabPress}
/>
}
<SpinnerIconButton
title={translate('RemoveFromQueue')}
name={icons.REMOVE}
isSpinning={isRemoving}
onPress={this.onRemoveQueueItemPress}
/>
</TableRowCell>
);
}
return null;
})
}
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
downloadId={downloadId}
title={title}
onModalClose={this.onInteractiveImportModalClose}
/>
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canChangeCategory={!!downloadClientHasPostImportCategory}
canIgnore={!!movie}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>
</TableRow>
);
}
}
QueueRow.propTypes = {
id: PropTypes.number.isRequired,
downloadId: PropTypes.string,
title: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string,
trackedDownloadState: PropTypes.string,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
movie: PropTypes.object,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
outputPath: PropTypes.string,
downloadClient: PropTypes.string,
downloadClientHasPostImportCategory: PropTypes.bool,
estimatedCompletionTime: PropTypes.string,
added: PropTypes.string,
timeleft: PropTypes.string,
size: PropTypes.number,
year: PropTypes.number,
sizeleft: PropTypes.number,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isGrabbing: PropTypes.bool.isRequired,
grabError: PropTypes.object,
isRemoving: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired,
onRemoveQueueItemPress: PropTypes.func.isRequired,
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
};
QueueRow.defaultProps = {
customFormats: [],
isGrabbing: false,
isRemoving: false
};
export default QueueRow;

View File

@@ -0,0 +1,361 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import { Error } from 'App/State/AppSectionState';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import useMovie from 'Movie/useMovie';
import { QualityModel } from 'Quality/Quality';
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import CustomFormat from 'typings/CustomFormat';
import { SelectStateInputProps } from 'typings/props';
import {
QueueTrackedDownloadState,
QueueTrackedDownloadStatus,
StatusMessage,
} from 'typings/Queue';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
import TimeleftCell from './TimeleftCell';
import styles from './QueueRow.css';
interface QueueRowProps {
id: number;
movieId?: number;
downloadId?: string;
title: string;
status: string;
trackedDownloadStatus?: QueueTrackedDownloadStatus;
trackedDownloadState?: QueueTrackedDownloadState;
statusMessages?: StatusMessage[];
errorMessage?: string;
languages: Language[];
quality: QualityModel;
customFormats?: CustomFormat[];
customFormatScore: number;
protocol: DownloadProtocol;
indexer?: string;
outputPath?: string;
downloadClient?: string;
downloadClientHasPostImportCategory?: boolean;
estimatedCompletionTime?: string;
added?: string;
timeleft?: string;
size: number;
sizeleft: number;
isGrabbing?: boolean;
grabError?: Error;
isRemoving?: boolean;
isSelected?: boolean;
columns: Column[];
onSelectedChange: (options: SelectStateInputProps) => void;
onQueueRowModalOpenOrClose: (isOpen: boolean) => void;
}
function QueueRow(props: QueueRowProps) {
const {
id,
movieId,
downloadId,
title,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage,
languages,
quality,
customFormats = [],
customFormatScore,
protocol,
indexer,
outputPath,
downloadClient,
downloadClientHasPostImportCategory,
estimatedCompletionTime,
added,
timeleft,
size,
sizeleft,
isGrabbing = false,
grabError,
isRemoving = false,
isSelected,
columns,
onSelectedChange,
onQueueRowModalOpenOrClose,
} = props;
const dispatch = useDispatch();
const movie = useMovie(movieId);
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] =
useState(false);
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
useState(false);
const handleGrabPress = useCallback(() => {
dispatch(grabQueueItem({ id }));
}, [id, dispatch]);
const handleInteractiveImportPress = useCallback(() => {
onQueueRowModalOpenOrClose(true);
setIsInteractiveImportModalOpen(true);
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
const handleInteractiveImportModalClose = useCallback(() => {
onQueueRowModalOpenOrClose(false);
setIsInteractiveImportModalOpen(false);
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
const handleRemoveQueueItemPress = useCallback(() => {
onQueueRowModalOpenOrClose(true);
setIsRemoveQueueItemModalOpen(true);
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
const handleRemoveQueueItemModalConfirmed = useCallback(
(payload: RemovePressProps) => {
onQueueRowModalOpenOrClose(false);
dispatch(removeQueueItem({ id, ...payload }));
setIsRemoveQueueItemModalOpen(false);
},
[id, setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose, dispatch]
);
const handleRemoveQueueItemModalClose = useCallback(() => {
onQueueRowModalOpenOrClose(false);
setIsRemoveQueueItemModalOpen(false);
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
const showInteractiveImport =
status === 'completed' && trackedDownloadStatus === 'warning';
const isPending =
status === 'delay' || status === 'downloadClientUnavailable';
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<QueueStatusCell
key={name}
sourceTitle={title}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
/>
);
}
if (name === 'movies.sortTitle') {
return (
<TableRowCell key={name}>
{movie ? (
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/>
) : (
title
)}
</TableRowCell>
);
}
if (name === 'year') {
return (
<TableRowCell key={name}>{movie ? movie.year : ''}</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages languages={languages} />
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
{quality ? <MovieQuality quality={quality} /> : null}
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats formats={customFormats} />
</TableRowCell>
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell key={name} className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'protocol') {
return (
<TableRowCell key={name}>
<ProtocolLabel protocol={protocol} />
</TableRowCell>
);
}
if (name === 'indexer') {
return <TableRowCell key={name}>{indexer}</TableRowCell>;
}
if (name === 'downloadClient') {
return <TableRowCell key={name}>{downloadClient}</TableRowCell>;
}
if (name === 'title') {
return <TableRowCell key={name}>{title}</TableRowCell>;
}
if (name === 'size') {
return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>;
}
if (name === 'outputPath') {
return <TableRowCell key={name}>{outputPath}</TableRowCell>;
}
if (name === 'estimatedCompletionTime') {
return (
<TimeleftCell
key={name}
status={status}
estimatedCompletionTime={estimatedCompletionTime}
timeleft={timeleft}
size={size}
sizeleft={sizeleft}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
/>
);
}
if (name === 'progress') {
return (
<TableRowCell key={name} className={styles.progress}>
{!!progress && (
<ProgressBar
progress={progress}
title={`${progress.toFixed(1)}%`}
/>
)}
</TableRowCell>
);
}
if (name === 'added') {
return <RelativeDateCell key={name} date={added} />;
}
if (name === 'actions') {
return (
<TableRowCell key={name} className={styles.actions}>
{showInteractiveImport ? (
<IconButton
name={icons.INTERACTIVE}
onPress={handleInteractiveImportPress}
/>
) : null}
{isPending ? (
<SpinnerIconButton
name={icons.DOWNLOAD}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
isSpinning={isGrabbing}
onPress={handleGrabPress}
/>
) : null}
<SpinnerIconButton
title={translate('RemoveFromQueue')}
name={icons.REMOVE}
isSpinning={isRemoving}
onPress={handleRemoveQueueItemPress}
/>
</TableRowCell>
);
}
return null;
})}
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
downloadId={downloadId}
modalTitle={title}
onModalClose={handleInteractiveImportModalClose}
/>
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canChangeCategory={!!downloadClientHasPostImportCategory}
canIgnore={!!movie}
isPending={isPending}
onRemovePress={handleRemoveQueueItemModalConfirmed}
onModalClose={handleRemoveQueueItemModalClose}
/>
</TableRow>
);
}
export default QueueRow;

View File

@@ -1,67 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import QueueRow from './QueueRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
const result = {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
result.movie = movie;
return result;
}
);
}
const mapDispatchToProps = {
grabQueueItem,
removeQueueItem
};
class QueueRowConnector extends Component {
//
// Listeners
onGrabPress = () => {
this.props.grabQueueItem({ id: this.props.id });
};
onRemoveQueueItemPress = (payload) => {
this.props.removeQueueItem({ id: this.props.id, ...payload });
};
//
// Render
render() {
return (
<QueueRow
{...this.props}
onGrabPress={this.onGrabPress}
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
/>
);
}
}
QueueRowConnector.propTypes = {
id: PropTypes.number.isRequired,
movie: PropTypes.object,
grabQueueItem: PropTypes.func.isRequired,
removeQueueItem: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);

View File

@@ -1,51 +1,59 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Icon, { IconProps } from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import { TooltipPosition } from 'Helpers/Props/tooltipPositions';
import {
QueueTrackedDownloadState,
QueueTrackedDownloadStatus,
StatusMessage,
} from 'typings/Queue';
import translate from 'Utilities/String/translate';
import styles from './QueueStatus.css';
function getDetailedPopoverBody(statusMessages) {
function getDetailedPopoverBody(statusMessages: StatusMessage[]) {
return (
<div>
{
statusMessages.map(({ title, messages }) => {
return (
<div
key={title}
className={messages.length ? undefined: styles.noMessages}
>
{title}
<ul>
{
messages.map((message) => {
return (
<li key={message}>
{message}
</li>
);
})
}
</ul>
</div>
);
})
}
{statusMessages.map(({ title, messages }) => {
return (
<div
key={title}
className={messages.length ? undefined : styles.noMessages}
>
{title}
<ul>
{messages.map((message) => {
return <li key={message}>{message}</li>;
})}
</ul>
</div>
);
})}
</div>
);
}
function QueueStatus(props) {
interface QueueStatusProps {
sourceTitle: string;
status: string;
trackedDownloadStatus?: QueueTrackedDownloadStatus;
trackedDownloadState?: QueueTrackedDownloadState;
statusMessages?: StatusMessage[];
errorMessage?: string;
position: TooltipPosition;
canFlip?: boolean;
}
function QueueStatus(props: QueueStatusProps) {
const {
sourceTitle,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
trackedDownloadStatus = 'ok',
trackedDownloadState = 'downloading',
statusMessages = [],
errorMessage,
position,
canFlip
canFlip = false,
} = props;
const hasWarning = trackedDownloadStatus === 'warning';
@@ -53,7 +61,7 @@ function QueueStatus(props) {
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind = kinds.DEFAULT;
let iconKind: IconProps['kind'] = kinds.DEFAULT;
let title = translate('Downloading');
if (status === 'paused') {
@@ -115,7 +123,8 @@ function QueueStatus(props) {
if (status === 'warning') {
iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING;
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
const warningMessage =
errorMessage || translate('CheckDownloadClientForDetails');
title = translate('DownloadWarning', { warningMessage });
}
@@ -133,35 +142,17 @@ function QueueStatus(props) {
return (
<Popover
anchor={
<Icon
name={iconName}
kind={iconKind}
/>
}
anchor={<Icon name={iconName} kind={iconKind} />}
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
body={
hasWarning || hasError
? getDetailedPopoverBody(statusMessages)
: sourceTitle
}
position={position}
canFlip={canFlip}
/>
);
}
QueueStatus.propTypes = {
sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
canFlip: PropTypes.bool.isRequired
};
QueueStatus.defaultProps = {
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading',
canFlip: false
};
export default QueueStatus;

View File

@@ -1,47 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { tooltipPositions } from 'Helpers/Props';
import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css';
function QueueStatusCell(props) {
const {
sourceTitle,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage
} = props;
return (
<TableRowCell className={styles.status}>
<QueueStatus
sourceTitle={sourceTitle}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
position={tooltipPositions.RIGHT}
/>
</TableRowCell>
);
}
QueueStatusCell.propTypes = {
sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string
};
QueueStatusCell.defaultProps = {
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading'
};
export default QueueStatusCell;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import {
QueueTrackedDownloadState,
QueueTrackedDownloadStatus,
StatusMessage,
} from 'typings/Queue';
import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css';
interface QueueStatusCellProps {
sourceTitle: string;
status: string;
trackedDownloadStatus?: QueueTrackedDownloadStatus;
trackedDownloadState?: QueueTrackedDownloadState;
statusMessages?: StatusMessage[];
errorMessage?: string;
}
function QueueStatusCell(props: QueueStatusCellProps) {
const {
sourceTitle,
status,
trackedDownloadStatus = 'ok',
trackedDownloadState = 'downloading',
statusMessages,
errorMessage,
} = props;
return (
<TableRowCell className={styles.status}>
<QueueStatus
sourceTitle={sourceTitle}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
position="right"
/>
</TableRowCell>
);
}
export default QueueStatusCell;

View File

@@ -12,7 +12,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemModal.css';
interface RemovePressProps {
export interface RemovePressProps {
remove: boolean;
changeCategory: boolean;
blocklist: boolean;
@@ -21,7 +21,7 @@ interface RemovePressProps {
interface RemoveQueueItemModalProps {
isOpen: boolean;
sourceTitle: string;
sourceTitle?: string;
canChangeCategory: boolean;
canIgnore: boolean;
isPending: boolean;
@@ -39,7 +39,7 @@ type BlocklistMethod =
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
const {
isOpen,
sourceTitle,
sourceTitle = '',
canIgnore,
canChangeCategory,
isPending,

View File

@@ -0,0 +1,37 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { fetchQueueStatus } from 'Store/Actions/queueActions';
import createQueueStatusSelector from './createQueueStatusSelector';
function QueueStatus() {
const dispatch = useDispatch();
const { isConnected, isReconnecting } = useSelector(
(state: AppState) => state.app
);
const { isPopulated, count, errors, warnings } = useSelector(
createQueueStatusSelector()
);
const wasReconnecting = usePrevious(isReconnecting);
useEffect(() => {
if (!isPopulated) {
dispatch(fetchQueueStatus());
}
}, [isPopulated, dispatch]);
useEffect(() => {
if (isConnected && wasReconnecting) {
dispatch(fetchQueueStatus());
}
}, [isConnected, wasReconnecting, dispatch]);
return (
<PageSidebarStatus count={count} errors={errors} warnings={warnings} />
);
}
export default QueueStatus;

View File

@@ -1,76 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
import { fetchQueueStatus } from 'Store/Actions/queueActions';
function createMapStateToProps() {
return createSelector(
(state) => state.app,
(state) => state.queue.status,
(state) => state.queue.options.includeUnknownMovieItems,
(app, status, includeUnknownMovieItems) => {
const {
errors,
warnings,
unknownErrors,
unknownWarnings,
count,
totalCount
} = status.item;
return {
isConnected: app.isConnected,
isReconnecting: app.isReconnecting,
isPopulated: status.isPopulated,
...status.item,
count: includeUnknownMovieItems ? totalCount : count,
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
warnings: includeUnknownMovieItems ? warnings || unknownWarnings : warnings
};
}
);
}
const mapDispatchToProps = {
fetchQueueStatus
};
class QueueStatusConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.fetchQueueStatus();
}
}
componentDidUpdate(prevProps) {
if (this.props.isConnected && prevProps.isReconnecting) {
this.props.fetchQueueStatus();
}
}
//
// Render
render() {
return (
<PageSidebarStatus
{...this.props}
/>
);
}
}
QueueStatusConnector.propTypes = {
isConnected: PropTypes.bool.isRequired,
isReconnecting: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
fetchQueueStatus: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);

View File

@@ -0,0 +1,32 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createQueueStatusSelector() {
return createSelector(
(state: AppState) => state.queue.status.isPopulated,
(state: AppState) => state.queue.status.item,
(state: AppState) => state.queue.options.includeUnknownMovieItems,
(isPopulated, status, includeUnknownMovieItems) => {
const {
errors,
warnings,
unknownErrors,
unknownWarnings,
count,
totalCount,
} = status;
return {
...status,
isPopulated,
count: includeUnknownMovieItems ? totalCount : count,
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
warnings: includeUnknownMovieItems
? warnings || unknownWarnings
: warnings,
};
}
);
}
export default createQueueStatusSelector;

View File

@@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
@@ -11,7 +10,18 @@ import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './TimeleftCell.css';
function TimeleftCell(props) {
interface TimeleftCellProps {
estimatedCompletionTime?: string;
timeleft?: string;
status: string;
size: number;
sizeleft: number;
showRelativeDates: boolean;
shortDateFormat: string;
timeFormat: string;
}
function TimeleftCell(props: TimeleftCellProps) {
const {
estimatedCompletionTime,
timeleft,
@@ -20,16 +30,18 @@ function TimeleftCell(props) {
sizeleft,
showRelativeDates,
shortDateFormat,
timeFormat
timeFormat,
} = props;
if (status === 'delay') {
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
showRelativeDates,
});
const time = formatTime(estimatedCompletionTime, timeFormat, {
includeMinuteZero: true,
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell className={styles.timeleft}>
@@ -47,9 +59,11 @@ function TimeleftCell(props) {
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
showRelativeDates,
});
const time = formatTime(estimatedCompletionTime, timeFormat, {
includeMinuteZero: true,
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell className={styles.timeleft}>
@@ -64,11 +78,7 @@ function TimeleftCell(props) {
}
if (!timeleft || status === 'completed' || status === 'failed') {
return (
<TableRowCell className={styles.timeleft}>
-
</TableRowCell>
);
return <TableRowCell className={styles.timeleft}>-</TableRowCell>;
}
const totalSize = formatBytes(size);
@@ -84,15 +94,4 @@ function TimeleftCell(props) {
);
}
TimeleftCell.propTypes = {
estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string,
status: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
sizeleft: PropTypes.number.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
export default TimeleftCell;

View File

@@ -131,7 +131,9 @@ class AddNewMovie extends Component {
<div className={styles.helpText}>
{translate('FailedLoadingSearchResults')}
</div>
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
<div>
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
{translate('WhySearchesCouldBeFailing')}

View File

@@ -1,16 +1,19 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import CheckInput from 'Components/Form/CheckInput';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import SpinnerButton from 'Components/Link/SpinnerButton';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate';
import styles from './AddNewMovieModalContent.css';
@@ -115,13 +118,28 @@ class AddNewMovieModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
<FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
onChange={onInputChange}
{...minimumAvailability}
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
/>
</FormGroup>

View File

@@ -91,6 +91,10 @@
margin-left: 5px;
}
.genres {
pointer-events: all;
}
.links {
margin-left: 5px;
pointer-events: all;

View File

@@ -10,6 +10,7 @@ import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import MovieStatusLabel from 'Movie/Details/MovieStatusLabel';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieGenres from 'Movie/MovieGenres';
import MoviePoster from 'Movie/MoviePoster';
import formatRuntime from 'Utilities/Date/formatRuntime';
import translate from 'Utilities/String/translate';
@@ -249,9 +250,7 @@ class AddNewMovieSearchResult extends Component {
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.slice(0, 3).join(', ')}
</span>
<MovieGenres className={styles.genres} genres={genres} />
</Label> :
null
}
@@ -280,7 +279,7 @@ class AddNewMovieSearchResult extends Component {
}
canFlip={true}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
position={tooltipPositions.TOP}
/>
{

View File

@@ -1,8 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import Icon from 'Components/Icon';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import Popover from 'Components/Tooltip/Popover';
import { icons, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieHeader.css';
@@ -46,7 +50,19 @@ function ImportMovieHeader(props) {
className={styles.minimumAvailability}
name="minimumAvailability"
>
{translate('MinAvailability')}
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.detailsIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.LEFT}
/>
</VirtualTableHeaderCell>
<VirtualTableHeaderCell

View File

@@ -81,7 +81,6 @@ ImportMovieRow.propTypes = {
selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
queued: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired

View File

@@ -131,7 +131,7 @@ class ImportMovieSelectMovie extends Component {
id={this._buttonId}
>
<Link
ref={ref}
// ref={ref}
className={styles.button}
component="div"
onPress={this.onPress}
@@ -255,7 +255,7 @@ class ImportMovieSelectMovie extends Component {
items.map((item) => {
return (
<ImportMovieSearchResultConnector
key={item.tvdbId}
key={item.tmdbId}
tmdbId={item.tmdbId}
title={item.title}
year={item.year}

View File

@@ -0,0 +1,27 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate';
function MovieMinimumAvailabilityPopoverContent() {
return (
<DescriptionList>
<DescriptionListItem
title={translate('Announced')}
data={translate('AnnouncedMovieAvailabilityDescription')}
/>
<DescriptionListItem
title={translate('InCinemas')}
data={translate('InCinemasMovieAvailabilityDescription')}
/>
<DescriptionListItem
title={translate('Released')}
data={translate('ReleasedMovieAvailabilityDescription')}
/>
</DescriptionList>
);
}
export default MovieMinimumAvailabilityPopoverContent;

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.Radarr.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,260 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import CollectionConnector from 'Collection/CollectionConnector';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
import MovieIndex from 'Movie/Index/MovieIndex';
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Profiles from 'Settings/Profiles/Profiles';
import QualityConnector from 'Settings/Quality/QualityConnector';
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';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
function AppRoutes(props) {
const {
app
} = props;
return (
<Switch>
{/*
Movies
*/}
<Route
exact={true}
path="/"
component={MovieIndex}
/>
{
window.Radarr.urlBase &&
<Route
exact={true}
path="/"
addUrlBase={false}
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/')}
component={app}
/>
);
}}
/>
}
<Route
path="/add/new"
component={AddNewMovieConnector}
/>
<Route
path="/collections"
component={CollectionConnector}
/>
<Route
path="/add/import"
component={ImportMovies}
/>
<Route
path="/add/discover"
component={DiscoverMovieConnector}
/>
<Route
path="/movie/:titleSlug"
component={MovieDetailsPageConnector}
/>
{/*
Calendar
*/}
<Route
path="/calendar"
component={CalendarPageConnector}
/>
{/*
Activity
*/}
<Route
path="/activity/history"
component={HistoryConnector}
/>
<Route
path="/activity/queue"
component={QueueConnector}
/>
<Route
path="/activity/blocklist"
component={BlocklistConnector}
/>
{/*
Wanted
*/}
<Route
path="/wanted/missing"
component={MissingConnector}
/>
<Route
path="/wanted/cutoffunmet"
component={CutoffUnmetConnector}
/>
{/*
Settings
*/}
<Route
exact={true}
path="/settings"
component={Settings}
/>
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route
path="/settings/profiles"
component={Profiles}
/>
<Route
path="/settings/quality"
component={QualityConnector}
/>
<Route
path="/settings/customformats"
component={CustomFormatSettingsPage}
/>
<Route
path="/settings/indexers"
component={IndexerSettingsConnector}
/>
<Route
path="/settings/downloadclients"
component={DownloadClientSettingsConnector}
/>
<Route
path="/settings/importlists"
component={ImportListSettingsConnector}
/>
<Route
path="/settings/connect"
component={NotificationSettings}
/>
<Route
path="/settings/metadata"
component={MetadataSettings}
/>
<Route
path="/settings/tags"
component={TagSettings}
/>
<Route
path="/settings/general"
component={GeneralSettingsConnector}
/>
<Route
path="/settings/ui"
component={UISettingsConnector}
/>
{/*
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,163 @@
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import Blocklist from 'Activity/Blocklist/Blocklist';
import History from 'Activity/History/History';
import Queue from 'Activity/Queue/Queue';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import CollectionConnector from 'Collection/CollectionConnector';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
import MovieIndex from 'Movie/Index/MovieIndex';
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Profiles from 'Settings/Profiles/Profiles';
import QualityConnector from 'Settings/Quality/QualityConnector';
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';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
function RedirectWithUrlBase() {
return <Redirect to={getPathWithUrlBase('/')} />;
}
function AppRoutes() {
return (
<Switch>
{/*
Movies
*/}
<Route exact={true} path="/" component={MovieIndex} />
{window.Radarr.urlBase && (
<Route
exact={true}
path="/"
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
addUrlBase={false}
render={RedirectWithUrlBase}
/>
)}
<Route path="/add/new" component={AddNewMovieConnector} />
<Route path="/collections" component={CollectionConnector} />
<Route path="/add/import" component={ImportMovies} />
<Route path="/add/discover" component={DiscoverMovieConnector} />
<Route path="/movie/:titleSlug" component={MovieDetailsPageConnector} />
{/*
Calendar
*/}
<Route path="/calendar" component={CalendarPageConnector} />
{/*
Activity
*/}
<Route path="/activity/history" component={History} />
<Route path="/activity/queue" component={Queue} />
<Route path="/activity/blocklist" component={Blocklist} />
{/*
Wanted
*/}
<Route path="/wanted/missing" component={MissingConnector} />
<Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} />
{/*
Settings
*/}
<Route exact={true} path="/settings" component={Settings} />
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route path="/settings/profiles" component={Profiles} />
<Route path="/settings/quality" component={QualityConnector} />
<Route
path="/settings/customformats"
component={CustomFormatSettingsPage}
/>
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
<Route
path="/settings/downloadclients"
component={DownloadClientSettingsConnector}
/>
<Route
path="/settings/importlists"
component={ImportListSettingsConnector}
/>
<Route path="/settings/connect" component={NotificationSettings} />
<Route path="/settings/metadata" component={MetadataSettings} />
<Route path="/settings/tags" component={TagSettings} />
<Route path="/settings/general" component={GeneralSettingsConnector} />
<Route path="/settings/ui" component={UISettingsConnector} />
{/*
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.Radarr.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.Radarr.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,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,10 +1,16 @@
import SortDirection from 'Helpers/Props/SortDirection';
import { FilterBuilderProp } from './AppState';
import Column from 'Components/Table/Column';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { ValidationFailure } from 'typings/pending';
import { FilterBuilderProp, PropertyFilter } from './AppState';
export interface Error {
responseJSON: {
message: string;
};
status?: number;
responseJSON:
| {
message: string | undefined;
}
| ValidationFailure[]
| undefined;
}
export interface AppSectionDeleteState {
@@ -23,8 +29,13 @@ export interface PagedAppSectionState {
totalPages: number;
totalRecords?: number;
}
export interface TableAppSectionState {
columns: Column[];
}
export interface AppSectionFilterState<T> {
selectedFilterKey: string;
filters: PropertyFilter[];
filterBuilderProps: FilterBuilderProp<T>[];
}
@@ -45,6 +56,16 @@ export interface AppSectionItemState<T> {
item: T;
}
export interface AppSectionProviderState<T>
extends AppSectionDeleteState,
AppSectionSaveState {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: T[];
pendingChanges: Partial<T>;
}
interface AppSectionState<T> {
isFetching: boolean;
isPopulated: boolean;

View File

@@ -1,12 +1,14 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import BlocklistAppState from './BlocklistAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import MovieCollectionAppState from './MovieCollectionAppState';
import MovieCreditAppState from './MovieCreditAppState';
import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
import ParseAppState from './ParseAppState';
import PathsAppState from './PathsAppState';
import QueueAppState from './QueueAppState';
import RootFolderAppState from './RootFolderAppState';
import SettingsAppState from './SettingsAppState';
@@ -46,6 +48,10 @@ export interface CustomFilter {
}
export interface AppSectionState {
isConnected: boolean;
isReconnecting: boolean;
version: string;
prevVersion?: string;
dimensions: {
isSmallScreen: boolean;
width: number;
@@ -61,10 +67,12 @@ interface AppState {
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState;
movieCredits: MovieCreditAppState;
movieFiles: MovieFilesAppState;
movieIndex: MovieIndexAppState;
movies: MoviesAppState;
parse: ParseAppState;
paths: PathsAppState;
queue: QueueAppState;
rootFolders: RootFolderAppState;
settings: SettingsAppState;

View File

@@ -1,8 +1,16 @@
import Blocklist from 'typings/Blocklist';
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
import AppSectionState, {
AppSectionFilterState,
PagedAppSectionState,
TableAppSectionState,
} from './AppSectionState';
interface BlocklistAppState
extends AppSectionState<Blocklist>,
AppSectionFilterState<Blocklist> {}
AppSectionFilterState<Blocklist>,
PagedAppSectionState,
TableAppSectionState {
isRemoving: boolean;
}
export default BlocklistAppState;

View File

@@ -1,10 +1,14 @@
import AppSectionState, {
AppSectionFilterState,
PagedAppSectionState,
TableAppSectionState,
} from 'App/State/AppSectionState';
import History from 'typings/History';
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History> {}
AppSectionFilterState<History>,
PagedAppSectionState,
TableAppSectionState {}
export default HistoryAppState;

View File

@@ -1,11 +1,20 @@
import AppSectionState from 'App/State/AppSectionState';
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
import ImportMode from 'InteractiveImport/ImportMode';
import InteractiveImport from 'InteractiveImport/InteractiveImport';
interface FavoriteFolder {
folder: string;
}
interface RecentFolder {
folder: string;
lastUsed: string;
}
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
originalItems: InteractiveImport[];
importMode: ImportMode;
favoriteFolders: FavoriteFolder[];
recentFolders: RecentFolder[];
}

View File

@@ -0,0 +1,6 @@
import { AppSectionProviderState } from 'App/State/AppSectionState';
import Metadata from 'typings/Metadata';
type MetadataAppState = AppSectionProviderState<Metadata>;
export default MetadataAppState;

View File

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

View File

@@ -3,7 +3,7 @@ import AppSectionState, {
AppSectionSaveState,
} from 'App/State/AppSectionState';
import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
import { SortDirection } from 'Helpers/Props/sortDirections';
import Movie from 'Movie/Movie';
import { Filter, FilterBuilderProp } from './AppState';
@@ -27,6 +27,7 @@ export interface MovieIndexAppState {
showTmdbRating: boolean;
showImdbRating: boolean;
showRottenTomatoesRating: boolean;
showTraktRating: boolean;
showTags: boolean;
showSearchAction: boolean;
};

View File

@@ -0,0 +1,29 @@
interface BasePath {
name: string;
path: string;
size: number;
lastModified: string;
}
interface File extends BasePath {
type: 'file';
}
interface Folder extends BasePath {
type: 'folder';
}
export type PathType = 'file' | 'folder' | 'drive' | 'computer' | 'parent';
export type Path = File | Folder;
interface PathsAppState {
currentPath: string;
isFetching: boolean;
isPopulated: boolean;
error: Error;
directories: Folder[];
files: File[];
parent: string | null;
}
export default PathsAppState;

View File

@@ -3,15 +3,29 @@ import AppSectionState, {
AppSectionFilterState,
AppSectionItemState,
Error,
PagedAppSectionState,
TableAppSectionState,
} from './AppSectionState';
export interface QueueStatus {
totalCount: number;
count: number;
unknownCount: number;
errors: boolean;
warnings: boolean;
unknownErrors: boolean;
unknownWarnings: boolean;
}
export interface QueueDetailsAppState extends AppSectionState<Queue> {
params: unknown;
}
export interface QueuePagedAppState
extends AppSectionState<Queue>,
AppSectionFilterState<Queue> {
AppSectionFilterState<Queue>,
PagedAppSectionState,
TableAppSectionState {
isGrabbing: boolean;
grabError: Error;
isRemoving: boolean;
@@ -19,9 +33,12 @@ export interface QueuePagedAppState
}
interface QueueAppState {
status: AppSectionItemState<Queue>;
status: AppSectionItemState<QueueStatus>;
details: QueueDetailsAppState;
paged: QueuePagedAppState;
options: {
includeUnknownMovieItems: boolean;
};
}
export default QueueAppState;

View File

@@ -16,17 +16,29 @@ import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General';
import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import UiSettings from 'typings/Settings/UiSettings';
import MetadataAppState from './MetadataAppState';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {}
AppSectionSaveState {
isTestingAll: boolean;
}
export interface GeneralAppState
extends AppSectionItemState<General>,
AppSectionSaveState {}
export interface NamingAppState
extends AppSectionItemState<NamingConfig>,
AppSectionSaveState {}
export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
@@ -35,7 +47,9 @@ export interface ImportListAppState
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {}
AppSectionSaveState {
isTestingAll: boolean;
}
export interface NotificationAppState
extends AppSectionState<Notification>,
@@ -45,6 +59,12 @@ export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
export interface ReleaseProfilesAppState
extends AppSectionState<ReleaseProfile>,
AppSectionSaveState {
pendingChanges: Partial<ReleaseProfile>;
}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
@@ -77,8 +97,12 @@ interface SettingsAppState {
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
metadata: MetadataAppState;
naming: NamingAppState;
namingExamples: NamingExamplesAppState;
notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState;
releaseProfiles: ReleaseProfilesAppState;
ui: UiSettingsAppState;
}

View File

@@ -1,10 +1,22 @@
import DiskSpace from 'typings/DiskSpace';
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 DiskSpaceAppState = AppSectionState<DiskSpace>;
export type HealthAppState = AppSectionState<Health>;
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type TaskAppState = AppSectionState<Task>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState {
diskSpace: DiskSpaceAppState;
health: HealthAppState;
status: SystemStatusAppState;
tasks: TaskAppState;
updates: UpdateAppState;
}
export default SystemAppState;

View File

@@ -22,7 +22,7 @@ function createMapStateToProps() {
return {
...collection,
movies: [...collection.movies].sort((a, b) => b.year - a.year),
genres: Array.from(new Set(allGenres)).slice(0, 3)
genres: Array.from(new Set(allGenres))
};
}
);

View File

@@ -10,6 +10,7 @@ import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import { icons, sizes } from 'Helpers/Props';
import MovieGenres from 'Movie/MovieGenres';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
@@ -242,12 +243,10 @@ class CollectionOverview extends Component {
size={sizes.MEDIUM}
>
<Icon
name={icons.PROFILE}
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.join(', ')}
</span>
<MovieGenres className={styles.genres} genres={genres} />
</Label>
}

View File

@@ -1,34 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { kinds } from 'Helpers/Props';
import styles from './Alert.css';
function Alert(props) {
const { className, kind, children, ...otherProps } = props;
return (
<div
className={classNames(
className,
styles[kind]
)}
{...otherProps}
>
{children}
</div>
);
}
Alert.propTypes = {
className: PropTypes.string,
kind: PropTypes.oneOf(kinds.all),
children: PropTypes.node.isRequired
};
Alert.defaultProps = {
className: styles.alert,
kind: kinds.INFO
};
export default Alert;

View File

@@ -0,0 +1,18 @@
import classNames from 'classnames';
import React from 'react';
import { Kind } from 'Helpers/Props/kinds';
import styles from './Alert.css';
interface AlertProps {
className?: string;
kind?: Extract<Kind, keyof typeof styles>;
children: React.ReactNode;
}
function Alert(props: AlertProps) {
const { className = styles.alert, kind = 'info', children } = props;
return <div className={classNames(className, styles[kind])}>{children}</div>;
}
export default Alert;

View File

@@ -1,60 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import styles from './Card.css';
class Card extends Component {
//
// Render
render() {
const {
className,
overlayClassName,
overlayContent,
children,
onPress
} = this.props;
if (overlayContent) {
return (
<div className={className}>
<Link
className={styles.underlay}
onPress={onPress}
/>
<div className={overlayClassName}>
{children}
</div>
</div>
);
}
return (
<Link
className={className}
onPress={onPress}
>
{children}
</Link>
);
}
}
Card.propTypes = {
className: PropTypes.string.isRequired,
overlayClassName: PropTypes.string.isRequired,
overlayContent: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onPress: PropTypes.func.isRequired
};
Card.defaultProps = {
className: styles.card,
overlayClassName: styles.overlay,
overlayContent: false
};
export default Card;

View File

@@ -0,0 +1,39 @@
import React from 'react';
import Link, { LinkProps } from 'Components/Link/Link';
import styles from './Card.css';
interface CardProps extends Pick<LinkProps, 'onPress'> {
// TODO: Consider using different properties for classname depending if it's overlaying content or not
className?: string;
overlayClassName?: string;
overlayContent?: boolean;
children: React.ReactNode;
}
function Card(props: CardProps) {
const {
className = styles.card,
overlayClassName = styles.overlay,
overlayContent = false,
children,
onPress,
} = props;
if (overlayContent) {
return (
<div className={className}>
<Link className={styles.underlay} onPress={onPress} />
<div className={overlayClassName}>{children}</div>
</div>
);
}
return (
<Link className={className} onPress={onPress}>
{children}
</Link>
);
}
export default Card;

View File

@@ -1,138 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import styles from './CircularProgressBar.css';
class CircularProgressBar extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
progress: 0
};
}
componentDidMount() {
this._progressStep();
}
componentDidUpdate(prevProps) {
const progress = this.props.progress;
if (prevProps.progress !== progress) {
this._cancelProgressStep();
this._progressStep();
}
}
componentWillUnmount() {
this._cancelProgressStep();
}
//
// Control
_progressStep() {
this.requestAnimationFrame = window.requestAnimationFrame(() => {
this.setState({
progress: this.state.progress + 1
}, () => {
if (this.state.progress < this.props.progress) {
this._progressStep();
}
});
});
}
_cancelProgressStep() {
if (this.requestAnimationFrame) {
window.cancelAnimationFrame(this.requestAnimationFrame);
}
}
//
// Render
render() {
const {
className,
containerClassName,
size,
strokeWidth,
strokeColor,
showProgressText
} = this.props;
const progress = this.state.progress;
const center = size / 2;
const radius = center - strokeWidth;
const circumference = Math.PI * (radius * 2);
const sizeInPixels = `${size}px`;
const strokeDashoffset = ((100 - progress) / 100) * circumference;
const progressText = `${Math.round(progress)}%`;
return (
<div
className={containerClassName}
style={{
width: sizeInPixels,
height: sizeInPixels,
lineHeight: sizeInPixels
}}
>
<svg
className={className}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
>
<circle
fill="transparent"
r={radius}
cx={center}
cy={center}
strokeDasharray={circumference}
style={{
stroke: strokeColor,
strokeWidth,
strokeDashoffset
}}
/>
</svg>
{
showProgressText &&
<div className={styles.circularProgressBarText}>
{progressText}
</div>
}
</div>
);
}
}
CircularProgressBar.propTypes = {
className: PropTypes.string,
containerClassName: PropTypes.string,
size: PropTypes.number,
progress: PropTypes.number.isRequired,
strokeWidth: PropTypes.number,
strokeColor: PropTypes.string,
showProgressText: PropTypes.bool
};
CircularProgressBar.defaultProps = {
className: styles.circularProgressBar,
containerClassName: styles.circularProgressBarContainer,
size: 60,
strokeWidth: 5,
strokeColor: '#ffc230',
showProgressText: false
};
export default CircularProgressBar;

View File

@@ -0,0 +1,99 @@
import React, { useCallback, useEffect, useState } from 'react';
import styles from './CircularProgressBar.css';
interface CircularProgressBarProps {
className?: string;
containerClassName?: string;
size?: number;
progress: number;
strokeWidth?: number;
strokeColor?: string;
showProgressText?: boolean;
}
function CircularProgressBar({
className = styles.circularProgressBar,
containerClassName = styles.circularProgressBarContainer,
size = 60,
strokeWidth = 5,
strokeColor = '#ffc230',
showProgressText = false,
progress,
}: CircularProgressBarProps) {
const [currentProgress, setCurrentProgress] = useState(0);
const raf = React.useRef<number>(0);
const center = size / 2;
const radius = center - strokeWidth;
const circumference = Math.PI * (radius * 2);
const sizeInPixels = `${size}px`;
const strokeDashoffset = ((100 - currentProgress) / 100) * circumference;
const progressText = `${Math.round(currentProgress)}%`;
const handleAnimation = useCallback(
(p: number) => {
setCurrentProgress((prevProgress) => {
if (prevProgress < p) {
return prevProgress + Math.min(1, p - prevProgress);
}
return prevProgress;
});
},
[setCurrentProgress]
);
useEffect(() => {
if (progress > currentProgress) {
cancelAnimationFrame(raf.current);
raf.current = requestAnimationFrame(() => handleAnimation(progress));
}
}, [progress, currentProgress, handleAnimation]);
useEffect(
() => {
return () => cancelAnimationFrame(raf.current);
},
// We only want to run this effect once
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
return (
<div
className={containerClassName}
style={{
width: sizeInPixels,
height: sizeInPixels,
lineHeight: sizeInPixels,
}}
>
<svg
className={className}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
>
<circle
fill="transparent"
r={radius}
cx={center}
cy={center}
strokeDasharray={circumference}
style={{
stroke: strokeColor,
strokeWidth,
strokeDashoffset,
}}
/>
</svg>
{showProgressText && (
<div className={styles.circularProgressBarText}>{progressText}</div>
)}
</div>
);
}
export default CircularProgressBar;

View File

@@ -1,33 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import styles from './DescriptionList.css';
class DescriptionList extends Component {
//
// Render
render() {
const {
className,
children
} = this.props;
return (
<dl className={className}>
{children}
</dl>
);
}
}
DescriptionList.propTypes = {
className: PropTypes.string.isRequired,
children: PropTypes.node
};
DescriptionList.defaultProps = {
className: styles.descriptionList
};
export default DescriptionList;

View File

@@ -0,0 +1,15 @@
import React from 'react';
import styles from './DescriptionList.css';
interface DescriptionListProps {
className?: string;
children?: React.ReactNode;
}
function DescriptionList(props: DescriptionListProps) {
const { className = styles.descriptionList, children } = props;
return <dl className={className}>{children}</dl>;
}
export default DescriptionList;

View File

@@ -1,46 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import DescriptionListItemDescription from './DescriptionListItemDescription';
import DescriptionListItemTitle from './DescriptionListItemTitle';
class DescriptionListItem extends Component {
//
// Render
render() {
const {
className,
titleClassName,
descriptionClassName,
title,
data
} = this.props;
return (
<div className={className}>
<DescriptionListItemTitle
className={titleClassName}
>
{title}
</DescriptionListItemTitle>
<DescriptionListItemDescription
className={descriptionClassName}
>
{data}
</DescriptionListItemDescription>
</div>
);
}
}
DescriptionListItem.propTypes = {
className: PropTypes.string,
titleClassName: PropTypes.string,
descriptionClassName: PropTypes.string,
title: PropTypes.string,
data: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node])
};
export default DescriptionListItem;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import DescriptionListItemDescription, {
DescriptionListItemDescriptionProps,
} from './DescriptionListItemDescription';
import DescriptionListItemTitle, {
DescriptionListItemTitleProps,
} from './DescriptionListItemTitle';
interface DescriptionListItemProps {
className?: string;
titleClassName?: DescriptionListItemTitleProps['className'];
descriptionClassName?: DescriptionListItemDescriptionProps['className'];
title?: DescriptionListItemTitleProps['children'];
data?: DescriptionListItemDescriptionProps['children'];
}
function DescriptionListItem(props: DescriptionListItemProps) {
const { className, titleClassName, descriptionClassName, title, data } =
props;
return (
<div className={className}>
<DescriptionListItemTitle className={titleClassName}>
{title}
</DescriptionListItemTitle>
<DescriptionListItemDescription className={descriptionClassName}>
{data}
</DescriptionListItemDescription>
</div>
);
}
export default DescriptionListItem;

View File

@@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './DescriptionListItemDescription.css';
function DescriptionListItemDescription(props) {
const {
className,
children
} = props;
return (
<dd className={className}>
{children}
</dd>
);
}
DescriptionListItemDescription.propTypes = {
className: PropTypes.string.isRequired,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node])
};
DescriptionListItemDescription.defaultProps = {
className: styles.description
};
export default DescriptionListItemDescription;

View File

@@ -0,0 +1,17 @@
import React, { ReactNode } from 'react';
import styles from './DescriptionListItemDescription.css';
export interface DescriptionListItemDescriptionProps {
className?: string;
children?: ReactNode;
}
function DescriptionListItemDescription(
props: DescriptionListItemDescriptionProps
) {
const { className = styles.description, children } = props;
return <dd className={className}>{children}</dd>;
}
export default DescriptionListItemDescription;

View File

@@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './DescriptionListItemTitle.css';
function DescriptionListItemTitle(props) {
const {
className,
children
} = props;
return (
<dt className={className}>
{children}
</dt>
);
}
DescriptionListItemTitle.propTypes = {
className: PropTypes.string.isRequired,
children: PropTypes.string
};
DescriptionListItemTitle.defaultProps = {
className: styles.title
};
export default DescriptionListItemTitle;

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