1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-26 17:44:24 -04:00

Compare commits

...

84 Commits

Author SHA1 Message Date
Bogdan
653b358fd3 Convert Delete Movie Modal to TypeScript
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-03-08 16:36:12 +02:00
Bogdan
6a7ed22b44 Convert Movie History to TypeScript
Closes #10755

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-03-08 16:04:08 +02:00
Mark McDowall
779292490a Convert SelectMovieRow to TypeScript
(cherry picked from commit 32ce09648cb9eb13c46b060f2665f3ce837261f2)
2025-03-08 15:16:12 +02:00
Mark McDowall
e4e96fc7f9 Convert Preview Rename to TypeScript
(cherry picked from commit a2fd23c84d0a9d01864119d2e643970845c9e49e)
2025-03-08 15:16:12 +02:00
Weblate
049bf7715e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.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/de/
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/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
2025-03-08 15:16:04 +02:00
Bogdan
df4dfaac0b Bump SixLabors.ImageSharp to 3.1.7 2025-03-07 19:17:58 +02:00
Bogdan
89c96b0a80 Increase input sizes in edit movie modal
Closes #10749
2025-03-07 19:03:28 +02:00
Bogdan
7db12b6e58 Convert EditMovieModal to TypeScript
Towards #10700

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-03-07 19:03:28 +02:00
Bogdan
28dee7bc01 Convert MoveMovieModal to TypeScript
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-03-07 19:03:19 +02:00
Bogdan
8ec60eb0a6 Convert Movie Formats/Status/CollectionLabel to TypeScript 2025-03-07 16:43:37 +02:00
Weblate
102849a697 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Chong Yao Jun <yaojun12345678910@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Stan Ulbrych <stanulbrych@gmail.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: pbarone <pbarone@live.com>
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/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/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
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/tr/
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
2025-03-07 14:03:05 +02:00
Bogdan
95da7d7b47 Convert Interactive Search to TypeScript 2025-03-04 15:15:04 +02:00
Weblate
22b5739967 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Eduardo045 <eduardomeirelles045@gmail.com>
Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Volodymyr <minecrafter7893@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: corwin007x <skopal.ondrej@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/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/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translation: Servarr/Radarr
2025-03-04 14:57:54 +02:00
Bogdan
cfba047d80 Fixed: Parsing some titles with FRA as French 2025-03-04 14:46:11 +02:00
Bogdan
576d404e70 Fixed: Replace diacritics in Clean Title naming tokens 2025-03-02 07:19:13 +02:00
Bogdan
5959d4e51a Fixed: Instance name must contain application name 2025-02-26 03:59:22 +02:00
Chaz Harris
2aca6c6e1d Bump devcontainer nodejs version to 20
(cherry picked from commit d8222c066c04d5219a21a6e7f9f3571a67e8dcca)
2025-02-25 20:06:02 +02:00
Bogdan
e8bbe0ee9f Bump Polly to 8.5.2 2025-02-25 19:51:19 +02:00
Bogdan
66332a110a Bump version to 5.20.0 2025-02-25 19:50:48 +02:00
Bogdan
36c66deb4b Recommend against using uTorrent 2025-02-25 12:50:11 +02:00
Weblate
edec432244 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-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/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/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-02-24 19:28:00 +02:00
Bogdan
554e15d438 New: Watch list sorting and rate limit for Trakt Import Lists 2025-02-23 14:59:38 +02:00
Bogdan
553645a07c Bump version to 5.19.2 2025-02-23 12:14:28 +02:00
Bogdan
7de7e83c5b New: Add Blu-ray link to movie details 2025-02-23 00:03:48 +02:00
Bogdan
b7a46bedb0 Fixed: Avoid checking for free space if other specifications fail first 2025-02-22 21:33:38 +02:00
Weblate
0925769377 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Al3xPdx007 <constantin.pdx@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Pablo <pablo@pabloarraiz.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: pelnoph <pierre.regnier.1984@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/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-02-22 18:34:51 +02:00
Servarr
72244362fe Automated API Docs update 2025-02-22 18:33:37 +02:00
Mark McDowall
c6526c34e9 Cleanse console log messages
(cherry picked from commit 609e964794e17343f63e1ecff3fef323e3d284ff)
2025-02-19 15:44:15 +02:00
Bogdan
efa2913dbc Translate Trakt popular list types 2025-02-19 15:28:05 +02:00
Mark McDowall
35c22a4ffa Fixed: Only show Additional Parameters on Trakt Popular list
(cherry picked from commit b122ee967009d53432f3d1dd196132487f3999e1)
2025-02-19 15:15:17 +02:00
Stevie Robinson
66d96e21da Fixed: Fallback to Instance Name for Discord notifications
(cherry picked from commit b99e06acc0a3ecae2857d9225b35424c82c67a2b)
2025-02-19 14:52:30 +02:00
Bogdan
36d4e9e6cd New: Movie Requested filter for interactive search 2025-02-18 04:42:10 +02:00
Weblate
7189d7b15c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: haru4a <haru4as95@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2025-02-16 23:04:54 +02:00
Servarr
6e80113987 Automated API Docs update 2025-02-16 23:03:09 +02:00
Bogdan
bb8a0dda63 Fixed: Processing existing movie files via Manage Files 2025-02-16 21:36:10 +02:00
Bogdan
525ed65687 Fix download links for FileList when passkey contains spaces 2025-02-16 12:19:17 +02:00
Bogdan
3fbccc6af3 Bump version to 5.19.2 2025-02-16 12:19:04 +02:00
Bogdan
8e10eecfac Fixed: Close Metadata settings modal on saving 2025-02-15 13:34:21 +02:00
Bogdan
a3b1512552 Fixed: Parsing some titles with FRE as French and ITA as Italian 2025-02-13 17:31:39 +02:00
epmt7w3ugk
d375b5ffbe Fixed: Parse GER/DE releases as German language
Fix parsing for German language to correctly detect "GER" and "DE"
Update test for GER/DE language parsing.
2025-02-10 17:17:43 +02:00
Bogdan
884abc0368 Bump version to 5.19.1 2025-02-09 17:50:50 +02:00
Weblate
f8da7aae03 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Gionatan Spedicato <natanoig444@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
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/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
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/
Translation: Servarr/Radarr
2025-02-07 19:14:05 -06:00
Connor Gallopo
c165118d4d Update README.md
Update Copyright Date
2025-02-07 14:01:42 -06:00
Robin Dadswell
b3dd571a92 New: Migrated StevenLu URL to new URL 2025-02-06 03:41:33 +02:00
Bogdan
dd900eb739 Building docs on ARM
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
(cherry picked from commit 147e732c9ca7a4c289d4f6386f1277650e11f15b)
2025-02-06 00:43:53 +02:00
Bogdan
66aae0c91c Fixed: Reject multi-part files with P1, P2, etc. 2025-02-04 03:40:06 +02:00
Servarr
d888a0a2b3 Automated API Docs update 2025-02-03 21:31:13 +02:00
Bogdan
cb5416a18c Improve message for unknown movie rejection in release searching 2025-02-03 18:06:48 +02:00
Bogdan
7977e0be05 Add reason enum to decision engine rejections
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2025-02-03 17:46:13 +02:00
Bogdan
cd836fef38 Bump version to 5.19.0 2025-02-03 14:12:21 +02:00
Mark McDowall
b0bfbe767c Add MediaInfo AudioLanguagesAll and update styling
(cherry picked from commit 572b8620c9693f6824ae0919d6a37ba69c7590b1)
2025-02-02 15:16:41 +02:00
Bogdan
528b93dabe Fixed: Format bitrate for primary streams in media info
Co-authored-by: Mark McDowall <markus.mcd5@gmail.com>
2025-02-02 13:58:44 +02:00
Bogdan
1edcbee5e1 Bump version to 5.18.4 2025-02-02 12:49:50 +02:00
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
272 changed files with 6493 additions and 4177 deletions

View File

@@ -6,7 +6,7 @@
"features": {
"ghcr.io/devcontainers/features/node:1": {
"nodeGypDependencies": true,
"version": "16",
"version": "20",
"nvmVersion": "latest"
}
},

View File

@@ -87,4 +87,4 @@ This project is also supported by DigitalOcean
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2024
* Copyright 2010-2025

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.17.2'
majorVersion: '5.20.0'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
@@ -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,7 +1223,7 @@ 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.3.11

15
docs.sh
View File

@@ -1,13 +1,18 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
PLATFORM=$1
ARCHITECTURE="${2:-x64}"
if [ "$PLATFORM" = "Windows" ]; then
RUNTIME="win-x64"
RUNTIME="win-$ARCHITECTURE"
elif [ "$PLATFORM" = "Linux" ]; then
RUNTIME="linux-x64"
RUNTIME="linux-$ARCHITECTURE"
elif [ "$PLATFORM" = "Mac" ]; then
RUNTIME="osx-x64"
RUNTIME="osx-$ARCHITECTURE"
else
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
echo "Platform must be provided as first argument: Windows, Linux or Mac"
exit 1
fi
@@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
dotnet new tool-manifest
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
sleep 45

View File

@@ -41,6 +41,7 @@ function HistoryDetails(props: HistoryDetailsProps) {
indexer,
releaseGroup,
movieMatchType,
releaseSource,
customFormatScore,
nzbInfoUrl,
downloadClient,
@@ -53,6 +54,31 @@ function HistoryDetails(props: HistoryDetailsProps) {
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
@@ -88,6 +114,14 @@ function HistoryDetails(props: HistoryDetailsProps) {
/>
) : null}
{releaseSource ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseSource')}
data={releaseSourceMessage}
/>
) : null}
{nzbInfoUrl ? (
<span>
<DescriptionListItemTitle>

View File

@@ -37,7 +37,7 @@ interface HistoryDetailsModalProps {
sourceTitle: string;
data: HistoryData;
downloadId?: string;
isMarkingAsFailed: boolean;
isMarkingAsFailed?: boolean;
onMarkAsFailedPress: () => void;
onModalClose: () => void;
}

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

@@ -1,15 +1,18 @@
import BlocklistAppState from './BlocklistAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import MovieBlocklistAppState from './MovieBlocklistAppState';
import MovieCollectionAppState from './MovieCollectionAppState';
import MovieCreditAppState from './MovieCreditAppState';
import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
import OrganizePreviewAppState from './OrganizePreviewAppState';
import ParseAppState from './ParseAppState';
import PathsAppState from './PathsAppState';
import QueueAppState from './QueueAppState';
import ReleasesAppState from './ReleasesAppState';
import RootFolderAppState from './RootFolderAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
@@ -66,14 +69,18 @@ interface AppState {
commands: CommandAppState;
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
movieBlocklist: MovieBlocklistAppState;
movieCollections: MovieCollectionAppState;
movieCredits: MovieCreditAppState;
movieFiles: MovieFilesAppState;
movieHistory: MovieHistoryAppState;
movieIndex: MovieIndexAppState;
movies: MoviesAppState;
organizePreview: OrganizePreviewAppState;
parse: ParseAppState;
paths: PathsAppState;
queue: QueueAppState;
releases: ReleasesAppState;
rootFolders: RootFolderAppState;
settings: SettingsAppState;
system: SystemAppState;

View File

@@ -5,6 +5,8 @@ import AppSectionState, {
} from 'App/State/AppSectionState';
import History from 'typings/History';
export type MovieHistoryAppState = AppSectionState<History>;
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History>,

View File

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

View File

@@ -1,7 +1,11 @@
import AppSectionState from 'App/State/AppSectionState';
import AppSectionState, {
AppSectionSaveState,
} from 'App/State/AppSectionState';
import MovieCollection from 'typings/MovieCollection';
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
interface MovieCollectionAppState
extends AppSectionState<MovieCollection>,
AppSectionSaveState {
itemMap: Record<number, number>;
}

View File

@@ -64,6 +64,8 @@ interface MoviesAppState
deleteOptions: {
addImportExclusion: boolean;
};
pendingChanges: Partial<Movie>;
}
export default MoviesAppState;

View File

@@ -0,0 +1,13 @@
import ModelBase from 'App/ModelBase';
import AppSectionState from 'App/State/AppSectionState';
export interface OrganizePreviewModel extends ModelBase {
movieId: number;
movieFileId: number;
existingPath: string;
newPath: string;
}
type OrganizePreviewAppState = AppSectionState<OrganizePreviewModel>;
export default OrganizePreviewAppState;

View File

@@ -0,0 +1,10 @@
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
import Release from 'typings/Release';
interface ReleasesAppState
extends AppSectionState<Release>,
AppSectionFilterState<Release> {}
export default ReleasesAppState;

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate';
@@ -172,7 +172,7 @@ class CollectionMovie extends Component {
collectionId={collectionId}
/>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={id}
onModalClose={this.onEditMovieModalClose}

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { SortDirection } from 'Helpers/Props/sortDirections';
type PropertyFunction<T> = () => T;
@@ -9,6 +10,7 @@ interface Column {
className?: string;
columnLabel?: string;
isSortable?: boolean;
fixedSortDirection?: SortDirection;
isVisible: boolean;
isModifiable?: boolean;
}

View File

@@ -15,7 +15,7 @@ function TraktRating(props: TraktRatingProps) {
const { ratings, iconSize = 14, hideIcon = false } = props;
const traktImage =
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgICAgdmlld0JveD0iMCAwIDE0NC44IDE0NC44IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxNDQuOCAxNDQuOCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+ICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik0yOS41LDExMS44YzEwLjYsMTEuNiwyNS45LDE4LjgsNDIuOSwxOC44YzguNywwLDE2LjktMS45LDI0LjMtNS4zTDU2LjMsODVMMjkuNSwxMTEuOHoiLz4gICAgPHBhdGggZmlsbD0iI0VEMjIyNCIgZD0iTTU2LjEsNjAuNkwyNS41LDkxLjFMMjEuNCw4N2wzMi4yLTMyLjJoMGwzNy42LTM3LjZjLTUuOS0yLTEyLjItMy4xLTE4LjgtMy4xYy0zMi4yLDAtNTguMywyNi4xLTU4LjMsNTguMyAgICAgICBjMCwxMy4xLDQuMywyNS4yLDExLjcsMzVsMzAuNS0zMC41bDIuMSwybDQzLjcsNDMuN2MwLjktMC41LDEuNy0xLDIuNS0xLjZMNTYuMyw3Mi43TDI3LDEwMmwtNC4xLTQuMWwzMy40LTMzLjRsMi4xLDJsNTEsNTAuOSAgICAgICBjMC44LTAuNiwxLjUtMS4zLDIuMi0xLjlsLTU1LTU1TDU2LjEsNjAuNnoiLz4gICAgPHBhdGggZmlsbD0iI0VEMUMyNCIgZD0iTTExNS43LDExMS40YzkuMy0xMC4zLDE1LTI0LDE1LTM5YzAtMjMuNC0xMy44LTQzLjUtMzMuNi01Mi44TDYwLjQsNTYuMkwxMTUuNywxMTEuNHogTTc0LjUsNjYuOGwtNC4xLTQuMSAgICAgICBsMjguOS0yOC45bDQuMSw0LjFMNzQuNSw2Ni44eiBNMTAxLjksMjcuMUw2OC42LDYwLjRsLTQuMS00LjFMOTcuOCwyM0wxMDEuOSwyNy4xeiIvPiAgICA8Zz4gICAgICAgPGc+ICAgICAgICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik03Mi40LDE0NC44QzMyLjUsMTQ0LjgsMCwxMTIuMywwLDcyLjRDMCwzMi41LDMyLjUsMCw3Mi40LDBzNzIuNCwzMi41LDcyLjQsNzIuNCAgICAgICAgICAgICBDMTQ0LjgsMTEyLjMsMTEyLjMsMTQ0LjgsNzIuNCwxNDQuOHogTTcyLjQsNy4zQzM2LjUsNy4zLDcuMywzNi41LDcuMyw3Mi40czI5LjIsNjUuMSw2NS4xLDY1LjFzNjUuMS0yOS4yLDY1LjEtNjUuMSAgICAgICAgICAgICBTMTA4LjMsNy4zLDcyLjQsNy4zeiIvPiAgICAgICA8L2c+ICAgIDwvZz48L2c+PC9zdmc+';
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICM5ZjQyYzY7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogI2ZmZjsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPGcgaWQ9Il94MkRfLXByb2R1Y3Rpb24iPgogICAgPGcgaWQ9ImxvZ29tYXJrLmNpcmNsZS5jb2xvciI+CiAgICAgIDxwYXRoIGlkPSJiYWNrZ3JvdW5kIiBjbGFzcz0iY2xzLTEiIGQ9Ik00OCwyNGMwLDYuNjItMi42OSwxMi42Mi03LjAzLDE2Ljk3LTQuMzQsNC4zNC0xMC4zNCw3LjAzLTE2Ljk3LDcuMDNDMTAuNzUsNDgsMCwzNy4yNSwwLDI0YzAtNi42MywyLjY5LTEyLjYzLDcuMDMtMTYuOTdDMTEuMzcsMi42OCwxNy4zNywwLDI0LDBzMTIuNjMsMi42OCwxNi45Nyw3LjAzYy4xNC4xNC4yNy4yOC40LjQyLjQ4LjUuOTQsMS4wMiwxLjM3LDEuNTYuMjEuMjYuNDEuNTIuNi43OS40My41Ny44MiwxLjE2LDEuMTgsMS43Ni4xOC4yOS4zNS41OC41MS44Ny4zNS42NC42OCwxLjI5Ljk2LDEuOTcsMS4zLDIuOTQsMi4wMSw2LjE4LDIuMDEsOS42WiIvPgogICAgICA8ZyBpZD0iY2hlY2tib3giPgogICAgICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEyLjkzLDE4LjY3bC0xLjQ3LDEuNDYsMTQuNCwxNC40LDEuNDctMS40Ny00LjMyLTQuMzEsMTkuNzMtMTkuNzRjLS40My0uNTQtLjg5LTEuMDYtMS4zNy0xLjU2bC0xOS44MywxOS44My04LjYxLTguNjFaTTI4LjAyLDMyLjM3bDEuNDYtMS40Ni0yLjE1LTIuMTYsMTcuMTktMTcuMTljLS4zNi0uNi0uNzUtMS4xOS0xLjE4LTEuNzZsLTE4Ljk0LDE4Ljk1LDMuNjIsMy42MlpNMzAuMTgsMzAuMjFsMTUuODEtMTUuODFjLS4yOC0uNjgtLjYxLTEuMzMtLjk2LTEuOTdsLTE2LjMyLDE2LjMyLDEuNDcsMS40NlpNMTMuNjIsMTcuOTdsNy45Miw3LjkyLDEuNDctMS40Ny03LjkyLTcuOTItMS40NywxLjQ3Wk0yNS4xNywyMi4yN2wtNy45Mi03LjkyLTEuNDcsMS40Nyw3LjkyLDcuOTIsMS40Ny0xLjQ3Wk0yNCw0MS4zMmMtOS41NSwwLTE3LjMyLTcuNzctMTcuMzItMTcuMzJTMTQuNDUsNi42NywyNCw2LjY3YzIuNiwwLDUuMTEuNTYsNy40NCwxLjY4bC44OS0xLjg3Yy0yLjYxLTEuMjUtNS40Mi0xLjg4LTguMzMtMS44OEMxMy4zMSw0LjYsNC42MSwxMy4zLDQuNjEsMjRzOC43LDE5LjQsMTkuNCwxOS40YzcuNjQsMCwxNC41OS00LjUxLDE3LjcxLTExLjQ4bC0xLjg5LS44NWMtMi43OSw2LjIzLTksMTAuMjYtMTUuODIsMTAuMjZaIi8+CiAgICAgIDwvZz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPg==';
const { value = 0, votes = 0 } = ratings.trakt;

View File

@@ -24,6 +24,7 @@ import {
reprocessInteractiveImportItems,
updateInteractiveImportItem,
} from 'Store/Actions/interactiveImportActions';
import CustomFormat from 'typings/CustomFormat';
import { SelectStateInputProps } from 'typings/props';
import Rejection from 'typings/Rejection';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -52,7 +53,7 @@ interface InteractiveImportRowProps {
quality?: QualityModel;
languages?: Language[];
size: number;
customFormats?: object[];
customFormats?: CustomFormat[];
customFormatScore?: number;
indexerFlags: number;
rejections: Rejection[];

View File

@@ -2,6 +2,7 @@ import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import Movie from 'Movie/Movie';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import Rejection from 'typings/Rejection';
export interface InteractiveImportCommandOptions {
@@ -27,7 +28,7 @@ interface InteractiveImport extends ModelBase {
languages: Language[];
movie?: Movie;
qualityWeight: number;
customFormats: object[];
customFormats: CustomFormat[];
indexerFlags: number;
rejections: Rejection[];
movieFileId?: number;

View File

@@ -65,7 +65,7 @@ interface RowItemData {
}
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
const { items, columns, onMovieSelect } = data;
const { items, onMovieSelect } = data;
const movie = index >= items.length ? null : items[index];
const handlePress = useCallback(() => {
@@ -88,13 +88,11 @@ function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
onPress={handlePress}
>
<SelectMovieRow
id={movie.id}
key={movie.id}
title={movie.title}
tmdbId={movie.tmdbId}
imdbId={movie.imdbId}
year={movie.year}
columns={columns}
onMovieSelect={onMovieSelect}
/>
</VirtualTableRowButton>
);

View File

@@ -1,55 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import styles from './SelectMovieRow.css';
class SelectMovieRow extends Component {
//
// Listeners
onPress = () => {
this.props.onMovieSelect(this.props.id);
};
//
// Render
render() {
return (
<>
<VirtualTableRowCell className={styles.title}>
{this.props.title}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.year}>
{this.props.year}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.imdbId}>
{
this.props.imdbId ?
<Label>{this.props.imdbId}</Label> :
null
}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.tmdbId}>
<Label>{this.props.tmdbId}</Label>
</VirtualTableRowCell>
</>
);
}
}
SelectMovieRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
tmdbId: PropTypes.number.isRequired,
imdbId: PropTypes.string,
year: PropTypes.number.isRequired,
onMovieSelect: PropTypes.func.isRequired
};
export default SelectMovieRow;

View File

@@ -0,0 +1,33 @@
import React from 'react';
import Label from 'Components/Label';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import styles from './SelectMovieRow.css';
interface SelectMovieRowProps {
title: string;
tmdbId: number;
imdbId?: string;
year: number;
}
function SelectMovieRow({ title, year, tmdbId, imdbId }: SelectMovieRowProps) {
return (
<>
<VirtualTableRowCell className={styles.title}>
{title}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.year}>{year}</VirtualTableRowCell>
<VirtualTableRowCell className={styles.imdbId}>
{imdbId ? <Label>{imdbId}</Label> : null}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.tmdbId}>
<Label>{tmdbId}</Label>
</VirtualTableRowCell>
</>
);
}
export default SelectMovieRow;

View File

@@ -1,240 +0,0 @@
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearch.css';
const columns = [
{
name: 'protocol',
label: () => translate('Source'),
isSortable: true,
isVisible: true
},
{
name: 'age',
label: () => translate('Age'),
isSortable: true,
isVisible: true
},
{
name: 'title',
label: () => translate('Title'),
isSortable: true,
isVisible: true
},
{
name: 'indexer',
label: () => translate('Indexer'),
isSortable: true,
isVisible: true
},
{
name: 'history',
label: () => translate('History'),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'size',
label: () => translate('Size'),
isSortable: true,
isVisible: true
},
{
name: 'peers',
label: () => translate('Peers'),
isSortable: true,
isVisible: true
},
{
name: 'languages',
label: () => translate('Language'),
isSortable: true,
isVisible: true
},
{
name: 'qualityWeight',
label: () => translate('Quality'),
isSortable: true,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
}
];
function InteractiveSearch(props) {
const {
searchPayload,
isFetching,
isPopulated,
error,
totalReleasesCount,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
longDateFormat,
timeFormat,
onSortPress,
onFilterSelect,
onGrabPress
} = props;
const errorMessage = getErrorMessage(error);
const type = 'movies';
return (
<div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
filterModalConnectorComponentProps={{ type }}
onFilterSelect={onFilterSelect}
/>
</div>
{
isFetching ? <LoadingIndicator /> : null
}
{
!isFetching && error ?
<Alert kind={kinds.DANGER} className={styles.alert}>
{
errorMessage ?
<Fragment>
{translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
</Fragment> :
translate('MovieSearchResultsLoadError')
}
</Alert> :
null
}
{
!isFetching && isPopulated && !totalReleasesCount ?
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('NoResultsFound')}
</Alert> :
null
}
{
!!totalReleasesCount && isPopulated && !items.length ?
<Alert kind={kinds.WARNING} className={styles.alert}>
{translate('AllResultsHiddenFilter')}
</Alert> :
null
}
{
isPopulated && !!items.length ?
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{
items.map((item) => {
return (
<InteractiveSearchRowConnector
key={`${item.indexerId}-${item.guid}`}
{...item}
searchPayload={searchPayload}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
onGrabPress={onGrabPress}
/>
);
})
}
</TableBody>
</Table> :
null
}
{
totalReleasesCount !== items.length && !!items.length ?
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('SomeResultsHiddenFilter')}
</Alert> :
null
}
</div>
);
}
InteractiveSearch.propTypes = {
searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalReleasesCount: PropTypes.number.isRequired,
items: 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,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
};
export default InteractiveSearch;

View File

@@ -0,0 +1,263 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import ReleasesAppState from 'App/State/ReleasesAppState';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import {
fetchReleases,
grabRelease,
setReleasesFilter,
setReleasesSort,
} from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModal from './InteractiveSearchFilterModal';
import InteractiveSearchPayload from './InteractiveSearchPayload';
import InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveSearch.css';
const columns: Column[] = [
{
name: 'protocol',
label: () => translate('Source'),
isSortable: true,
isVisible: true,
},
{
name: 'age',
label: () => translate('Age'),
isSortable: true,
isVisible: true,
},
{
name: 'title',
label: () => translate('Title'),
isSortable: true,
isVisible: true,
},
{
name: 'indexer',
label: () => translate('Indexer'),
isSortable: true,
isVisible: true,
},
{
name: 'history',
label: () => translate('History'),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true,
},
{
name: 'size',
label: () => translate('Size'),
isSortable: true,
isVisible: true,
},
{
name: 'peers',
label: () => translate('Peers'),
isSortable: true,
isVisible: true,
},
{
name: 'languages',
label: () => translate('Language'),
isSortable: true,
isVisible: true,
},
{
name: 'qualityWeight',
label: () => translate('Quality'),
isSortable: true,
isVisible: true,
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore'),
}),
isSortable: true,
isVisible: true,
},
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags'),
}),
isSortable: true,
isVisible: true,
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections'),
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true,
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true,
},
];
interface InteractiveSearchProps {
searchPayload: InteractiveSearchPayload;
}
function InteractiveSearch({ searchPayload }: InteractiveSearchProps) {
const {
isFetching,
isPopulated,
error,
items,
totalItems,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
}: ReleasesAppState & ClientSideCollectionAppState = useSelector(
createClientSideCollectionSelector('releases')
);
const dispatch = useDispatch();
const handleFilterSelect = useCallback(
(selectedFilterKey: string | number) => {
dispatch(setReleasesFilter({ selectedFilterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string, sortDirection?: SortDirection) => {
dispatch(setReleasesSort({ sortKey, sortDirection }));
},
[dispatch]
);
const handleGrabPress = useCallback(
(payload: object) => {
dispatch(grabRelease(payload));
},
[dispatch]
);
useEffect(
() => {
// Only fetch releases if they are not already being fetched and not yet populated.
if (!isFetching && !isPopulated) {
dispatch(fetchReleases(searchPayload));
const { movieId } = searchPayload;
if (movieId) {
dispatch(fetchMovieBlocklist({ movieId }));
dispatch(fetchMovieHistory({ movieId }));
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const errorMessage = getErrorMessage(error);
return (
<div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModal}
filterModalConnectorComponentProps={{ type: 'movies' }}
onFilterSelect={handleFilterSelect}
/>
</div>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER} className={styles.alert}>
{errorMessage ? (
<>
{translate('InteractiveSearchResultsFailedErrorMessage', {
message:
errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1),
})}
</>
) : (
translate('MovieSearchResultsLoadError')
)}
</Alert>
) : null}
{!isFetching && isPopulated && !totalItems ? (
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('NoResultsFound')}
</Alert>
) : null}
{!!totalItems && isPopulated && !items.length ? (
<Alert kind={kinds.WARNING} className={styles.alert}>
{translate('AllResultsHiddenFilter')}
</Alert>
) : null}
{isPopulated && !!items.length ? (
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<InteractiveSearchRow
key={`${item.indexerId}-${item.guid}`}
{...item}
searchPayload={searchPayload}
onGrabPress={handleGrabPress}
/>
);
})}
</TableBody>
</Table>
) : null}
{totalItems !== items.length && !!items.length ? (
<Alert kind={kinds.INFO} className={styles.alert}>
{translate('SomeResultsHiddenFilter')}
</Alert>
) : null}
</div>
);
}
export default InteractiveSearch;

View File

@@ -1,109 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearch from './InteractiveSearch';
function createMapStateToProps(appState) {
return createSelector(
(state) => state.releases.items.length,
createClientSideCollectionSelector('releases'),
createUISettingsSelector(),
(totalReleasesCount, releases, uiSettings) => {
return {
totalReleasesCount,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat,
...releases
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchReleases(payload) {
dispatch(releaseActions.fetchReleases(payload));
},
dispatchFetchMovieHistory({ movieId }) {
dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
},
onSortPress(sortKey, sortDirection) {
dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection }));
},
onFilterSelect(selectedFilterKey) {
dispatch(releaseActions.setReleasesFilter({ selectedFilterKey }));
},
onGrabPress(payload) {
dispatch(releaseActions.grabRelease(payload));
}
};
}
class InteractiveSearchConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
searchPayload,
isPopulated,
dispatchFetchReleases,
dispatchFetchMovieHistory
} = this.props;
// If search results are not yet isPopulated fetch them,
// otherwise re-show the existing props.
if (!isPopulated) {
dispatchFetchReleases(searchPayload);
}
dispatchFetchMovieHistory(searchPayload);
}
componentWillUnmount() {
this.props.dispatchClearMovieHistory();
}
//
// Render
render() {
const {
dispatchFetchReleases,
dispatchFetchMovieHistory,
dispatchClearMovieHistory,
...otherProps
} = this.props;
return (
<InteractiveSearch
{...otherProps}
/>
);
}
}
InteractiveSearchConnector.propTypes = {
searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired,
dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);

View File

@@ -0,0 +1,55 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setReleasesFilter } from 'Store/Actions/releaseActions';
function createReleasesSelector() {
return createSelector(
(state: AppState) => state.releases.items,
(releases) => {
return releases;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.releases.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface InteractiveSearchFilterModalProps {
isOpen: boolean;
}
export default function InteractiveSearchFilterModal({
...otherProps
}: InteractiveSearchFilterModalProps) {
const sectionItems = useSelector(createReleasesSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setReleasesFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...otherProps}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType="releases"
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -1,29 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setReleasesFilter } from 'Store/Actions/releaseActions';
function createMapStateToProps() {
return createSelector(
(state) => state.releases.items,
(state) => state.releases.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'releases'
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchSetFilter(payload) {
const action = setReleasesFilter;
dispatch(action(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);

View File

@@ -0,0 +1,7 @@
interface MovieSearchPayload {
movieId: number;
}
type InteractiveSearchPayload = MovieSearchPayload;
export default InteractiveSearchPayload;

View File

@@ -1,5 +1,8 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import AppState from 'App/State/AppState';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -8,21 +11,18 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import MovieBlocklist from 'typings/MovieBlocklist';
import MovieHistory from 'typings/MovieHistory';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import Release from 'typings/Release';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import InteractiveSearchPayload from './InteractiveSearchPayload';
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
import Peers from './Peers';
import styles from './InteractiveSearchRow.css';
@@ -71,37 +71,42 @@ function getDownloadTooltip(
return translate('AddToDownloadQueue');
}
interface InteractiveSearchRowProps {
guid: string;
protocol: DownloadProtocol;
age: number;
ageHours: number;
ageMinutes: number;
publishDate: string;
title: string;
infoUrl: string;
indexerId: number;
indexer: string;
size: number;
seeders?: number;
leechers?: number;
quality: QualityModel;
languages: Language[];
customFormats: CustomFormat[];
customFormatScore: number;
mappedMovieId?: number;
indexerFlags: string[];
rejections: string[];
downloadAllowed: boolean;
isGrabbing: boolean;
isGrabbed: boolean;
grabError?: string;
historyFailedData?: MovieHistory;
historyGrabbedData?: MovieHistory;
blocklistData?: MovieBlocklist;
longDateFormat: string;
timeFormat: string;
searchPayload: object;
function releaseHistorySelector({ guid }: Release) {
return createSelector(
(state: AppState) => state.movieHistory.items,
(state: AppState) => state.movieBlocklist.items,
(movieHistory, movieBlocklist) => {
let historyFailedData = null;
let blocklistedData = null;
const historyGrabbedData = movieHistory.find(
({ eventType, data }) =>
eventType === 'grabbed' && 'guid' in data && data.guid === guid
);
if (historyGrabbedData) {
historyFailedData = movieHistory.find(
({ eventType, sourceTitle }) =>
eventType === 'downloadFailed' &&
sourceTitle === historyGrabbedData.sourceTitle
);
blocklistedData = movieBlocklist.find(
(item) => item.sourceTitle === historyGrabbedData.sourceTitle
);
}
return {
historyGrabbedData,
historyFailedData,
blocklistedData,
};
}
);
}
interface InteractiveSearchRowProps extends Release {
searchPayload: InteractiveSearchPayload;
onGrabPress(...args: unknown[]): void;
}
@@ -130,16 +135,18 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
downloadAllowed,
isGrabbing = false,
isGrabbed = false,
longDateFormat,
timeFormat,
grabError,
historyGrabbedData = {} as MovieHistory,
historyFailedData = {} as MovieHistory,
blocklistData = {} as MovieBlocklist,
searchPayload,
onGrabPress,
} = props;
const { longDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const { historyGrabbedData, historyFailedData, blocklistedData } =
useSelector(releaseHistorySelector(props));
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
@@ -211,44 +218,52 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
<TableRowCell className={styles.history}>
{historyGrabbedData?.date && !historyFailedData?.date ? (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DEFAULT}
title={`${translate('Grabbed')}: ${formatDateTime(
historyGrabbedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
<Tooltip
anchor={<Icon name={icons.DOWNLOADING} kind={kinds.DEFAULT} />}
tooltip={translate('GrabbedAt', {
date: formatDateTime(
historyGrabbedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
),
})}
kind={kinds.INVERSE}
position={tooltipPositions.LEFT}
/>
) : null}
{historyFailedData?.date ? (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={`${translate('Failed')}: ${formatDateTime(
historyFailedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
<Tooltip
anchor={<Icon name={icons.DOWNLOADING} kind={kinds.DANGER} />}
tooltip={translate('FailedAt', {
date: formatDateTime(
historyFailedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
),
})}
kind={kinds.INVERSE}
position={tooltipPositions.LEFT}
/>
) : null}
{blocklistData?.date ? (
{blocklistedData?.date ? (
<Icon
className={
historyGrabbedData || historyFailedData ? styles.blocklist : ''
}
name={icons.BLOCKLIST}
kind={kinds.DANGER}
title={`${translate('Blocklisted')}: ${formatDateTime(
blocklistData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
)}`}
title={translate('BlocklistedAt', {
date: formatDateTime(
blocklistedData.date,
longDateFormat,
timeFormat,
{ includeSeconds: true }
),
})}
/>
) : null}
</TableRowCell>

View File

@@ -1,62 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import InteractiveSearchRow from './InteractiveSearchRow';
function createMapStateToProps() {
return createSelector(
(state, { guid }) => guid,
(state) => state.movieHistory.items,
(state) => state.movieBlocklist.items,
(guid, movieHistory, movieBlocklist) => {
let blocklistData = {};
let historyFailedData = {};
const historyGrabbedData = movieHistory.find((movie) => movie.eventType === 'grabbed' && movie.data.guid === guid);
if (historyGrabbedData) {
historyFailedData = movieHistory.find((movie) => movie.eventType === 'downloadFailed' && movie.sourceTitle === historyGrabbedData.sourceTitle);
blocklistData = movieBlocklist.find((item) => item.sourceTitle === historyGrabbedData.sourceTitle);
}
return {
historyGrabbedData,
historyFailedData,
blocklistData
};
}
);
}
class InteractiveSearchRowConnector extends Component {
//
// Render
render() {
const {
historyGrabbedData,
historyFailedData,
blocklistData,
...otherProps
} = this.props;
return (
<InteractiveSearchRow
historyGrabbedData={historyGrabbedData}
historyFailedData={historyFailedData}
blocklistData={blocklistData}
{...otherProps}
/>
);
}
}
InteractiveSearchRowConnector.propTypes = {
historyGrabbedData: PropTypes.object,
historyFailedData: PropTypes.object,
blocklistData: PropTypes.object
};
export default connect(createMapStateToProps)(InteractiveSearchRowConnector);

View File

@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
function getKind(seeders) {
function getKind(seeders: number = 0) {
if (seeders > 50) {
return kinds.PRIMARY;
}
@@ -19,7 +18,7 @@ function getKind(seeders) {
return kinds.DANGER;
}
function getPeersTooltipPart(peers, peersUnit) {
function getPeersTooltipPart(peers: number | undefined, peersUnit: string) {
if (peers == null) {
return `Unknown ${peersUnit}s`;
}
@@ -31,27 +30,27 @@ function getPeersTooltipPart(peers, peersUnit) {
return `${peers} ${peersUnit}s`;
}
function Peers(props) {
const {
seeders,
leechers
} = props;
interface PeersProps {
seeders?: number;
leechers?: number;
}
function Peers(props: PeersProps) {
const { seeders, leechers } = props;
const kind = getKind(seeders);
return (
<Label
kind={kind}
title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(leechers, 'leecher')}`}
title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(
leechers,
'leecher'
)}`}
>
{seeders == null ? '-' : seeders} / {leechers == null ? '-' : leechers}
</Label>
);
}
Peers.propTypes = {
seeders: PropTypes.number,
leechers: PropTypes.number
};
export default Peers;

View File

@@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteMovieModalContentConnector from './DeleteMovieModalContentConnector';
function DeleteMovieModal(props) {
const {
isOpen,
onModalClose,
previousMovie,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={onModalClose}
>
<DeleteMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
previousMovie={previousMovie}
/>
</Modal>
);
}
DeleteMovieModal.propTypes = {
...DeleteMovieModalContentConnector.propTypes,
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired,
previousMovie: PropTypes.string
};
export default DeleteMovieModal;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteMovieModalContent, {
DeleteMovieModalContentProps,
} from './DeleteMovieModalContent';
interface DeleteMovieModalProps extends DeleteMovieModalContentProps {
isOpen: boolean;
}
function DeleteMovieModal({
isOpen,
onModalClose,
...otherProps
}: DeleteMovieModalProps) {
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<DeleteMovieModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default DeleteMovieModal;

View File

@@ -1,163 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
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 { icons, inputTypes, kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './DeleteMovieModalContent.css';
class DeleteMovieModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
};
onDeleteMovieConfirmed = () => {
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = this.props.deleteOptions.addImportExclusion;
this.setState({ deleteFiles: false });
this.props.onDeletePress(deleteFiles, addImportExclusion);
};
//
// Render
render() {
const {
title,
path,
statistics = {},
deleteOptions,
onModalClose,
onDeleteOptionChange
} = this.props;
const {
movieFileCount = 0,
sizeOnDisk = 0
} = statistics;
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = deleteOptions.addImportExclusion;
return (
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
{translate('DeleteHeader', { title })}
</ModalHeader>
<ModalBody>
<div className={styles.pathContainer}>
<Icon
className={styles.pathIcon}
name={icons.FOLDER}
/>
{path}
</div>
<FormGroup>
<FormLabel>
{translate('AddListExclusion')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="addImportExclusion"
value={addImportExclusion}
helpText={translate('AddListExclusionMovieHelpText')}
kind={kinds.DANGER}
onChange={onDeleteOptionChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{movieFileCount === 0 ? translate('DeleteMovieFolder') : translate('DeleteMovieFiles', { movieFileCount })}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="deleteFiles"
value={deleteFiles}
helpText={movieFileCount === 0 ? translate('DeleteMovieFolderHelpText') : translate('DeleteMovieFilesHelpText')}
kind={kinds.DANGER}
onChange={this.onDeleteFilesChange}
/>
</FormGroup>
{
deleteFiles ?
<div className={styles.deleteFilesMessage}>
<div><InlineMarkdown data={translate('DeleteMovieFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div>
{
movieFileCount ?
<div className={styles.deleteCount}>
{translate('DeleteMovieFolderMovieCount', { movieFileCount, size: formatBytes(sizeOnDisk) })}
</div> :
null
}
</div> :
null
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteMovieConfirmed}
>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteMovieModalContent.propTypes = {
title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
hasFile: PropTypes.bool.isRequired,
deleteOptions: PropTypes.object.isRequired,
onDeleteOptionChange: PropTypes.func.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
DeleteMovieModalContent.defaultProps = {
statistics: {}
};
export default DeleteMovieModalContent;

View File

@@ -0,0 +1,149 @@
import React, { useCallback, useState } 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 Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
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 { icons, inputTypes, kinds } from 'Helpers/Props';
import { Statistics } from 'Movie/Movie';
import useMovie from 'Movie/useMovie';
import { deleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
import { CheckInputChanged } from 'typings/inputs';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './DeleteMovieModalContent.css';
export interface DeleteMovieModalContentProps {
movieId: number;
onModalClose: () => void;
}
function DeleteMovieModalContent({
movieId,
onModalClose,
}: DeleteMovieModalContentProps) {
const dispatch = useDispatch();
const {
title,
path,
collection,
statistics = {} as Statistics,
} = useMovie(movieId)!;
const { addImportExclusion } = useSelector(
(state: AppState) => state.movies.deleteOptions
);
const { movieFileCount = 0, sizeOnDisk = 0 } = statistics;
const [deleteFiles, setDeleteFiles] = useState(false);
const handleDeleteFilesChange = useCallback(
({ value }: CheckInputChanged) => {
setDeleteFiles(value);
},
[]
);
const handleDeleteMovieConfirmed = useCallback(() => {
dispatch(
deleteMovie({
id: movieId,
collectionTmdbId: collection?.tmdbId,
deleteFiles,
addImportExclusion,
})
);
}, [movieId, collection, addImportExclusion, deleteFiles, dispatch]);
const handleDeleteOptionChange = useCallback(
({ name, value }: CheckInputChanged) => {
dispatch(setDeleteOption({ [name]: value }));
},
[dispatch]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('DeleteHeader', { title })}</ModalHeader>
<ModalBody>
<div className={styles.pathContainer}>
<Icon className={styles.pathIcon} name={icons.FOLDER} />
{path}
</div>
<FormGroup>
<FormLabel>{translate('AddListExclusion')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="addImportExclusion"
value={addImportExclusion}
helpText={translate('AddListExclusionMovieHelpText')}
kind={kinds.DANGER}
onChange={handleDeleteOptionChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{movieFileCount === 0
? translate('DeleteMovieFolder')
: translate('DeleteMovieFiles', { movieFileCount })}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="deleteFiles"
value={deleteFiles}
helpText={
movieFileCount === 0
? translate('DeleteMovieFolderHelpText')
: translate('DeleteMovieFilesHelpText')
}
kind={kinds.DANGER}
onChange={handleDeleteFilesChange}
/>
</FormGroup>
{deleteFiles ? (
<div className={styles.deleteFilesMessage}>
<div>
<InlineMarkdown
data={translate('DeleteMovieFolderConfirmation', { path })}
blockClassName={styles.folderPath}
/>
</div>
{movieFileCount ? (
<div className={styles.deleteCount}>
{translate('DeleteMovieFolderMovieCount', {
movieFileCount,
size: formatBytes(sizeOnDisk),
})}
</div>
) : null}
</div>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={handleDeleteMovieConfirmed}>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default DeleteMovieModalContent;

View File

@@ -1,45 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import DeleteMovieModalContent from './DeleteMovieModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.movies.deleteOptions,
createMovieSelector(),
(deleteOptions, movie) => {
return {
...movie,
deleteOptions
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onDeleteOptionChange(option) {
dispatch(
setDeleteOption({
[option.name]: option.value
})
);
},
onDeletePress(deleteFiles, addImportExclusion) {
dispatch(
deleteMovie({
id: props.movieId,
collectionTmdbId: this.collection?.tmdbId,
deleteFiles,
addImportExclusion
})
);
props.onModalClose(true);
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteMovieModalContent);

View File

@@ -21,19 +21,19 @@ import TmdbRating from 'Components/TmdbRating';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import TraktRating from 'Components/TraktRating';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import { icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
import MovieCollectionLabel from 'Movie/MovieCollectionLabel';
import MovieGenres from 'Movie/MovieGenres';
import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import OrganizePreviewModal from 'Organize/OrganizePreviewModal';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import fonts from 'Styles/Variables/fonts';
import * as keyCodes from 'Utilities/Constants/keyCodes';
@@ -609,7 +609,7 @@ class MovieDetails extends Component {
size={sizes.LARGE}
>
<div className={styles.collection}>
<MovieCollectionLabelConnector
<MovieCollectionLabel
tmdbId={collection.tmdbId}
/>
</div>
@@ -724,13 +724,13 @@ class MovieDetails extends Component {
</FieldSet>
</div>
<OrganizePreviewModalConnector
<OrganizePreviewModal
isOpen={isOrganizeModalOpen}
movieId={id}
onModalClose={this.onOrganizeModalClose}
/>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={id}
onModalClose={this.onEditMovieModalClose}
@@ -747,17 +747,20 @@ class MovieDetails extends Component {
isOpen={isDeleteMovieModalOpen}
movieId={id}
onModalClose={this.onDeleteMovieModalClose}
nextMovieRelativePath={`/movie/${nextMovie.titleSlug}`}
/>
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
movieId={id}
modalTitle={translate('ManageFiles')}
title={title}
folder={path}
initialSortKey="relativePath"
initialSortDirection={sortDirections.ASCENDING}
showMovie={false}
allowMovieChange={false}
showFilterExistingFiles={true}
showDelete={true}
showImportMode={false}
modalTitle={translate('ManageFiles')}
onModalClose={this.onInteractiveImportModalClose}
/>

View File

@@ -8,11 +8,9 @@ import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearExtraFiles, fetchExtraFiles } from 'Store/Actions/extraFileActions';
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
@@ -188,12 +186,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchClearExtraFiles() {
dispatch(clearExtraFiles());
},
dispatchClearReleases() {
dispatch(clearReleases());
},
dispatchCancelFetchReleases() {
dispatch(cancelFetchReleases());
},
dispatchFetchQueueDetails({ movieId }) {
dispatch(fetchQueueDetails({ movieId }));
},
@@ -211,12 +203,6 @@ function createMapDispatchToProps(dispatch, props) {
},
onGoToMovie(titleSlug) {
dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`));
},
dispatchFetchMovieBlocklist({ movieId }) {
dispatch(fetchMovieBlocklist({ movieId }));
},
dispatchClearMovieBlocklist() {
dispatch(clearMovieBlocklist());
}
};
}
@@ -270,7 +256,6 @@ class MovieDetailsConnector extends Component {
const movieId = this.props.id;
this.props.dispatchFetchMovieFiles({ movieId });
this.props.dispatchFetchMovieBlocklist({ movieId });
this.props.dispatchFetchExtraFiles({ movieId });
this.props.dispatchFetchMovieCredits({ movieId });
this.props.dispatchFetchQueueDetails({ movieId });
@@ -278,13 +263,10 @@ class MovieDetailsConnector extends Component {
};
unpopulate = () => {
this.props.dispatchCancelFetchReleases();
this.props.dispatchClearMovieBlocklist();
this.props.dispatchClearMovieFiles();
this.props.dispatchClearExtraFiles();
this.props.dispatchClearMovieCredits();
this.props.dispatchClearQueueDetails();
this.props.dispatchClearReleases();
};
//
@@ -341,15 +323,11 @@ MovieDetailsConnector.propTypes = {
dispatchClearExtraFiles: PropTypes.func.isRequired,
dispatchFetchMovieCredits: PropTypes.func.isRequired,
dispatchClearMovieCredits: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired,
dispatchCancelFetchReleases: PropTypes.func.isRequired,
dispatchToggleMovieMonitored: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchClearQueueDetails: PropTypes.func.isRequired,
dispatchFetchImportListSchema: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired,
dispatchFetchMovieBlocklist: PropTypes.func.isRequired,
dispatchClearMovieBlocklist: PropTypes.func.isRequired,
onGoToMovie: PropTypes.func.isRequired
};

View File

@@ -92,6 +92,19 @@ function MovieDetailsLinks(props: MovieDetailsLinksProps) {
MDBList
</Label>
</Link>
<Link
className={styles.link}
to={`https://www.blu-ray.com/search/?quicksearch=1&quicksearch_keyword=${imdbId}&section=theatrical`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
Blu-ray
</Label>
</Link>
</>
) : null}

View File

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

View File

@@ -0,0 +1,32 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMovieModalContent, {
EditMovieModalContentProps,
} from './EditMovieModalContent';
interface EditMovieModalProps extends EditMovieModalContentProps {
isOpen: boolean;
}
function EditMovieModal({
isOpen,
onModalClose,
...otherProps
}: EditMovieModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section: 'movies' }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
<EditMovieModalContent {...otherProps} onModalClose={handleModalClose} />
</Modal>
);
}
export default EditMovieModal;

View File

@@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditMovieModal from './EditMovieModal';
const mapDispatchToProps = {
clearPendingChanges
};
class EditMovieModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'movies' });
this.props.onModalClose();
};
//
// Render
render() {
return (
<EditMovieModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditMovieModalConnector.propTypes = {
...EditMovieModal.propTypes,
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(EditMovieModalConnector);

View File

@@ -1,217 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
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 Button from 'Components/Link/Button';
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 Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import translate from 'Utilities/String/translate';
import styles from './EditMovieModalContent.css';
class EditMovieModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isConfirmMoveModalOpen: false
};
}
//
// Listeners
onCancelPress = () => {
this.setState({ isConfirmMoveModalOpen: false });
};
onSavePress = () => {
const {
isPathChanging,
onSavePress
} = this.props;
if (isPathChanging && !this.state.isConfirmMoveModalOpen) {
this.setState({ isConfirmMoveModalOpen: true });
} else {
this.setState({ isConfirmMoveModalOpen: false });
onSavePress(false);
}
};
onMoveMoviePress = () => {
this.setState({ isConfirmMoveModalOpen: false });
this.props.onSavePress(true);
};
//
// Render
render() {
const {
title,
item,
isSaving,
originalPath,
onInputChange,
onModalClose,
onDeleteMoviePress,
...otherProps
} = this.props;
const {
monitored,
qualityProfileId,
minimumAvailability,
// Id,
path,
tags
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Edit')} - {title}
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredHelpText')}
{...monitored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<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"
{...minimumAvailability}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Path')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="path"
{...path}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteMoviePress}
>
{translate('Delete')}
</Button>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerButton
isSpinning={isSaving}
onPress={this.onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
<MoveMovieModal
originalPath={originalPath}
destinationPath={path.value}
isOpen={this.state.isConfirmMoveModalOpen}
onModalClose={this.onCancelPress}
onSavePress={this.onSavePress}
onMoveMoviePress={this.onMoveMoviePress}
/>
</ModalContent>
);
}
}
EditMovieModalContent.propTypes = {
movieId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
item: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,
isPathChanging: PropTypes.bool.isRequired,
originalPath: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteMoviePress: PropTypes.func.isRequired
};
export default EditMovieModalContent;

View File

@@ -0,0 +1,235 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import AppState from 'App/State/AppState';
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 Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import usePrevious from 'Helpers/Hooks/usePrevious';
import {
icons,
inputTypes,
kinds,
sizes,
tooltipPositions,
} from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import useMovie from 'Movie/useMovie';
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
import selectSettings from 'Store/Selectors/selectSettings';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './EditMovieModalContent.css';
export interface EditMovieModalContentProps {
movieId: number;
onModalClose: () => void;
onDeleteMoviePress: () => void;
}
function EditMovieModalContent({
movieId,
onModalClose,
onDeleteMoviePress,
}: EditMovieModalContentProps) {
const dispatch = useDispatch();
const {
title,
monitored,
minimumAvailability,
qualityProfileId,
path,
tags,
} = useMovie(movieId)!;
const { isSaving, saveError, pendingChanges } = useSelector(
(state: AppState) => state.movies
);
const wasSaving = usePrevious(isSaving);
const isPathChanging = pendingChanges.path && path !== pendingChanges.path;
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
const { settings, ...otherSettings } = useMemo(() => {
return selectSettings(
{
monitored,
minimumAvailability,
qualityProfileId,
path,
tags,
},
pendingChanges,
saveError
);
}, [
monitored,
minimumAvailability,
qualityProfileId,
path,
tags,
pendingChanges,
saveError,
]);
const handleInputChange = useCallback(
({ name, value }: InputChanged) => {
// @ts-expect-error actions aren't typed
dispatch(setMovieValue({ name, value }));
},
[dispatch]
);
const handleCancelPress = useCallback(() => {
setIsConfirmMoveModalOpen(false);
}, []);
const handleSavePress = useCallback(() => {
if (isPathChanging && !isConfirmMoveModalOpen) {
setIsConfirmMoveModalOpen(true);
} else {
setIsConfirmMoveModalOpen(false);
dispatch(
saveMovie({
id: movieId,
moveFiles: false,
})
);
}
}, [movieId, isPathChanging, isConfirmMoveModalOpen, dispatch]);
const handleMoveMoviePress = useCallback(() => {
setIsConfirmMoveModalOpen(false);
dispatch(
saveMovie({
id: movieId,
moveFiles: true,
})
);
}, [movieId, dispatch]);
useEffect(() => {
if (!isSaving && wasSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('EditMovieModalHeader', { title })}</ModalHeader>
<ModalBody>
<Form {...otherSettings}>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredMovieHelpText')}
{...settings.monitored}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<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"
{...settings.minimumAvailability}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...settings.qualityProfileId}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('Path')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="path"
{...settings.path}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
{...settings.tags}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteMoviePress}
>
{translate('Delete')}
</Button>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
error={saveError}
isSpinning={isSaving}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
<MoveMovieModal
originalPath={path}
destinationPath={pendingChanges.path}
isOpen={isConfirmMoveModalOpen}
onModalClose={handleCancelPress}
onSavePress={handleSavePress}
onMoveMoviePress={handleMoveMoviePress}
/>
</ModalContent>
);
}
export default EditMovieModalContent;

View File

@@ -1,115 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import EditMovieModalContent from './EditMovieModalContent';
function createIsPathChangingSelector() {
return createSelector(
(state) => state.movies.pendingChanges,
createMovieSelector(),
(pendingChanges, movie) => {
const path = pendingChanges.path;
if (path == null) {
return false;
}
return movie.path !== path;
}
);
}
function createMapStateToProps() {
return createSelector(
(state) => state.movies,
createMovieSelector(),
createIsPathChangingSelector(),
(moviesState, movie, isPathChanging) => {
const {
isSaving,
saveError,
pendingChanges
} = moviesState;
const movieSettings = {
monitored: movie.monitored,
qualityProfileId: movie.qualityProfileId,
minimumAvailability: movie.minimumAvailability,
path: movie.path,
tags: movie.tags
};
const settings = selectSettings(movieSettings, pendingChanges, saveError);
return {
title: movie.title,
isSaving,
saveError,
isPathChanging,
originalPath: movie.path,
item: settings.settings,
...settings
};
}
);
}
const mapDispatchToProps = {
dispatchSetMovieValue: setMovieValue,
dispatchSaveMovie: saveMovie
};
class EditMovieModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetMovieValue({ name, value });
};
onSavePress = (moveFiles) => {
this.props.dispatchSaveMovie({
id: this.props.movieId,
moveFiles
});
};
//
// Render
render() {
return (
<EditMovieModalContent
{...this.props}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
onMoveMoviePress={this.onMoveMoviePress}
/>
);
}
}
EditMovieModalContentConnector.propTypes = {
movieId: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchSetMovieValue: PropTypes.func.isRequired,
dispatchSaveMovie: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditMovieModalContentConnector);

View File

@@ -1,33 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieHistoryModalContentConnector from './MovieHistoryModalContentConnector';
function MovieHistoryModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
size={sizes.EXTRA_EXTRA_LARGE}
>
<MovieHistoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieHistoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModal;

View File

@@ -0,0 +1,28 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieHistoryModalContent, {
MovieHistoryModalContentProps,
} from 'Movie/History/MovieHistoryModalContent';
interface MovieHistoryModalProps extends MovieHistoryModalContentProps {
isOpen: boolean;
}
function MovieHistoryModal({
isOpen,
onModalClose,
...otherProps
}: MovieHistoryModalProps) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_EXTRA_LARGE}
onModalClose={onModalClose}
>
<MovieHistoryModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default MovieHistoryModal;

View File

@@ -1,141 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
class MovieHistoryModalContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress,
onModalClose
} = this.props;
const hasItems = !!items.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('History')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
}
{
isPopulated && !hasItems && !error &&
<div>{translate('NoHistory')}</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
MovieHistoryModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModalContent;

View File

@@ -0,0 +1,148 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import {
clearMovieHistory,
fetchMovieHistory,
movieHistoryMarkAsFailed,
} from 'Store/Actions/movieHistoryActions';
import translate from 'Utilities/String/translate';
import MovieHistoryRow from './MovieHistoryRow';
const columns: Column[] = [
{
name: 'eventType',
label: '',
isVisible: true,
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true,
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true,
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true,
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true,
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore'),
}),
isSortable: true,
isVisible: true,
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true,
},
{
name: 'actions',
label: '',
isVisible: true,
},
];
export interface MovieHistoryModalContentProps {
movieId: number;
onModalClose: () => void;
}
function MovieHistoryModalContent({
movieId,
onModalClose,
}: MovieHistoryModalContentProps) {
const dispatch = useDispatch();
const { isFetching, isPopulated, error, items } = useSelector(
(state: AppState) => state.movieHistory
);
const hasItems = !!items.length;
const handleMarkAsFailedPress = useCallback(
(historyId: number) => {
dispatch(
movieHistoryMarkAsFailed({
historyId,
movieId,
})
);
},
[movieId, dispatch]
);
useEffect(() => {
dispatch(fetchMovieHistory({ movieId }));
return () => {
dispatch(clearMovieHistory());
};
}, [movieId, dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('History')}</ModalHeader>
<ModalBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
) : null}
{isPopulated && !hasItems && !error ? (
<div>{translate('NoHistory')}</div>
) : null}
{isPopulated && hasItems && !error && (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return (
<MovieHistoryRow
key={item.id}
{...item}
onMarkAsFailedPress={handleMarkAsFailedPress}
/>
);
})}
</TableBody>
</Table>
)}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default MovieHistoryModalContent;

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 { clearMovieHistory, fetchMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryModalContent from './MovieHistoryModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.movieHistory,
(movieHistory) => {
return movieHistory;
}
);
}
const mapDispatchToProps = {
fetchMovieHistory,
clearMovieHistory,
movieHistoryMarkAsFailed
};
class MovieHistoryModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
movieId
} = this.props;
this.props.fetchMovieHistory({
movieId
});
}
componentWillUnmount() {
this.props.clearMovieHistory();
}
//
// Listeners
onMarkAsFailedPress = (historyId) => {
const {
movieId
} = this.props;
this.props.movieHistoryMarkAsFailed({
historyId,
movieId
});
};
//
// Render
render() {
return (
<MovieHistoryModalContent
{...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress}
/>
);
}
}
MovieHistoryModalContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
fetchMovieHistory: PropTypes.func.isRequired,
clearMovieHistory: PropTypes.func.isRequired,
movieHistoryMarkAsFailed: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector);

View File

@@ -1,181 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
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 formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './MovieHistoryRow.css';
class MovieHistoryRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isMarkAsFailedModalOpen: false,
isDetailsModalOpen: false
};
}
//
// Listeners
onMarkAsFailedPress = () => {
this.setState({ isMarkAsFailedModalOpen: true });
};
onConfirmMarkAsFailed = () => {
this.props.onMarkAsFailedPress(this.props.id);
this.setState({ isMarkAsFailedModalOpen: false });
};
onMarkAsFailedModalClose = () => {
this.setState({ isMarkAsFailedModalOpen: false });
};
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
//
// Render
render() {
const {
eventType,
sourceTitle,
quality,
customFormats,
customFormatScore,
languages,
qualityCutoffNotMet,
date,
data,
downloadId,
isMarkingAsFailed,
shortDateFormat,
timeFormat
} = this.props;
const {
isMarkAsFailedModalOpen
} = this.state;
return (
<TableRow>
<HistoryEventTypeCell
eventType={eventType}
data={data}
/>
<TableRowCell className={styles.sourceTitle}>
{sourceTitle}
</TableRowCell>
<TableRowCell>
<MovieLanguages
languages={languages}
/>
</TableRowCell>
<TableRowCell>
<MovieQuality
quality={quality}
isCutoffNotMet={qualityCutoffNotMet}
/>
</TableRowCell>
<TableRowCell>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCell
date={date}
includeSeconds={true}
includeTime={true}
/>
<TableRowCell className={styles.actions}>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
{
eventType === 'grabbed' &&
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={isMarkAsFailedModalOpen}
kind={kinds.DANGER}
title={translate('MarkAsFailed')}
message={translate('MarkAsFailedMessageText', [sourceTitle])}
confirmLabel={translate('MarkAsFailed')}
onConfirm={this.onConfirmMarkAsFailed}
onCancel={this.onMarkAsFailedModalClose}
/>
<HistoryDetailsModal
isOpen={this.state.isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
isMarkingAsFailed={isMarkingAsFailed}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
onMarkAsFailedPress={this.onMarkAsFailedPress}
onModalClose={this.onDetailsModalClose}
/>
</TableRow>
);
}
}
MovieHistoryRow.propTypes = {
id: PropTypes.number.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
downloadId: PropTypes.string,
isMarkingAsFailed: PropTypes.bool,
movie: PropTypes.object.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
export default MovieHistoryRow;

View File

@@ -0,0 +1,134 @@
import React, { useCallback, useState } from 'react';
import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import { HistoryData, HistoryEventType } from 'typings/History';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import styles from './MovieHistoryRow.css';
interface MovieHistoryRowProps {
id: number;
eventType: HistoryEventType;
sourceTitle: string;
languages?: Language[];
quality: QualityModel;
qualityCutoffNotMet: boolean;
customFormats?: CustomFormat[];
customFormatScore: number;
date: string;
data: HistoryData;
downloadId?: string;
onMarkAsFailedPress: (historyId: number) => void;
}
function MovieHistoryRow({
id,
eventType,
sourceTitle,
languages = [],
quality,
qualityCutoffNotMet,
customFormats = [],
customFormatScore,
date,
data,
downloadId,
onMarkAsFailedPress,
}: MovieHistoryRowProps) {
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
const [isMarkAsFailedModalOpen, setIsMarkAsFailedModalOpen] = useState(false);
const handleDetailsPress = useCallback(() => {
setIsDetailsModalOpen(true);
}, [setIsDetailsModalOpen]);
const handleDetailsModalClose = useCallback(() => {
setIsDetailsModalOpen(false);
}, [setIsDetailsModalOpen]);
const handleMarkAsFailedPress = useCallback(() => {
setIsMarkAsFailedModalOpen(true);
}, []);
const handleConfirmMarkAsFailed = useCallback(() => {
onMarkAsFailedPress(id);
setIsMarkAsFailedModalOpen(false);
}, [id, onMarkAsFailedPress]);
const handleMarkAsFailedModalClose = useCallback(() => {
setIsMarkAsFailedModalOpen(false);
}, []);
return (
<TableRow>
<HistoryEventTypeCell eventType={eventType} data={data} />
<TableRowCell className={styles.sourceTitle}>{sourceTitle}</TableRowCell>
<TableRowCell>
<MovieLanguages languages={languages} />
</TableRowCell>
<TableRowCell>
<MovieQuality quality={quality} isCutoffNotMet={qualityCutoffNotMet} />
</TableRowCell>
<TableRowCell>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}>
{formatCustomFormatScore(customFormatScore, customFormats.length)}
</TableRowCell>
<RelativeDateCell date={date} includeSeconds={true} includeTime={true} />
<TableRowCell className={styles.actions}>
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
{eventType === 'grabbed' ? (
<IconButton
title={translate('MarkAsFailed')}
name={icons.REMOVE}
size={14}
onPress={handleMarkAsFailedPress}
/>
) : null}
</TableRowCell>
<ConfirmModal
isOpen={isMarkAsFailedModalOpen}
kind={kinds.DANGER}
title={translate('MarkAsFailed')}
message={translate('MarkAsFailedConfirmation', { sourceTitle })}
confirmLabel={translate('MarkAsFailed')}
onConfirm={handleConfirmMarkAsFailed}
onCancel={handleMarkAsFailedModalClose}
/>
<HistoryDetailsModal
isOpen={isDetailsModalOpen}
eventType={eventType}
sourceTitle={sourceTitle}
data={data}
downloadId={downloadId}
onMarkAsFailedPress={handleMarkAsFailedPress}
onModalClose={handleDetailsModalClose}
/>
</TableRow>
);
}
export default MovieHistoryRow;

View File

@@ -1,27 +0,0 @@
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 MovieHistoryRow from './MovieHistoryRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
return {
movie,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
const mapDispatchToProps = {
fetchHistory,
markAsFailed
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryRow);

View File

@@ -11,7 +11,7 @@ import Popover from 'Components/Tooltip/Popover';
import { icons } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
@@ -250,7 +250,7 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
</div>
</div>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={movieId}
onModalClose={onEditMovieModalClose}

View File

@@ -15,7 +15,7 @@ import TraktRating from 'Components/TraktRating';
import { icons } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
@@ -384,7 +384,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
showTags={showTags}
/>
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={movieId}
onModalClose={onEditMovieModalClose}

View File

@@ -18,7 +18,7 @@ import TraktRating from 'Components/TraktRating';
import { icons, kinds } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import EditMovieModal from 'Movie/Edit/EditMovieModal';
import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector';
import { Statistics } from 'Movie/Movie';
import MoviePopularityIndex from 'Movie/MoviePopularityIndex';
@@ -480,7 +480,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
return null;
})}
<EditMovieModalConnector
<EditMovieModal
isOpen={isEditMovieModalOpen}
movieId={movieId}
onModalClose={onEditMovieModalClose}

View File

@@ -3,3 +3,7 @@
margin-right: auto;
}
.folderRenameMessage {
margin-top: 20px;
}

View File

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

View File

@@ -1,93 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
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 { kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './MoveMovieModal.css';
function MoveMovieModal(props) {
const {
originalPath,
destinationPath,
destinationRootFolder,
isOpen,
onModalClose,
onSavePress,
onMoveMoviePress
} = props;
if (
isOpen &&
!originalPath &&
!destinationPath &&
!destinationRootFolder
) {
console.error('orginalPath and destinationPath OR destinationRootFolder must be provided');
}
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<ModalContent
showCloseButton={true}
onModalClose={onModalClose}
>
<ModalHeader>
{translate('MoveFiles')}
</ModalHeader>
<ModalBody>
{
destinationRootFolder ?
translate('MoveFolders1', [destinationRootFolder]) :
translate('MoveFolders2', [originalPath, destinationPath])
}
{
destinationRootFolder ?
<div>
{translate('FolderMoveRenameWarning')}
</div> :
null
}
</ModalBody>
<ModalFooter>
<Button
className={styles.doNotMoveButton}
onPress={onSavePress}
>
{translate('NoMoveFilesSelf')}
</Button>
<Button
kind={kinds.DANGER}
onPress={onMoveMoviePress}
>
{translate('YesMoveFiles')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
MoveMovieModal.propTypes = {
originalPath: PropTypes.string,
destinationPath: PropTypes.string,
destinationRootFolder: PropTypes.string,
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onMoveMoviePress: PropTypes.func.isRequired
};
export default MoveMovieModal;

View File

@@ -0,0 +1,82 @@
import React from 'react';
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 { kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './MoveMovieModal.css';
interface MoveMovieModalProps {
originalPath?: string;
destinationPath?: string;
destinationRootFolder?: string;
isOpen: boolean;
onModalClose: () => void;
onSavePress: () => void;
onMoveMoviePress: () => void;
}
function MoveMovieModal({
originalPath,
destinationPath,
destinationRootFolder,
isOpen,
onModalClose,
onSavePress,
onMoveMoviePress,
}: MoveMovieModalProps) {
if (isOpen && !originalPath && !destinationPath && !destinationRootFolder) {
console.error(
'originalPath and destinationPath OR destinationRootFolder must be provided'
);
}
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<ModalContent showCloseButton={true} onModalClose={onModalClose}>
<ModalHeader>{translate('MoveFiles')}</ModalHeader>
<ModalBody>
{destinationRootFolder
? translate('MoveMovieFoldersToRootFolder', {
destinationRootFolder,
})
: null}
{originalPath && destinationPath
? translate('MoveMovieFoldersToNewPath', {
originalPath,
destinationPath,
})
: null}
{destinationRootFolder ? (
<div className={styles.folderRenameMessage}>
{translate('MoveMovieFoldersRenameFolderWarning')}
</div>
) : null}
</ModalBody>
<ModalFooter>
<Button className={styles.doNotMoveButton} onPress={onSavePress}>
{translate('MoveMovieFoldersDontMoveFiles')}
</Button>
<Button kind={kinds.DANGER} onPress={onMoveMoviePress}>
{translate('MoveMovieFoldersMoveFiles')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default MoveMovieModal;

View File

@@ -18,6 +18,7 @@ export interface Image {
}
export interface Collection {
tmdbId: number;
title: string;
}
@@ -74,11 +75,12 @@ interface Movie extends ModelBase {
ratings: Ratings;
popularity: number;
certification: string;
statistics: Statistics;
statistics?: Statistics;
tags: number[];
images: Image[];
movieFile: MovieFile;
hasFile: boolean;
grabbed?: boolean;
lastSearchTime?: string;
isAvailable: boolean;
isSaving?: boolean;

View File

@@ -1,46 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import styles from './MovieCollectionLabel.css';
class MovieCollectionLabel extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false
};
}
render() {
const {
title,
monitored,
onMonitorTogglePress
} = this.props;
return (
<div>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={15}
onPress={onMonitorTogglePress}
/>
{title}
</div>
);
}
}
MovieCollectionLabel.propTypes = {
title: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
onMonitorTogglePress: PropTypes.func.isRequired
};
export default MovieCollectionLabel;

View File

@@ -0,0 +1,46 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
import { createCollectionSelectorForHook } from 'Store/Selectors/createCollectionSelector';
import MovieCollection from 'typings/MovieCollection';
import styles from './MovieCollectionLabel.css';
interface MovieCollectionLabelProps {
tmdbId: number;
}
function MovieCollectionLabel({ tmdbId }: MovieCollectionLabelProps) {
const {
id,
monitored,
title,
isSaving = false,
} = useSelector(createCollectionSelectorForHook(tmdbId)) as MovieCollection;
const dispatch = useDispatch();
const handleMonitorTogglePress = useCallback(
(value: boolean) => {
dispatch(
toggleCollectionMonitored({ collectionId: id, monitored: value })
);
},
[id, dispatch]
);
return (
<div>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
isSaving={isSaving}
size={15}
onPress={handleMonitorTogglePress}
/>
{title}
</div>
);
}
export default MovieCollectionLabel;

View File

@@ -1,57 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
import MovieCollectionLabel from './MovieCollectionLabel';
function createMapStateToProps() {
return createSelector(
(state, { tmdbId }) => tmdbId,
(state) => state.movieCollections.items,
(tmdbId, collections) => {
const collection = collections.find((movie) => movie.tmdbId === tmdbId);
return {
...collection
};
}
);
}
const mapDispatchToProps = {
toggleCollectionMonitored
};
class MovieCollectionLabelConnector extends Component {
//
// Listeners
onMonitorTogglePress = (monitored) => {
this.props.toggleCollectionMonitored({
collectionId: this.props.id,
monitored
});
};
//
// Render
render() {
return (
<MovieCollectionLabel
{...this.props}
onMonitorTogglePress={this.onMonitorTogglePress}
/>
);
}
}
MovieCollectionLabelConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
id: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
toggleCollectionMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionLabelConnector);

View File

@@ -1,33 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
function MovieFormats({ formats }) {
return (
<div>
{
formats.map((format) => {
return (
<Label
key={format.id}
kind={kinds.INFO}
>
{format.name}
</Label>
);
})
}
</div>
);
}
MovieFormats.propTypes = {
formats: PropTypes.arrayOf(PropTypes.object).isRequired
};
MovieFormats.defaultProps = {
formats: []
};
export default MovieFormats;

View File

@@ -0,0 +1,22 @@
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import CustomFormat from 'typings/CustomFormat';
interface MovieFormatsProps {
formats: CustomFormat[];
}
function MovieFormats({ formats }: MovieFormatsProps) {
return (
<div>
{formats.map(({ id, name }) => (
<Label key={id} kind={kinds.INFO}>
{name}
</Label>
))}
</div>
);
}
export default MovieFormats;

View File

@@ -1,32 +1,40 @@
import PropTypes from 'prop-types';
import React from 'react';
import { useSelector } from 'react-redux';
import QueueDetails from 'Activity/Queue/QueueDetails';
import Icon from 'Components/Icon';
import ProgressBar from 'Components/ProgressBar';
import { icons, kinds, sizes } from 'Helpers/Props';
import Movie from 'Movie/Movie';
import useMovie, { MovieEntity } from 'Movie/useMovie';
import useMovieFile from 'MovieFile/useMovieFile';
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
import translate from 'Utilities/String/translate';
import MovieQuality from './MovieQuality';
import styles from './MovieStatus.css';
function MovieStatus(props) {
interface MovieStatusProps {
movieId: number;
movieEntity?: MovieEntity;
movieFileId: number | undefined;
}
function MovieStatus({ movieId, movieFileId }: MovieStatusProps) {
const {
isAvailable,
monitored,
grabbed,
queueItem,
movieFile
} = props;
grabbed = false,
} = useMovie(movieId) as Movie;
const queueItem = useSelector(createQueueItemSelectorForHook(movieId));
const movieFile = useMovieFile(movieFileId);
const hasMovieFile = !!movieFile;
const isQueued = !!queueItem;
if (isQueued) {
const {
sizeleft,
size
} = queueItem;
const { sizeleft, size } = queueItem;
const progress = size ? (100 - sizeleft / size * 100) : 0;
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
return (
<div className={styles.center}>
@@ -86,30 +94,16 @@ function MovieStatus(props) {
if (isAvailable) {
return (
<div className={styles.center}>
<Icon
name={icons.MISSING}
title={translate('MovieMissingFromDisk')}
/>
<Icon name={icons.MISSING} title={translate('MovieMissingFromDisk')} />
</div>
);
}
return (
<div className={styles.center}>
<Icon
name={icons.NOT_AIRED}
title={translate('MovieIsNotAvailable')}
/>
<Icon name={icons.NOT_AIRED} title={translate('MovieIsNotAvailable')} />
</div>
);
}
MovieStatus.propTypes = {
isAvailable: PropTypes.bool.isRequired,
monitored: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
movieFile: PropTypes.object
};
export default MovieStatus;

View File

@@ -1,50 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieStatus from 'Movie/MovieStatus';
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
import { createMovieByEntitySelector } from 'Store/Selectors/createMovieSelector';
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
function createMapStateToProps() {
return createSelector(
createMovieByEntitySelector(),
createQueueItemSelector(),
createMovieFileSelector(),
(movie, queueItem, movieFile) => {
const result = _.pick(movie, [
'isAvailable',
'monitored',
'grabbed'
]);
result.queueItem = queueItem;
result.movieFile = movieFile;
return result;
}
);
}
class MovieStatusConnector extends Component {
//
// Render
render() {
return (
<MovieStatus
{...this.props}
/>
);
}
}
MovieStatusConnector.propTypes = {
movieId: PropTypes.number.isRequired,
movieFileId: PropTypes.number.isRequired
};
export default connect(createMapStateToProps, null)(MovieStatusConnector);

View File

@@ -2,6 +2,8 @@ import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import { clearMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { clearMovieHistory } from 'Store/Actions/movieHistoryActions';
import {
cancelFetchReleases,
clearReleases,
@@ -24,6 +26,9 @@ function MovieInteractiveSearchModal(props: MovieInteractiveSearchModalProps) {
dispatch(cancelFetchReleases());
dispatch(clearReleases());
dispatch(clearMovieBlocklist());
dispatch(clearMovieHistory());
onModalClose();
}, [dispatch, onModalClose]);

View File

@@ -6,7 +6,9 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { scrollDirections } from 'Helpers/Props';
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
import { clearMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { clearMovieHistory } from 'Store/Actions/movieHistoryActions';
import {
cancelFetchReleases,
clearReleases,
@@ -30,6 +32,9 @@ function MovieInteractiveSearchModalContent(
return () => {
dispatch(cancelFetchReleases());
dispatch(clearReleases());
dispatch(clearMovieBlocklist());
dispatch(clearMovieHistory());
};
}, [dispatch]);
@@ -44,7 +49,7 @@ function MovieInteractiveSearchModalContent(
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
<InteractiveSearchConnector searchPayload={{ movieId }} />
<InteractiveSearch searchPayload={{ movieId }} />
</ModalBody>
<ModalFooter>

View File

@@ -2,6 +2,13 @@ import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export type MovieEntity =
| 'calendar'
| 'movies'
| 'interactiveImport.movies'
| 'wanted.cutoffUnmet'
| 'wanted.missing';
export function createMovieSelector(movieId?: number) {
return createSelector(
(state: AppState) => state.movies.itemMap,
@@ -12,7 +19,7 @@ export function createMovieSelector(movieId?: number) {
);
}
function useMovie(movieId?: number) {
function useMovie(movieId: number | undefined) {
return useSelector(createMovieSelector(movieId));
}

View File

@@ -2,6 +2,7 @@ import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import MediaInfoProps from 'typings/MediaInfo';
import formatBitrate from 'Utilities/Number/formatBitrate';
import getEntries from 'Utilities/Object/getEntries';
function MediaInfo(props: MediaInfoProps) {
@@ -16,9 +17,19 @@ function MediaInfo(props: MediaInfoProps) {
return null;
}
return (
<DescriptionListItem key={key} title={title} data={props[key]} />
);
if (key === 'audioBitrate' || key === 'videoBitrate') {
return (
<DescriptionListItem
key={key}
title={title}
data={
<span title={value.toString()}>{formatBitrate(value)}</span>
}
/>
);
}
return <DescriptionListItem key={key} title={title} data={value} />;
})}
</DescriptionList>
);

View File

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

View File

@@ -0,0 +1,37 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions';
import OrganizePreviewModalContent, {
OrganizePreviewModalContentProps,
} from './OrganizePreviewModalContent';
interface OrganizePreviewModalProps extends OrganizePreviewModalContentProps {
isOpen: boolean;
onModalClose: () => void;
}
function OrganizePreviewModal({
isOpen,
onModalClose,
...otherProps
}: OrganizePreviewModalProps) {
const dispatch = useDispatch();
const handleOnModalClose = useCallback(() => {
dispatch(clearOrganizePreview());
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal isOpen={isOpen} onModalClose={handleOnModalClose}>
{isOpen ? (
<OrganizePreviewModalContent
{...otherProps}
onModalClose={handleOnModalClose}
/>
) : null}
</Modal>
);
}
export default OrganizePreviewModal;

View File

@@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions';
import OrganizePreviewModal from './OrganizePreviewModal';
const mapDispatchToProps = {
clearOrganizePreview
};
class OrganizePreviewModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearOrganizePreview();
this.props.onModalClose();
};
//
// Render
render() {
return (
<OrganizePreviewModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
OrganizePreviewModalConnector.propTypes = {
clearOrganizePreview: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(OrganizePreviewModalConnector);

View File

@@ -1,196 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import CheckInput from 'Components/Form/CheckInput';
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 translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import OrganizePreviewRow from './OrganizePreviewRow';
import styles from './OrganizePreviewModalContent.css';
function getValue(allSelected, allUnselected) {
if (allSelected) {
return true;
} else if (allUnselected) {
return false;
}
return null;
}
class OrganizePreviewModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
};
}
//
// 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);
});
};
onOrganizePress = () => {
this.props.onOrganizePress(this.getSelectedIds());
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
renameMovies,
standardMovieFormat,
path,
onModalClose
} = this.props;
const {
allSelected,
allUnselected,
selectedState
} = this.state;
const selectAllValue = getValue(allSelected, allUnselected);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('OrganizeModalHeader')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
}
{
!isFetching && isPopulated && !items.length &&
<div>
{
renameMovies ?
<div>{translate('OrganizeNothingToRename')}</div> :
<div>{translate('OrganizeRenamingDisabled')}</div>
}
</div>
}
{
!isFetching && isPopulated && !!items.length &&
<div>
<Alert>
<div>
<InlineMarkdown data={translate('OrganizeRelativePaths', { path })} blockClassName={styles.path} />
</div>
<div>
<InlineMarkdown data={translate('OrganizeNamingPattern', { standardMovieFormat })} blockClassName={styles.standardMovieFormat} />
</div>
</Alert>
<div className={styles.previews}>
{
items.map((item) => {
return (
<OrganizePreviewRow
key={item.movieFileId}
id={item.movieFileId}
existingPath={item.existingPath}
newPath={item.newPath}
isSelected={selectedState[item.movieFileId]}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</div>
</div>
}
</ModalBody>
<ModalFooter>
{
isPopulated && !!items.length &&
<CheckInput
className={styles.selectAllInput}
containerClassName={styles.selectAllInputContainer}
name="selectAll"
value={selectAllValue}
onChange={this.onSelectAllChange}
/>
}
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={this.onOrganizePress}
>
{translate('Organize')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
OrganizePreviewModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
path: PropTypes.string.isRequired,
renameMovies: PropTypes.bool,
standardMovieFormat: PropTypes.string,
onOrganizePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default OrganizePreviewModalContent;

View File

@@ -0,0 +1,193 @@
import React, { useCallback, useEffect } 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 CheckInput from 'Components/Form/CheckInput';
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 useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props';
import useMovie from 'Movie/useMovie';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions';
import { fetchNamingSettings } from 'Store/Actions/settingsActions';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import OrganizePreviewRow from './OrganizePreviewRow';
import styles from './OrganizePreviewModalContent.css';
function getValue(allSelected: boolean, allUnselected: boolean) {
if (allSelected) {
return true;
} else if (allUnselected) {
return false;
}
return null;
}
export interface OrganizePreviewModalContentProps {
movieId: number;
onModalClose: () => void;
}
function OrganizePreviewModalContent({
movieId,
onModalClose,
}: OrganizePreviewModalContentProps) {
const dispatch = useDispatch();
const {
items,
isFetching: isPreviewFetching,
isPopulated: isPreviewPopulated,
error: previewError,
} = useSelector((state: AppState) => state.organizePreview);
const {
isFetching: isNamingFetching,
isPopulated: isNamingPopulated,
error: namingError,
item: naming,
} = useSelector((state: AppState) => state.settings.naming);
const movie = useMovie(movieId)!;
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const isFetching = isPreviewFetching || isNamingFetching;
const isPopulated = isPreviewPopulated && isNamingPopulated;
const error = previewError || namingError;
const { renameMovies, standardMovieFormat } = naming;
const selectAllValue = getValue(allSelected, allUnselected);
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 handleOrganizePress = useCallback(() => {
const files = getSelectedIds(selectedState);
dispatch(
executeCommand({
name: commandNames.RENAME_FILES,
files,
movieId,
})
);
onModalClose();
}, [movieId, selectedState, dispatch, onModalClose]);
useEffect(() => {
dispatch(fetchOrganizePreview({ movieId }));
dispatch(fetchNamingSettings());
}, [movieId, dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('OrganizeModalHeader')}</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
) : null}
{!isFetching && isPopulated && !items.length ? (
<div>
{renameMovies ? (
<div>{translate('OrganizeNothingToRename')}</div>
) : (
<div>{translate('OrganizeRenamingDisabled')}</div>
)}
</div>
) : null}
{!isFetching && isPopulated && items.length ? (
<div>
<Alert>
<div>
<InlineMarkdown
data={translate('OrganizeRelativePaths', {
path: movie.path,
})}
blockClassName={styles.path}
/>
</div>
<div>
<InlineMarkdown
data={translate('OrganizeNamingPattern', {
standardMovieFormat,
})}
blockClassName={styles.standardMovieFormat}
/>
</div>
</Alert>
<div className={styles.previews}>
{items.map((item) => {
return (
<OrganizePreviewRow
key={item.movieFileId}
id={item.movieFileId}
existingPath={item.existingPath}
newPath={item.newPath}
isSelected={selectedState[item.movieFileId]}
onSelectedChange={handleSelectedChange}
/>
);
})}
</div>
</div>
) : null}
</ModalBody>
<ModalFooter>
{isPopulated && items.length ? (
<CheckInput
className={styles.selectAllInput}
containerClassName={styles.selectAllInputContainer}
name="selectAll"
value={selectAllValue}
onChange={handleSelectAllChange}
/>
) : null}
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.PRIMARY} onPress={handleOrganizePress}>
{translate('Organize')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default OrganizePreviewModalContent;

View File

@@ -1,88 +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 { executeCommand } from 'Store/Actions/commandActions';
import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions';
import { fetchNamingSettings } from 'Store/Actions/settingsActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import OrganizePreviewModalContent from './OrganizePreviewModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.organizePreview,
(state) => state.settings.naming,
createMovieSelector(),
(organizePreview, naming, movie) => {
const props = { ...organizePreview };
props.isFetching = organizePreview.isFetching || naming.isFetching;
props.isPopulated = organizePreview.isPopulated && naming.isPopulated;
props.error = organizePreview.error || naming.error;
props.renameMovies = naming.item.renameMovies;
props.standardMovieFormat = naming.item.standardMovieFormat;
props.path = movie.path;
return props;
}
);
}
const mapDispatchToProps = {
fetchOrganizePreview,
fetchNamingSettings,
executeCommand
};
class OrganizePreviewModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
movieId
} = this.props;
this.props.fetchOrganizePreview({
movieId
});
this.props.fetchNamingSettings();
}
//
// Listeners
onOrganizePress = (files) => {
this.props.executeCommand({
name: commandNames.RENAME_FILES,
movieId: this.props.movieId,
files
});
this.props.onModalClose();
};
//
// Render
render() {
return (
<OrganizePreviewModalContent
{...this.props}
onOrganizePress={this.onOrganizePress}
/>
);
}
}
OrganizePreviewModalContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
fetchOrganizePreview: PropTypes.func.isRequired,
fetchNamingSettings: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(OrganizePreviewModalContentConnector);

View File

@@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CheckInput from 'Components/Form/CheckInput';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import styles from './OrganizePreviewRow.css';
class OrganizePreviewRow extends Component {
//
// Lifecycle
componentDidMount() {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value: true });
}
//
// Listeners
onSelectedChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
};
//
// Render
render() {
const {
id,
existingPath,
newPath,
isSelected
} = this.props;
return (
<div className={styles.row}>
<CheckInput
containerClassName={styles.selectedContainer}
name={id.toString()}
value={isSelected}
onChange={this.onSelectedChange}
/>
<div>
<div>
<Icon
name={icons.SUBTRACT}
kind={kinds.DANGER}
/>
<span className={styles.path}>
{existingPath}
</span>
</div>
<div>
<Icon
name={icons.ADD}
kind={kinds.SUCCESS}
/>
<span className={styles.path}>
{newPath}
</span>
</div>
</div>
</div>
);
}
}
OrganizePreviewRow.propTypes = {
id: PropTypes.number.isRequired,
existingPath: PropTypes.string.isRequired,
newPath: PropTypes.string.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
export default OrganizePreviewRow;

View File

@@ -0,0 +1,61 @@
import React, { useCallback, useEffect } from 'react';
import CheckInput from 'Components/Form/CheckInput';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import styles from './OrganizePreviewRow.css';
interface OrganizePreviewRowProps {
id: number;
existingPath: string;
newPath: string;
isSelected?: boolean;
onSelectedChange: (props: SelectStateInputProps) => void;
}
function OrganizePreviewRow({
id,
existingPath,
newPath,
isSelected,
onSelectedChange,
}: OrganizePreviewRowProps) {
const handleSelectedChange = useCallback(
({ value, shiftKey }: CheckInputChanged) => {
onSelectedChange({ id, value, shiftKey });
},
[id, onSelectedChange]
);
useEffect(() => {
onSelectedChange({ id, value: true, shiftKey: false });
}, [id, onSelectedChange]);
return (
<div className={styles.row}>
<CheckInput
containerClassName={styles.selectedContainer}
name={id.toString()}
value={isSelected}
onChange={handleSelectedChange}
/>
<div>
<div>
<Icon name={icons.SUBTRACT} kind={kinds.DANGER} />
<span className={styles.path}>{existingPath}</span>
</div>
<div>
<Icon name={icons.ADD} kind={kinds.SUCCESS} />
<span className={styles.path}>{newPath}</span>
</div>
</div>
</div>
);
}
export default OrganizePreviewRow;

View File

@@ -21,8 +21,8 @@
display: flex;
color: var(--helpTextColor);
.icon {
margin-top: 3px;
.identifier {
margin-top: 8px;
margin-right: 5px;
padding: 2px;
}

View File

@@ -3,7 +3,7 @@
interface CssExports {
'footNote': string;
'groups': string;
'icon': string;
'identifier': string;
'namingSelect': string;
'namingSelectContainer': string;
}

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
import FieldSet from 'Components/FieldSet';
import SelectInput from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import Modal from 'Components/Modal/Modal';
@@ -10,7 +9,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { icons, sizes } from 'Helpers/Props';
import { sizes } from 'Helpers/Props';
import NamingConfig from 'typings/Settings/NamingConfig';
import translate from 'Utilities/String/translate';
import NamingOption from './NamingOption';
@@ -88,32 +87,32 @@ const fileNameTokens = [
];
const movieTokens = [
{ token: '{Movie Title}', example: "Movie's Title", footNote: true },
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: true },
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: true },
{ token: '{Movie Title}', example: "Movie's Title", footNotes: '1' },
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNotes: '1' },
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNotes: '1' },
{
token: '{Movie CleanTitle:DE}',
example: 'Titel des Films',
footNote: true,
footNotes: '1',
},
{ token: '{Movie TitleThe}', example: "Movie's Title, The", footNote: true },
{ token: '{Movie TitleThe}', example: "Movie's Title, The", footNotes: '1' },
{
token: '{Movie CleanTitleThe}',
example: 'Movies Title, The',
footNote: true,
footNotes: '1',
},
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: true },
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNotes: '1' },
{
token: '{Movie CleanOriginalTitle}',
example: 'Τίτλος ταινίας',
footNote: true,
footNotes: '1',
},
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
{
token: '{Movie Collection}',
example: 'The Movie Collection',
footNote: true,
footNotes: '1',
},
{ token: '{Movie Certification}', example: 'R' },
{ token: '{Release Year}', example: '2009' },
@@ -131,12 +130,21 @@ const qualityTokens = [
const mediaInfoTokens = [
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: true },
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNotes: '1' },
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: true },
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: true },
{
token: '{MediaInfo AudioLanguages}',
example: '[EN+DE]',
footNotes: '1,2',
},
{
token: '{MediaInfo AudioLanguagesAll}',
example: '[EN]',
footNotes: '1',
},
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNotes: '1' },
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
@@ -146,11 +154,11 @@ const mediaInfoTokens = [
];
const releaseGroupTokens = [
{ token: '{Release Group}', example: 'Rls Grp', footNote: true },
{ token: '{Release Group}', example: 'Rls Grp', footNotes: '1' },
];
const editionTokens = [
{ token: '{Edition Tags}', example: 'IMAX', footNote: true },
{ token: '{Edition Tags}', example: 'IMAX', footNotes: '1' },
];
const customFormatTokens = [
@@ -287,13 +295,13 @@ function NamingModal(props: NamingModalProps) {
<FieldSet legend={translate('Movie')}>
<div className={styles.groups}>
{movieTokens.map(({ token, example, footNote }) => {
{movieTokens.map(({ token, example, footNotes }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
footNote={footNote}
footNotes={footNotes}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
@@ -303,7 +311,7 @@ function NamingModal(props: NamingModalProps) {
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<sup className={styles.identifier}>1</sup>
<InlineMarkdown data={translate('MovieFootNote')} />
</div>
</FieldSet>
@@ -346,13 +354,13 @@ function NamingModal(props: NamingModalProps) {
<FieldSet legend={translate('MediaInfo')}>
<div className={styles.groups}>
{mediaInfoTokens.map(({ token, example, footNote }) => {
{mediaInfoTokens.map(({ token, example, footNotes }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
footNote={footNote}
footNotes={footNotes}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
@@ -362,20 +370,25 @@ function NamingModal(props: NamingModalProps) {
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<sup className={styles.identifier}>1</sup>
<InlineMarkdown data={translate('MediaInfoFootNote')} />
</div>
<div className={styles.footNote}>
<sup className={styles.identifier}>2</sup>
<InlineMarkdown data={translate('MediaInfoFootNote2')} />
</div>
</FieldSet>
<FieldSet legend={translate('ReleaseGroup')}>
<div className={styles.groups}>
{releaseGroupTokens.map(({ token, example, footNote }) => {
{releaseGroupTokens.map(({ token, example, footNotes }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
footNote={footNote}
footNotes={footNotes}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
@@ -385,20 +398,20 @@ function NamingModal(props: NamingModalProps) {
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<sup className={styles.identifier}>1</sup>
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('Edition')}>
<div className={styles.groups}>
{editionTokens.map(({ token, example, footNote }) => {
{editionTokens.map(({ token, example, footNotes }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
footNote={footNote}
footNotes={footNotes}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
@@ -408,7 +421,7 @@ function NamingModal(props: NamingModalProps) {
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<sup className={styles.identifier}>1</sup>
<InlineMarkdown data={translate('EditionFootNote')} />
</div>
</FieldSet>

View File

@@ -40,7 +40,7 @@
padding: 6px;
background-color: var(--popoverBodyBackgroundColor);
.footNote {
.footNotes {
padding: 2px;
color: #aaa;
}

View File

@@ -2,7 +2,7 @@
// Please do not change this file!
interface CssExports {
'example': string;
'footNote': string;
'footNotes': string;
'isFullFilename': string;
'large': string;
'lower': string;

View File

@@ -1,8 +1,6 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import { Size } from 'Helpers/Props/sizes';
import TokenCase from './TokenCase';
import TokenSeparator from './TokenSeparator';
@@ -14,7 +12,7 @@ interface NamingOptionProps {
example: string;
tokenCase: TokenCase;
isFullFilename?: boolean;
footNote?: boolean;
footNotes?: string;
size?: Extract<Size, keyof typeof styles>;
onPress: ({
isFullFilename,
@@ -32,7 +30,7 @@ function NamingOption(props: NamingOptionProps) {
example,
tokenCase,
isFullFilename = false,
footNote = false,
footNotes,
size = 'small',
onPress,
} = props;
@@ -66,8 +64,10 @@ function NamingOption(props: NamingOptionProps) {
<div className={styles.example}>
{example.replace(/ /g, tokenSeparator)}
{footNote ? (
<Icon className={styles.footNote} name={icons.FOOTNOTE} />
{footNotes ? (
<div className={styles.footNotes}>
<sup>{footNotes}</sup>
</div>
) : null}
</div>
</Link>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
@@ -13,6 +13,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { inputTypes } from 'Helpers/Props';
import {
saveMetadata,
@@ -41,6 +42,8 @@ function EditMetadataModalContent({
(state: AppState) => state.settings.metadata
);
const wasSaving = usePrevious(isSaving);
const { settings, ...otherSettings } = useMemo(() => {
const item = items.find((item) => item.id === id)!;
@@ -69,6 +72,12 @@ function EditMetadataModalContent({
dispatch(saveMetadata({ id }));
}, [id, dispatch]);
useEffect(() => {
if (wasSaving && !isSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>

View File

@@ -86,7 +86,7 @@ export const actionHandlers = handleThunks({
section,
...item,
term,
queued: true,
isQueued: true,
items: []
}));
@@ -151,6 +151,8 @@ export const actionHandlers = handleThunks({
abortCurrentLookup = abortRequest;
request.done((data) => {
const selectedMovie = queued.selectedMovie || data[0];
dispatch(updateItem({
section,
id: queued.id,
@@ -158,8 +160,8 @@ export const actionHandlers = handleThunks({
isPopulated: true,
error: null,
items: data,
queued: false,
selectedMovie: queued.selectedMovie || data[0],
isQueued: false,
selectedMovie,
updateOnly: true
}));
});
@@ -171,7 +173,7 @@ export const actionHandlers = handleThunks({
isFetching: false,
isPopulated: false,
error: xhr,
queued: false,
isQueued: false,
updateOnly: true
}));
});
@@ -278,7 +280,23 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({
[CANCEL_LOOKUP_MOVIE]: function(state) {
return Object.assign({}, state, { isLookingUpMovie: false });
queue.splice(0, queue.length);
const items = state.items.map((item) => {
if (item.isQueued) {
return {
...item,
isQueued: false
};
}
return item;
});
return Object.assign({}, state, {
isLookingUpMovie: false,
items
});
},
[CLEAR_IMPORT_MOVIE]: function(state) {

View File

@@ -210,6 +210,12 @@ export const defaultState = {
name: 'rejectionCount',
label: () => translate('RejectionCount'),
type: filterBuilderTypes.NUMBER
},
{
name: 'movieRequested',
label: () => translate('MovieRequested'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
}
],
selectedFilterKey: 'all'

View File

@@ -1,6 +1,15 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export function createCollectionSelectorForHook(tmdbId: number) {
return createSelector(
(state: AppState) => state.movieCollections.items,
(collections) => {
return collections.find((item) => item.tmdbId === tmdbId);
}
);
}
function createCollectionSelector() {
return createSelector(
(_: AppState, { collectionId }: { collectionId: number }) => collectionId,

View File

@@ -1,6 +1,19 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export function createQueueItemSelectorForHook(movieId: number) {
return createSelector(
(state: AppState) => state.queue.details.items,
(details) => {
if (!movieId || !details) {
return null;
}
return details.find((item) => item.movieId === movieId);
}
);
}
function createQueueItemSelector() {
return createSelector(
(_: AppState, { movieId }: { movieId: number }) => movieId,

View File

@@ -0,0 +1,19 @@
import { filesize } from 'filesize';
function formatBitrate(input: string | number) {
const size = Number(input);
if (isNaN(size)) {
return '';
}
const { value, symbol } = filesize(size, {
base: 10,
round: 1,
output: 'object',
});
return `${value} ${symbol}/s`;
}
export default formatBitrate;

View File

@@ -6,7 +6,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import movieEntities from 'Movie/movieEntities';
import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatusConnector from 'Movie/MovieStatusConnector';
import MovieStatus from 'Movie/MovieStatus';
import MovieTitleLink from 'Movie/MovieTitleLink';
import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
import styles from './CutoffUnmetRow.css';
@@ -127,7 +127,7 @@ function CutoffUnmetRow(props) {
key={name}
className={styles.status}
>
<MovieStatusConnector
<MovieStatus
movieId={id}
movieFileId={movieFileId}
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}

View File

@@ -6,7 +6,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import movieEntities from 'Movie/movieEntities';
import MovieSearchCell from 'Movie/MovieSearchCell';
import MovieStatusConnector from 'Movie/MovieStatusConnector';
import MovieStatus from 'Movie/MovieStatus';
import MovieTitleLink from 'Movie/MovieTitleLink';
import styles from './MissingRow.css';
@@ -117,7 +117,7 @@ function MissingRow(props) {
key={name}
className={styles.status}
>
<MovieStatusConnector
<MovieStatus
movieId={id}
movieFileId={movieFileId}
movieEntity={movieEntities.WANTED_MISSING}

View File

@@ -23,12 +23,13 @@ const error = console.error;
function logError(...parameters: any[]) {
const filter = parameters.find((parameter) => {
return (
parameter.includes(
typeof parameter === 'string' &&
(parameter.includes(
'Support for defaultProps will be removed from function components in a future major release'
) ||
parameter.includes(
'findDOMNode is deprecated and will be removed in the next major release'
)
parameter.includes(
'findDOMNode is deprecated and will be removed in the next major release'
))
);
});

View File

@@ -12,6 +12,7 @@ interface MovieCollection extends ModelBase {
movies: Movie[];
missingMovies: number;
tags: number[];
isSaving?: boolean;
}
export default MovieCollection;

View File

@@ -0,0 +1,35 @@
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
interface Release {
guid: string;
protocol: DownloadProtocol;
age: number;
ageHours: number;
ageMinutes: number;
publishDate: string;
title: string;
infoUrl: string;
indexerId: number;
indexer: string;
size: number;
seeders?: number;
leechers?: number;
quality: QualityModel;
languages: Language[];
customFormats: CustomFormat[];
customFormatScore: number;
mappedMovieId?: number;
indexerFlags: string[];
rejections: string[];
movieRequested: boolean;
downloadAllowed: boolean;
isGrabbing?: boolean;
isGrabbed?: boolean;
grabError?: string;
}
export default Release;

View File

@@ -5,4 +5,6 @@ export type InputChanged<T = unknown> = {
export type InputOnChange<T> = (change: InputChanged<T>) => void;
export type CheckInputChanged = InputChanged<boolean>;
export interface CheckInputChanged extends InputChanged<boolean> {
shiftKey: boolean;
}

View File

@@ -341,10 +341,11 @@ namespace NzbDrone.Common.Disk
var isCifs = targetDriveFormat == "cifs";
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
if (mode.HasFlag(TransferMode.Copy))
{
if (isBtrfs)
if (isBtrfs || isZfs)
{
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
{
@@ -358,7 +359,7 @@ namespace NzbDrone.Common.Disk
if (mode.HasFlag(TransferMode.Move))
{
if (isBtrfs)
if (isBtrfs || isZfs)
{
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
{

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