1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-17 21:26:22 -04:00

Compare commits

..

159 Commits

Author SHA1 Message Date
Bogdan
957be99401 Fixed: Bump media info revision for DV HDR10Plus 2023-12-16 02:41:34 +02:00
Bogdan
4bcde25e29 Improve messaging for accepted Custom Formats scoring upgrades
Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>

Closes #9496
2023-12-16 00:38:05 +02:00
Bogdan
1d70f36e7d New: 3D and HDR metadata for Trakt connection 2023-12-15 17:13:47 +02:00
Bogdan
cc0a448bc8 New: Rate limiting for Trakt connection 2023-12-15 17:13:47 +02:00
Bogdan
c9e977baea Simplify mapping in Trakt connection 2023-12-15 17:13:47 +02:00
Mark McDowall
6cb9a46cd4 Fixed: Imported movies updating on Calendar
(cherry picked from commit 5a3bc49392b700650a34536ff3794bce614f64a4)

Closes #9491
2023-12-15 16:50:06 +02:00
Agneev Mukherjee
eef379277a Enable browser navigation buttons for PWA
(cherry picked from commit da9a60691f363323565a293ed9eaeb6349ceccb6)

Closes #9487
2023-12-15 16:36:28 +02:00
Chad A Simmons
41fef47684 New: Support for DV HDR10Plus from media info 2023-12-15 03:36:32 +02:00
Weblate
fcda6faf3d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ROSERAT Ugo <roserat.ugo@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: SHUAI.W <x@ousui.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/lv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-12-13 16:41:49 +02:00
Bogdan
79bbf9c50b Fixed: Movie status label in add movie search results 2023-12-12 21:55:41 +02:00
Bogdan
43d2f2804b New: IMDb ratings and genres in add movie search results 2023-12-12 21:55:09 +02:00
Qstick
fa62f3f66a Fixed: Correctly handle Migration when PG Host has ".db"
(cherry picked from commit 97ee24507f4306e3b62c3d00cd3ade6a09d1b957)

Closes #9478
2023-12-12 15:36:21 +02:00
Bogdan
229d91fe40 Implement DatabaseConnectionInfo
Co-authored-by: Qstick <qstick@gmail.com>
2023-12-12 15:36:14 +02:00
Bogdan
2673d1eee4 Fixed: Movie poster in search results after adding
Fixes #8029
2023-12-11 19:30:31 +02:00
Bogdan
e59fd1118f Fixed: Downloading status post-adding movie 2023-12-11 19:27:05 +02:00
Bogdan
c1fd33b152 Fix categories for NZBFinder 2023-12-10 15:50:19 +02:00
Bogdan
2f58c8676f Bump dotnet to 6.0.25 2023-12-10 15:35:59 +02:00
Fossil
defc448304 Update NZBFinder categories and remove OZnzb & NZB-Tortuga from default definitions (#9474)
NZB Finder will consolidate WEBDL & X265 into SD,HD,UHD so removed 2080 and 2090 categories.

OZnzb and NZB Tortuga are dead so removed it from the presets list.
2023-12-10 15:14:13 +02:00
Bogdan
3ec3358728 Bump version to 5.2.5 2023-12-10 13:47:06 +02:00
Weblate
d4072cdfe2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Jurriaan Den Toonder <jur.den.toonder@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translation: Servarr/Radarr
2023-12-08 18:00:35 +02:00
Weblate
136a030c07 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Hajiroxx <luypanda@163.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-12-08 16:03:26 +02:00
Weblate
6d89ae89a4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Augusto Poletti <augustpolet@gmail.com>
Co-authored-by: Dominika Matějková <dominika.matejkova@outlook.cz>
Co-authored-by: VisoTC <szlytlyt@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 米大饭 <1246333567@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-12-06 16:01:05 +02:00
Servarr
98e4273b7a Automated API Docs update 2023-12-06 16:00:42 +02:00
Bogdan
ecf9983ea6 Fix minimum availability label in movie table row 2023-12-06 14:35:55 +02:00
Bogdan
a059a700eb New: Minimum Availability in bulk manage import lists
Fixes #9461
2023-12-06 14:35:20 +02:00
Taloth Saldono
ced624c2ff Small helper in UI to access Radarr API more easily
(cherry picked from commit 090cdc364ef335fbfea8cf540696af813f6ecea4)
2023-12-06 13:16:02 +02:00
Bogdan
7c32061e17 Add existing flag for Discover Movie Posters 2023-12-06 13:12:49 +02:00
Bogdan
bc4847cdc7 New: Improve fields selection for Discord connection 2023-12-06 11:03:57 +02:00
Bogdan
65d79dd078 Fixed: Progress bar for collection movies in queue 2023-12-04 13:17:29 +02:00
Bogdan
238ddbbe1f Fixed: Improve Required Flags selection for indexers 2023-12-04 12:53:33 +02:00
Bogdan
3f444406da Fixed: (PassThePopcorn) Support for half leech releases 2023-12-04 12:34:22 +02:00
Bogdan
d7aaa1cdc2 Fixed: Cleanup orphaned movies 2023-12-03 22:02:56 +02:00
Mark McDowall
263534717d Always validate Custom Script path
(cherry picked from commit c922cc5dc617dd776d4523cbf62376821c5a4ad9)
2023-12-03 20:03:07 +02:00
Weblate
073d15160d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: David Molero <contact@dolvem.com>
Co-authored-by: Patatra <patrice.chevreau@gmail.com>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: liimee <git.taaa@fedora.email>
Co-authored-by: resi23 <x-resistant-x@gmx.de>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translation: Servarr/Radarr
2023-12-01 04:03:43 +02:00
Stevie Robinson
c5075e5d49 Fixed Custom Format Deletion confirmation message
(cherry picked from commit b76bf373717edff8e475fde31fbaec86c65903fe)

Closes #9410
2023-11-26 08:43:02 +02:00
Weblate
fc345047ee Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Appoxo <appoxo@appoxo.de>
Co-authored-by: Charlie <zola@zipmail.pw>
Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
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/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-11-26 08:28:12 +02:00
Weblate
bffab87da7 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-11-26 08:26:37 +02:00
Bogdan
a8a9d3b833 Bump version to 5.2.4 2023-11-26 07:05:58 +02:00
Stevie Robinson
ff1987be84 New: Remove defunct Boxcar notifications
(cherry picked from commit c6ad2396bb98dc8eb1ad47bf5d066b227a47f8b5)

Closes #9451
2023-11-25 22:29:33 +02:00
Bogdan
cb08c0767d Switch assignment to operator 2023-11-22 21:50:29 +02:00
Bogdan
5f1d7ddc11 Improve discover movies sorting by release dates 2023-11-21 22:45:23 +02:00
Bogdan
0ba3c08ea6 Small UI fixes to discover movies 2023-11-21 22:37:36 +02:00
Bogdan
6b9a378eaf Fixed: Minimum refresh interval for import list presets 2023-11-21 19:52:58 +02:00
Bogdan
b4562e6236 Fixed: Movie grabbed history on interactive search 2023-11-21 03:45:19 +02:00
Bogdan
bbffff78ed Fixed: Disable swipe on movie details when a modal is open 2023-11-20 22:16:01 +02:00
Bogdan
740f0f1e5f Cleanup unused logic in Movie Details 2023-11-20 21:37:13 +02:00
Bogdan
45b38b44c1 Fixed: Interactive search modal on mobile 2023-11-20 07:52:48 +02:00
Mark McDowall
318d59bb99 Fixed force saving provider triggering testing
(cherry picked from commit 65cb1ccafd54479fa3fca1f1eaa4b96222b0176b)
2023-11-20 07:37:42 +02:00
Bogdan
ed54d071c4 Fixed: Wrap long lines in media info popup 2023-11-20 07:06:45 +02:00
Bogdan
cff15de4fc Translate custom format score for history actions 2023-11-20 05:48:02 +02:00
Bogdan
88c0e24c58 Fixed: Clear movie search results when navigating to another page
(cherry picked from commit 67dc8987970aa2a9eade48c02ae72be1851fa196)
2023-11-19 21:10:43 -06:00
Qstick
8e0645670b New: Rework Movie Details view 2023-11-19 21:10:43 -06:00
Qstick
40eeb31a21 New: Move Interactive search to toolbar 2023-11-19 21:10:43 -06:00
Qstick
3e534cf8bf Bump version to 5.2.3 2023-11-19 18:31:23 -06:00
Bogdan
c96b3c4b0b Fixed: Autotagging Genres are case insensitive
(cherry picked from commit 87ecbf39c1c0cc8a3a3f4ee1d1878b34ea49f6b8)

Closes #9436
2023-11-20 00:59:06 +02:00
Mark McDowall
78bc9f9b4b Fixed: Saving indexer, download client, etc settings
(cherry picked from commit 804a5921b3b620e2407d5d6a7fd69fb1fd9b0cbf)
2023-11-20 00:53:45 +02:00
Servarr
b737f05a83 Automated API Docs update 2023-11-19 22:40:17 +02:00
Bogdan
8d96fd2387 More cleanup appName tokens 2023-11-19 22:35:57 +02:00
servarr[bot]
6c487ead00 Fixed: Disable SSL when using the removed SslCertHash configuration (#9426)
(cherry picked from commit d95660d3c78d1ee11a7966d58e78a82a8df01393)

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2023-11-19 21:46:06 +02:00
Bogdan
22e3cf844c New: Confirmation before clearing blocklist
(cherry picked from commit 366d8a4e78e024bc045e4791d3a51af391f4e95a)

Closes #9429
Fixes #9401
2023-11-19 21:17:00 +02:00
Bogdan
14b4b5e122 Cleanup appName tokens
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>

Closes #9430
2023-11-19 21:14:48 +02:00
Mark McDowall
10f5f3c5c8 New: Require password confirmation when setting or changing password
(cherry picked from commit b248163df598dc611ee919d525eb7357256d73d5)
2023-11-19 21:10:52 +02:00
Bogdan
c687b552f0 Bump version to 5.2.2 2023-11-19 07:06:29 +02:00
Stevie Robinson
92c8c8a7f5 Translate System pages
(cherry picked from commit 93e8ff0ac7610fa8739f2e577ece98c2c06c8881)

Closes #8852
2023-11-18 23:44:43 +02:00
Stevie Robinson
86a16c3c0c Fix translation token for Update Check
(cherry picked from commit cd1d8a3ff04b0d06f6fa7dfeb7a7ab6dd88b36b3)

Closes #9341
2023-11-18 23:44:41 +02:00
Weblate
e8c280db34 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Baptiste Mongin <baptiste.mongin@gmail.com>
Co-authored-by: Francisco Cachado <franciscomcachado@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Javier Parada <jparada@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bowsefather <husseinali39@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
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/tr/
Translation: Servarr/Radarr
2023-11-18 03:21:28 +02:00
Nesego
ea65e2174c Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (1336 of 1336 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
2023-11-18 03:01:28 +02:00
Weblate
4aa2466693 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Baptiste Mongin <baptiste.mongin@gmail.com>
Co-authored-by: Francisco Cachado <franciscomcachado@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Javier Parada <jparada@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bowsefather <husseinali39@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
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/tr/
Translation: Servarr/Radarr
2023-11-18 02:59:39 +02:00
Bogdan
4df0f0f721 Revert translation keys 2023-11-18 02:49:58 +02:00
Mark McDowall
d7bee375e8 Use named tokens for backend translations
(cherry picked from commit 11f96c31048c2d1aafca0c91736d439f7f9a95a8)
2023-11-18 02:37:33 +02:00
Bogdan
906295466d Use variable for App name in translations
Co-authored-by: Qstick <qstick@gmail.com>
2023-11-18 02:37:33 +02:00
Mark McDowall
f86060eca2 Don't retest unchanged providers
New: Don't retest connections, indexers, download clients, etc if re-saved with the exact same settings

Closes #9411
Fixes ##9397

(cherry picked from commit 71fd09f162b2880c461e03cba4317c34ee3203dc)
2023-11-17 16:27:42 +02:00
Mark McDowall
bf9a0b62f2 Rename 'ReturnUrl' to 'returnUrl' for forms auth redirection
(cherry picked from commit 812712e2843a738054c065a6d5c1b7c81c5f8e7b)
2023-11-17 03:30:03 +02:00
Bogdan
ccc62f0450 Fixed: Enforce validation warnings when testing providers
(cherry picked from commit c3b4126d0c4f449a41e2cf7ea438b20e25370995)
2023-11-17 02:40:39 +02:00
bakerboy448
524657ad78 Improve sample detection logging (#9405)
Sonarr 6181
2023-11-15 19:38:58 +02:00
Qstick
7a394ff864 Update menu background on dark theme 2023-11-14 20:57:26 -06:00
Bogdan
d8862eedd3 Fixed: Refresh discovery movies state on refresh lists 2023-11-14 20:36:12 +02:00
Bogdan
71f700e240 Fixed: (HDBits) Add labels for categories, codecs and mediums 2023-11-14 17:13:34 +02:00
Bogdan
ae5dd84e0a Fixed: (HDBits) Increase search limit to 100 releases 2023-11-14 15:13:45 +02:00
Bogdan
17b398cf62 Bump version to 5.2.1 2023-11-12 16:52:53 +02:00
Qstick
d00678c1ba Bump ffprobe to 5.1.4
(cherry picked from commit e68b13940d4dd4c5fd008884d909e614521c8cee)
2023-11-11 20:04:59 +02:00
Bogdan
03df9b7f07 Add troubleshooting link when movie searches fail 2023-11-11 18:45:34 +02:00
Bogdan
3442a0ecca Bump version to 5.2.0 2023-11-10 06:24:33 +02:00
Bogdan
3376a467ca Fixed: Record status for notifications on tests 2023-11-10 05:07:29 +02:00
Servarr
1650ce17fb Automated API Docs update 2023-11-10 03:33:52 +02:00
Bogdan
2f2004faa2 New: Resolve download client by name using 'downloadClient' for pushed releases
(cherry picked from commit 0496e728dc1e21b16b9ee2fee010de9707bdff85)
2023-11-10 03:14:38 +02:00
Mark McDowall
437e2f4597 Don't store successful results for invalid providers
(cherry picked from commit de23182d593e2284972103d505e66dd8d812dfdb)
2023-11-10 03:13:27 +02:00
Bogdan
17b8605751 Simply queue item for movie details page 2023-11-09 20:53:49 +02:00
Bogdan
b2a52e52b6 Fixed: (UI) Progress bar status for searching existing movies 2023-11-09 20:53:49 +02:00
Servarr
0f5fabdfcd Automated API Docs update 2023-11-09 15:11:53 +02:00
Bogdan
6362ee9b7d Fixed: (UI) Import Lists enabled status doesn't rely on automatic add
Fixes #9375
2023-11-09 14:27:10 +02:00
Bogdan
50465fd482 New: Add enabled to manage import lists bulk 2023-11-09 14:16:03 +02:00
Bogdan
54d447d55f New: Parse VIE as Vietnamese language 2023-11-08 20:37:07 +02:00
Bogdan
50f48277e5 Don't show already imported movies as downloading in Movies index 2023-11-08 17:49:29 +02:00
Weblate
c2e206b7ac Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Baptiste Mongin <baptiste.mongin@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Javier Parada <jparada@gmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2023-11-07 20:29:36 +02:00
Bogdan
7a46de602f Increase the wait timeout for integration tests init 2023-11-06 00:29:02 +02:00
bakerboy448
89820c1ff7 Lower log level for missing translation/culture resource (#8956)
Fixes #8150
2023-11-04 16:21:47 +02:00
Stevie Robinson
67e6e129ff Fix event ids for history custom filters 2023-11-02 00:37:54 +02:00
Bogdan
b6001238e5 Sort root folders by path in UI
Fixes #9280
2023-11-01 13:13:21 +02:00
Stevie Robinson
7bbdcc81bb Use Diacritical.Net library for TitleFirstCharacter token
(cherry picked from commit 59ea524e0ce85333779f430b867e93aae366289f)

Closes #9324
2023-11-01 13:13:21 +02:00
Stevie Robinson
3e8cbc497e Fixed: Cleanup First Character in Title when using 'TitleFirstCharacter'
(cherry picked from commit b3c691859a0841cde9bb2655c01c240cc5279d74)

Closes #9249
2023-11-01 13:13:21 +02:00
Bogdan
60d2df043b Rename instances of Profile to QualityProfile
Closes #9337
2023-11-01 13:13:21 +02:00
Mark McDowall
da41cb8840 Fixed: Blocking unknown indexers from pushed releases
(cherry picked from commit 44d8dbaac81706691124ae5f8317289f0a3e5d73)
2023-10-30 14:21:29 +02:00
Bogdan
a4b7c99d91 Fix full disk releases regex for DVDs
(cherry picked from commit aa8659eecd0a0fedd56eb1066730d1d1ec32df0d)

Closes #9346
2023-10-29 18:50:38 +02:00
Bogdan
8fb21e073b Bump version to 5.1.3 2023-10-29 10:35:11 +02:00
Stevie Robinson
dbf424d454 New: Relative path as default Sort order on Manual Import
(cherry picked from commit 6e2162ebf4b3366df540bd35ad91bd5527976728)

Closes #9344
2023-10-29 01:12:45 +03:00
Bogdan
a6dda70c0a New: Add Download Client validation for indexers
(cherry picked from commit e53b7f8c945e3597ca1719961e82540f1f01f0e9)

Closes #9338
2023-10-29 01:05:50 +03:00
Bogdan
e6fa14b1e6 Allow 0 as valid value in QualityProfileExistsValidator
(cherry picked from commit 36ca24e55a5eda859047d82855f65c401cc0b30f)
2023-10-29 00:59:16 +03:00
Bogdan
b5c0d515ee Improve UI notice for delayed queue items
(cherry picked from commit f2cae4e2b2f87924973a00431be7216296648dac)
2023-10-29 00:59:02 +03:00
Bogdan
b7aee25d0d Set proper default props for Queue details and status
(cherry picked from commit dc099a77ca9e6993d386a2b70f1aff1fed34a32b)
2023-10-29 00:58:30 +03:00
Bogdan
233b85aaf3 Sort Custom Formats by name
(cherry picked from commit e9bb1d52a72b20a58d1a672ecfa3797eda6f081a)
2023-10-29 00:58:11 +03:00
Weblate
80db9a7dd4 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: ID-86 <id86dev@gmail.com>
Co-authored-by: Jordy <prive@jordyhoebergen.nl>
Co-authored-by: LeDaFeEs <leonardo.fonseca85300@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bai0012 <baicongrui@gmail.com>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-10-28 22:05:58 +03:00
Bogdan
660d3d7643 Fixed: Don't reject DVDRip/DVDRemux as full DVD disk releases 2023-10-28 22:05:12 +03:00
Bogdan
d999aea36f Add missing translation in release profiles 2023-10-26 00:18:52 +03:00
Jordy
5d45f1de89 Update link to Docker instructions in readme (#9318) 2023-10-25 11:21:51 +03:00
Weblate
3e5089719c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Ruben Lourenco <ruben.lourenco01@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: jianl <jianjianfengyun@126.com>
Co-authored-by: 宿命 <331874545@qq.com>
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/zh_CN/
Translation: Servarr/Radarr
2023-10-22 10:53:19 +03:00
Bogdan
ec69dfaabb Bump version to 5.1.2 2023-10-22 09:37:23 +03:00
Qstick
aa13a40bad Fixed: Update movie collection status in Radarr if removed on TMDB 2023-10-21 20:11:49 -05:00
Bogdan
9b458812f1 Fixed: Set Busy Timeout for SQLite 2023-10-21 22:47:19 +03:00
Bogdan
1bdc48a889 Revert "Bump SQLite to 3.42.0 (1.0.118)"
This reverts commit e3160466e0.
2023-10-20 22:28:49 +03:00
Servarr
e5d479a162 Automated API Docs update 2023-10-17 19:13:07 +03:00
Bogdan
9a50fcb82a Sort movies by name in filter builder 2023-10-17 18:59:34 +03:00
Stevie Robinson
f2357e0b60 Fixed: Reduce font size for series title on series details
(cherry picked from commit 03f5174a4b2a005aab8d1a1540f4bcb272682f2e)

Closes #9301
Closes #9302
2023-10-17 18:23:33 +03:00
Mark McDowall
0591d05c3b New: History custom filters
(cherry picked from commit 2fe8f3084c90688e6dd01d600796396e74f43ff9)

Closes #9298
2023-10-17 18:23:07 +03:00
Mark McDowall
299d50d56c Cleanup Calendar custom filters
(cherry picked from commit fd789343b587da252461d84bafba2d72651a11df)

Closes #9296
2023-10-17 14:41:55 +03:00
Mark McDowall
7d3c01114b New: Queue custom filters
(cherry picked from commit e357d17b187378b92377f8acb077b12c1e7ea527)

Closes #9297
2023-10-17 14:38:49 +03:00
bakerboy448
70376af70b Fixed: Re-run Removed Movie health check after movie is deleted (#9277) 2023-10-17 12:39:20 +03:00
Bogdan
9ef031bd9e Fixed: Don't die when adding existing exclusions 2023-10-16 05:45:24 +03:00
Weblate
3a9b276c43 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: DavidHenryThoreau <sorau@protonmail.com>
Co-authored-by: Dlgeri123 <bornemiszageri@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: bai0012 <baicongrui@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-10-15 18:33:49 +03:00
Bogdan
aabf209a07 Bump version to 5.1.1 2023-10-15 07:52:43 +03:00
Mark McDowall
79c03f2fe6 Fixed: Reject full DVD disk releases
(cherry picked from commit df2e867528249cf707788d8341c4a26293e179ba)
2023-10-14 21:06:36 -04:00
Bogdan
9b36404071 Fixed: Don't die in Collections when a collection doesn't have movies 2023-10-14 20:11:36 +03:00
Bogdan
ecfaea3885 Fixed: Don't die in FileNameBuilder when OriginalTitle is null 2023-10-13 19:49:31 +03:00
Bogdan
bfbeb4c62e Fixed: Ignore case when cleansing announce URLs 2023-10-12 05:03:22 +03:00
Bogdan
4b98d27f31 New: Tooltips for dates in MovieReleaseDates 2023-10-11 15:02:43 +03:00
Weblate
604d74270d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: Timo <Tclemens@live.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-10-11 06:33:42 +03:00
Bogdan
15bb9139d1 New: Sort movies by release dates 2023-10-11 06:32:30 +03:00
Servarr
32722eb704 Automated API Docs update 2023-10-11 05:07:00 +03:00
Mark McDowall
e0c8a8f0d6 New: Download client option for redownloading failed releases from Interactive Search
(cherry picked from commit 87e0a7983a437a4d166aa8b9c9eaf78ea5431969)

Closes #9260
2023-10-11 04:50:39 +03:00
Bogdan
a3bb0541f0 Prevent NullRef on header assert 2023-10-11 04:29:20 +03:00
Bogdan
e78bc34514 Fixed: Fetch import lists without depending on Automatic Add 2023-10-11 03:38:28 +03:00
Servarr
35c4538288 Automated API Docs update 2023-10-11 02:27:16 +03:00
Bogdan
3981e816cd Remove PagingResourceFilter 2023-10-11 02:19:28 +03:00
Mark McDowall
9354031571 Log executing health check
Towards #6076

(cherry picked from commit 78b39bd2fecda60e04a1fef17ae17f62bd2b6914)
2023-10-10 07:04:52 +03:00
Mark McDowall
a01328dc8c Paging params in API docs
(cherry picked from commit bfaa7291e14a8d3847ef2154a52c363944560803)

Closes #9248
2023-10-10 07:03:41 +03:00
Stevie Robinson
8cb6295ddc New: Additional tooltips for icon buttons
(cherry picked from commit 8c07f0d3d19a48ed96d1ded54399c66bf2977b2a)

Closes #9253
2023-10-10 06:51:57 +03:00
Bogdan
99f7d8bcf5 New: Auto tag based on movie's quality profile
(cherry picked from commit 6de3e7c950bd939bab96ef2ae74337108ad5a212)

Closes #9254
2023-10-10 06:50:36 +03:00
Bogdan
f13d479b88 Add status test all button for IndexerLongTermStatusCheck
(cherry picked from commit 4ffa1816bd2305550abee20cea27e1296a99ddf6)
2023-10-10 06:48:04 +03:00
Bogdan
23eb637bc3 Fixed: Avoid logging evaluations when not using any Remote Path Mappings
(cherry picked from commit 44eb729ccc13237f4439006159bd616e8bdb5750)
2023-10-10 06:47:49 +03:00
Bogdan
3a786d0b9d Fixed: Displaying multiple values when adding custom filters
Closes #5810
2023-10-10 05:55:55 +03:00
Bogdan
6fb127235c Fixed: Calendar's agenda on mobile 2023-10-10 01:42:46 +03:00
Qstick
5517e578b6 Bump version to 5.1.0 2023-10-07 23:07:42 -05:00
nuxen
bced2e7b2e Fixed: Updated BR-DISK quality parsing 2023-10-08 06:49:37 +03:00
Bogdan
f7313369b5 Fixed: Show year in collection movies as labels 2023-10-08 06:12:18 +03:00
Servarr
b14e93e11f Automated API Docs update 2023-10-08 01:50:59 +03:00
Bogdan
f5692d6cf1 Fixed: Show year and fix sorting for collection movies 2023-10-08 01:41:24 +03:00
Weblate
a2d505c795 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Florian <sephrat.flo@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: blankhang <blankhang@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2023-10-07 16:53:23 -05:00
Qstick
3d46bd2d8f Revert cover mapping for collections, optimize translation mapping 2023-10-07 15:43:59 -05:00
Bogdan
017f272201 Log Notifiarr errors as warnings 2023-10-07 22:56:22 +03:00
331 changed files with 7027 additions and 5015 deletions

View File

@@ -2,7 +2,7 @@
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop) [![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget) [![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation#docker) [![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation/docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg) ![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers) [![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors) [![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.0.3' majorVersion: '5.2.5'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.413' dotnetVersion: '6.0.417'
nodeVersion: '16.X' nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'

View File

@@ -36,6 +36,7 @@ class Blocklist extends Component {
lastToggled: null, lastToggled: null,
selectedState: {}, selectedState: {},
isConfirmRemoveModalOpen: false, isConfirmRemoveModalOpen: false,
isConfirmClearModalOpen: false,
items: props.items items: props.items
}; };
} }
@@ -90,6 +91,19 @@ class Blocklist extends Component {
this.setState({ isConfirmRemoveModalOpen: false }); this.setState({ isConfirmRemoveModalOpen: false });
}; };
onClearBlocklistPress = () => {
this.setState({ isConfirmClearModalOpen: true });
};
onClearBlocklistConfirmed = () => {
this.props.onClearBlocklistPress();
this.setState({ isConfirmClearModalOpen: false });
};
onConfirmClearModalClose = () => {
this.setState({ isConfirmClearModalOpen: false });
};
// //
// Render // Render
@@ -103,7 +117,6 @@ class Blocklist extends Component {
totalRecords, totalRecords,
isRemoving, isRemoving,
isClearingBlocklistExecuting, isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -111,7 +124,8 @@ class Blocklist extends Component {
allSelected, allSelected,
allUnselected, allUnselected,
selectedState, selectedState,
isConfirmRemoveModalOpen isConfirmRemoveModalOpen,
isConfirmClearModalOpen
} = this.state; } = this.state;
const selectedIds = this.getSelectedIds(); const selectedIds = this.getSelectedIds();
@@ -131,8 +145,9 @@ class Blocklist extends Component {
<PageToolbarButton <PageToolbarButton
label={translate('Clear')} label={translate('Clear')}
iconName={icons.CLEAR} iconName={icons.CLEAR}
isDisabled={!items.length}
isSpinning={isClearingBlocklistExecuting} isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress} onPress={this.onClearBlocklistPress}
/> />
</PageToolbarSection> </PageToolbarSection>
@@ -215,6 +230,16 @@ class Blocklist extends Component {
onConfirm={this.onRemoveSelectedConfirmed} onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose} onCancel={this.onConfirmRemoveModalClose}
/> />
<ConfirmModal
isOpen={isConfirmClearModalOpen}
kind={kinds.DANGER}
title={translate('ClearBlocklist')}
message={translate('ClearBlocklistMessageText')}
confirmLabel={translate('Clear')}
onConfirm={this.onClearBlocklistConfirmed}
onCancel={this.onConfirmClearModalClose}
/>
</PageContent> </PageContent>
); );
} }

View File

@@ -14,6 +14,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
import TablePager from 'Components/Table/TablePager'; import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props'; import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector'; import HistoryRowConnector from './HistoryRowConnector';
class History extends Component { class History extends Component {
@@ -33,6 +34,7 @@ class History extends Component {
columns, columns,
selectedFilterKey, selectedFilterKey,
filters, filters,
customFilters,
totalRecords, totalRecords,
onFilterSelect, onFilterSelect,
onFirstPagePress, onFirstPagePress,
@@ -70,7 +72,8 @@ class History extends Component {
alignMenu={align.RIGHT} alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={[]} customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={onFilterSelect} onFilterSelect={onFilterSelect}
/> />
</PageToolbarSection> </PageToolbarSection>
@@ -144,8 +147,9 @@ History.propTypes = {
moviesError: PropTypes.object, moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number, totalRecords: PropTypes.number,
onFilterSelect: PropTypes.func.isRequired, onFilterSelect: PropTypes.func.isRequired,
onFirstPagePress: PropTypes.func.isRequired onFirstPagePress: PropTypes.func.isRequired

View File

@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import withCurrentPage from 'Components/withCurrentPage'; import withCurrentPage from 'Components/withCurrentPage';
import * as historyActions from 'Store/Actions/historyActions'; import * as historyActions from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History'; import History from './History';
@@ -11,11 +12,13 @@ function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.history, (state) => state.history,
(state) => state.movies, (state) => state.movies,
(history, movies) => { createCustomFiltersSelector('history'),
(history, movies, customFilters) => {
return { return {
isMoviesFetching: movies.isFetching, isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated, isMoviesPopulated: movies.isPopulated,
moviesError: movies.error, moviesError: movies.error,
customFilters,
...history ...history
}; };
} }

View File

@@ -0,0 +1,54 @@
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 { setHistoryFilter } from 'Store/Actions/historyActions';
function createHistorySelector() {
return createSelector(
(state: AppState) => state.history.items,
(queueItems) => {
return queueItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.history.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface HistoryFilterModalProps {
isOpen: boolean;
}
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
const sectionItems = useSelector(createHistorySelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'history';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setHistoryFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -21,6 +22,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll'; import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import QueueFilterModal from './QueueFilterModal';
import QueueOptionsConnector from './QueueOptionsConnector'; import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector'; import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemsModal from './RemoveQueueItemsModal'; import RemoveQueueItemsModal from './RemoveQueueItemsModal';
@@ -153,11 +155,16 @@ class Queue extends Component {
isMoviesPopulated, isMoviesPopulated,
moviesError, moviesError,
columns, columns,
selectedFilterKey,
filters,
customFilters,
count,
totalRecords, totalRecords,
isGrabbing, isGrabbing,
isRemoving, isRemoving,
isRefreshMonitoredDownloadsExecuting, isRefreshMonitoredDownloadsExecuting,
onRefreshPress, onRefreshPress,
onFilterSelect,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -220,6 +227,15 @@ class Queue extends Component {
iconName={icons.TABLE} iconName={icons.TABLE}
/> />
</TableOptionsModalWrapper> </TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={QueueFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection> </PageToolbarSection>
</PageToolbar> </PageToolbar>
@@ -241,7 +257,11 @@ class Queue extends Component {
{ {
isAllPopulated && !hasError && !items.length ? isAllPopulated && !hasError && !items.length ?
<Alert kind={kinds.INFO}> <Alert kind={kinds.INFO}>
{translate('QueueIsEmpty')} {
selectedFilterKey !== 'all' && count > 0 ?
translate('QueueFilterHasNoItems') :
translate('QueueIsEmpty')
}
</Alert> : </Alert> :
null null
} }
@@ -325,13 +345,22 @@ Queue.propTypes = {
moviesError: PropTypes.object, moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
count: PropTypes.number.isRequired,
totalRecords: PropTypes.number, totalRecords: PropTypes.number,
isGrabbing: PropTypes.bool.isRequired, isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired, isRemoving: PropTypes.bool.isRequired,
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired, isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired, onRefreshPress: PropTypes.func.isRequired,
onGrabSelectedPress: PropTypes.func.isRequired, onGrabSelectedPress: PropTypes.func.isRequired,
onRemoveSelectedPress: PropTypes.func.isRequired onRemoveSelectedPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
Queue.defaultProps = {
count: 0
}; };
export default Queue; export default Queue;

View File

@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage'; import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import * as queueActions from 'Store/Actions/queueActions'; import * as queueActions from 'Store/Actions/queueActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Queue from './Queue'; import Queue from './Queue';
@@ -15,12 +16,16 @@ function createMapStateToProps() {
(state) => state.movies, (state) => state.movies,
(state) => state.queue.options, (state) => state.queue.options,
(state) => state.queue.paged, (state) => state.queue.paged,
(state) => state.queue.status.item,
createCustomFiltersSelector('queue'),
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS), createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => { (movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
return { return {
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
isMoviesFetching: movies.isFetching, isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated, isMoviesPopulated: movies.isPopulated,
moviesError: movies.error, moviesError: movies.error,
customFilters,
isRefreshMonitoredDownloadsExecuting, isRefreshMonitoredDownloadsExecuting,
...options, ...options,
...queue ...queue
@@ -106,6 +111,10 @@ class QueueConnector extends Component {
this.props.setQueueSort({ sortKey }); this.props.setQueueSort({ sortKey });
}; };
onFilterSelect = (selectedFilterKey) => {
this.props.setQueueFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => { onTableOptionChange = (payload) => {
this.props.setQueueTableOption(payload); this.props.setQueueTableOption(payload);
@@ -140,6 +149,7 @@ class QueueConnector extends Component {
onLastPagePress={this.onLastPagePress} onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect} onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress} onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange} onTableOptionChange={this.onTableOptionChange}
onRefreshPress={this.onRefreshPress} onRefreshPress={this.onRefreshPress}
onGrabSelectedPress={this.onGrabSelectedPress} onGrabSelectedPress={this.onGrabSelectedPress}
@@ -162,6 +172,7 @@ QueueConnector.propTypes = {
gotoQueueLastPage: PropTypes.func.isRequired, gotoQueueLastPage: PropTypes.func.isRequired,
gotoQueuePage: PropTypes.func.isRequired, gotoQueuePage: PropTypes.func.isRequired,
setQueueSort: PropTypes.func.isRequired, setQueueSort: PropTypes.func.isRequired,
setQueueFilter: PropTypes.func.isRequired,
setQueueTableOption: PropTypes.func.isRequired, setQueueTableOption: PropTypes.func.isRequired,
clearQueue: PropTypes.func.isRequired, clearQueue: PropTypes.func.isRequired,
grabQueueItems: PropTypes.func.isRequired, grabQueueItems: PropTypes.func.isRequired,

View File

@@ -81,4 +81,9 @@ QueueDetails.propTypes = {
progressBar: PropTypes.node.isRequired progressBar: PropTypes.node.isRequired
}; };
QueueDetails.defaultProps = {
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading'
};
export default QueueDetails; export default QueueDetails;

View File

@@ -0,0 +1,54 @@
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 { setQueueFilter } from 'Store/Actions/queueActions';
function createQueueSelector() {
return createSelector(
(state: AppState) => state.queue.paged.items,
(queueItems) => {
return queueItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.queue.paged.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface QueueFilterModalProps {
isOpen: boolean;
}
export default function QueueFilterModal(props: QueueFilterModalProps) {
const sectionItems = useSelector(createQueueSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'queue';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setQueueFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { tooltipPositions } from 'Helpers/Props'; import { tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import QueueStatus from './QueueStatus'; import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css'; import styles from './QueueStatusCell.css';
@@ -41,8 +40,8 @@ QueueStatusCell.propTypes = {
}; };
QueueStatusCell.defaultProps = { QueueStatusCell.defaultProps = {
trackedDownloadStatus: translate('Ok'), trackedDownloadStatus: 'ok',
trackedDownloadState: translate('Downloading') trackedDownloadState: 'downloading'
}; };
export default QueueStatusCell; export default QueueStatusCell;

View File

@@ -1,6 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatTime from 'Utilities/Date/formatTime'; import formatTime from 'Utilities/Date/formatTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
@@ -25,11 +28,13 @@ function TimeleftCell(props) {
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return ( return (
<TableRowCell <TableRowCell className={styles.timeleft}>
className={styles.timeleft} <Tooltip
title={translate('DelayingDownloadUntil', { date, time })} anchor={<Icon name={icons.INFO} />}
> tooltip={translate('DelayingDownloadUntil', { date, time })}
- kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
</TableRowCell> </TableRowCell>
); );
} }
@@ -39,11 +44,13 @@ function TimeleftCell(props) {
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return ( return (
<TableRowCell <TableRowCell className={styles.timeleft}>
className={styles.timeleft} <Tooltip
title={translate('RetryingDownloadOn', { date, time })} anchor={<Icon name={icons.INFO} />}
> tooltip={translate('RetryingDownloadOn', { date, time })}
- kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
</TableRowCell> </TableRowCell>
); );
} }

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
@@ -130,7 +131,12 @@ class AddNewMovie extends Component {
<div className={styles.helpText}> <div className={styles.helpText}>
{translate('FailedLoadingSearchResults')} {translate('FailedLoadingSearchResults')}
</div> </div>
<div>{getErrorMessage(error)}</div> <Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
<div>
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
{translate('WhySearchesCouldBeFailing')}
</Link>
</div>
</div> : null </div> : null
} }

View File

@@ -85,8 +85,13 @@
margin-top: 20px; margin-top: 20px;
} }
.studio,
.genres {
margin-left: 5px;
}
.links { .links {
margin-left: 8px; margin-left: 5px;
pointer-events: all; pointer-events: all;
} }

View File

@@ -5,6 +5,7 @@ interface CssExports {
'certification': string; 'certification': string;
'content': string; 'content': string;
'exclusionIcon': string; 'exclusionIcon': string;
'genres': string;
'icons': string; 'icons': string;
'links': string; 'links': string;
'overlay': string; 'overlay': string;
@@ -14,6 +15,7 @@ interface CssExports {
'runtime': string; 'runtime': string;
'searchResult': string; 'searchResult': string;
'statusContainer': string; 'statusContainer': string;
'studio': string;
'title': string; 'title': string;
'titleContainer': string; 'titleContainer': string;
'titleRow': string; 'titleRow': string;

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import Label from 'Components/Label'; import Label from 'Components/Label';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
@@ -61,11 +62,13 @@ class AddNewMovieSearchResult extends Component {
titleSlug, titleSlug,
year, year,
studio, studio,
genres,
status, status,
overview, overview,
ratings, ratings,
folder, folder,
images, images,
existingMovieId,
isExistingMovie, isExistingMovie,
isExclusionMovie, isExclusionMovie,
isSmallScreen, isSmallScreen,
@@ -74,8 +77,8 @@ class AddNewMovieSearchResult extends Component {
monitored, monitored,
hasFile, hasFile,
isAvailable, isAvailable,
queueStatus, movieFile,
queueState, queueItem,
runtime, runtime,
movieRuntimeFormat, movieRuntimeFormat,
certification certification
@@ -120,13 +123,13 @@ class AddNewMovieSearchResult extends Component {
{ {
isExistingMovie && isExistingMovie &&
<MovieIndexProgressBar <MovieIndexProgressBar
movieId={existingMovieId}
movieFile={movieFile}
monitored={monitored} monitored={monitored}
hasFile={hasFile} hasFile={hasFile}
status={status} status={status}
width={posterWidth} width={posterWidth}
detailedProgressBar={true} detailedProgressBar={true}
queueStatus={queueStatus}
queueState={queueState}
isAvailable={isAvailable} isAvailable={isAvailable}
/> />
} }
@@ -197,13 +200,46 @@ class AddNewMovieSearchResult extends Component {
/> />
</Label> </Label>
{
ratings.imdb ?
<Label size={sizes.LARGE}>
<ImdbRating
ratings={ratings}
iconSize={13}
/>
</Label> :
null
}
{ {
!!studio && !!studio &&
<Label size={sizes.LARGE}> <Label size={sizes.LARGE}>
{studio} <Icon
name={icons.STUDIO}
size={13}
/>
<span className={styles.studio}>
{studio}
</span>
</Label> </Label>
} }
{
genres.length > 0 ?
<Label size={sizes.LARGE}>
<Icon
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.slice(0, 3).join(', ')}
</span>
</Label> :
null
}
<Tooltip <Tooltip
anchor={ anchor={
<Label <Label
@@ -215,15 +251,15 @@ class AddNewMovieSearchResult extends Component {
/> />
<span className={styles.links}> <span className={styles.links}>
Links {translate('Links')}
</span> </span>
</Label> </Label>
} }
tooltip={ tooltip={
<MovieDetailsLinks <MovieDetailsLinks
tmdbId={tmdbId} tmdbId={tmdbId}
youTubeTrailerId={youTubeTrailerId}
imdbId={imdbId} imdbId={imdbId}
youTubeTrailerId={youTubeTrailerId}
/> />
} }
canFlip={true} canFlip={true}
@@ -237,6 +273,7 @@ class AddNewMovieSearchResult extends Component {
hasMovieFiles={hasFile} hasMovieFiles={hasFile}
monitored={monitored} monitored={monitored}
isAvailable={isAvailable} isAvailable={isAvailable}
queueItem={queueItem}
id={id} id={id}
useLabel={true} useLabel={true}
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
@@ -273,25 +310,30 @@ AddNewMovieSearchResult.propTypes = {
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
year: PropTypes.number.isRequired, year: PropTypes.number.isRequired,
studio: PropTypes.string, studio: PropTypes.string,
genres: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
overview: PropTypes.string, overview: PropTypes.string,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired, folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
existingMovieId: PropTypes.number,
isExistingMovie: PropTypes.bool.isRequired, isExistingMovie: PropTypes.bool.isRequired,
isExclusionMovie: PropTypes.bool.isRequired, isExclusionMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
id: PropTypes.number, id: PropTypes.number,
queueItems: PropTypes.arrayOf(PropTypes.object),
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired, hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired, isAvailable: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
queueItem: PropTypes.object,
colorImpairedMode: PropTypes.bool, colorImpairedMode: PropTypes.bool,
queueStatus: PropTypes.string,
queueState: PropTypes.string,
runtime: PropTypes.number.isRequired, runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired, movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string certification: PropTypes.string
}; };
AddNewMovieSearchResult.defaultProps = {
genres: []
};
export default AddNewMovieSearchResult; export default AddNewMovieSearchResult;

View File

@@ -12,15 +12,17 @@ function createMapStateToProps() {
createDimensionsSelector(), createDimensionsSelector(),
(state) => state.queue.details.items, (state) => state.queue.details.items,
(state, { internalId }) => internalId, (state, { internalId }) => internalId,
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId) => { (state) => state.settings.ui.item.movieRuntimeFormat,
const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0); (isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId, movieRuntimeFormat) => {
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
return { return {
existingMovieId: internalId,
isExistingMovie, isExistingMovie,
isExclusionMovie, isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
queueStatus: firstQueueItem ? firstQueueItem.status : null, queueItem,
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null movieRuntimeFormat
}; };
} }
); );

View File

@@ -5,12 +5,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ImportMovieSelectFolder from './ImportMovieSelectFolder'; import ImportMovieSelectFolder from './ImportMovieSelectFolder';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.rootFolders, createRootFoldersSelector(),
createSystemStatusSelector(), createSystemStatusSelector(),
(rootFolders, systemStatus) => { (rootFolders, systemStatus) => {
return { return {

View File

@@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('AppUpdated', { appName: 'Radarr' })} {translate('AppUpdated')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Radarr', version })} blockClassName={styles.version} /> <InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
</div> </div>
{ {

View File

@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody> <ModalBody>
<div> <div>
{translate('ConnectionLostToBackend', { appName: 'Radarr' })} {translate('ConnectionLostToBackend')}
</div> </div>
<div className={styles.automatic}> <div className={styles.automatic}>
{translate('ConnectionLostReconnect', { appName: 'Radarr' })} {translate('ConnectionLostReconnect')}
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>

View File

@@ -1,4 +1,5 @@
import SortDirection from 'Helpers/Props/SortDirection'; import SortDirection from 'Helpers/Props/SortDirection';
import { FilterBuilderProp } from './AppState';
export interface Error { export interface Error {
responseJSON: { responseJSON: {
@@ -20,6 +21,10 @@ export interface PagedAppSectionState {
pageSize: number; pageSize: number;
} }
export interface AppSectionFilterState<T> {
filterBuilderProps: FilterBuilderProp<T>[];
}
export interface AppSectionSchemaState<T> { export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean; isSchemaFetching: boolean;
isSchemaPopulated: boolean; isSchemaPopulated: boolean;

View File

@@ -1,6 +1,7 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState'; import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import CalendarAppState from './CalendarAppState'; import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState'; import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import MovieCollectionAppState from './MovieCollectionAppState'; import MovieCollectionAppState from './MovieCollectionAppState';
import MovieFilesAppState from './MovieFilesAppState'; import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState'; import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
@@ -46,6 +47,7 @@ export interface CustomFilter {
interface AppState { interface AppState {
calendar: CalendarAppState; calendar: CalendarAppState;
commands: CommandAppState; commands: CommandAppState;
history: HistoryAppState;
interactiveImport: InteractiveImportAppState; interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState; movieCollections: MovieCollectionAppState;
movieFiles: MovieFilesAppState; movieFiles: MovieFilesAppState;

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import AppSectionState from 'App/State/AppSectionState'; import AppSectionState from 'App/State/AppSectionState';
import MovieCollection from 'typings/MovieCollection'; import MovieCollection from 'typings/MovieCollection';
type MovieCollectionAppState = AppSectionState<MovieCollection>; interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
itemMap: Record<number, number>;
}
export default MovieCollectionAppState; export default MovieCollectionAppState;

View File

@@ -1,41 +1,17 @@
import ModelBase from 'App/ModelBase'; import Queue from 'typings/Queue';
import Language from 'Language/Language'; import AppSectionState, {
import { QualityModel } from 'Quality/Quality'; AppSectionFilterState,
import CustomFormat from 'typings/CustomFormat'; AppSectionItemState,
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState'; Error,
} from './AppSectionState';
export interface StatusMessage {
title: string;
messages: string[];
}
export interface Queue extends ModelBase {
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
size: number;
title: string;
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
status: string;
trackedDownloadStatus: string;
trackedDownloadState: string;
statusMessages: StatusMessage[];
errorMessage: string;
downloadId: string;
protocol: string;
downloadClient: string;
outputPath: string;
movieHasFile: boolean;
movieId?: number;
}
export interface QueueDetailsAppState extends AppSectionState<Queue> { export interface QueueDetailsAppState extends AppSectionState<Queue> {
params: unknown; params: unknown;
} }
export interface QueuePagedAppState extends AppSectionState<Queue> { export interface QueuePagedAppState
extends AppSectionState<Queue>,
AppSectionFilterState<Queue> {
isGrabbing: boolean; isGrabbing: boolean;
grabError: Error; grabError: Error;
isRemoving: boolean; isRemoving: boolean;

View File

@@ -42,9 +42,9 @@ function Agenda(props) {
<div className={styles.agenda}> <div className={styles.agenda}>
{ {
items.map((item, index) => { items.map((item, index) => {
const momentDate = moment(item.inCinemas); const momentDate = moment(item.sortDate);
const showDate = index === 0 || const showDate = index === 0 ||
!moment(items[index - 1].inCinemas).isSame(momentDate, 'day'); !moment(items[index - 1].sortDate).isSame(momentDate, 'day');
return ( return (
<AgendaEventConnector <AgendaEventConnector

View File

@@ -88,7 +88,7 @@
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {
.event { .overlay {
flex-direction: column; flex-direction: column;
} }
@@ -111,5 +111,4 @@
.releaseIcon { .releaseIcon {
margin-right: 20px; margin-right: 20px;
width: 25px; width: 25px;
text-align: right;
} }

View File

@@ -95,7 +95,7 @@ class AgendaEvent extends Component {
<div className={styles.overlay}> <div className={styles.overlay}>
<div className={styles.date}> <div className={styles.date}>
{(showDate) ? startTime.format(longDateFormat) : null} {showDate ? startTime.format(longDateFormat) : null}
</div> </div>
<div className={styles.releaseIcon}> <div className={styles.releaseIcon}>

View File

@@ -55,7 +55,7 @@ class CalendarConnector extends Component {
gotoCalendarToday gotoCalendarToday
} = this.props; } = this.props;
registerPagePopulator(this.repopulate); registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
if (useCurrentPage) { if (useCurrentPage) {
fetchCalendar(); fetchCalendar();

View File

@@ -23,13 +23,11 @@ function createFilterBuilderPropsSelector() {
); );
} }
interface SeriesIndexFilterModalProps { interface CalendarFilterModalProps {
isOpen: boolean; isOpen: boolean;
} }
export default function CalendarFilterModal( export default function CalendarFilterModal(props: CalendarFilterModalProps) {
props: SeriesIndexFilterModalProps
) {
const sectionItems = useSelector(createCalendarSelector()); const sectionItems = useSelector(createCalendarSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'calendar'; const customFilterType = 'calendar';

View File

@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition'; import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions'; import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import scrollPositions from 'Store/scrollPositions'; import scrollPositions from 'Store/scrollPositions';
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector'; import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
@@ -38,6 +39,12 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchRootFolders() { dispatchFetchRootFolders() {
dispatch(fetchRootFolders()); dispatch(fetchRootFolders());
}, },
dispatchFetchQueueDetails() {
dispatch(fetchQueueDetails());
},
dispatchClearQueueDetails() {
dispatch(clearQueueDetails());
},
onUpdateSelectedPress(payload) { onUpdateSelectedPress(payload) {
dispatch(saveMovieCollections(payload)); dispatch(saveMovieCollections(payload));
}, },
@@ -63,10 +70,12 @@ class CollectionConnector extends Component {
componentDidMount() { componentDidMount() {
registerPagePopulator(this.repopulate); registerPagePopulator(this.repopulate);
this.props.dispatchFetchRootFolders(); this.props.dispatchFetchRootFolders();
this.props.dispatchFetchQueueDetails();
} }
componentWillUnmount() { componentWillUnmount() {
unregisterPagePopulator(this.repopulate); unregisterPagePopulator(this.repopulate);
this.props.dispatchClearQueueDetails();
} }
// //
@@ -99,7 +108,9 @@ CollectionConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
onUpdateSelectedPress: PropTypes.func.isRequired, onUpdateSelectedPress: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchClearQueueDetails: PropTypes.func.isRequired
}; };
export default withScrollPosition( export default withScrollPosition(

View File

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

View File

@@ -61,6 +61,7 @@ class CollectionMovie extends Component {
const { const {
id, id,
title, title,
status,
overview, overview,
year, year,
tmdbId, tmdbId,
@@ -69,6 +70,7 @@ class CollectionMovie extends Component {
hasFile, hasFile,
folder, folder,
isAvailable, isAvailable,
movieFile,
isExistingMovie, isExistingMovie,
posterWidth, posterWidth,
posterHeight, posterHeight,
@@ -123,13 +125,15 @@ class CollectionMovie extends Component {
<div className={styles.overlay}> <div className={styles.overlay}>
<div className={styles.overlayTitle}> <div className={styles.overlayTitle}>
{title} {title} {year > 0 ? `(${year})` : ''}
</div> </div>
{ {
id && id ?
<div className={styles.overlayStatus}> <div className={styles.overlayStatus}>
<MovieIndexProgressBar <MovieIndexProgressBar
movieId={id}
movieFile={movieFile}
monitored={monitored} monitored={monitored}
hasFile={hasFile} hasFile={hasFile}
status={status} status={status}
@@ -138,7 +142,8 @@ class CollectionMovie extends Component {
detailedProgressBar={detailedProgressBar} detailedProgressBar={detailedProgressBar}
isAvailable={isAvailable} isAvailable={isAvailable}
/> />
</div> </div> :
null
} }
</div> </div>
</Link> </Link>
@@ -171,12 +176,14 @@ CollectionMovie.propTypes = {
id: PropTypes.number, id: PropTypes.number,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired, year: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired, overview: PropTypes.string.isRequired,
monitored: PropTypes.bool, monitored: PropTypes.bool,
collectionId: PropTypes.number.isRequired, collectionId: PropTypes.number.isRequired,
hasFile: PropTypes.bool, hasFile: PropTypes.bool,
folder: PropTypes.string, folder: PropTypes.string,
isAvailable: PropTypes.bool, isAvailable: PropTypes.bool,
movieFile: PropTypes.object,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired, posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired, posterHeight: PropTypes.number.isRequired,

View File

@@ -5,7 +5,7 @@
margin: 2px 4px; margin: 2px 4px;
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
border-radius: 4px; border-radius: 4px;
background-color: #eee; background-color: var(--inputBackgroundColor);
cursor: default; cursor: default;
} }
@@ -17,7 +17,7 @@
padding: 0 4px; padding: 0 4px;
border-left: 4px; border-left: 4px;
border-left-style: solid; border-left-style: solid;
background-color: var(--white); background-color: var(--themeLightColor);
color: var(--defaultColor); color: var(--defaultColor);
} }

View File

@@ -14,6 +14,7 @@ class CollectionMovieLabel extends Component {
const { const {
id, id,
title, title,
year,
status, status,
monitored, monitored,
isAvailable, isAvailable,
@@ -35,9 +36,7 @@ class CollectionMovieLabel extends Component {
} }
<span> <span>
{ {title} {year > 0 ? `(${year})` : ''}
title
}
</span> </span>
</div> </div>
@@ -62,6 +61,7 @@ class CollectionMovieLabel extends Component {
CollectionMovieLabel.propTypes = { CollectionMovieLabel.propTypes = {
id: PropTypes.number, id: PropTypes.number,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
status: PropTypes.string, status: PropTypes.string,
isAvailable: PropTypes.bool, isAvailable: PropTypes.bool,
monitored: PropTypes.bool, monitored: PropTypes.bool,

View File

@@ -28,7 +28,6 @@ function calculatePosterWidth(posterSize, isSmallScreen) {
} }
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) { function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
const heights = [ const heights = [
overviewOptions.showPosters ? posterHeight : 75, overviewOptions.showPosters ? posterHeight : 75,
isSmallScreen ? columnPaddingSmallScreen : columnPadding isSmallScreen ? columnPaddingSmallScreen : columnPadding
@@ -122,8 +121,8 @@ class CollectionOverviews extends Component {
overviewOptions overviewOptions
} = this.props; } = this.props;
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen); const posterWidth = overviewOptions.showPosters ? calculatePosterWidth(overviewOptions.size, isSmallScreen) : 0;
const posterHeight = calculatePosterHeight(posterWidth); const posterHeight = overviewOptions.showPosters ? calculatePosterHeight(posterWidth) : 0;
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions); const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
this.setState({ this.setState({

View File

@@ -1,9 +1,7 @@
.description {
line-height: $lineHeight;
}
.description { .description {
margin-left: 0; margin-left: 0;
line-height: $lineHeight;
overflow-wrap: break-word;
} }
@media (min-width: 768px) { @media (min-width: 768px) {

View File

@@ -6,9 +6,12 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector'; import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue'; import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue';
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
@@ -58,9 +61,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.DATE: case filterBuilderValueTypes.DATE:
return DateFilterBuilderRowValue; return DateFilterBuilderRowValue;
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
return HistoryEventTypeFilterBuilderRowValue;
case filterBuilderValueTypes.INDEXER: case filterBuilderValueTypes.INDEXER:
return IndexerFilterBuilderRowValueConnector; return IndexerFilterBuilderRowValueConnector;
case filterBuilderValueTypes.LANGUAGE:
return LanguageFilterBuilderRowValue;
case filterBuilderValueTypes.PROTOCOL: case filterBuilderValueTypes.PROTOCOL:
return ProtocolFilterBuilderRowValue; return ProtocolFilterBuilderRowValue;
@@ -70,6 +79,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.QUALITY_PROFILE: case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValueConnector; return QualityProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.MOVIE:
return MovieFilterBuilderRowValue;
case filterBuilderValueTypes.RELEASE_STATUS: case filterBuilderValueTypes.RELEASE_STATUS:
return ReleaseStatusFilterBuilderRowValue; return ReleaseStatusFilterBuilderRowValue;

View File

@@ -0,0 +1,16 @@
import { FilterBuilderProp } from 'App/State/AppState';
interface FilterBuilderRowOnChangeProps {
name: string;
value: unknown[];
}
interface FilterBuilderRowValueProps {
filterType?: string;
filterValue: string | number | object | string[] | number[] | object[];
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
sectionItem: unknown[];
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
}
export default FilterBuilderRowValueProps;

View File

@@ -1,5 +1,5 @@
.tag { .tag {
height: 21px; display: flex;
&.isLastTag { &.isLastTag {
.or { .or {
@@ -18,4 +18,5 @@
.or { .or {
margin: 0 3px; margin: 0 3px;
color: var(--themeDarkColor); color: var(--themeDarkColor);
line-height: 31px;
} }

View File

@@ -7,7 +7,7 @@ import styles from './FilterBuilderRowValueTag.css';
function FilterBuilderRowValueTag(props) { function FilterBuilderRowValueTag(props) {
return ( return (
<span <div
className={styles.tag} className={styles.tag}
> >
<TagInputTag <TagInputTag
@@ -22,7 +22,7 @@ function FilterBuilderRowValueTag(props) {
{translate('Or')} {translate('Or')}
</div> </div>
} }
</span> </div>
); );
} }

View File

@@ -0,0 +1,51 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
const EVENT_TYPE_OPTIONS = [
{
id: 1,
get name() {
return translate('Grabbed');
},
},
{
id: 3,
get name() {
return translate('Imported');
},
},
{
id: 4,
get name() {
return translate('Failed');
},
},
{
id: 6,
get name() {
return translate('Deleted');
},
},
{
id: 8,
get name() {
return translate('Renamed');
},
},
{
id: 9,
get name() {
return translate('Ignored');
},
},
];
function HistoryEventTypeFilterBuilderRowValue(
props: FilterBuilderRowValueProps
) {
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
}
export default HistoryEventTypeFilterBuilderRowValue;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
const { items } = useSelector(createLanguagesSelector());
return <FilterBuilderRowValue {...props} tagList={items} />;
}
export default LanguageFilterBuilderRowValue;

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Movie from 'Movie/Movie';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import sortByName from 'Utilities/Array/sortByName';
import FilterBuilderRowValue from './FilterBuilderRowValue';
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
const allMovies: Movie[] = useSelector(createAllMoviesSelector());
const tagList = allMovies
.map((movie) => ({ id: movie.id, name: movie.title }))
.sort(sortByName);
return <FilterBuilderRowValue {...props} tagList={tagList} />;
}
export default MovieFilterBuilderRowValue;

View File

@@ -2,8 +2,10 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-right: $formLabelRightMarginWidth; margin-right: $formLabelRightMarginWidth;
padding-top: 8px;
min-height: 35px;
text-align: end;
font-weight: bold; font-weight: bold;
line-height: 35px;
} }
.hasError { .hasError {

View File

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

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addRootFolder } from 'Store/Actions/rootFolderActions'; import { addRootFolder } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import RootFolderSelectInput from './RootFolderSelectInput'; import RootFolderSelectInput from './RootFolderSelectInput';
@@ -10,7 +11,7 @@ const ADD_NEW_KEY = 'addNew';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.rootFolders, createRootFoldersSelector(),
(state, { value }) => value, (state, { value }) => value,
(state, { includeMissingValue }) => includeMissingValue, (state, { includeMissingValue }) => includeMissingValue,
(state, { includeNoChange }) => includeNoChange, (state, { includeNoChange }) => includeNoChange,

View File

@@ -63,6 +63,12 @@
width: 1280px; width: 1280px;
} }
.extraExtraLarge {
composes: modal;
width: 1600px;
}
@media only screen and (max-width: $breakpointExtraLarge) { @media only screen and (max-width: $breakpointExtraLarge) {
.modal.extraLarge { .modal.extraLarge {
width: 90%; width: 90%;
@@ -90,7 +96,8 @@
.modal.small, .modal.small,
.modal.medium, .modal.medium,
.modal.large, .modal.large,
.modal.extraLarge { .modal.extraLarge,
.modal.extraExtraLarge {
max-height: 100%; max-height: 100%;
width: 100%; width: 100%;
height: 100% !important; height: 100% !important;

View File

@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'extraExtraLarge': string;
'extraLarge': string; 'extraLarge': string;
'large': string; 'large': string;
'medium': string; 'medium': string;

View File

@@ -3,6 +3,7 @@ import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ModalContent.css'; import styles from './ModalContent.css';
function ModalContent(props) { function ModalContent(props) {
@@ -28,6 +29,7 @@ function ModalContent(props) {
<Icon <Icon
name={icons.CLOSE} name={icons.CLOSE}
size={18} size={18}
title={translate('Close')}
/> />
</Link> </Link>
} }

View File

@@ -82,6 +82,7 @@ class PageHeader extends Component {
aria-label="Donate" aria-label="Donate"
to="https://radarr.video/donate" to="https://radarr.video/donate"
size={14} size={14}
title={translate('Donate')}
/> />
<IconButton <IconButton
className={styles.translate} className={styles.translate}

View File

@@ -24,6 +24,7 @@ function PageHeaderActionsMenu(props) {
<MenuButton className={styles.menuButton} aria-label="Menu Button"> <MenuButton className={styles.menuButton} aria-label="Menu Button">
<Icon <Icon
name={icons.INTERACTIVE} name={icons.INTERACTIVE}
title={translate('Menu')}
/> />
</MenuButton> </MenuButton>

View File

@@ -187,6 +187,8 @@ class SignalRConnector extends Component {
repopulatePage('movieFileUpdated'); repopulatePage('movieFileUpdated');
} else if (body.action === 'deleted') { } else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id }); this.props.dispatchRemoveItem({ section, id: body.resource.id });
repopulatePage('movieFileDeleted');
} }
}; };

View File

@@ -15,5 +15,5 @@
"start_url": "../../../../", "start_url": "../../../../",
"theme_color": "#3a3f51", "theme_color": "#3a3f51",
"background_color": "#3a3f51", "background_color": "#3a3f51",
"display": "standalone" "display": "minimal-ui"
} }

View File

@@ -0,0 +1,120 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
// This file contains some helpers for power users in a browser console
let hasWarned = false;
function checkActivationWarning() {
if (!hasWarned) {
console.log('Activated RadarrApi console helpers.');
console.warn('Be warned: There will be no further confirmation checks.');
hasWarned = true;
}
}
function attachAsyncActions(promise) {
promise.filter = function() {
const args = arguments;
const res = this.then((d) => d.filter(...args));
attachAsyncActions(res);
return res;
};
promise.map = function() {
const args = arguments;
const res = this.then((d) => d.map(...args));
attachAsyncActions(res);
return res;
};
promise.all = function() {
const res = this.then((d) => Promise.all(d));
attachAsyncActions(res);
return res;
};
promise.forEach = function(action) {
const res = this.then((d) => Promise.all(d.map(action)));
attachAsyncActions(res);
return res;
};
}
class ResourceApi {
constructor(api, url) {
this.api = api;
this.url = url;
}
single(id) {
return this.api.fetch(`${this.url}/${id}`);
}
all() {
return this.api.fetch(this.url);
}
filter(pred) {
return this.all().filter(pred);
}
update(resource) {
return this.api.fetch(`${this.url}/${resource.id}`, { method: 'PUT', data: resource });
}
delete(resource) {
if (typeof resource === 'object' && resource !== null && resource.id) {
resource = resource.id;
}
if (!resource || !Number.isInteger(resource)) {
throw Error('Invalid resource', resource);
}
return this.api.fetch(`${this.url}/${resource}`, { method: 'DELETE' });
}
fetch(url, options) {
return this.api.fetch(`${this.url}${url}`, options);
}
}
class ConsoleApi {
constructor() {
this.movie = new ResourceApi(this, '/movie');
}
resource(url) {
return new ResourceApi(this, url);
}
fetch(url, options) {
checkActivationWarning();
options = options || {};
const req = {
url,
method: options.method || 'GET'
};
if (options.data) {
req.dataType = 'json';
req.data = JSON.stringify(options.data);
}
const promise = createAjaxRequest(req).request;
promise.fail((xhr) => {
console.error(`Failed to fetch ${url}`, xhr);
});
attachAsyncActions(promise);
return promise;
}
}
window.RadarrApi = new ConsoleApi();
export default ConsoleApi;

View File

@@ -78,7 +78,8 @@ function createMapDispatchToProps(dispatch, props) {
onImportListSyncPress() { onImportListSyncPress() {
dispatch(executeCommand({ dispatch(executeCommand({
name: commandNames.IMPORT_LIST_SYNC name: commandNames.IMPORT_LIST_SYNC,
commandFinished: this.dispatchFetchListMovies
})); }));
} }
}; };

View File

@@ -50,7 +50,7 @@ $hoverScale: 1.05;
.title { .title {
@add-mixin truncate; @add-mixin truncate;
background-color: #fafbfc; background-color: var(--movieBackgroundColor);
text-align: center; text-align: center;
font-size: $smallFontSize; font-size: $smallFontSize;
} }
@@ -68,6 +68,19 @@ $hoverScale: 1.05;
color: var(--white); color: var(--white);
} }
.existing {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 0;
height: 0;
border-width: 25px 25px 0 0;
border-style: solid;
border-color: #37bc9b transparent transparent;
color: var(--white);
}
.controls { .controls {
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;

View File

@@ -7,6 +7,7 @@ interface CssExports {
'controls': string; 'controls': string;
'editorSelect': string; 'editorSelect': string;
'excluded': string; 'excluded': string;
'existing': string;
'externalLinks': string; 'externalLinks': string;
'link': string; 'link': string;
'overlayTitle': string; 'overlayTitle': string;

View File

@@ -92,6 +92,7 @@ class DiscoverMoviePoster extends Component {
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
timeFormat, timeFormat,
movieRuntimeFormat,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -110,7 +111,7 @@ class DiscoverMoviePoster extends Component {
return ( return (
<div className={styles.content}> <div className={styles.content}>
<div className={styles.posterContainer}> <div className={styles.posterContainer} title={title}>
{ {
<div className={styles.editorSelect}> <div className={styles.editorSelect}>
<CheckInput <CheckInput
@@ -158,6 +159,14 @@ class DiscoverMoviePoster extends Component {
/> />
} }
{
isExisting &&
<div
className={styles.existing}
title={translate('Existing')}
/>
}
<Link <Link
className={styles.link} className={styles.link}
style={elementStyle} style={elementStyle}
@@ -185,7 +194,7 @@ class DiscoverMoviePoster extends Component {
{ {
showTitle && showTitle &&
<div className={styles.title}> <div className={styles.title} title={title}>
{title} {title}
</div> </div>
} }
@@ -194,6 +203,7 @@ class DiscoverMoviePoster extends Component {
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}
movieRuntimeFormat={movieRuntimeFormat}
{...otherProps} {...otherProps}
/> />
@@ -236,6 +246,7 @@ DiscoverMoviePoster.propTypes = {
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
isExisting: PropTypes.bool.isRequired, isExisting: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool.isRequired, isExcluded: PropTypes.bool.isRequired,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,

View File

@@ -5,9 +5,11 @@ import DiscoverMoviePoster from './DiscoverMoviePoster';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.ui.item.movieRuntimeFormat,
createDimensionsSelector(), createDimensionsSelector(),
( dimensions) => { (movieRuntimeFormat, dimensions) => {
return { return {
movieRuntimeFormat,
isSmallScreen: dimensions.isSmallScreen isSmallScreen: dimensions.isSmallScreen
}; };
} }

View File

@@ -1,5 +1,5 @@
.info { .info {
background-color: #fafbfc; background-color: var(--movieBackgroundColor);
text-align: center; text-align: center;
font-size: $smallFontSize; font-size: $smallFontSize;
} }

View File

@@ -1,9 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import { icons } from 'Helpers/Props';
import { getMovieStatusDetails } from 'Movie/MovieStatus'; import { getMovieStatusDetails } from 'Movie/MovieStatus';
import formatRuntime from 'Utilities/Date/formatRuntime'; import formatRuntime from 'Utilities/Date/formatRuntime';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate';
import styles from './DiscoverMoviePosterInfo.css'; import styles from './DiscoverMoviePosterInfo.css';
function DiscoverMoviePosterInfo(props) { function DiscoverMoviePosterInfo(props) {
@@ -19,12 +22,13 @@ function DiscoverMoviePosterInfo(props) {
sortKey, sortKey,
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
timeFormat timeFormat,
movieRuntimeFormat
} = props; } = props;
if (sortKey === 'status' && status) { if (sortKey === 'status' && status) {
return ( return (
<div className={styles.info}> <div className={styles.info} title={translate('Status')}>
{getMovieStatusDetails(status).title} {getMovieStatusDetails(status).title}
</div> </div>
); );
@@ -32,7 +36,7 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'studio' && studio) { if (sortKey === 'studio' && studio) {
return ( return (
<div className={styles.info}> <div className={styles.info} title={translate('Studio')}>
{studio} {studio}
</div> </div>
); );
@@ -50,8 +54,8 @@ function DiscoverMoviePosterInfo(props) {
); );
return ( return (
<div className={styles.info}> <div className={styles.info} title={translate('InCinemas')}>
{`In Cinemas ${inCinemasDate}`} <Icon name={icons.IN_CINEMAS} /> {inCinemasDate}
</div> </div>
); );
} }
@@ -68,8 +72,8 @@ function DiscoverMoviePosterInfo(props) {
); );
return ( return (
<div className={styles.info}> <div className={styles.info} title={translate('DigitalRelease')}>
{`Digital ${digitalReleaseDate}`} <Icon name={icons.MOVIE_FILE} /> {digitalReleaseDate}
</div> </div>
); );
} }
@@ -86,15 +90,15 @@ function DiscoverMoviePosterInfo(props) {
); );
return ( return (
<div className={styles.info}> <div className={styles.info} title={translate('PhysicalRelease')}>
{`Released ${physicalReleaseDate}`} <Icon name={icons.DISC} /> {physicalReleaseDate}
</div> </div>
); );
} }
if (sortKey === 'certification' && certification) { if (sortKey === 'certification' && certification) {
return ( return (
<div className={styles.info}> <div className={styles.info} title={translate('Certification')}>
{certification} {certification}
</div> </div>
); );
@@ -102,8 +106,8 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'runtime' && runtime) { if (sortKey === 'runtime' && runtime) {
return ( return (
<div className={styles.info}> <div className={styles.info} title={translate('Runtime')}>
{formatRuntime(runtime)} {formatRuntime(runtime, movieRuntimeFormat)}
</div> </div>
); );
} }
@@ -111,9 +115,7 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'ratings' && ratings) { if (sortKey === 'ratings' && ratings) {
return ( return (
<div className={styles.info}> <div className={styles.info}>
<TmdbRating <TmdbRating ratings={ratings} />
ratings={ratings}
/>
</div> </div>
); );
} }
@@ -133,7 +135,8 @@ DiscoverMoviePosterInfo.propTypes = {
sortKey: PropTypes.string.isRequired, sortKey: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired timeFormat: PropTypes.string.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired
}; };
export default DiscoverMoviePosterInfo; export default DiscoverMoviePosterInfo;

View File

@@ -76,6 +76,7 @@ class DiscoverMovieRow extends Component {
ratings, ratings,
popularity, popularity,
certification, certification,
movieRuntimeFormat,
collection, collection,
columns, columns,
isExisting, isExisting,
@@ -230,7 +231,7 @@ class DiscoverMovieRow extends Component {
key={name} key={name}
className={styles[name]} className={styles[name]}
> >
{formatRuntime(runtime)} {formatRuntime(runtime, movieRuntimeFormat)}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@@ -397,6 +398,7 @@ DiscoverMovieRow.propTypes = {
popularity: PropTypes.number.isRequired, popularity: PropTypes.number.isRequired,
certification: PropTypes.string, certification: PropTypes.string,
collection: PropTypes.object, collection: PropTypes.object,
movieRuntimeFormat: PropTypes.string.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isExisting: PropTypes.bool.isRequired, isExisting: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool.isRequired, isExcluded: PropTypes.bool.isRequired,

View File

@@ -5,9 +5,11 @@ import DiscoverMovieRow from './DiscoverMovieRow';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.ui.item.movieRuntimeFormat,
createDimensionsSelector(), createDimensionsSelector(),
(dimensions) => { (movieRuntimeFormat, dimensions) => {
return { return {
movieRuntimeFormat,
isSmallScreen: dimensions.isSmallScreen isSmallScreen: dimensions.isSmallScreen
}; };
} }

View File

@@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
authenticationMethod, authenticationMethod,
authenticationRequired, authenticationRequired,
username, username,
password password,
passwordConfirmation
} = settings; } = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
@@ -63,7 +64,7 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert} className={styles.authRequiredAlert}
kind={kinds.WARNING} kind={kinds.WARNING}
> >
{translate('AuthenticationRequiredWarning', { appName: 'Radarr' })} {translate('AuthenticationRequiredWarning')}
</Alert> </Alert>
{ {
@@ -76,7 +77,7 @@ function AuthenticationRequiredModalContent(props) {
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="authenticationMethod" name="authenticationMethod"
values={authenticationMethodOptions} values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Radarr' })} helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined} helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication" helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
onChange={onInputChange} onChange={onInputChange}
@@ -120,6 +121,18 @@ function AuthenticationRequiredModalContent(props) {
{...password} {...password}
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
{...passwordConfirmation}
/>
</FormGroup>
</div> : </div> :
null null
} }

View File

@@ -2,10 +2,13 @@ export const BOOL = 'bool';
export const BYTES = 'bytes'; export const BYTES = 'bytes';
export const DATE = 'date'; export const DATE = 'date';
export const DEFAULT = 'default'; export const DEFAULT = 'default';
export const HISTORY_EVENT_TYPE = 'historyEventType';
export const INDEXER = 'indexer'; export const INDEXER = 'indexer';
export const LANGUAGE = 'language';
export const PROTOCOL = 'protocol'; export const PROTOCOL = 'protocol';
export const QUALITY = 'quality'; export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile'; export const QUALITY_PROFILE = 'qualityProfile';
export const MOVIE = 'movie';
export const RELEASE_STATUS = 'releaseStatus'; export const RELEASE_STATUS = 'releaseStatus';
export const MINIMUM_AVAILABILITY = 'minimumAvailability'; export const MINIMUM_AVAILABILITY = 'minimumAvailability';
export const TAG = 'tag'; export const TAG = 'tag';

View File

@@ -3,5 +3,6 @@ export const SMALL = 'small';
export const MEDIUM = 'medium'; export const MEDIUM = 'medium';
export const LARGE = 'large'; export const LARGE = 'large';
export const EXTRA_LARGE = 'extraLarge'; export const EXTRA_LARGE = 'extraLarge';
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE]; export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];

View File

@@ -3,13 +3,16 @@ import React, { Fragment } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; 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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sortDirections } from 'Helpers/Props'; import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector'; import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearchContent.css'; import styles from './InteractiveSearch.css';
const columns = [ const columns = [
{ {
@@ -24,23 +27,6 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{ {
name: 'title', name: 'title',
label: () => translate('Title'), label: () => translate('Title'),
@@ -84,12 +70,6 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'customFormat',
label: () => translate('Formats'),
isSortable: true,
isVisible: true
},
{ {
name: 'customFormatScore', name: 'customFormatScore',
label: React.createElement(Icon, { label: React.createElement(Icon, {
@@ -107,10 +87,27 @@ const columns = [
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
} }
]; ];
function InteractiveSearchContent(props) { function InteractiveSearch(props) {
const { const {
searchPayload, searchPayload,
isFetching, isFetching,
@@ -118,18 +115,36 @@ function InteractiveSearchContent(props) {
error, error,
totalReleasesCount, totalReleasesCount,
items, items,
selectedFilterKey,
filters,
customFilters,
sortKey, sortKey,
sortDirection, sortDirection,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
onSortPress, onSortPress,
onFilterSelect,
onGrabPress onGrabPress
} = props; } = props;
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
const type = 'movies';
return ( return (
<div> <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 ? <LoadingIndicator /> : null
} }
@@ -203,19 +218,23 @@ function InteractiveSearchContent(props) {
); );
} }
InteractiveSearchContent.propTypes = { InteractiveSearch.propTypes = {
searchPayload: PropTypes.object.isRequired, searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
totalReleasesCount: PropTypes.number.isRequired, totalReleasesCount: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).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, sortKey: PropTypes.string,
sortDirection: PropTypes.string, sortDirection: PropTypes.string,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired onGrabPress: PropTypes.func.isRequired
}; };
export default InteractiveSearchContent; export default InteractiveSearch;

View File

@@ -2,10 +2,11 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import * as releaseActions from 'Store/Actions/releaseActions'; import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearchContent from './InteractiveSearchContent'; import InteractiveSearch from './InteractiveSearch';
function createMapStateToProps(appState) { function createMapStateToProps(appState) {
return createSelector( return createSelector(
@@ -29,8 +30,12 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(releaseActions.fetchReleases(payload)); dispatch(releaseActions.fetchReleases(payload));
}, },
dispatchClearReleases(payload) { dispatchFetchMovieHistory({ movieId }) {
dispatch(releaseActions.clearReleases(payload)); dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
}, },
onSortPress(sortKey, sortDirection) { onSortPress(sortKey, sortDirection) {
@@ -38,8 +43,7 @@ function createMapDispatchToProps(dispatch, props) {
}, },
onFilterSelect(selectedFilterKey) { onFilterSelect(selectedFilterKey) {
const action = releaseActions.setReleasesFilter; dispatch(releaseActions.setReleasesFilter({ selectedFilterKey }));
dispatch(action({ selectedFilterKey }));
}, },
onGrabPress(payload) { onGrabPress(payload) {
@@ -48,7 +52,7 @@ function createMapDispatchToProps(dispatch, props) {
}; };
} }
class InteractiveSearchContentConnector extends Component { class InteractiveSearchConnector extends Component {
// //
// Lifecycle // Lifecycle
@@ -57,7 +61,8 @@ class InteractiveSearchContentConnector extends Component {
const { const {
searchPayload, searchPayload,
isPopulated, isPopulated,
dispatchFetchReleases dispatchFetchReleases,
dispatchFetchMovieHistory
} = this.props; } = this.props;
// If search results are not yet isPopulated fetch them, // If search results are not yet isPopulated fetch them,
@@ -65,6 +70,12 @@ class InteractiveSearchContentConnector extends Component {
if (!isPopulated) { if (!isPopulated) {
dispatchFetchReleases(searchPayload); dispatchFetchReleases(searchPayload);
} }
dispatchFetchMovieHistory(searchPayload);
}
componentWillUnmount() {
this.props.dispatchClearMovieHistory();
} }
// //
@@ -73,24 +84,26 @@ class InteractiveSearchContentConnector extends Component {
render() { render() {
const { const {
dispatchFetchReleases, dispatchFetchReleases,
dispatchClearReleases, dispatchFetchMovieHistory,
dispatchClearMovieHistory,
...otherProps ...otherProps
} = this.props; } = this.props;
return ( return (
<InteractiveSearchContent <InteractiveSearch
{...otherProps} {...otherProps}
/> />
); );
} }
} }
InteractiveSearchContentConnector.propTypes = { InteractiveSearchConnector.propTypes = {
searchPayload: PropTypes.object.isRequired, searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired, dispatchFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector); export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);

View File

@@ -4,7 +4,7 @@ import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton'; import PageMenuButton from 'Components/Menu/PageMenuButton';
import { align } from 'Helpers/Props'; import { align } from 'Helpers/Props';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import styles from './InteractiveSearchContent.css'; import styles from './InteractiveSearch.css';
function InteractiveSearchFilterMenu(props) { function InteractiveSearchFilterMenu(props) {
const { const {

View File

@@ -1,23 +1,25 @@
.cell {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.protocol { .protocol {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px; width: 80px;
} }
.titleContent {
display: flex;
align-items: center;
justify-content: space-between;
word-break: break-all;
}
.indexer { .indexer {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 85px; width: 85px;
} }
.quality, .quality,
.customFormat,
.languages { .languages {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
} }
.languages { .languages {
@@ -25,7 +27,7 @@
} }
.customFormatScore { .customFormatScore {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px; width: 55px;
font-weight: bold; font-weight: bold;
@@ -33,31 +35,28 @@
} }
.rejected, .rejected,
.indexerFlags { .indexerFlags,
composes: cell; .download {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px; width: 50px;
} }
.age, .age,
.size { .size {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
white-space: nowrap; white-space: nowrap;
} }
.peers { .peers {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px; width: 75px;
} }
.titleContent {
overflow-wrap: break-word;
}
.history { .history {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px; width: 75px;
} }
@@ -67,7 +66,7 @@
} }
.download { .download {
composes: cell; composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px; width: 80px;
} }

View File

@@ -3,8 +3,6 @@
interface CssExports { interface CssExports {
'age': string; 'age': string;
'blocklist': string; 'blocklist': string;
'cell': string;
'customFormat': string;
'customFormatScore': string; 'customFormatScore': string;
'download': string; 'download': string;
'downloadIcon': string; 'downloadIcon': string;

View File

@@ -133,9 +133,9 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
longDateFormat, longDateFormat,
timeFormat, timeFormat,
grabError, grabError,
historyGrabbedData, historyGrabbedData = {} as MovieHistory,
historyFailedData, historyFailedData = {} as MovieHistory,
blocklistData, blocklistData = {} as MovieBlocklist,
searchPayload, searchPayload,
onGrabPress, onGrabPress,
} = props; } = props;
@@ -199,53 +199,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
{formatAge(age, ageHours, ageMinutes)} {formatAge(age, ageHours, ageMinutes)}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.RIGHT}
/>
) : null}
</TableRowCell>
<TableRowCell> <TableRowCell>
<div className={styles.titleContent}> <div className={styles.titleContent}>
<Link to={infoUrl} title={title}> <Link to={infoUrl} title={title}>
@@ -316,10 +269,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
<MovieQuality quality={quality} /> <MovieQuality quality={quality} />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
<Tooltip <Tooltip
anchor={formatCustomFormatScore( anchor={formatCustomFormatScore(
@@ -348,6 +297,53 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
) : null} ) : null}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<ConfirmModal <ConfirmModal
isOpen={isConfirmGrabModalOpen} isOpen={isConfirmGrabModalOpen}
kind={kinds.WARNING} kind={kinds.WARNING}

View File

@@ -1,16 +0,0 @@
import React from 'react';
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
function InteractiveSearchTable(props) {
return (
<InteractiveSearchContentConnector
searchPayload={props}
/>
);
}
InteractiveSearchTable.propTypes = {
};
export default InteractiveSearchTable;

View File

@@ -1,11 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Label from 'Components/Label'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot'; import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector'; import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css'; import styles from '../MovieCreditPoster.css';
class MovieCastPoster extends Component { class MovieCastPoster extends Component {
@@ -60,7 +57,7 @@ class MovieCastPoster extends Component {
images, images,
posterWidth, posterWidth,
posterHeight, posterHeight,
importListId importList
} = this.props; } = this.props;
const { const {
@@ -69,36 +66,31 @@ class MovieCastPoster extends Component {
const elementStyle = { const elementStyle = {
width: `${posterWidth}px`, width: `${posterWidth}px`,
height: `${posterHeight}px` height: `${posterHeight}px`,
borderRadius: '5px'
}; };
const contentStyle = { const contentStyle = {
width: `${posterWidth}px` width: `${posterWidth}px`
}; };
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return ( return (
<div <div
className={styles.content} className={styles.content}
style={contentStyle} style={contentStyle}
> >
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
<Label className={styles.controls}> <div className={styles.controls}>
{ <MonitorToggleButton
importListId > 0 ? className={styles.action}
<IconButton monitored={monitored}
className={styles.action} size={20}
name={icons.EDIT} onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
title={translate('EditPerson')} />
onPress={this.onEditImportListPress} </div>
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title={translate('FollowPerson')}
onPress={this.onAddImportListPress}
/>
}
</Label>
<div <div
style={elementStyle} style={elementStyle}
@@ -148,12 +140,8 @@ MovieCastPoster.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired, posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired, posterHeight: PropTypes.number.isRequired,
importListId: PropTypes.number.isRequired, importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired onImportListSelect: PropTypes.func.isRequired
}; };
MovieCastPoster.defaultProps = {
importListId: 0
};
export default MovieCastPoster; export default MovieCastPoster;

View File

@@ -1,11 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Label from 'Components/Label'; import MonitorToggleButton from 'Components/MonitorToggleButton';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot'; import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector'; import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css'; import styles from '../MovieCreditPoster.css';
class MovieCrewPoster extends Component { class MovieCrewPoster extends Component {
@@ -60,7 +57,7 @@ class MovieCrewPoster extends Component {
images, images,
posterWidth, posterWidth,
posterHeight, posterHeight,
importListId importList
} = this.props; } = this.props;
const { const {
@@ -69,36 +66,31 @@ class MovieCrewPoster extends Component {
const elementStyle = { const elementStyle = {
width: `${posterWidth}px`, width: `${posterWidth}px`,
height: `${posterHeight}px` height: `${posterHeight}px`,
borderRadius: '5px'
}; };
const contentStyle = { const contentStyle = {
width: `${posterWidth}px` width: `${posterWidth}px`
}; };
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return ( return (
<div <div
className={styles.content} className={styles.content}
style={contentStyle} style={contentStyle}
> >
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
<Label className={styles.controls}> <div className={styles.controls}>
{ <MonitorToggleButton
importListId > 0 ? className={styles.action}
<IconButton monitored={monitored}
className={styles.action} size={20}
name={icons.EDIT} onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
title={translate('EditPerson')} />
onPress={this.onEditImportListPress} </div>
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title={translate('FollowPerson')}
onPress={this.onAddImportListPress}
/>
}
</Label>
<div <div
style={elementStyle} style={elementStyle}
@@ -148,12 +140,8 @@ MovieCrewPoster.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired, posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired, posterHeight: PropTypes.number.isRequired,
importListId: PropTypes.number.isRequired, importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired onImportListSelect: PropTypes.func.isRequired
}; };
MovieCrewPoster.defaultProps = {
importListId: 0
};
export default MovieCrewPoster; export default MovieCrewPoster;

View File

@@ -18,7 +18,7 @@ function createMapStateToProps() {
}, []); }, []);
return { return {
items: crew items: _.uniqBy(crew, 'personName')
}; };
} }
); );

View File

@@ -1,17 +1,13 @@
$hoverScale: 1.05; $hoverScale: 1.05;
.content { .content {
border-radius: '5px';
transition: all 200ms ease-in; transition: all 200ms ease-in;
&:hover { &:hover {
z-index: 2; z-index: 2;
box-shadow: 0 0 12px var(--black); box-shadow: 0 0 12px var(--black);
transition: all 200ms ease-in; transition: all 200ms ease-in;
.controls {
opacity: 0.9;
transition: opacity 200ms linear 150ms;
}
} }
} }
@@ -50,22 +46,18 @@ $hoverScale: 1.05;
.controls { .controls {
position: absolute; position: absolute;
bottom: 10px; top: 10px;
left: 10px;
z-index: 3; z-index: 3;
border-radius: 4px;
background-color: #707070;
color: var(--white);
font-size: $smallFontSize;
opacity: 0;
transition: opacity 0;
} }
.action { .action {
composes: button from '~Components/Link/IconButton.css'; composes: toggleButton from '~Components/MonitorToggleButton.css';
width: 25px;
color: var(--white);
&:hover { &:hover {
color: var(--radarrYellow); color: var(--iconButtonHoverLightColor);
} }
} }

View File

@@ -1,11 +1,19 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions'; import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector'; import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
function createMapStateToProps() { function createMapStateToProps() {
return createMovieCreditListSelector(); return createSelector(
createMovieCreditListSelector(),
(importList) => {
return {
importList
};
}
);
} }
const mapDispatchToProps = { const mapDispatchToProps = {
@@ -20,7 +28,7 @@ class MovieCreditPosterConnector extends Component {
// Listeners // Listeners
onImportListSelect = () => { onImportListSelect = () => {
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', presetName: undefined }); this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', implementationName: 'TMDb Person', presetName: undefined });
this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() }); this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` }); this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` });
}; };

View File

@@ -2,6 +2,10 @@
flex: 1 0 auto; flex: 1 0 auto;
} }
.movie {
padding: 10px;
}
.container { .container {
padding: 10px; padding: 10px;
} }

View File

@@ -3,6 +3,7 @@
interface CssExports { interface CssExports {
'container': string; 'container': string;
'grid': string; 'grid': string;
'movie': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View File

@@ -1,12 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized'; import { Navigation } from 'swiper';
import Measure from 'Components/Measure'; import { Swiper, SwiperSlide } from 'swiper/react';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import MovieCreditPosterConnector from './MovieCreditPosterConnector'; import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCreditPosters.css'; import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions // Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding); const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
@@ -65,39 +68,11 @@ class MovieCreditPosters extends Component {
}; };
this._isInitialized = false; this._isInitialized = false;
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
const {
items
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
if (this._grid &&
(prevState.width !== width ||
prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
} }
// //
// Control // Control
setGridRef = (ref) => {
this._grid = ref;
};
calculateGrid = (width = this.state.width, isSmallScreen) => { calculateGrid = (width = this.state.width, isSmallScreen) => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
@@ -117,7 +92,10 @@ class MovieCreditPosters extends Component {
}); });
}; };
cellRenderer = ({ key, rowIndex, columnIndex, style }) => { //
// Render
render() {
const { const {
items, items,
itemComponent itemComponent
@@ -126,99 +104,44 @@ class MovieCreditPosters extends Component {
const { const {
posterWidth, posterWidth,
posterHeight, posterHeight,
columnCount
} = this.state;
const movieIdx = rowIndex * columnCount + columnIndex;
const movie = items[movieIdx];
if (!movie) {
return null;
}
return (
<div
className={styles.container}
key={key}
style={style}
>
<MovieCreditPosterConnector
key={movie.order}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={movie.personTmdbId}
personName={movie.personName}
job={movie.job}
character={movie.character}
images={movie.images}
/>
</div>
);
};
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
};
//
// Render
render() {
const {
items
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight rowHeight
} = this.state; } = this.state;
const rowCount = Math.ceil(items.length / columnCount);
return ( return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={undefined}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return ( <div className={styles.sliderContainer}>
<div ref={registerChild}> <Swiper
<Grid slidesPerView='auto'
ref={this.setGridRef} spaceBetween={10}
className={styles.grid} slidesPerGroup={3}
autoHeight={true} loop={false}
height={height} loopFillGroupWithBlank={true}
columnCount={columnCount} className="mySwiper"
columnWidth={columnWidth} modules={[Navigation]}
rowCount={rowCount} onInit={(swiper) => {
rowHeight={rowHeight} swiper.params.navigation.prevEl = this._swiperPrevRef;
width={width} swiper.params.navigation.nextEl = this._swiperNextRef;
onScroll={onChildScroll} swiper.navigation.init();
scrollTop={scrollTop} swiper.navigation.update();
overscanRowCount={2} }}
cellRenderer={this.cellRenderer} >
scrollToAlignment={'start'} {items.map((credit) => (
isScrollingOptOut={true} <SwiperSlide key={credit.id} style={{ width: posterWidth, height: rowHeight }}>
/> <MovieCreditPosterConnector
</div> key={credit.id}
); component={itemComponent}
} posterWidth={posterWidth}
} posterHeight={posterHeight}
</WindowScroller> tmdbId={credit.personTmdbId}
</Measure> personName={credit.personName}
job={credit.job}
character={credit.character}
images={credit.images}
/>
</SwiperSlide>
))}
</Swiper>
</div>
); );
} }
} }

View File

@@ -1,3 +0,0 @@
.alternateTitle {
white-space: nowrap;
}

View File

@@ -1,28 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './MovieAlternateTitles.css';
function MovieAlternateTitles({ alternateTitles }) {
return (
<ul>
{
alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
return (
<li
key={alternateTitle}
className={styles.alternateTitle}
>
{alternateTitle}
</li>
);
})
}
</ul>
);
}
MovieAlternateTitles.propTypes = {
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default MovieAlternateTitles;

View File

@@ -5,7 +5,7 @@
.header { .header {
position: relative; position: relative;
width: 100%; width: 100%;
height: 375px; height: 425px;
} }
.errorMessage { .errorMessage {
@@ -39,10 +39,11 @@
} }
.poster { .poster {
z-index: 2;
flex-shrink: 0; flex-shrink: 0;
margin-right: 35px; margin-right: 35px;
width: 217px; width: 250px;
height: 319px; height: 368px;
} }
.info { .info {
@@ -202,6 +203,12 @@
.headerContent { .headerContent {
padding: 15px; padding: 15px;
} }
.title {
font-weight: 300;
font-size: 30px;
line-height: 30px;
}
} }
@media only screen and (max-width: $breakpointLarge) { @media only screen and (max-width: $breakpointLarge) {

View File

@@ -1,9 +1,9 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import TextTruncate from 'react-text-truncate'; import TextTruncate from 'react-text-truncate';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating'; import ImdbRating from 'Components/ImdbRating';
import InfoLabel from 'Components/InfoLabel'; import InfoLabel from 'Components/InfoLabel';
@@ -23,12 +23,11 @@ import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieHistoryTable from 'Movie/History/MovieHistoryTable'; import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable'; import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable'; import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -38,13 +37,11 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
import formatRuntime from 'Utilities/Date/formatRuntime'; import formatRuntime from 'Utilities/Date/formatRuntime';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector'; import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector'; import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector'; import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
import MovieDetailsLinks from './MovieDetailsLinks'; import MovieDetailsLinks from './MovieDetailsLinks';
import MovieReleaseDatesConnector from './MovieReleaseDatesConnector'; import MovieReleaseDates from './MovieReleaseDates';
import MovieStatusLabel from './MovieStatusLabel'; import MovieStatusLabel from './MovieStatusLabel';
import MovieTagsConnector from './MovieTagsConnector'; import MovieTagsConnector from './MovieTagsConnector';
import MovieTitlesTable from './Titles/MovieTitlesTable'; import MovieTitlesTable from './Titles/MovieTitlesTable';
@@ -57,14 +54,6 @@ function getFanartUrl(images) {
return _.find(images, { coverType: 'fanart' })?.url; return _.find(images, { coverType: 'fanart' })?.url;
} }
function getExpandedState(newState) {
return {
allExpanded: newState.allSelected,
allCollapsed: newState.allUnselected,
expandedState: newState.selectedState
};
}
class MovieDetails extends Component { class MovieDetails extends Component {
// //
@@ -78,10 +67,8 @@ class MovieDetails extends Component {
isEditMovieModalOpen: false, isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false, isDeleteMovieModalOpen: false,
isInteractiveImportModalOpen: false, isInteractiveImportModalOpen: false,
allExpanded: false, isInteractiveSearchModalOpen: false,
allCollapsed: false, isMovieHistoryModalOpen: false,
expandedState: {},
selectedTabIndex: 0,
overviewHeight: 0, overviewHeight: 0,
titleWidth: 0 titleWidth: 0
}; };
@@ -114,10 +101,6 @@ class MovieDetails extends Component {
this.setState({ isOrganizeModalOpen: false }); this.setState({ isOrganizeModalOpen: false });
}; };
onManageEpisodesPress = () => {
this.setState({ isManageEpisodesOpen: true });
};
onInteractiveImportPress = () => { onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true }); this.setState({ isInteractiveImportModalOpen: true });
}; };
@@ -134,6 +117,14 @@ class MovieDetails extends Component {
this.setState({ isEditMovieModalOpen: false }); this.setState({ isEditMovieModalOpen: false });
}; };
onInteractiveSearchPress = () => {
this.setState({ isInteractiveSearchModalOpen: true });
};
onInteractiveSearchModalClose = () => {
this.setState({ isInteractiveSearchModalOpen: false });
};
onDeleteMoviePress = () => { onDeleteMoviePress = () => {
this.setState({ this.setState({
isEditMovieModalOpen: false, isEditMovieModalOpen: false,
@@ -145,27 +136,12 @@ class MovieDetails extends Component {
this.setState({ isDeleteMovieModalOpen: false }); this.setState({ isDeleteMovieModalOpen: false });
}; };
onExpandAllPress = () => { onMovieHistoryPress = () => {
const { this.setState({ isMovieHistoryModalOpen: true });
allExpanded,
expandedState
} = this.state;
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
}; };
onExpandPress = (seasonNumber, isExpanded) => { onMovieHistoryModalClose = () => {
this.setState((state) => { this.setState({ isMovieHistoryModalOpen: false });
const convertedState = {
allSelected: state.allExpanded,
allUnselected: state.allCollapsed,
selectedState: state.expandedState
};
const newState = toggleSelected(convertedState, [], seasonNumber, isExpanded, false);
return getExpandedState(newState);
});
}; };
onMeasure = ({ height }) => { onMeasure = ({ height }) => {
@@ -204,7 +180,12 @@ class MovieDetails extends Component {
if ( if (
touchStart < 50 || touchStart < 50 ||
this.props.isSidebarVisible || this.props.isSidebarVisible ||
this.state.isEventModalOpen this.state.isOrganizeModalOpen ||
this.state.isEditMovieModalOpen ||
this.state.isDeleteMovieModalOpen ||
this.state.isInteractiveImportModalOpen ||
this.state.isInteractiveSearchModalOpen ||
this.state.isMovieHistoryModalOpen
) { ) {
return; return;
} }
@@ -239,10 +220,6 @@ class MovieDetails extends Component {
} }
}; };
onTabSelect = (index, lastIndex) => {
this.setState({ selectedTabIndex: index });
};
// //
// Render // Render
@@ -286,7 +263,7 @@ class MovieDetails extends Component {
onMonitorTogglePress, onMonitorTogglePress,
onRefreshPress, onRefreshPress,
onSearchPress, onSearchPress,
queueItems, queueItem,
movieRuntimeFormat movieRuntimeFormat
} = this.props; } = this.props;
@@ -295,9 +272,10 @@ class MovieDetails extends Component {
isEditMovieModalOpen, isEditMovieModalOpen,
isDeleteMovieModalOpen, isDeleteMovieModalOpen,
isInteractiveImportModalOpen, isInteractiveImportModalOpen,
isInteractiveSearchModalOpen,
isMovieHistoryModalOpen,
overviewHeight, overviewHeight,
titleWidth, titleWidth
selectedTabIndex
} = this.state; } = this.state;
const fanartUrl = getFanartUrl(images); const fanartUrl = getFanartUrl(images);
@@ -324,6 +302,14 @@ class MovieDetails extends Component {
onPress={onSearchPress} onPress={onSearchPress}
/> />
<PageToolbarButton
label={translate('InteractiveSearch')}
iconName={icons.INTERACTIVE}
isSpinning={isSearching}
title={undefined}
onPress={this.onInteractiveSearchPress}
/>
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
@@ -339,6 +325,12 @@ class MovieDetails extends Component {
onPress={this.onInteractiveImportPress} onPress={this.onInteractiveImportPress}
/> />
<PageToolbarButton
label={translate('History')}
iconName={icons.HISTORY}
onPress={this.onMovieHistoryPress}
/>
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
@@ -433,7 +425,7 @@ class MovieDetails extends Component {
} }
title={translate('ReleaseDates')} title={translate('ReleaseDates')}
body={ body={
<MovieReleaseDatesConnector <MovieReleaseDates
inCinemas={inCinemas} inCinemas={inCinemas}
physicalRelease={physicalRelease} physicalRelease={physicalRelease}
digitalRelease={digitalRelease} digitalRelease={digitalRelease}
@@ -544,7 +536,7 @@ class MovieDetails extends Component {
hasMovieFiles={hasMovieFiles} hasMovieFiles={hasMovieFiles}
monitored={monitored} monitored={monitored}
isAvailable={isAvailable} isAvailable={isAvailable}
queueItem={(queueItems.length > 0) ? queueItems[0] : null} queueItem={queueItem}
/> />
</span> </span>
</InfoLabel> </InfoLabel>
@@ -654,101 +646,33 @@ class MovieDetails extends Component {
null null
} }
<Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}> <FieldSet legend={translate('Files')}>
<TabList <MovieFileEditorTable
className={styles.tabList} movieId={id}
> />
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('History')}
</Tab>
<Tab <ExtraFileTable
className={styles.tab} movieId={id}
selectedClassName={styles.selectedTab} />
> </FieldSet>
{translate('Search')}
</Tab>
<Tab <FieldSet legend={translate('Cast')}>
className={styles.tab} <MovieCastPostersConnector
selectedClassName={styles.selectedTab} isSmallScreen={isSmallScreen}
> />
{translate('Files')} </FieldSet>
</Tab>
<Tab <FieldSet legend={translate('Crew')}>
className={styles.tab} <MovieCrewPostersConnector
selectedClassName={styles.selectedTab} isSmallScreen={isSmallScreen}
> />
{translate('Titles')} </FieldSet>
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Cast')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Crew')}
</Tab>
{
selectedTabIndex === 1 &&
<div className={styles.filterIcon}>
<InteractiveSearchFilterMenuConnector />
</div>
}
</TabList>
<TabPanel>
<MovieHistoryTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<InteractiveSearchTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieFileEditorTable
movieId={id}
/>
<ExtraFileTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieTitlesTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
<TabPanel>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
</Tabs>
<FieldSet legend={translate('Titles')}>
<MovieTitlesTable
movieId={id}
/>
</FieldSet>
</div> </div>
<OrganizePreviewModalConnector <OrganizePreviewModalConnector
@@ -764,6 +688,12 @@ class MovieDetails extends Component {
onDeleteMoviePress={this.onDeleteMoviePress} onDeleteMoviePress={this.onDeleteMoviePress}
/> />
<MovieHistoryModal
isOpen={isMovieHistoryModalOpen}
movieId={id}
onModalClose={this.onMovieHistoryModalClose}
/>
<DeleteMovieModal <DeleteMovieModal
isOpen={isDeleteMovieModalOpen} isOpen={isDeleteMovieModalOpen}
movieId={id} movieId={id}
@@ -780,6 +710,12 @@ class MovieDetails extends Component {
showImportMode={false} showImportMode={false}
onModalClose={this.onInteractiveImportModalClose} onModalClose={this.onInteractiveImportModalClose}
/> />
<MovieInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
movieId={id}
onModalClose={this.onInteractiveSearchModalClose}
/>
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );
@@ -830,7 +766,7 @@ MovieDetails.propTypes = {
onRefreshPress: PropTypes.func.isRequired, onRefreshPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired,
onGoToMovie: PropTypes.func.isRequired, onGoToMovie: PropTypes.func.isRequired,
queueItems: PropTypes.arrayOf(PropTypes.object), queueItem: PropTypes.object,
movieRuntimeFormat: PropTypes.string.isRequired movieRuntimeFormat: PropTypes.string.isRequired
}; };

View File

@@ -11,7 +11,6 @@ import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions'; import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions'; import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions'; import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions'; import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import { fetchImportListSchema } from 'Store/Actions/settingsActions'; import { fetchImportListSchema } from 'Store/Actions/settingsActions';
@@ -145,6 +144,8 @@ function createMapStateToProps() {
return acc; return acc;
}, []); }, []);
const queueItem = queueItems.find((item) => item.movieId === movie.id);
return { return {
...movie, ...movie,
alternateTitles, alternateTitles,
@@ -165,7 +166,7 @@ function createMapStateToProps() {
nextMovie, nextMovie,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
isSidebarVisible, isSidebarVisible,
queueItems, queueItem,
movieRuntimeFormat movieRuntimeFormat
}; };
} }
@@ -180,12 +181,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchClearMovieFiles() { dispatchClearMovieFiles() {
dispatch(clearMovieFiles()); dispatch(clearMovieFiles());
}, },
dispatchFetchMovieHistory({ movieId }) {
dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
},
dispatchFetchMovieCredits({ movieId }) { dispatchFetchMovieCredits({ movieId }) {
dispatch(fetchMovieCredits({ movieId })); dispatch(fetchMovieCredits({ movieId }));
}, },
@@ -281,7 +276,6 @@ class MovieDetailsConnector extends Component {
this.props.dispatchFetchMovieFiles({ movieId }); this.props.dispatchFetchMovieFiles({ movieId });
this.props.dispatchFetchMovieBlocklist({ movieId }); this.props.dispatchFetchMovieBlocklist({ movieId });
this.props.dispatchFetchMovieHistory({ movieId });
this.props.dispatchFetchExtraFiles({ movieId }); this.props.dispatchFetchExtraFiles({ movieId });
this.props.dispatchFetchMovieCredits({ movieId }); this.props.dispatchFetchMovieCredits({ movieId });
this.props.dispatchFetchQueueDetails({ movieId }); this.props.dispatchFetchQueueDetails({ movieId });
@@ -292,7 +286,6 @@ class MovieDetailsConnector extends Component {
this.props.dispatchCancelFetchReleases(); this.props.dispatchCancelFetchReleases();
this.props.dispatchClearMovieBlocklist(); this.props.dispatchClearMovieBlocklist();
this.props.dispatchClearMovieFiles(); this.props.dispatchClearMovieFiles();
this.props.dispatchClearMovieHistory();
this.props.dispatchClearExtraFiles(); this.props.dispatchClearExtraFiles();
this.props.dispatchClearMovieCredits(); this.props.dispatchClearMovieCredits();
this.props.dispatchClearQueueDetails(); this.props.dispatchClearQueueDetails();
@@ -349,8 +342,6 @@ MovieDetailsConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
dispatchFetchMovieFiles: PropTypes.func.isRequired, dispatchFetchMovieFiles: PropTypes.func.isRequired,
dispatchClearMovieFiles: PropTypes.func.isRequired, dispatchClearMovieFiles: PropTypes.func.isRequired,
dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired,
dispatchFetchExtraFiles: PropTypes.func.isRequired, dispatchFetchExtraFiles: PropTypes.func.isRequired,
dispatchClearExtraFiles: PropTypes.func.isRequired, dispatchClearExtraFiles: PropTypes.func.isRequired,
dispatchFetchMovieCredits: PropTypes.func.isRequired, dispatchFetchMovieCredits: PropTypes.func.isRequired,

View File

@@ -1,67 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import { icons } from 'Helpers/Props';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import styles from './MovieReleaseDates.css';
function MovieReleaseDates(props) {
const {
showRelativeDates,
shortDateFormat,
timeFormat,
inCinemas,
physicalRelease,
digitalRelease
} = props;
return (
<div>
{
!!inCinemas &&
<div >
<div className={styles.dateIcon}>
<Icon
name={icons.IN_CINEMAS}
/>
</div>
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
</div>
}
{
!!digitalRelease &&
<div >
<div className={styles.dateIcon}>
<Icon
name={icons.MOVIE_FILE}
/>
</div>
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
</div>
}
{
!!physicalRelease &&
<div >
<div className={styles.dateIcon}>
<Icon
name={icons.DISC}
/>
</div>
{getRelativeDate(physicalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
</div>
}
</div>
);
}
MovieReleaseDates.propTypes = {
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
inCinemas: PropTypes.string,
physicalRelease: PropTypes.string,
digitalRelease: PropTypes.string
};
export default MovieReleaseDates;

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Icon from 'Components/Icon';
import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate';
import styles from './MovieReleaseDates.css';
interface MovieReleaseDatesProps {
inCinemas: string;
physicalRelease: string;
digitalRelease: string;
}
function MovieReleaseDates(props: MovieReleaseDatesProps) {
const { inCinemas, physicalRelease, digitalRelease } = props;
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
return (
<div>
{inCinemas ? (
<div title={translate('InCinemas')}>
<div className={styles.dateIcon}>
<Icon name={icons.IN_CINEMAS} />
</div>
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: false,
})}
</div>
) : null}
{digitalRelease ? (
<div title={translate('DigitalRelease')}>
<div className={styles.dateIcon}>
<Icon name={icons.MOVIE_FILE} />
</div>
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: false,
})}
</div>
) : null}
{physicalRelease ? (
<div title={translate('PhysicalRelease')}>
<div className={styles.dateIcon}>
<Icon name={icons.DISC} />
</div>
{getRelativeDate(
physicalRelease,
shortDateFormat,
showRelativeDates,
{ timeFormat, timeForToday: false }
)}
</div>
) : null}
</div>
);
}
export default MovieReleaseDates;

View File

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

View File

@@ -8,7 +8,6 @@ import translate from 'Utilities/String/translate';
import styles from './MovieStatusLabel.css'; import styles from './MovieStatusLabel.css';
function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) { function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) {
if (queueItem) { if (queueItem) {
const queueStatus = queueItem.status; const queueStatus = queueItem.status;
const queueState = queueItem.trackedDownloadStatus; const queueState = queueItem.trackedDownloadStatus;
@@ -116,8 +115,4 @@ MovieStatusLabel.propTypes = {
colorImpairedMode: PropTypes.bool colorImpairedMode: PropTypes.bool
}; };
MovieStatusLabel.defaultProps = {
title: ''
};
export default MovieStatusLabel; export default MovieStatusLabel;

View File

@@ -1,5 +1,4 @@
.container { .container {
margin-top: 20px;
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
border-radius: 4px; border-radius: 4px;
background-color: var(--inputBackgroundColor); background-color: var(--inputBackgroundColor);

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector'; import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
import styles from './MovieTitlesTable.css';
function MovieTitlesTable(props) { function MovieTitlesTable(props) {
const { const {
@@ -7,9 +8,11 @@ function MovieTitlesTable(props) {
} = props; } = props;
return ( return (
<MovieTitlesTableContentConnector <div className={styles.container}>
{...otherProps} <MovieTitlesTableContentConnector
/> {...otherProps}
/>
</div>
); );
} }

View File

@@ -0,0 +1,33 @@
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_LARGE}
>
<MovieHistoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieHistoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModal;

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