1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-11 15:20:34 -04:00

Compare commits

..

127 Commits

Author SHA1 Message Date
Qstick
793cf22c3b WIP: New: Exclusion Lists 2020-10-17 02:16:17 -04:00
Qstick
76cabb4927 Use new multi-select component for indexer languages 2020-10-17 01:38:41 -04:00
nitsua
5b83d09d5e Remove some instances of moment that are not needed on the index to reduce the load 2020-10-17 01:04:50 -04:00
Daniel Martin Gonzalez
82eadcffaa New: Add Option to localize metadata written in .nfo if available (#5060)
* Add Option to localize metadata written in .nfo if available

* Fix Pull Request comments
2020-10-17 01:03:46 -04:00
Qstick
1ebb71db59 Fixed: Import Extra files from Subfolders
Fixes: #4646
2020-10-17 00:17:38 -04:00
Qstick
662b3894c2 Fixed: Make Notification trigger texts consistent
Fixes #5012
2020-10-17 00:06:23 -04:00
Qstick
a1c21af9b5 Fixed: (Windows) clean up extraneous files in build folder during installation
Fixes #5200
Fixes #5203

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 23:33:03 -04:00
Qstick
67fca87c44 Added PrivacyLevel option to FieldDefinition for later usage
Fixes #5107

Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
2020-10-16 23:27:01 -04:00
Qstick
6ee2780370 Fixed some mediainfo subtitle codes
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 23:06:17 -04:00
Qstick
0086e2699e Protect against undefined SizeOnDisk
Fixes #5205

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 22:59:21 -04:00
Qstick
37c9701237 Fixed: Rerender Overviews on Overview option changes
Fixes #5150

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 22:55:28 -04:00
Qstick
6d452d8479 New: Bulk remove from Blacklist
Fixes #5194

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 22:45:23 -04:00
Qstick
e82f0c01d8 Re-saving edited providers will forcibly save them
Fixes #5192

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-16 22:28:19 -04:00
Qstick
ee456c3291 Fixed: MapCoversToLocal for tmdbid queries to all movies endpoint 2020-10-16 22:19:17 -04:00
kingii98
9986f0119b Translated using Weblate (Russian) [skip ci]
Currently translated at 6.7% (57 of 844 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/ru/
2020-10-16 21:48:58 +00:00
Csaba
5d4da26195 Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (844 of 844 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-16 21:48:58 +00:00
memnos
5ed448c930 Translated using Weblate (Italian) [skip ci]
Currently translated at 100.0% (844 of 844 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/it/
2020-10-16 21:48:58 +00:00
jpalenz77
9263e31b7b Translated using Weblate (Spanish) [skip ci]
Currently translated at 100.0% (844 of 844 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/es/
2020-10-16 21:48:57 +00:00
angrycuban13
516122c6f3 Translated using Weblate (Spanish) [skip ci]
Currently translated at 100.0% (843 of 843 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/es/
2020-10-16 03:00:36 +00:00
foXaCe
256a50abab Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (843 of 843 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-16 03:00:36 +00:00
EnorMOZ
315929bc5e Update "TagsHelpText" with similar language as sonarr (#5218)
* Update "TagsHelpText" with similar language as sonarr

* Use new translation key
2020-10-15 23:00:31 -04:00
Qstick
4a681601b2 Skip Screenshot on 2nd build attempt 2020-10-15 21:14:27 -04:00
Csaba
ab3b5bdf8b Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (842 of 842 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-15 14:38:36 +00:00
Qstick
e52288bd67 New: Edit RlsGroup, Flags, and Edition for Movie Files (#5183)
* New: Edit RlsGroup and Edition for Movie Files

* fixup! remove console log

* fixup! translation
2020-10-15 10:38:30 -04:00
servarr[bot]
f2f26d88b9 New: Differentiate between short term and long term (more than 6 hours) indexer failures (#5202)
* New: Differentiate between short term and long term (more than 6 hours) indexer failures

(cherry picked from commit 2adedb97da5ad31b65f0dc2eec5c263efe95731f)

* fixup! Mock Localization

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
Co-authored-by: Qstick <qstick@gmail.com>
2020-10-15 07:20:02 -04:00
nitsua
27354507cb Fix issue with IMDb lists (some not all) not parsing properly causing an issue when trying to add them 2020-10-14 15:08:08 -04:00
Qstick
b7aa1df219 Fix Automation/cleanup build yml (#5211)
* Cleanup build yml

* fixup! bump chrome driver
2020-10-13 21:53:17 -04:00
ta264
2d7942d69c Fixed: Speed up RSS sync 2020-10-13 20:54:33 -04:00
ta264
0a8dd85856 Fixed: Speed up initial movie load when opening the UI 2020-10-13 20:54:33 -04:00
ta264
f917d0e9bc Add FileInfo utility functions to DiskProvider 2020-10-13 20:54:33 -04:00
kingii98
024e4df99c Translated using Weblate (Russian) [skip ci]
Currently translated at 6.1% (52 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/ru/
2020-10-13 16:48:58 +00:00
Csaba
49fa402c55 Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-13 16:48:57 +00:00
Qstick
02c95658c4 Windows installer improvements
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-11 22:43:08 -04:00
Mark McDowall
3bc4231640 New: Health events for Webhooks
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-11 22:40:18 -04:00
Mark McDowall
a9a0d47f9f Fixed: Copying passwords
(cherry picked from commit c871b3f9487b9bfeb3726d763a632a772b420a0a)
2020-10-11 20:36:46 -04:00
Florian
0dd05b2dac Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-11 21:48:51 +00:00
Qstick
3986433884 Fixed: Default a Movie object with Empty Ratings 2020-10-11 01:26:53 -04:00
Mark McDowall
f637976530 Fixed: Parsing of URLs with double slashes in the path
(cherry picked from commit 0c7743e786749b333333d282412ff76fc10aba65)
2020-10-10 17:16:21 -04:00
Qstick
603d26bb5f Take Screenshot on Automation tests for build status notifications 2020-10-09 21:51:59 -04:00
Will Segatto
6b41ad7442 Translated using Weblate (Portuguese) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/pt/
2020-10-10 00:49:05 +00:00
hotio
4c049ac3d9 Translated using Weblate (Dutch) [skip ci]
Currently translated at 97.9% (823 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/nl/
2020-10-10 00:49:04 +00:00
Csaba
84df3e8b5b Translated using Weblate (Hungarian) [skip ci]
Currently translated at 91.4% (768 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-10 00:49:03 +00:00
jpalenz77
0ff76e14bb Translated using Weblate (Spanish) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/es/
2020-10-10 00:48:55 +00:00
Florian
5433c6364c Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-10 00:48:50 +00:00
geogolem
038e3d5c44 update trayIcon with new Radarr Icon 2020-10-08 14:27:07 -04:00
Qstick
71b126024b Remove unused mac startup script 2020-10-07 23:07:47 -04:00
Qstick
76565d4ab5 Fixed: Old Icon being used on Console App 2020-10-07 23:06:43 -04:00
ta264
28c15bc425 Log out SQL trace on error
Fixes #4910

Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2020-10-07 22:55:54 -04:00
Qstick
aeda4cba32 Fixed: Render CustomFormats under profile option for Large Screens 2020-10-07 22:01:58 -04:00
Qstick
a826c1dc25 New: MultiSelect input control for provider settings
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
2020-10-07 21:27:17 -04:00
Csaba
00022fd206 Translated using Weblate (Hungarian) [skip ci]
Currently translated at 32.3% (272 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-08 00:48:53 +00:00
Will Segatto
0647663f46 Translated using Weblate (Portuguese) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/pt/
2020-10-07 05:48:57 +00:00
Csaba
b83bfb045b Translated using Weblate (Hungarian) [skip ci]
Currently translated at 25.1% (211 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/hu/
2020-10-07 05:48:57 +00:00
memnos
cfbac482e5 Translated using Weblate (Italian) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/it/
2020-10-07 05:48:53 +00:00
foXaCe
b43b35a295 Translated using Weblate (French) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-07 05:48:50 +00:00
reloxx
e75d52de0e Translated using Weblate (German) [skip ci]
Currently translated at 100.0% (840 of 840 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/de/
2020-10-07 05:48:49 +00:00
nitsua
f850e75d4e Fix issue with push missing the URL base 2020-10-06 11:05:43 -04:00
nitsua
6c9e4994d8 Fix broken translations on index 2020-10-06 09:35:08 -04:00
Qstick
708a2e31d5 Improve load speeds by being more efficient with config language 2020-10-06 07:48:58 -04:00
Qstick
37c1b5b28c New: Add Bulgarian Language
Fixes #4111
2020-10-06 00:33:33 -04:00
Qstick
e28bea14b3 New: Add Thai Language 2020-10-06 00:33:33 -04:00
Qstick
2823099237 New: Add Hindi and Romanian Languages
Fixes #3597
2020-10-06 00:33:33 -04:00
Qstick
53eeee8b91 New: Allow Selection Original Movie Languge in Profile 2020-10-06 00:33:33 -04:00
nitsua
e880eb0e00 More translations 2020-10-06 00:29:45 -04:00
Qstick
561f84adff Added translation using Weblate (Thai) [skip ci] 2020-10-06 04:09:34 +00:00
Qstick
4fc6a14d1b Added translation using Weblate (Bulgarian) [skip ci] 2020-10-06 04:09:24 +00:00
foXaCe
b8d5a0b6a2 Translated using Weblate (French) [skip ci]
Currently translated at 99.5% (822 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-05 08:48:49 +00:00
Mark McDowall
73b0a461d3 Fix tooltip max width on larger screens
(cherry picked from commit f4f2a6f5fc14244f9acf8186cbacda7f9c1e0481)
2020-10-05 00:14:28 -04:00
Qstick
d86402efb1 Fixed: formatTimeSpan shows incorrect when over 1 month 2020-10-04 23:38:10 -04:00
Qstick
1c892d7357 Fixed: Sorting of Queue time left 2020-10-04 23:37:39 -04:00
Qstick
f7e21ec2a4 Fixed: Tooltip jumping around
Fixes: #5136
Fixes: #4966
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-04 23:34:27 -04:00
Qstick
1d0771c9a4 New: Add size to movie files in Webhook payload
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-04 22:31:49 -04:00
Qstick
cc384d9297 Remove unused popper 2 package 2020-10-04 22:29:21 -04:00
Qstick
7898100d95 Revert "Pull Sonarr commit 'Import lists in settings overview' (#5138)"
This reverts commit 4b279e87cf.
2020-10-04 22:28:46 -04:00
servarr[bot]
4b279e87cf Pull Sonarr commit 'Import lists in settings overview' (#5138)
* Import lists in settings overview

(cherry picked from commit f45b27f507953724e9469ce16c4e555985312150)

* fixup! Sonarr wording

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
Co-authored-by: Qstick <qstick@gmail.com>
2020-10-04 22:26:14 -04:00
Qstick
3d5570dfd9 Update frontend packages
Fixes: #5115
Fixes: #5116
Fixes: #5117
Fixes: #5118
Fixes: #5119
Fixes: #5120
Fixes: #5121
Fixes: #5122
Fixes: #5123
Fixes: #5124
Fixes: #5125
Fixes: #5126
Fixes: #5127
Fixes: #5128
Fixes: #5129
Fixes: #5130
Fixes: #5131
Fixes: #5132
Fixes: #5133
Fixes: #5134
Fixes: #5139
Fixes: #5140
2020-10-04 21:49:16 -04:00
Qstick
269462e0a2 Update stalebot exempt labels [skip ci] 2020-10-04 02:45:56 -04:00
bakerboy448
5799b3dc47 Fixed: Parser Detecting DTS-Audio formats and Blu-Ray as groups (#5090)
* Fix DTS-XXX Audio & Blu-ray false Group Positives

* Add test cases

* fixup! spaces

Co-authored-by: Qstick <qstick@gmail.com>
2020-10-04 01:59:31 -04:00
Qstick
dfbbb7d9bd Fixup Filelist test to use new url 2020-10-04 01:57:37 -04:00
Qstick
6faa484d4e Change Discord invite to point to Welcome page 2020-10-04 01:20:18 -04:00
Qstick
86363d5bf1 Fixed: Change Filelist default URL 2020-10-04 01:20:18 -04:00
Qstick
30c51ec4f3 Fixed: Handle obfuscated files using abc.xyz pattern
Fixes #5105

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-04 01:20:17 -04:00
Qstick
a66b2cf416 New: Add more information to Webhook payload
Fixes #5104

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-04 01:20:17 -04:00
Qstick
37197150be Some cleanup of things marked for removal in v3
Fixes #5102

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-10-04 01:20:17 -04:00
Qstick
c8bbd21615 Cleanup TaskManager.cs 2020-10-04 01:20:17 -04:00
Florian
fdf2d1c9b3 Translated using Weblate (French) [skip ci]
Currently translated at 99.5% (822 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-04 04:48:54 +00:00
foXaCe
eb4cd9633a Translated using Weblate (French) [skip ci]
Currently translated at 99.5% (822 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-04 04:48:54 +00:00
reloxx
3fa17bf7f6 Translated using Weblate (German) [skip ci]
Currently translated at 100.0% (826 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/de/
2020-10-04 04:48:48 +00:00
Qstick
22e9dff76b Don't check for updates every 30 minutes on nightly 2020-10-03 23:49:06 -04:00
Qstick
7d31eb1f55 Fixed: Test for empty strings using isNullOrEmpty 2020-10-03 21:58:31 -04:00
Qstick
4ec71538b9 Fixed: Avoid zero-length array memory allocations 2020-10-03 21:58:31 -04:00
Qstick
295b975046 Fixed: Do not use Count/LongCount when Any can be used 2020-10-03 21:58:31 -04:00
Qstick
0198c7a3b1 Fixed: Use Length/Count property instead of Enumerable.Count method 2020-10-03 21:58:31 -04:00
Qstick
088ffc34df Fixed: Use Append(Char) for single character Stringbuilder additions 2020-10-03 21:58:31 -04:00
bakerboy448
1740ad337c Clarify Branches/ Release Channels [skip ci] (#5100) 2020-10-03 09:26:37 -04:00
nitsua
a6758e4bf4 Ongoing updates to api docs (#5082) [skip ci] 2020-10-03 09:25:35 -04:00
memnos
2161f08140 Translated using Weblate (Italian) [skip ci]
Currently translated at 100.0% (826 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/it/
2020-10-03 06:48:53 +00:00
foXaCe
1a92372506 Translated using Weblate (French) [skip ci]
Currently translated at 70.8% (585 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-10-03 06:48:52 +00:00
bakerboy448
5bd23be133 Update v3 binaries links [skip ci] (#5099)
* Update README.md

* Update Nightly Binary Links

* Fix table formatting
2020-10-01 23:39:15 -04:00
nitsua
eca816db86 Fix movie title stretching the add window
Fixes: #5096
2020-10-01 18:12:19 -04:00
bakerboy448
c4f19a813d Minor Branch Updates/Clarifcation [Skip Ci] 2020-10-01 11:20:13 -04:00
bakerboy448
24cee7e4fe Correct Contributing (don't have forums or IRC) [skip ci] 2020-10-01 11:15:11 -04:00
Qstick
22531294be Remove duplicate translation key 2020-09-30 21:44:08 -04:00
Qstick
464d92bb70 Calendar status fixes
Fixes #4747

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-09-30 21:43:52 -04:00
Qstick
8ee16b81ec Fixed: Indexer being disabled due to download client rejecting it
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-09-30 21:25:12 -04:00
Qstick
a7a1d48e0d Fixed: Log path when import fails for movie import
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2020-09-30 21:24:18 -04:00
Qstick
a070279993 New: Allow naming with original Movie title (in original language) 2020-09-30 16:10:56 -04:00
Qstick
95918c4053 Fixed: Speed up Unmapped Folder fetch for large number of root folders 2020-09-30 07:41:56 -04:00
Will Segatto
646b86f8c9 Translated using Weblate (Portuguese) [skip ci]
Currently translated at 100.0% (826 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/pt/
2020-09-29 05:59:11 +00:00
jpalenz77
28835a1857 Translated using Weblate (Spanish) [skip ci]
Currently translated at 100.0% (826 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/es/
2020-09-29 05:59:10 +00:00
nitsua
6b22481a00 Fix the sorting on the agenda to respect using all dates instead of only cinema date to sort 2020-09-28 23:55:50 -04:00
Qstick
aef8a8fd04 Fixed: Fetch blacklist by Movie instead of all
Fixes #5066
2020-09-28 23:39:15 -04:00
Qstick
a1e69c3c2b Lodash replacements: take 2 2020-09-28 22:02:02 -04:00
hotio
995d257d3d Fix docker tag (#5081) [skip ci] 2020-09-28 21:51:26 -04:00
nitsua
cf804f7dac Fix issue with arrow keys switching movie when using them inside a textbox (or any element really) 2020-09-28 17:28:09 +01:00
ta264
081fe64bff Revert "Convert some instances (filter, find, pick) to native from lodash"
This reverts commit d8a0aac9c3.
2020-09-28 06:26:23 +01:00
ta264
9075fdc1c1 Revert "Fix find error on movie details"
This reverts commit 6bdd24e62d.
2020-09-28 06:26:13 +01:00
foXaCe
5db1faad0b Translated using Weblate (French) [skip ci]
Currently translated at 62.5% (517 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/fr/
2020-09-28 06:00:42 +00:00
reloxx
57f3805763 Translated using Weblate (German) [skip ci]
Currently translated at 100.0% (826 of 826 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/radarr/radarr/de/
2020-09-28 06:00:38 +00:00
nitsua
6bdd24e62d Fix find error on movie details 2020-09-28 06:51:03 +01:00
Qstick
d5ec2914e2 New: Support existing 'keyart' in Kodi Metadata
Fixes #5059
2020-09-27 22:44:12 -04:00
Qstick
d8a0aac9c3 Convert some instances (filter, find, pick) to native from lodash 2020-09-27 22:33:21 -04:00
nitsua
987ed357d5 Add a link to the github issue if it is added to the change notes 2020-09-27 22:14:08 -04:00
Qstick
9044976393 Update StaleBot config exempt labels [skip ci] 2020-09-27 09:30:48 -04:00
nitsua
b53def3da5 Fixed: Issue with arrow navigation from details working outside of the details page (#5071) 2020-09-26 23:36:36 -04:00
Qstick
2ecb988c6a Remove aphrodite links from Readme [skip ci] 2020-09-26 22:19:17 -04:00
Qstick
41cf722ab5 Change over aphrodite references 2020-09-26 22:03:14 -04:00
304 changed files with 5495 additions and 1806 deletions

View File

@@ -185,17 +185,9 @@ dotnet_diagnostic.CA1814.severity = suggestion
dotnet_diagnostic.CA1815.severity = suggestion
dotnet_diagnostic.CA1816.severity = suggestion
dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1820.severity = suggestion
dotnet_diagnostic.CA1821.severity = suggestion
dotnet_diagnostic.CA1822.severity = suggestion
dotnet_diagnostic.CA1823.severity = suggestion
dotnet_diagnostic.CA1824.severity = suggestion
dotnet_diagnostic.CA1825.severity = suggestion
dotnet_diagnostic.CA1826.severity = suggestion
dotnet_diagnostic.CA1827.severity = suggestion
dotnet_diagnostic.CA1828.severity = suggestion
dotnet_diagnostic.CA1829.severity = suggestion
dotnet_diagnostic.CA1834.severity = suggestion
dotnet_diagnostic.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion

View File

@@ -6,7 +6,7 @@
**Just because you receive an exception in your logs, doesn't mean it's a bug and should be reported here. Often it's something else, such as a permission error. If you are unsure ask on the Discord or Subreddit first.**
Visit our [Discord server](https://discord.gg/NWYch8M) or [Subreddit](https://reddit.com/r/radarr) for support or longer discussions. Support questions posed on here will be closed immediately.
Visit our [Discord server](https://discord.gg/r5wJPt9) or [Subreddit](https://reddit.com/r/radarr) for support or longer discussions. Support questions posed on here will be closed immediately.
Provide a description of the feature request or bug here, the more details the better.
Please also include the following if you are reporting a bug. If you do not include it, the issue will probably be closed as we cannot help you. -->

View File

@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Support via Discord
url: https://discord.gg/AD3UP37
url: https://discord.gg/r5wJPt9
about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/radarr

5
.github/stale.yml vendored
View File

@@ -7,7 +7,10 @@ exemptLabels:
- feature request
- parser
- confirmed
- aphrodite
- sonarr-pull
- lidarr-pull
- readarr-pull
- v3
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable

2
.github/support.yml vendored
View File

@@ -6,7 +6,7 @@ supportLabel: support
# to a support page, or set to `false` to disable
supportComment: >
We use the issue tracker exclusively for bug reports and feature requests.
However, this issue appears to be a support request. Please hop over onto our [Discord](https://discord.gg/ZDmT7qb) or [Subreddit](https://reddit.com/r/radarr)
However, this issue appears to be a support request. Please hop over onto our [Discord](https://discord.gg/r5wJPt9) or [Subreddit](https://reddit.com/r/radarr)
# Whether to close issues marked as support requests
close: true
# Whether to lock issues marked as support requests

View File

@@ -14,7 +14,7 @@ See the readme for information on setting up your development environment.
- Rebase from Radarr's develop branch, don't merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on the forums or on IRC if you have any questions
- Reach out to us on the discord if you have any questions
- Add tests (unit/integration)
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
- One feature/bug fix per pull request to keep things clean and easy to understand

View File

@@ -28,15 +28,18 @@ If you are using Docker please ensure your Docker paths are setup correctly usin
## Downloads
Please note that v0.2 will only have critical bugs resolved as of August 2020. Any additional development or features will be soley in V3.
| Release Type | Branch: develop (stable) | Branch: nightly (semi-unstable) | Branch: aphrodite (very-unstable) |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Binary Releases | [![GitHub Releases](https://img.shields.io/badge/downloads-releases-brightgreen.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/releases) | [![AppVeyor Builds](https://img.shields.io/badge/downloads-nightly-green.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/radarr-usby1/branch/develop/artifacts) | |
| Docker - lsio | [![Docker release](https://img.shields.io/badge/linuxserver-radarr:latest-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) | [![Docker nightly](https://img.shields.io/badge/linuxserver-radarr:nightly-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) | [![Docker aphrodite](https://img.shields.io/badge/linuxserver-radarr:preview-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) |
| Docker - hotio | [![Docker release](https://img.shields.io/badge/hotio-radarr:latest-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) | [![Docker nightly](https://img.shields.io/badge/hotio-radarr:unstable-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) | [![Docker aphrodite](https://img.shields.io/badge/hotio-radarr:aphrodite-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) |
Each push to the "develop" branch creates a build on "nightly" release channel (release channel is the "branch" within radarr's settings), once we push a build to Github it will show up on "develop" release channel.
| Release Channel Type | Branch: develop (stable) (v0.2) | Branch: nightly (semi-unstable) (v3.0) |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Binary Releases | [![GitHub Releases](https://img.shields.io/badge/downloads-releases-brightgreen.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/releases) | [![Azure Build](https://img.shields.io/badge/downloads-Windows_X64-green.svg?maxAge=60&style=flat-square)](https://radarr.servarr.com/v1/update/nightly/updatefile?os=windows&runtime=netcore&arch=x64) <br> [![Azure Build](https://img.shields.io/badge/downloads-Linux_X64-green.svg?maxAge=60&style=flat-square)](https://radarr.servarr.com/v1/update/nightly/updatefile?os=linux&runtime=netcore&arch=x64) <br> [![Azure Build](https://img.shields.io/badge/downloads-Linux_ARM64-green.svg?maxAge=60&style=flat-square)](https://radarr.servarr.com/v1/update/nightly/updatefile?os=linux&runtime=netcore&arch=arm64) [![Azure Build](https://img.shields.io/badge/downloads-Linux_ARM-green.svg?maxAge=60&style=flat-square)](https://radarr.servarr.com/v1/update/nightly/updatefile?os=linux&runtime=netcore&arch=arm) <br> [![Azure Build](https://img.shields.io/badge/downloads-macOS-green.svg?maxAge=60&style=flat-square)](https://radarr.servarr.com/v1/update/nightly/updatefile?os=osx&runtime=netcore&arch=x64)
| Docker - lsio | [![Docker release](https://img.shields.io/badge/linuxserver-radarr:latest-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) | [![Docker nightly](https://img.shields.io/badge/linuxserver-radarr:nightly-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) |
| Docker - hotio | [![Docker release](https://img.shields.io/badge/hotio-radarr:latest-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) | [![Docker nightly](https://img.shields.io/badge/hotio-radarr:nightly-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) |
## Support
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60&style=flat-square)](https://discord.gg/AD3UP37)
[![Discord](https://img.shields.io/badge/discord-chat-r5wJPt9.svg?maxAge=60&style=flat-square)](https://discord.gg/r5wJPt9)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60&style=flat-square)](https://www.reddit.com/r/radarr)
[![GitHub](https://img.shields.io/badge/github-issues-red.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/issues)
[![GitHub Wiki](https://img.shields.io/badge/github-wiki-181717.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki)

View File

@@ -20,11 +20,10 @@ trigger:
branches:
include:
- develop
- aphrodite
- master
pr:
- develop
- aphrodite
stages:
- stage: Setup
@@ -40,7 +39,7 @@ stages:
displayName: Set Build Name
- bash: |
if [[ $BUILD_REASON == "PullRequest" ]]; then
git diff origin/aphrodite...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)"
git diff origin/develop...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)"
echo $? > not_backend_update
else
echo 0 > not_backend_update
@@ -302,14 +301,22 @@ stages:
sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}"
sentry-cli releases -p radarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
sentry-cli releases set-commits --auto "${RELEASENAME}"
sentry-cli releases deploys "${RELEASENAME}" new -e aphrodite
if [[ ${BUILD_SOURCEBRANCH} == "refs/heads/develop" ]]; then
sentry-cli releases deploys "${RELEASENAME}" new -e nightly
else
sentry-cli releases deploys "${RELEASENAME}" new -e production
fi
if [ $? -gt 0 ]; then
echo "##vso[task.logissue type=warning]Error uploading source maps."
fi
exit 0
displayName: Publish Sentry Source Maps
continueOnError: true
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/aphrodite'))
condition: |
or
(
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
)
env:
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
SENTRY_ORG: $(sentryOrg)
@@ -379,11 +386,6 @@ stages:
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- bash: |
wget https://github.com/acoustid/chromaprint/releases/download/v1.4.3/chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz
sudo tar xf chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz --strip-components=1 --directory /usr/bin
displayName: Install fpcalc
condition: and(succeeded(), eq(variables['osName'], 'Linux'))
- bash: |
SYMLINK=6_6_0
MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK
@@ -686,24 +688,21 @@ stages:
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
displayName: Move Package Contents
- bash: |
if [[ $OSNAME == "Mac" ]]; then
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-macos.tar.gz
elif [[ $OSNAME == "Linux" ]]; then
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-linux64.tar.gz
else
echo "Unhandled OS"
exit 1
fi
curl -s -L "$url" | tar -xz
chmod +x geckodriver
mv geckodriver _tests
displayName: Install Gecko Driver
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh ${OSNAME} Automation Test
displayName: Run Automation Tests
- task: CopyFiles@2
displayName: 'Copy Screenshot to: $(Build.ArtifactStagingDirectory)'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**/*_test_screenshot.png
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
- publish: $(Build.ArtifactStagingDirectory)/screenshots
artifact: '$(osName)AutomationScreenshots'
displayName: Publish Screenshot Bundle
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
@@ -852,8 +851,15 @@ stages:
- job:
displayName: Discord Notification
pool:
vmImage: 'ubuntu-18.04'
vmImage: 'windows-2019'
steps:
- task: DownloadPipelineArtifact@2
continueOnError: true
displayName: Download Screenshot Artifact
inputs:
buildType: 'current'
artifactName: 'WindowsAutomationScreenshots'
targetPath: $(Build.SourcesDirectory)
- checkout: none
- powershell: |
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))

View File

@@ -75,7 +75,7 @@
"function-parentheses-newline-inside": "never-multi-line",
"function-parentheses-space-inside": "never",
"function-url-quotes": "always",
"function-url-scheme-blacklist": [
"function-url-scheme-disallowed-list": [
"data"
],
"function-whitespace-after": "always",

View File

@@ -7,6 +7,7 @@ const errorHandler = require('./helpers/errorHandler');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackPluginHtmlTags = require('html-webpack-plugin/lib/html-tags');
const TerserPlugin = require('terser-webpack-plugin');
const uiFolder = 'UI';
@@ -14,7 +15,7 @@ const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = process.argv.indexOf('--production') > -1;
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
const inlineWebWorkers = true;
const inlineWebWorkers = 'no-fallback';
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
@@ -32,14 +33,19 @@ const cssVarsFiles = [
].map(require.resolve);
// Override the way HtmlWebpackPlugin injects the scripts
// TODO: Find a better way to get these paths without
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
const head = assetTags.head.map((v) => {
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` };
return this.createHtmlTag(v);
const head = assetTags.headTags.map((v) => {
const href = v.attributes.href
.replace('\\', '/')
.replace('%5C', '/');
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${href}` };
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
});
const body = assetTags.body.map((v) => {
const body = assetTags.bodyTags.map((v) => {
v.attributes = { src: `/${v.attributes.src}` };
return this.createHtmlTag(v);
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
});
return html
@@ -125,9 +131,8 @@ const config = {
use: {
loader: 'worker-loader',
options: {
name: '[name].js',
inline: inlineWebWorkers,
fallback: !inlineWebWorkers
filename: '[name].js',
inline: inlineWebWorkers
}
}
},

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -10,12 +11,84 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
}
//
// Render
@@ -27,15 +100,33 @@ class Blacklist extends Component {
items,
columns,
totalRecords,
isRemoving,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return (
<PageContent title={translate('Blacklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
@@ -81,8 +172,12 @@ class Blacklist extends Component {
isPopulated && !error && !!items.length &&
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
@@ -90,8 +185,10 @@ class Blacklist extends Component {
return (
<BlacklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
@@ -107,6 +204,16 @@ class Blacklist extends Component {
</div>
}
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title="Remove Selected"
message={'Are you sure you want to remove the selected items from the blacklist?'}
confirmLabel="Remove Selected"
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
@@ -119,7 +226,9 @@ Blacklist.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
};

View File

@@ -89,6 +89,10 @@ class BlacklistConnector extends Component {
this.props.gotoBlacklistPage({ page });
}
onRemoveSelected = (ids) => {
this.props.removeBlacklistItems({ ids });
}
onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey });
}
@@ -124,6 +128,7 @@ class BlacklistConnector extends Component {
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
@@ -143,6 +148,7 @@ BlacklistConnector.propTypes = {
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
removeBlacklistItems: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired,

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
@@ -42,6 +43,7 @@ class BlacklistRow extends Component {
render() {
const {
id,
movie,
sourceTitle,
quality,
@@ -51,7 +53,9 @@ class BlacklistRow extends Component {
protocol,
indexer,
message,
isSelected,
columns,
onSelectedChange,
onRemovePress
} = this.props;
@@ -61,6 +65,12 @@ class BlacklistRow extends Component {
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
@@ -194,7 +204,9 @@ BlacklistRow.propTypes = {
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired
};

View File

@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import BlacklistRow from './BlacklistRow';
@@ -18,7 +18,7 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeFromBlacklist({ id: props.id }));
dispatch(removeBlacklistItem({ id: props.id }));
}
};
}

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@@ -8,10 +7,10 @@ function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return _.pick(uiSettings, [
'shortDateFormat',
'timeFormat'
]);
return {
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}

View File

@@ -11,14 +11,14 @@ function QueueDetails(props) {
size,
sizeleft,
estimatedCompletionTime,
status: queueStatus,
status,
trackedDownloadState,
trackedDownloadStatus,
errorMessage,
progressBar
} = props;
const status = queueStatus.toLowerCase();
const progress = (100 - sizeleft / size * 100);
const progress = size ? (100 - sizeleft / size * 100) : 0;
if (status === 'pending') {
return (
@@ -40,7 +40,35 @@ function QueueDetails(props) {
);
}
// TODO: show an icon when download is complete, but not imported yet?
if (trackedDownloadStatus === 'warning') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.WARNING}
title={'Downloaded - Unable to Import: check logs for details'}
/>
);
}
if (trackedDownloadState === 'importPending') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={`${translate('Downloaded')} - ${translate('WaitingToImport')}`}
/>
);
}
if (trackedDownloadState === 'importing') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={`${translate('Downloaded')} - ${translate('Importing')}`}
/>
);
}
}
if (errorMessage) {
@@ -91,6 +119,8 @@ QueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired
};

View File

@@ -117,7 +117,7 @@ class AddNewMovieModalContent extends Component {
<FormGroup>
<FormLabel>
Monitor
{translate('Monitor')}
</FormLabel>
<FormInputGroup
@@ -168,7 +168,7 @@ class AddNewMovieModalContent extends Component {
<ModalFooter className={styles.modalFooter}>
<label className={styles.searchForMissingMovieLabelContainer}>
<span className={styles.searchForMissingMovieLabel}>
Start search for missing movie
{translate('StartSearchForMissingMovie')}
</span>
<CheckInput
@@ -186,7 +186,7 @@ class AddNewMovieModalContent extends Component {
isSpinning={isAdding}
onPress={this.onAddMoviePress}
>
Add {title}
{translate('AddMovie')}
</SpinnerButton>
</ModalFooter>
</ModalContent>

View File

@@ -170,6 +170,7 @@ class AddNewMovieSearchResult extends Component {
imdbId={imdbId}
/>
}
canFlip={true}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>

View File

@@ -10,6 +10,7 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContentFooter from 'Components/Page/PageContentFooter';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieFooter.css';
const MIXED = 'mixed';
@@ -112,7 +113,7 @@ class ImportMovieFooter extends Component {
<PageContentFooter>
<div className={styles.inputContainer}>
<div className={styles.label}>
Monitor
{translate('Monitor')}
</div>
<FormInputGroup
@@ -127,7 +128,7 @@ class ImportMovieFooter extends Component {
<div className={styles.inputContainer}>
<div className={styles.label}>
Minimum Availability
{translate('MinimumAvailability')}
</div>
<FormInputGroup
@@ -142,7 +143,7 @@ class ImportMovieFooter extends Component {
<div className={styles.inputContainer}>
<div className={styles.label}>
Quality Profile
{translate('QualityProfile')}
</div>
<FormInputGroup
@@ -168,7 +169,7 @@ class ImportMovieFooter extends Component {
isDisabled={!selectedCount || isLookingUpMovie}
onPress={onImportPress}
>
Import {selectedCount} {selectedCount > 1 ? 'Movies' : 'Movie'}
{translate('Import')} {selectedCount} {selectedCount > 1 ? translate('Movies') : translate('Movie')}
</SpinnerButton>
{
@@ -178,7 +179,7 @@ class ImportMovieFooter extends Component {
kind={kinds.WARNING}
onPress={onCancelLookupPress}
>
Cancel Processing
{translate('CancelProcessing')}
</Button> :
null
}
@@ -190,7 +191,7 @@ class ImportMovieFooter extends Component {
kind={kinds.SUCCESS}
onPress={onLookupPress}
>
Start Processing
{translate('StartProcessing')}
</Button> :
null
}
@@ -206,7 +207,7 @@ class ImportMovieFooter extends Component {
{
isLookingUpMovie ?
'Processing Folders' :
translate('ProcessingFolders') :
null
}
@@ -220,7 +221,7 @@ class ImportMovieFooter extends Component {
kind={kinds.WARNING}
/>
}
title="Import Errors"
title={translate('ImportErrors')}
body={
<ul>
{

View File

@@ -3,6 +3,7 @@ import React from 'react';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieHeader.css';
function ImportMovieHeader(props) {
@@ -24,35 +25,35 @@ function ImportMovieHeader(props) {
className={styles.folder}
name="folder"
>
Folder
{translate('Folder')}
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.monitor}
name="monitor"
>
Monitor
{translate('Monitor')}
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.minimumAvailability}
name="minimumAvailability"
>
Min Availability
{translate('MinAvailability')}
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.qualityProfile}
name="qualityProfileId"
>
Quality Profile
{translate('QualityProfile')}
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.movie}
name="movie"
>
Movie
{translate('Movie')}
</VirtualTableHeaderCell>
</VirtualTableHeader>
);

View File

@@ -9,6 +9,7 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Portal from 'Components/Portal';
import { icons, kinds } from 'Helpers/Props';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import translate from 'Utilities/String/translate';
import ImportMovieSearchResultConnector from './ImportMovieSearchResultConnector';
import ImportMovieTitle from './ImportMovieTitle';
import styles from './ImportMovieSelectMovie.css';
@@ -174,7 +175,7 @@ class ImportMovieSelectMovie extends Component {
kind={kinds.WARNING}
/>
No match found!
{translate('NoMatchFound')}
</div> :
null
}
@@ -189,7 +190,7 @@ class ImportMovieSelectMovie extends Component {
kind={kinds.WARNING}
/>
Search failed, please try again later.
{translate('SearchFailedPleaseTryAgainLater')}
</div> :
null
}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieTitle.css';
function ImportMovieTitle(props) {
@@ -33,7 +34,7 @@ function ImportMovieTitle(props) {
<Label
kind={kinds.WARNING}
>
Existing
{translate('Existing')}
</Label>
}
</div>

View File

@@ -101,12 +101,8 @@ class ImportMovieSelectFolder extends Component {
<div className={styles.tips}>
{translate('ImportTipsMessage')}
<ul>
<li className={styles.tip}>
Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>movie.2008.bluray.mkv</span>
</li>
<li className={styles.tip}>
Point Radarr to the folder containing all of your movies, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\movies' : '/movies'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}"</span>
</li>
<li className={styles.tip} dangerouslySetInnerHTML={{ __html: translate('ImportIncludeQuality', ['<code>movie.2008.bluray.mkv</code>']) }} />
<li className={styles.tip} dangerouslySetInnerHTML={{ __html: translate('ImportRootPath', [`<code>${isWindows ? 'C:\\movies' : '/movies'}</code>`, `<code>${isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}</code>`]) }} />
</ul>
</div>
@@ -158,7 +154,7 @@ class ImportMovieSelectFolder extends Component {
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
Start Import
{translate('StartImport')}
</Button>
</div>
}

View File

@@ -6,9 +6,38 @@ import styles from './Agenda.css';
function Agenda(props) {
const {
items
items,
start,
end
} = props;
const startDateParsed = Date.parse(start);
const endDateParsed = Date.parse(end);
items.forEach((item) => {
const cinemaDateParsed = Date.parse(item.inCinemas);
const digitalDateParsed = Date.parse(item.digitalRelease);
const physicalDateParsed = Date.parse(item.physicalRelease);
const dates = [];
if (cinemaDateParsed > 0 && cinemaDateParsed >= startDateParsed && cinemaDateParsed <= endDateParsed) {
dates.push(cinemaDateParsed);
}
if (digitalDateParsed > 0 && digitalDateParsed >= startDateParsed && digitalDateParsed <= endDateParsed) {
dates.push(digitalDateParsed);
}
if (physicalDateParsed > 0 && physicalDateParsed >= startDateParsed && physicalDateParsed <= endDateParsed) {
dates.push(physicalDateParsed);
}
item.sortDate = Math.min(...dates);
item.cinemaDateParsed = cinemaDateParsed;
item.digitalDateParsed = digitalDateParsed;
item.physicalDateParsed = physicalDateParsed;
});
items.sort((a, b) => ((a.sortDate > b.sortDate) ? 1 : -1));
return (
<div className={styles.agenda}>
{
@@ -32,7 +61,9 @@ function Agenda(props) {
}
Agenda.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired
items: PropTypes.arrayOf(PropTypes.object).isRequired,
start: PropTypes.string.isRequired,
end: PropTypes.string.isRequired
};
export default Agenda;

View File

@@ -92,6 +92,5 @@
}
.dateIcon {
display: inline;
margin-right: 10px;
width: 25px;
}

View File

@@ -55,23 +55,26 @@ class AgendaEvent extends Component {
showCutoffUnmetIcon,
longDateFormat,
colorImpairedMode,
startDate,
endDate
cinemaDateParsed,
digitalDateParsed,
physicalDateParsed,
sortDate
} = this.props;
const agendaStart = Date.parse(startDate);
const agendaEnd = Date.parse(endDate);
const cinemaDate = Date.parse(inCinemas);
const digitalDate = Date.parse(digitalRelease);
let startTime = physicalRelease;
let releaseIcon = icons.DISC;
let startTime = null;
let releaseIcon = null;
if (digitalDate >= agendaStart && digitalDate <= agendaEnd) {
if (physicalDateParsed === sortDate) {
startTime = physicalRelease;
releaseIcon = icons.DISC;
}
if (digitalDateParsed === sortDate) {
startTime = digitalRelease;
releaseIcon = icons.MOVIE_FILE;
}
if (cinemaDate >= agendaStart && cinemaDate <= agendaEnd) {
if (cinemaDateParsed === sortDate) {
startTime = inCinemas;
releaseIcon = icons.IN_CINEMAS;
}
@@ -92,13 +95,14 @@ class AgendaEvent extends Component {
)}
to={link}
>
<div className={styles.dateIcon}>
<Icon
name={releaseIcon}
kind={kinds.DEFAULT}
/>
</div>
<div className={styles.date}>
<div className={styles.dateIcon}>
<Icon
name={releaseIcon}
kind={kinds.DEFAULT}
/>
</div>
{(showDate) ? startTime.format(longDateFormat) : null}
</div>
@@ -176,8 +180,10 @@ AgendaEvent.propTypes = {
timeFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
startDate: PropTypes.date,
endDate: PropTypes.date
cinemaDateParsed: PropTypes.number,
digitalDateParsed: PropTypes.number,
physicalDateParsed: PropTypes.number,
sortDate: PropTypes.number
};
AgendaEvent.defaultProps = {

View File

@@ -13,9 +13,7 @@ function createMapStateToProps() {
createMovieFileSelector(),
createQueueItemSelector(),
createUISettingsSelector(),
(state) => state.calendar.start,
(state) => state.calendar.end,
(calendarOptions, movie, movieFile, queueItem, uiSettings, startDate, endDate) => {
(calendarOptions, movie, movieFile, queueItem, uiSettings) => {
return {
movie,
movieFile,
@@ -23,9 +21,7 @@ function createMapStateToProps() {
...calendarOptions,
timeFormat: uiSettings.timeFormat,
longDateFormat: uiSettings.longDateFormat,
colorImpairedMode: uiSettings.enableColorImpairedMode,
startDate,
endDate
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);

View File

@@ -12,10 +12,12 @@ function CalendarEventQueueDetails(props) {
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
errorMessage
} = props;
const progress = (100 - sizeleft / size * 100);
const progress = size ? (100 - sizeleft / size * 100) : 0;
return (
<QueueDetails
@@ -24,6 +26,8 @@ function CalendarEventQueueDetails(props) {
sizeleft={sizeleft}
estimatedCompletionTime={estimatedCompletionTime}
status={status}
trackedDownloadState={trackedDownloadState}
trackedDownloadStatus={trackedDownloadStatus}
errorMessage={errorMessage}
progressBar={
<div title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}>
@@ -45,6 +49,8 @@ CalendarEventQueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string
};

View File

@@ -24,7 +24,7 @@ function getTitle(time, start, end, view, longDateFormat) {
} else if (view === 'month') {
return timeMoment.format('MMMM YYYY');
} else if (view === 'agenda') {
return 'Agenda';
return `Agenda: ${startMoment.format('MMM D')} - ${endMoment.format('MMM D')}`;
}
let startFormat = 'MMM D YYYY';

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -14,19 +13,16 @@ function createMapStateToProps() {
createDimensionsSelector(),
createUISettingsSelector(),
(calendar, dimensions, uiSettings) => {
const result = _.pick(calendar, [
'isFetching',
'view',
'time',
'start',
'end'
]);
result.isSmallScreen = dimensions.isSmallScreen;
result.collapseViewButtons = dimensions.isLargeScreen;
result.longDateFormat = uiSettings.longDateFormat;
return result;
return {
isFetching: calendar.isFetching,
view: calendar.view,
time: calendar.time,
start: calendar.start,
end: calendar.end,
isSmallScreen: dimensions.isSmallScreen,
collapseViewButtons: dimensions.isLargeScreen,
longDateFormat: uiSettings.longDateFormat
};
}
);
}

View File

@@ -58,11 +58,30 @@ function getSelectedIndex(props) {
values
} = props;
if (Array.isArray(value)) {
return values.findIndex((v) => {
return value.size && v.key === value[0];
});
}
return values.findIndex((v) => {
return v.key === value;
});
}
function isSelectedItem(index, props) {
const {
value,
values
} = props;
if (Array.isArray(value)) {
return value.includes(values[index].key);
}
return values[index].key === value;
}
function getKey(selectedIndex, values) {
return values[selectedIndex].key;
}
@@ -92,7 +111,7 @@ class EnhancedSelectInput extends Component {
this._scheduleUpdate();
}
if (prevProps.value !== this.props.value) {
if (!Array.isArray(this.props.value) && prevProps.value !== this.props.value) {
this.setState({
selectedIndex: getSelectedIndex(this.props)
});
@@ -134,7 +153,7 @@ class EnhancedSelectInput extends Component {
const button = document.getElementById(this._buttonId);
const options = document.getElementById(this._optionsId);
if (!button || this.state.isMobile) {
if (!button || !event.target.isConnected || this.state.isMobile) {
return;
}
@@ -177,7 +196,7 @@ class EnhancedSelectInput extends Component {
}
if (
selectedIndex == null ||
selectedIndex == null || selectedIndex === -1 ||
getSelectedOption(selectedIndex, values).isDisabled
) {
if (keyCode === keyCodes.UP_ARROW) {
@@ -235,12 +254,27 @@ class EnhancedSelectInput extends Component {
}
onSelect = (value) => {
this.setState({ isOpen: false });
if (Array.isArray(this.props.value)) {
let newValue = null;
const index = this.props.value.indexOf(value);
if (index === -1) {
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
} else {
newValue = [...this.props.value];
newValue.splice(index, 1);
}
this.props.onChange({
name: this.props.name,
value: newValue
});
} else {
this.setState({ isOpen: false });
this.props.onChange({
name: this.props.name,
value
});
this.props.onChange({
name: this.props.name,
value
});
}
}
onMeasure = ({ width }) => {
@@ -258,6 +292,7 @@ class EnhancedSelectInput extends Component {
const {
className,
disabledClassName,
value,
values,
isDisabled,
hasError,
@@ -275,6 +310,7 @@ class EnhancedSelectInput extends Component {
isMobile
} = this.state;
const isMultiSelect = Array.isArray(value);
const selectedOption = getSelectedOption(selectedIndex, values);
return (
@@ -303,9 +339,12 @@ class EnhancedSelectInput extends Component {
onPress={this.onPress}
>
<SelectedValueComponent
value={value}
values={values}
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
isMultiSelect={isMultiSelect}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
@@ -359,11 +398,17 @@ class EnhancedSelectInput extends Component {
>
{
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey);
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
depth={depth}
isSelected={isSelectedItem(index, this.props)}
isDisabled={parentSelected}
isMultiSelect={isMultiSelect}
{...valueOptions}
{...v}
isMobile={false}
@@ -401,11 +446,17 @@ class EnhancedSelectInput extends Component {
<Scroller className={styles.optionsModalScroller}>
{
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey);
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
depth={depth}
isSelected={isSelectedItem(index, this.props)}
isMultiSelect={isMultiSelect}
isDisabled={parentSelected}
{...valueOptions}
{...v}
isMobile={true}
@@ -429,9 +480,9 @@ EnhancedSelectInput.propTypes = {
className: PropTypes.string,
disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool,
isDisabled: PropTypes.bool.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
valueOptions: PropTypes.object.isRequired,

View File

@@ -11,6 +11,18 @@
}
}
.optionCheck {
composes: container from '~./CheckInput.css';
flex: 0 0 0;
}
.optionCheckInput {
composes: input from '~./CheckInput.css';
margin-top: 0;
}
.isSelected {
background-color: #e2e2e2;

View File

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import CheckInput from './CheckInput';
import styles from './EnhancedSelectInputOption.css';
class EnhancedSelectInputOption extends Component {
@@ -20,15 +21,21 @@ class EnhancedSelectInputOption extends Component {
onSelect(id);
}
onCheckPress = () => {
// CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation.
}
//
// Render
render() {
const {
className,
id,
isSelected,
isDisabled,
isHidden,
isMultiSelect,
isMobile,
children
} = this.props;
@@ -37,8 +44,8 @@ class EnhancedSelectInputOption extends Component {
<Link
className={classNames(
className,
isSelected && styles.isSelected,
isDisabled && styles.isDisabled,
isSelected && !isMultiSelect && styles.isSelected,
isDisabled && !isMultiSelect && styles.isDisabled,
isHidden && styles.isHidden,
isMobile && styles.isMobile
)}
@@ -46,6 +53,19 @@ class EnhancedSelectInputOption extends Component {
isDisabled={isDisabled}
onPress={this.onPress}
>
{
isMultiSelect &&
<CheckInput
className={styles.optionCheckInput}
containerClassName={styles.optionCheck}
name={`select-${id}`}
value={isSelected}
isDisabled={isDisabled}
onChange={this.onCheckPress}
/>
}
{children}
{
@@ -67,6 +87,7 @@ EnhancedSelectInputOption.propTypes = {
isSelected: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
isHidden: PropTypes.bool.isRequired,
isMultiSelect: PropTypes.bool.isRequired,
isMobile: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onSelect: PropTypes.func.isRequired
@@ -75,7 +96,8 @@ EnhancedSelectInputOption.propTypes = {
EnhancedSelectInputOption.defaultProps = {
className: styles.option,
isDisabled: false,
isHidden: false
isHidden: false,
isMultiSelect: false
};
export default EnhancedSelectInputOption;

View File

@@ -10,6 +10,7 @@ import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
import EnhancedSelectInput from './EnhancedSelectInput';
import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
import NumberInput from './NumberInput';
@@ -66,6 +67,9 @@ function getComponent(type) {
case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector;
case inputTypes.SELECT:
return EnhancedSelectInput;

View File

@@ -6,14 +6,23 @@ import styles from './HintedSelectInputOption.css';
function HintedSelectInputOption(props) {
const {
id,
value,
hint,
isSelected,
isDisabled,
isMultiSelect,
isMobile,
...otherProps
} = props;
return (
<EnhancedSelectInputOption
id={id}
isSelected={isSelected}
isDisabled={isDisabled}
isHidden={isDisabled}
isMultiSelect={isMultiSelect}
isMobile={isMobile}
{...otherProps}
>
@@ -36,9 +45,19 @@ function HintedSelectInputOption(props) {
}
HintedSelectInputOption.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.string.isRequired,
hint: PropTypes.node,
isSelected: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
isMultiSelect: PropTypes.bool.isRequired,
isMobile: PropTypes.bool.isRequired
};
HintedSelectInputOption.defaultProps = {
isDisabled: false,
isHidden: false,
isMultiSelect: false
};
export default HintedSelectInputOption;

View File

@@ -1,23 +1,43 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import styles from './HintedSelectInputSelectedValue.css';
function HintedSelectInputSelectedValue(props) {
const {
value,
values,
hint,
isMultiSelect,
includeHint,
...otherProps
} = props;
const valuesMap = isMultiSelect && _.keyBy(values, 'key');
return (
<EnhancedSelectInputSelectedValue
className={styles.selectedValue}
{...otherProps}
>
<div className={styles.valueText}>
{value}
{
isMultiSelect &&
value.map((key, index) => {
const v = valuesMap[key];
return (
<Label key={key}>
{v ? v.value : key}
</Label>
);
})
}
{
!isMultiSelect && value
}
</div>
{
@@ -31,12 +51,15 @@ function HintedSelectInputSelectedValue(props) {
}
HintedSelectInputSelectedValue.propTypes = {
value: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
hint: PropTypes.string,
isMultiSelect: PropTypes.bool.isRequired,
includeHint: PropTypes.bool.isRequired
};
HintedSelectInputSelectedValue.defaultProps = {
isMultiSelect: false,
includeHint: true
};

View File

@@ -0,0 +1,70 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { indexerFlags }) => indexerFlags,
(state) => state.settings.indexerFlags,
(selectedFlags, indexerFlags) => {
const value = [];
indexerFlags.items.forEach((item) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & item.id) === item.id) {
value.push(item.id);
}
});
const values = indexerFlags.items.map(({ id, name }) => {
return {
key: id,
value: name
};
});
return {
value,
values
};
}
);
}
class IndexerFlagsSelectInputConnector extends Component {
onChange = ({ name, value }) => {
let indexerFlags = 0;
value.forEach((flagId) => {
indexerFlags += flagId;
});
this.props.onChange({ name, value: indexerFlags });
}
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
IndexerFlagsSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
indexerFlags: PropTypes.number.isRequired,
value: PropTypes.arrayOf(PropTypes.number).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps)(IndexerFlagsSelectInputConnector);

View File

@@ -3,10 +3,17 @@ import React from 'react';
import TextInput from './TextInput';
import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input
function onCopy(e) {
e.preventDefault();
e.nativeEvent.stopImmediatePropagation();
}
function PasswordInput(props) {
return (
<TextInput
{...props}
onCopy={onCopy}
/>
);
}

View File

@@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
function getType(type) {
function getType(type, value) {
switch (type) {
case 'captcha':
return inputTypes.CAPTCHA;
@@ -45,7 +45,8 @@ function getSelectValues(selectOptions) {
return _.reduce(selectOptions, (result, option) => {
result.push({
key: option.value,
value: option.name
value: option.name,
hint: option.hint
});
return result;
@@ -87,7 +88,7 @@ function ProviderFieldFormGroup(props) {
<FormLabel>{label}</FormLabel>
<FormInputGroup
type={getType(type)}
type={getType(type, value)}
name={name}
label={label}
helpText={helpText}

View File

@@ -15,10 +15,12 @@
.value {
display: flex;
max-width: 500px;
}
.movieFolder {
flex: 0 0 auto;
@add-mixin truncate;
color: $disabledColor;
}

View File

@@ -130,7 +130,8 @@ class TextInput extends Component {
step,
min,
max,
onBlur
onBlur,
onCopy
} = this.props;
return (
@@ -155,6 +156,8 @@ class TextInput extends Component {
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={onBlur}
onCopy={onCopy}
onCut={onCopy}
onKeyUp={this.onKeyUp}
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
@@ -180,6 +183,7 @@ TextInput.propTypes = {
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onCopy: PropTypes.func,
onSelectionChange: PropTypes.func
};

View File

@@ -2,8 +2,24 @@ import React from 'react';
import styles from './LoadingMessage.css';
const messages = [
'Welcome to Radarr Aphrodite Preview. Enjoy'
// TODO Add some messages here
'Downloading more RAM',
'Now in Technicolor',
'Previously on Radarr...',
'Bleep Bloop.',
'Locating the required gigapixels to render...',
'Spinning up the hamster wheel...',
'At least you\'re not on hold',
'Hum something loud while others stare',
'Loading humorous message... Please Wait',
'I could\'ve been faster in Python',
'Don\'t forget to rewind your tracks',
'Congratulations! you are the 1000th visitor.',
'HELP! I\'m being held hostage and forced to write these stupid lines!',
'RE-calibrating the internet...',
'I\'ll be here all week',
'Don\'t forget to tip your waitress',
'Apply directly to the forehead',
'Loading Battlestation'
];
let message = null;

View File

@@ -6,7 +6,7 @@ import { createSelector } from 'reselect';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchMovies } from 'Store/Actions/movieActions';
import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -48,6 +48,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.ui.isPopulated,
(state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.languages.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated,
(
@@ -56,6 +57,7 @@ const selectIsPopulated = createSelector(
uiSettingsIsPopulated,
qualityProfilesIsPopulated,
languagesIsPopulated,
indexerFlagsIsPopulated,
importListsIsPopulated,
systemStatusIsPopulated
) => {
@@ -65,6 +67,7 @@ const selectIsPopulated = createSelector(
uiSettingsIsPopulated &&
qualityProfilesIsPopulated &&
languagesIsPopulated &&
indexerFlagsIsPopulated &&
importListsIsPopulated &&
systemStatusIsPopulated
);
@@ -77,6 +80,7 @@ const selectErrors = createSelector(
(state) => state.settings.ui.error,
(state) => state.settings.qualityProfiles.error,
(state) => state.settings.languages.error,
(state) => state.settings.indexerFlags.error,
(state) => state.settings.importLists.error,
(state) => state.system.status.error,
(
@@ -85,6 +89,7 @@ const selectErrors = createSelector(
uiSettingsError,
qualityProfilesError,
languagesError,
indexerFlagsError,
importListsError,
systemStatusError
) => {
@@ -94,6 +99,7 @@ const selectErrors = createSelector(
uiSettingsError ||
qualityProfilesError ||
languagesError ||
indexerFlagsError ||
importListsError ||
systemStatusError
);
@@ -105,6 +111,7 @@ const selectErrors = createSelector(
uiSettingsError,
qualityProfilesError,
languagesError,
indexerFlagsError,
importListsError,
systemStatusError
};
@@ -153,6 +160,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchLanguages() {
dispatch(fetchLanguages());
},
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchImportLists() {
dispatch(fetchImportLists());
},
@@ -191,6 +201,7 @@ class PageConnector extends Component {
this.props.dispatchFetchTags();
this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguages();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
@@ -215,6 +226,7 @@ class PageConnector extends Component {
dispatchFetchTags,
dispatchFetchQualityProfiles,
dispatchFetchLanguages,
dispatchFetchIndexerFlags,
dispatchFetchImportLists,
dispatchFetchUISettings,
dispatchFetchStatus,
@@ -254,6 +266,7 @@ PageConnector.propTypes = {
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@@ -8,12 +7,12 @@ function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return _.pick(uiSettings, [
'showRelativeDates',
'shortDateFormat',
'longDateFormat',
'timeFormat'
]);
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}

View File

@@ -2,7 +2,7 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { DndProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { HTML5Backend } from 'react-dnd-html5-backend';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';

View File

@@ -156,3 +156,35 @@
.body {
padding: 5px;
}
.verticalContainer {
max-height: 300px;
}
.horizontalContainer {
max-width: calc($breakpointExtraSmall - 20px);
}
@media only screen and (min-width: $breakpointExtraSmall) {
.horizontalContainer {
max-width: calc($breakpointSmall * 0.8);
}
}
@media only screen and (min-width: $breakpointSmall) {
.horizontalContainer {
max-width: calc($breakpointMedium * 0.8);
}
}
@media only screen and (min-width: $breakpointMedium) {
.horizontalContainer {
max-width: calc($breakpointLarge * 0.8);
}
}
/* @media only screen and (max-width: $breakpointLarge) {
.horizontalContainer {
max-width: calc($breakpointLarge * 0.8);
}
} */

View File

@@ -4,9 +4,26 @@ import React, { Component } from 'react';
import { Manager, Popper, Reference } from 'react-popper';
import Portal from 'Components/Portal';
import { kinds, tooltipPositions } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import styles from './Tooltip.css';
let maxWidth = null;
function getMaxWidth() {
const windowWidth = window.innerWidth;
if (windowWidth >= parseInt(dimensions.breakpointLarge)) {
maxWidth = 800;
} else if (windowWidth >= parseInt(dimensions.breakpointMedium)) {
maxWidth = 650;
} else if (windowWidth >= parseInt(dimensions.breakpointSmall)) {
maxWidth = 500;
} else {
maxWidth = 450;
}
}
class Tooltip extends Component {
//
@@ -17,6 +34,7 @@ class Tooltip extends Component {
this._scheduleUpdate = null;
this._closeTimeout = null;
this._maxWidth = maxWidth || getMaxWidth();
this.state = {
isOpen: false
@@ -54,9 +72,11 @@ class Tooltip extends Component {
} else if ((/^bottom/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom - 20;
} else if ((/^right/).test(data.placement)) {
data.styles.maxWidth = windowWidth - right - 50;
data.styles.maxWidth = Math.min(this._maxWidth, windowWidth - right - 20);
data.styles.maxHeight = top - 20;
} else {
data.styles.maxWidth = left - 35;
data.styles.maxWidth = Math.min(this._maxWidth, left - 20);
data.styles.maxHeight = top - 20;
}
return data;
@@ -144,10 +164,16 @@ class Tooltip extends Component {
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
const popperPlacement = placement ? placement.split('-')[0] : position;
const vertical = popperPlacement === 'top' || popperPlacement === 'bottom';
return (
<div
ref={ref}
className={styles.tooltipContainer}
className={classNames(
styles.tooltipContainer,
vertical ? styles.verticalContainer : styles.horizontalContainer
)}
style={style}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
@@ -156,7 +182,7 @@ class Tooltip extends Component {
className={this.state.isOpen ? classNames(
styles.arrow,
styles[kind],
styles[placement.split('-')[0]]
styles[popperPlacement]
) : styles.arrowDisabled}
ref={arrowProps.ref}
style={arrowProps.style}
@@ -201,7 +227,7 @@ Tooltip.defaultProps = {
bodyClassName: styles.body,
kind: kinds.DEFAULT,
position: tooltipPositions.TOP,
canFlip: true
canFlip: false
};
export default Tooltip;

View File

@@ -10,6 +10,7 @@ export const PASSWORD = 'password';
export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const SELECT = 'select';
export const TAG = 'tag';
export const TEXT = 'text';
@@ -30,6 +31,7 @@ export const all = [
PATH,
QUALITY_PROFILE_SELECT,
ROOT_FOLDER_SELECT,
INDEXER_FLAGS_SELECT,
SELECT,
TAG,
TEXT,

View File

@@ -18,11 +18,14 @@ function createMapStateToProps() {
items
} = languages;
const filterItems = ['Any', 'Original'];
const filteredLanguages = items.filter((lang) => !filterItems.includes(lang.name));
return {
isFetching,
isPopulated,
error,
items
items: filteredLanguages
};
}
);
@@ -54,7 +57,9 @@ class SelectLanguageModalContentConnector extends Component {
const language = _.find(this.props.items,
(item) => item.id === parseInt(languageId));
languages.push(language);
if (language !== undefined) {
languages.push(language);
}
});
this.props.dispatchUpdateInteractiveImportItems({

View File

@@ -8,8 +8,8 @@ function createMapStateToProps() {
return createSelector(
(state, { guid }) => guid,
(state) => state.movieHistory.items,
(state) => state.blacklist.items,
(guid, movieHistory, blacklist) => {
(state) => state.movieBlacklist.items,
(guid, movieHistory, movieBlacklist) => {
let blacklistData = {};
let historyFailedData = {};
@@ -17,7 +17,7 @@ function createMapStateToProps() {
const historyGrabbedData = movieHistory.find((movie) => movie.eventType === 'grabbed' && movie.data.guid === guid);
if (historyGrabbedData) {
historyFailedData = movieHistory.find((movie) => movie.eventType === 'downloadFailed' && movie.sourceTitle === historyGrabbedData.sourceTitle);
blacklistData = blacklist.find((item) => item.sourceTitle === historyGrabbedData.sourceTitle);
blacklistData = movieBlacklist.find((item) => item.sourceTitle === historyGrabbedData.sourceTitle);
}
return {

View File

@@ -35,8 +35,8 @@ class DeleteMovieModalContentConnector extends Component {
this.props.onModalClose(true);
if (this.props.previousMovie) {
this.props.push(this.props.previousMovie);
if (this.props.nextMovieRelativePath) {
this.props.push(window.Radarr.urlBase + this.props.nextMovieRelativePath);
}
}
@@ -58,7 +58,7 @@ DeleteMovieModalContentConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
deleteMovie: PropTypes.func.isRequired,
push: PropTypes.func.isRequired,
previousMovie: PropTypes.string
nextMovieRelativePath: PropTypes.string
};
export default connect(createMapStateToProps, mapDispatchToProps)(DeleteMovieModalContentConnector);

View File

@@ -17,6 +17,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
@@ -100,6 +101,7 @@ class MovieDetails extends Component {
window.removeEventListener('touchend', this.onTouchEnd);
window.removeEventListener('touchcancel', this.onTouchCancel);
window.removeEventListener('touchmove', this.onTouchMove);
window.removeEventListener('keyup', this.onKeyUp);
}
//
@@ -176,11 +178,13 @@ class MovieDetails extends Component {
}
onKeyUp = (event) => {
if (event.keyCode === keyCodes.LEFT_ARROW) {
this.props.onGoToMovie(this.props.previousMovie.titleSlug);
}
if (event.keyCode === keyCodes.RIGHT_ARROW) {
this.props.onGoToMovie(this.props.nextMovie.titleSlug);
if (event.path.length === 4) {
if (event.keyCode === keyCodes.LEFT_ARROW) {
this.props.onGoToMovie(this.props.previousMovie.titleSlug);
}
if (event.keyCode === keyCodes.RIGHT_ARROW) {
this.props.onGoToMovie(this.props.nextMovie.titleSlug);
}
}
}
@@ -236,6 +240,10 @@ class MovieDetails extends Component {
}
}
onTabSelect = (index, lastIndex) => {
this.setState({ selectedTabIndex: index });
}
//
// Render
@@ -454,15 +462,14 @@ class MovieDetails extends Component {
{
<span className={styles.links}>
<Popover
<Tooltip
anchor={
<Icon
name={icons.EXTERNAL_LINK}
size={20}
/>
}
title={translate('Links')}
body={
tooltip={
<MovieDetailsLinks
tmdbId={tmdbId}
imdbId={imdbId}
@@ -477,15 +484,14 @@ class MovieDetails extends Component {
{
!!tags.length &&
<span>
<Popover
<Tooltip
anchor={
<Icon
name={icons.TAGS}
size={20}
/>
}
title={translate('Tags')}
body={
tooltip={
<MovieTagsConnector movieId={id} />
}
position={tooltipPositions.BOTTOM}
@@ -543,7 +549,7 @@ class MovieDetails extends Component {
>
<span className={styles.sizeOnDisk}>
{
formatBytes(sizeOnDisk)
formatBytes(sizeOnDisk || 0)
}
</span>
</InfoLabel>
@@ -613,7 +619,7 @@ class MovieDetails extends Component {
</div>
}
<Tabs selectedIndex={this.state.tabIndex} onSelect={(tabIndex) => this.setState({ selectedTabIndex: tabIndex })}>
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}>
<TabList
className={styles.tabList}
>
@@ -727,7 +733,7 @@ class MovieDetails extends Component {
isOpen={isDeleteMovieModalOpen}
movieId={id}
onModalClose={this.onDeleteMovieModalClose}
previousMovie={`/movie/${previousMovie.titleSlug}`}
nextMovieRelativePath={`/movie/${nextMovie.titleSlug}`}
/>
<InteractiveImportModal

View File

@@ -5,10 +5,10 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { fetchBlacklist } from 'Store/Actions/blacklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearExtraFiles, fetchExtraFiles } from 'Store/Actions/extraFileActions';
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { clearMovieBlacklist, fetchMovieBlacklist } from 'Store/Actions/movieBlacklistActions';
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
@@ -222,9 +222,11 @@ function createMapDispatchToProps(dispatch, props) {
onGoToMovie(titleSlug) {
dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`));
},
dispatchFetchBlacklist() {
// TODO: Allow for passing a movie id to fetch a single movie's blacklist data
dispatch(fetchBlacklist());
dispatchFetchMovieBlacklist({ movieId }) {
dispatch(fetchMovieBlacklist({ movieId }));
},
dispatchClearMovieBlacklist() {
dispatch(clearMovieBlacklist());
}
};
}
@@ -278,20 +280,17 @@ class MovieDetailsConnector extends Component {
const movieId = this.props.id;
this.props.dispatchFetchMovieFiles({ movieId });
this.props.dispatchFetchMovieBlacklist({ movieId });
this.props.dispatchFetchMovieHistory({ movieId });
this.props.dispatchFetchExtraFiles({ movieId });
this.props.dispatchFetchMovieCredits({ movieId });
this.props.dispatchFetchQueueDetails({ movieId });
this.props.dispatchFetchImportListSchema();
this.props.dispatchFetchBlacklist();
}
repopulate = () => {
this.props.dispatchFetchBlacklist();
}
unpopulate = () => {
this.props.dispatchCancelFetchReleases();
this.props.dispatchClearMovieBlacklist();
this.props.dispatchClearMovieFiles();
this.props.dispatchClearMovieHistory();
this.props.dispatchClearExtraFiles();
@@ -363,7 +362,8 @@ MovieDetailsConnector.propTypes = {
dispatchClearQueueDetails: PropTypes.func.isRequired,
dispatchFetchImportListSchema: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired,
dispatchFetchBlacklist: PropTypes.func.isRequired,
dispatchFetchMovieBlacklist: PropTypes.func.isRequired,
dispatchClearMovieBlacklist: PropTypes.func.isRequired,
onGoToMovie: PropTypes.func.isRequired
};

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@@ -8,12 +7,12 @@ function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return _.pick(uiSettings, [
'showRelativeDates',
'shortDateFormat',
'longDateFormat',
'timeFormat'
]);
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -36,13 +35,13 @@ function createMapStateToProps() {
pendingChanges
} = moviesState;
const movieSettings = _.pick(movie, [
'monitored',
'qualityProfileId',
'minimumAvailability',
'path',
'tags'
]);
const movieSettings = {
monitored: movie.monitored,
qualityProfileId: movie.qualityProfileId,
minimumAvailability: movie.minimumAvailability,
path: movie.path,
tags: movie.tags
};
const settings = selectSettings(movieSettings, pendingChanges, saveError);

View File

@@ -72,7 +72,8 @@ class MovieIndexOverviews extends Component {
sortKey,
overviewOptions,
jumpToCharacter,
isMovieEditorActive
isMovieEditorActive,
isSmallScreen
} = this.props;
const {
@@ -82,13 +83,15 @@ class MovieIndexOverviews extends Component {
if (prevProps.sortKey !== sortKey ||
prevProps.overviewOptions !== overviewOptions) {
this.calculateGrid();
this.calculateGrid(this.state.width, isSmallScreen);
}
if (this._grid &&
if (
this._grid &&
(prevState.width !== width ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items) ||
prevProps.overviewOptions !== overviewOptions ||
prevProps.isMovieEditorActive !== isMovieEditorActive)) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './MovieIndexPosterInfo.css';
function MovieIndexPosterInfo(props) {
@@ -51,7 +52,7 @@ function MovieIndexPosterInfo(props) {
return (
<div className={styles.info}>
{`Added ${addedDate}`}
{translate('Added')}: {addedDate}
</div>
);
}
@@ -69,7 +70,7 @@ function MovieIndexPosterInfo(props) {
return (
<div className={styles.info}>
{`In Cinemas ${inCinemasDate}`}
{translate('InCinemas')}: {inCinemasDate}
</div>
);
}
@@ -87,7 +88,7 @@ function MovieIndexPosterInfo(props) {
return (
<div className={styles.info}>
{`Digital ${digitalReleaseDate}`}
{translate('Digital')}: {digitalReleaseDate}
</div>
);
}
@@ -105,7 +106,7 @@ function MovieIndexPosterInfo(props) {
return (
<div className={styles.info}>
{`Released ${physicalReleaseDate}`}
{translate('Released')}: {physicalReleaseDate}
</div>
);
}

View File

@@ -8,8 +8,8 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import TagListConnector from 'Components/TagListConnector';
import Popover from 'Components/Tooltip/Popover';
import { icons } from 'Helpers/Props';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
@@ -369,21 +369,22 @@ class MovieIndexRow extends Component {
className={styles[name]}
>
<span className={styles.externalLinks}>
<Popover
<Tooltip
anchor={
<Icon
name={icons.EXTERNAL_LINK}
size={12}
/>
}
title={translate('Links')}
body={
tooltip={
<MovieDetailsLinks
tmdbId={tmdbId}
imdbId={imdbId}
youTubeTrailerId={youTubeTrailerId}
/>
}
canFlip={true}
kind={kinds.INVERSE}
/>
</span>

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -10,16 +9,13 @@ function createMapStateToProps() {
return createSelector(
createMovieSelector(),
(movie) => {
const result = _.pick(movie, [
'inCinemas',
'isAvailable',
'monitored',
'grabbed'
]);
result.movieFile = movie.movieFile;
return result;
return {
inCinemas: movie.inCinemas,
isAvailable: movie.isAvailable,
monitored: movie.monitored,
grabbed: movie.grabbed,
movieFile: movie.movieFile
};
}
);
}

View File

@@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import FileEditModalContentConnector from './FileEditModalContentConnector';
class FileEditModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<FileEditModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
FileEditModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default FileEditModal;

View File

@@ -0,0 +1,237 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class FileEditModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
qualityId,
languageIds,
indexerFlags,
proper,
real,
edition,
releaseGroup
} = props;
this.state = {
qualityId,
languageIds,
indexerFlags,
proper,
real,
edition,
releaseGroup
};
}
//
// Listeners
onQualityChange = ({ value }) => {
this.setState({ qualityId: parseInt(value) });
}
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
}
onSaveInputs = () => {
this.props.onSaveInputs(this.state);
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
qualities,
languages,
relativePath,
onModalClose
} = this.props;
const {
qualityId,
languageIds,
indexerFlags,
proper,
real,
edition,
releaseGroup
} = this.state;
const qualityOptions = qualities.map(({ id, name }) => {
return {
key: id,
value: name
};
});
const languageOptions = languages.map(({ id, name }) => {
return {
key: id,
value: name
};
});
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('EditMovieFile')} - {relativePath}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadQualities')}
</div>
}
{
isPopulated && !error &&
<Form>
<FormGroup>
<FormLabel>{translate('Quality')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="quality"
value={qualityId}
values={qualityOptions}
onChange={this.onQualityChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Proper')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="proper"
value={proper}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Real')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="real"
value={real}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Languages')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="languageIds"
value={languageIds}
values={languageOptions}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('IndexerFlags')}</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_FLAGS_SELECT}
name="indexerFlags"
indexerFlags={indexerFlags}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Edition')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="edition"
value={edition}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="releaseGroup"
value={releaseGroup}
onChange={this.onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onSaveInputs}
>
{translate('Save')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
FileEditModalContent.propTypes = {
qualityId: PropTypes.number.isRequired,
proper: PropTypes.bool.isRequired,
real: PropTypes.bool.isRequired,
relativePath: PropTypes.string.isRequired,
edition: PropTypes.string.isRequired,
releaseGroup: PropTypes.string.isRequired,
languageIds: PropTypes.arrayOf(PropTypes.number).isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerFlags: PropTypes.number.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveInputs: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default FileEditModalContent;

View File

@@ -0,0 +1,139 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { updateMovieFiles } from 'Store/Actions/movieFileActions';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
import getQualities from 'Utilities/Quality/getQualities';
import FileEditModalContent from './FileEditModalContent';
function createMapStateToProps() {
return createSelector(
createMovieFileSelector(),
(state) => state.settings.qualityProfiles,
(state) => state.settings.languages,
(movieFile, qualityProfiles, languages) => {
const filterItems = ['Any', 'Original'];
const filteredLanguages = languages.items.filter((lang) => !filterItems.includes(lang.name));
const quality = movieFile.quality;
return {
isFetching: qualityProfiles.isSchemaFetching || languages.isFetching,
isPopulated: qualityProfiles.isSchemaPopulated && languages.isPopulated,
error: qualityProfiles.error || languages.error,
qualityId: quality ? quality.quality.id : 0,
real: quality ? quality.revision.real > 0 : false,
proper: quality ? quality.revision.version > 1 : false,
qualities: getQualities(qualityProfiles.schema.items),
languageIds: movieFile.languages ? movieFile.languages.map((l) => l.id) : [],
languages: filteredLanguages,
indexerFlags: movieFile.indexerFlags,
edition: movieFile.edition,
releaseGroup: movieFile.releaseGroup,
relativePath: movieFile.relativePath
};
}
);
}
const mapDispatchToProps = {
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema,
dispatchUpdateMovieFiles: updateMovieFiles
};
class FileEditModalContentConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
if (!this.props.isPopulated) {
this.props.dispatchFetchQualityProfileSchema();
}
}
//
// Listeners
onSaveInputs = ( payload ) => {
const {
qualityId,
real,
proper,
languageIds,
edition,
releaseGroup,
indexerFlags
} = payload;
const quality = this.props.qualities.find((item) => item.id === qualityId);
const languages = [];
languageIds.forEach((languageId) => {
const language = this.props.languages.find((item) => item.id === parseInt(languageId));
if (language !== undefined) {
languages.push(language);
}
});
const revision = {
version: proper ? 2 : 1,
real: real ? 1 : 0
};
const movieFileIds = [this.props.movieFileId];
this.props.dispatchUpdateMovieFiles({
movieFileIds,
languages,
indexerFlags,
edition,
releaseGroup,
quality: {
quality,
revision
}
});
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<FileEditModalContent
{...this.props}
onSaveInputs={this.onSaveInputs}
/>
);
}
}
FileEditModalContentConnector.propTypes = {
movieFileId: PropTypes.number.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
languageIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerFlags: PropTypes.number.isRequired,
qualityId: PropTypes.number.isRequired,
real: PropTypes.bool.isRequired,
edition: PropTypes.string.isRequired,
releaseGroup: PropTypes.string.isRequired,
relativePath: PropTypes.string.isRequired,
proper: PropTypes.bool.isRequired,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
dispatchUpdateMovieFiles: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(FileEditModalContentConnector);

View File

@@ -3,16 +3,14 @@ import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import SelectLanguageModal from 'MovieFile/Language/SelectLanguageModal';
import FileEditModal from 'MovieFile/Edit/FileEditModal';
import MediaInfoConnector from 'MovieFile/MediaInfoConnector';
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
import SelectQualityModal from 'MovieFile/Quality/SelectQualityModal';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import FileDetailsModal from '../FileDetailsModal';
@@ -28,32 +26,15 @@ class MovieFileEditorRow extends Component {
super(props, context);
this.state = {
isSelectQualityModalOpen: false,
isSelectLanguageModalOpen: false,
isConfirmDeleteModalOpen: false,
isFileDetailsModalOpen: false
isFileDetailsModalOpen: false,
isFileEditModalOpen: false
};
}
//
// Listeners
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
}
onSelectLanguagePress = () => {
this.setState({ isSelectLanguageModalOpen: true });
}
onSelectQualityModalClose = () => {
this.setState({ isSelectQualityModalOpen: false });
}
onSelectLanguageModalClose = () => {
this.setState({ isSelectLanguageModalOpen: false });
}
onDeletePress = () => {
this.setState({ isConfirmDeleteModalOpen: true });
}
@@ -76,6 +57,14 @@ class MovieFileEditorRow extends Component {
this.setState({ isFileDetailsModalOpen: false });
}
onFileEditPress = () => {
this.setState({ isFileEditModalOpen: true });
}
onFileEditModalClose = () => {
this.setState({ isFileEditModalOpen: false });
}
//
// Render
@@ -92,9 +81,8 @@ class MovieFileEditorRow extends Component {
} = this.props;
const {
isSelectQualityModalOpen,
isSelectLanguageModalOpen,
isFileDetailsModalOpen,
isFileEditModalOpen,
isConfirmDeleteModalOpen
} = this.state;
@@ -132,10 +120,8 @@ class MovieFileEditorRow extends Component {
{formatBytes(size)}
</TableRowCell>
<TableRowCellButton
<TableRowCell
className={styles.language}
title={translate('ClickToChangeLanguage')}
onPress={this.onSelectLanguagePress}
>
{
showLanguagePlaceholder &&
@@ -149,12 +135,10 @@ class MovieFileEditorRow extends Component {
languages={languages}
/>
}
</TableRowCellButton>
</TableRowCell>
<TableRowCellButton
<TableRowCell
className={styles.quality}
title={translate('ClickToChangeQuality')}
onPress={this.onSelectQualityPress}
>
{
showQualityPlaceholder &&
@@ -169,7 +153,7 @@ class MovieFileEditorRow extends Component {
isCutoffNotMet={qualityCutoffNotMet}
/>
}
</TableRowCellButton>
</TableRowCell>
<TableRowCell
className={styles.formats}
@@ -180,6 +164,11 @@ class MovieFileEditorRow extends Component {
</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
name={icons.EDIT}
onPress={this.onFileEditPress}
/>
<IconButton
name={icons.MEDIA_INFO}
onPress={this.onFileDetailsPress}
@@ -198,6 +187,12 @@ class MovieFileEditorRow extends Component {
mediaInfo={mediaInfo}
/>
<FileEditModal
movieFileId={id}
isOpen={isFileEditModalOpen}
onModalClose={this.onFileEditModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
ids={[id]}
@@ -208,22 +203,6 @@ class MovieFileEditorRow extends Component {
onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose}
/>
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
ids={[id]}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose}
/>
<SelectLanguageModal
isOpen={isSelectLanguageModalOpen}
ids={[id]}
languageIds={languages ? languages.map((l) => l.id) : []}
onModalClose={this.onSelectLanguageModalClose}
/>
</TableRow>
);
}

View File

@@ -18,7 +18,7 @@ function createMapStateToProps() {
items
} = languages;
const filterItems = ['Any'];
const filterItems = ['Any', 'Original'];
const filteredLanguages = items.filter((lang) => !filterItems.includes(lang.name));
return {
@@ -57,7 +57,9 @@ class SelectLanguageModalContentConnector extends Component {
const language = _.find(this.props.items,
(item) => item.id === parseInt(languageId));
languages.push(language);
if (language !== undefined) {
languages.push(language);
}
});
this.props.dispatchupdateMovieFiles({

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { DndProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { HTML5Backend } from 'react-dnd-html5-backend';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';

View File

@@ -143,7 +143,7 @@ EditRemotePathMappingModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.shape(remotePathMappingShape).isRequired,
downloadClientHosts: PropTypes.arrayOf(PropTypes.string).isRequired,
downloadClientHosts: PropTypes.arrayOf(PropTypes.object).isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,

View File

@@ -49,6 +49,12 @@ function EditImportListModalContent(props) {
fields
} = item;
const importListTypeOptions = [
{ key: 'manual', value: 'Manual' },
{ key: 'automatic', value: 'Automatic Add' },
{ key: 'exclusion', value: 'Exclusion List' }
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -100,8 +106,9 @@ function EditImportListModalContent(props) {
<FormLabel>{translate('EnableAutomaticAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
type={inputTypes.SELECT}
name="enableAuto"
values={importListTypeOptions}
helpText={translate('EnableAutoHelpText')}
{...enableAuto}
onChange={onInputChange}
@@ -173,7 +180,7 @@ function EditImportListModalContent(props) {
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('TagsHelpText')}
helpText={translate('ListTagsHelpText')}
{...tags}
onChange={onInputChange}
/>

View File

@@ -36,7 +36,7 @@ function EditRestrictionModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? 'Edit Restriction' : 'Add Restriction'}
{id ? translate('EditRestriction') : translate('AddRestriction')}
</ModalHeader>
<ModalBody>
@@ -51,7 +51,7 @@ function EditRestrictionModalContent(props) {
name="required"
helpText={translate('RequiredHelpText')}
kind={kinds.SUCCESS}
placeholder={translate('RequiredPlaceHolder')}
placeholder={translate('RequiredRestrictionPlaceHolder')}
{...required}
onChange={onInputChange}
/>

View File

@@ -116,8 +116,10 @@ class NamingModal extends Component {
const movieTokens = [
{ token: '{Movie Title}', example: 'Movie Title!' },
{ token: '{Movie Title:DE}', example: 'Filetitle' },
{ token: '{Movie CleanTitle}', example: 'Movie Title' },
{ token: '{Movie TitleThe}', example: 'Movie Title, The' },
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας' },
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
{ token: '{Movie Collection}', example: 'The Movie Collection' },
{ token: '{Movie Certification}', example: 'R' },

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { DndProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import { HTML5Backend } from 'react-dnd-html5-backend';
import Link from 'Components/Link/Link';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';

View File

@@ -3,7 +3,8 @@
flex-wrap: wrap;
}
.formGroupWrapper {
.formGroupWrapper,
.formatItemLarge {
flex: 0 0 calc($formGroupSmallWidth - 100px);
}
@@ -11,8 +12,20 @@
margin-right: auto;
}
@media only screen and (max-width: $breakpointLarge) {
.formatItemSmall {
display: none;
}
@media only screen and (max-width: calc($breakpointLarge + 100px)) {
.formGroupsContainer {
display: block;
}
.formatItemSmall {
display: block;
}
.formatItemLarge {
display: none;
}
}

View File

@@ -21,6 +21,17 @@ import styles from './EditQualityProfileModalContent.css';
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
function getCustomFormatRender(formatItems, otherProps) {
return (
<QualityProfileFormatItems
profileFormatItems={formatItems.value}
errors={formatItems.errors}
warnings={formatItems.warnings}
{...otherProps}
/>
);
}
class EditQualityProfileModalContent extends Component {
//
@@ -251,6 +262,10 @@ class EditQualityProfileModalContent extends Component {
onChange={onLanguageChange}
/>
</FormGroup>
<div className={styles.formatItemLarge}>
{getCustomFormatRender(formatItems, ...otherProps)}
</div>
</div>
<div className={styles.formGroupWrapper}>
@@ -263,13 +278,8 @@ class EditQualityProfileModalContent extends Component {
/>
</div>
<div className={styles.formGroupWrapper}>
<QualityProfileFormatItems
profileFormatItems={formatItems.value}
errors={formatItems.errors}
warnings={formatItems.warnings}
{...otherProps}
/>
<div className={styles.formatItemSmall}>
{getCustomFormatRender(formatItems, otherProps)}
</div>
</div>
</Form>

View File

@@ -65,6 +65,8 @@ class UISettings extends Component {
...otherProps
} = this.props;
const uiLanguages = languages.filter((item) => item.value !== 'Original');
return (
<PageContent title={translate('UISettings')}>
<SettingsToolbarConnector
@@ -213,7 +215,7 @@ class UISettings extends Component {
<FormInputGroup
type={inputTypes.SELECT}
name="uiLanguage"
values={languages}
values={uiLanguages}
helpText={translate('UILanguageHelpText')}
helpTextWarning={translate('UILanguageHelpTextWarning')}
onChange={onInputChange}

View File

@@ -1,10 +1,12 @@
import $ from 'jquery';
import _ from 'lodash';
import { batchActions } from 'redux-batched-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getProviderState from 'Utilities/State/getProviderState';
import { set, updateItem } from '../baseActions';
const abortCurrentRequests = {};
let lastSaveData = null;
export function createCancelSaveProviderHandler(section) {
return function(getState, payload, dispatch) {
@@ -26,25 +28,33 @@ function createSaveProviderHandler(section, url, options = {}) {
} = payload;
const saveData = getProviderState({ id, ...otherPayload }, getState, section);
const requestUrl = id ? `${url}/${id}` : url;
const params = { ...queryParams };
// If the user is re-saving the same provider without changes
// force it to be saved. Only applies to editing existing providers.
if (id && _.isEqual(saveData, lastSaveData)) {
params.forceSave = true;
}
lastSaveData = saveData;
const ajaxOptions = {
url: `${url}?${$.param(queryParams, true)}`,
method: 'POST',
url: `${requestUrl}?${$.param(params, true)}`,
method: id ? 'PUT' : 'POST',
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(saveData)
};
if (id) {
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
ajaxOptions.method = 'PUT';
}
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
abortCurrentRequests[section] = abortRequest;
request.done((data) => {
lastSaveData = null;
dispatch(batchActions([
updateItem({ section, ...data }),

View File

@@ -0,0 +1,48 @@
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.indexerFlags';
//
// Actions Types
export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
//
// Action Creators
export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
items: []
},
//
// Action Handlers
actionHandlers: {
[FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
},
//
// Reducers
reducers: {
}
};

View File

@@ -1,8 +1,11 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
import translate from 'Utilities/String/translate';
import { set, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
@@ -25,6 +28,7 @@ export const defaultState = {
sortDirection: sortDirections.DESCENDING,
error: null,
items: [],
isRemoving: false,
columns: [
{
@@ -96,7 +100,8 @@ export const GOTO_LAST_BLACKLIST_PAGE = 'blacklist/gotoBlacklistLastPage';
export const GOTO_BLACKLIST_PAGE = 'blacklist/gotoBlacklistPage';
export const SET_BLACKLIST_SORT = 'blacklist/setBlacklistSort';
export const SET_BLACKLIST_TABLE_OPTION = 'blacklist/setBlacklistTableOption';
export const REMOVE_FROM_BLACKLIST = 'blacklist/removeFromBlacklist';
export const REMOVE_BLACKLIST_ITEM = 'blacklist/removeBlacklistItem';
export const REMOVE_BLACKLIST_ITEMS = 'blacklist/removeBlacklistItems';
export const CLEAR_BLACKLIST = 'blacklist/clearBlacklist';
//
@@ -110,7 +115,8 @@ export const gotoBlacklistLastPage = createThunk(GOTO_LAST_BLACKLIST_PAGE);
export const gotoBlacklistPage = createThunk(GOTO_BLACKLIST_PAGE);
export const setBlacklistSort = createThunk(SET_BLACKLIST_SORT);
export const setBlacklistTableOption = createAction(SET_BLACKLIST_TABLE_OPTION);
export const removeFromBlacklist = createThunk(REMOVE_FROM_BLACKLIST);
export const removeBlacklistItem = createThunk(REMOVE_BLACKLIST_ITEM);
export const removeBlacklistItems = createThunk(REMOVE_BLACKLIST_ITEMS);
export const clearBlacklist = createAction(CLEAR_BLACKLIST);
//
@@ -131,7 +137,53 @@ export const actionHandlers = handleThunks({
[serverSideCollectionHandlers.SORT]: SET_BLACKLIST_SORT
}),
[REMOVE_FROM_BLACKLIST]: createRemoveItemHandler(section, '/blacklist')
[REMOVE_BLACKLIST_ITEM]: createRemoveItemHandler(section, '/blacklist'),
[REMOVE_BLACKLIST_ITEMS]: function(getState, payload, dispatch) {
const {
ids
} = payload;
dispatch(batchActions([
...ids.map((id) => {
return updateItem({
section,
id,
isRemoving: true
});
}),
set({ section, isRemoving: true })
]));
const promise = createAjaxRequest({
url: '/blacklist/bulk',
method: 'DELETE',
dataType: 'json',
data: JSON.stringify({ ids })
}).request;
promise.done((data) => {
// Don't use batchActions with thunks
dispatch(fetchBlacklist());
dispatch(set({ section, isRemoving: false }));
});
promise.fail((xhr) => {
dispatch(batchActions([
...ids.map((id) => {
return updateItem({
section,
id,
isRemoving: false
});
}),
set({ section, isRemoving: false })
]));
});
}
});
//

View File

@@ -11,6 +11,7 @@ import * as history from './historyActions';
import * as importMovie from './importMovieActions';
import * as interactiveImportActions from './interactiveImportActions';
import * as movies from './movieActions';
import * as movieBlacklist from './movieBlacklistActions';
import * as movieCredits from './movieCreditsActions';
import * as movieFiles from './movieFileActions';
import * as movieHistory from './movieHistoryActions';
@@ -48,6 +49,7 @@ export default [
releases,
rootFolders,
movies,
movieBlacklist,
movieHistory,
movieIndex,
movieCredits,

View File

@@ -0,0 +1,82 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { set, update } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'movieBlacklist';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_MOVIE_BLACKLIST = 'movieBlacklist/fetchMovieBlacklist';
export const CLEAR_MOVIE_BLACKLIST = 'movieBlacklist/clearMovieBlacklist';
//
// Action Creators
export const fetchMovieBlacklist = createThunk(FETCH_MOVIE_BLACKLIST);
export const clearMovieBlacklist = createAction(CLEAR_MOVIE_BLACKLIST);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_MOVIE_BLACKLIST]: function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({
url: '/blacklist/movie',
data: payload
}).request;
promise.done((data) => {
dispatch(batchActions([
update({ section, data }),
set({
section,
isFetching: false,
isPopulated: true,
error: null
})
]));
});
promise.fail((xhr) => {
dispatch(set({
section,
isFetching: false,
isPopulated: false,
error: xhr
}));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
[CLEAR_MOVIE_BLACKLIST]: (state) => {
return Object.assign({}, state, defaultState);
}
}, defaultState, section);

View File

@@ -141,7 +141,10 @@ export const actionHandlers = handleThunks({
const {
movieFileIds,
languages,
quality
indexerFlags,
quality,
edition,
releaseGroup
} = payload;
dispatch(set({ section, isSaving: true }));
@@ -154,10 +157,22 @@ export const actionHandlers = handleThunks({
data.languages = languages;
}
if (indexerFlags !== undefined) {
data.indexerFlags = indexerFlags;
}
if (quality) {
data.quality = quality;
}
if (releaseGroup) {
data.releaseGroup = releaseGroup;
}
if (edition) {
data.edition = edition;
}
const promise = createAjaxRequest({
url: '/movieFile/editor',
method: 'PUT',
@@ -174,10 +189,22 @@ export const actionHandlers = handleThunks({
props.languages = languages;
}
if (indexerFlags) {
props.indexerFlags = indexerFlags;
}
if (quality) {
props.quality = quality;
}
if (edition) {
props.edition = edition;
}
if (releaseGroup) {
props.releaseGroup = releaseGroup;
}
return updateItem({ section, id, ...props });
}),

View File

@@ -1,4 +1,5 @@
import _ from 'lodash';
import moment from 'moment';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props';
@@ -142,6 +143,11 @@ export const defaultState = {
isModifiable: false
}
]
},
sortPredicates: {
estimatedCompletionTime: function(item, direction) {
return moment.duration(item.timeleft).asMilliseconds();
}
}
};
@@ -308,9 +314,9 @@ export const actionHandlers = handleThunks({
}).request;
promise.done((data) => {
dispatch(batchActions([
fetchQueue(),
dispatch(fetchQueue());
dispatch(batchActions([
...ids.map((id) => {
return updateItem({
section: paged,
@@ -394,10 +400,10 @@ export const actionHandlers = handleThunks({
}).request;
promise.done((data) => {
dispatch(batchActions([
set({ section: paged, isRemoving: false }),
fetchQueue()
]));
// Don't use batchActions with thunks
dispatch(fetchQueue());
dispatch(set({ section: paged, isRemoving: false }));
});
promise.fail((xhr) => {

View File

@@ -10,6 +10,7 @@ import general from './Settings/general';
import importExclusions from './Settings/importExclusions';
import importListOptions from './Settings/importListOptions';
import importLists from './Settings/importLists';
import indexerFlags from './Settings/indexerFlags';
import indexerOptions from './Settings/indexerOptions';
import indexers from './Settings/indexers';
import languages from './Settings/languages';
@@ -31,6 +32,7 @@ export * from './Settings/delayProfiles';
export * from './Settings/downloadClients';
export * from './Settings/downloadClientOptions';
export * from './Settings/general';
export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions';
export * from './Settings/indexers';
export * from './Settings/languages';
@@ -66,6 +68,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState,
general: general.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState,
indexers: indexers.defaultState,
languages: languages.defaultState,
@@ -109,6 +112,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers,
...general.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers,
...indexers.actionHandlers,
...languages.actionHandlers,
@@ -143,6 +147,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers,
...downloadClientOptions.reducers,
...general.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers,
...indexers.reducers,
...languages.reducers,

View File

@@ -19,6 +19,7 @@ function getInternalLink(source) {
case 'IndexerRssCheck':
case 'IndexerSearchCheck':
case 'IndexerStatusCheck':
case 'IndexerLongTermStatusCheck':
return (
<IconButton
name={icons.SETTINGS}

View File

@@ -22,7 +22,7 @@ class MoreInfo extends Component {
<DescriptionListItemTitle>Discord</DescriptionListItemTitle>
<DescriptionListItemDescription>
<Link to="https://discord.gg/AD3UP37">discord.gg/AD3UP37</Link>
<Link to="https://discord.gg/r5wJPt9">discord.gg/r5wJPt9</Link>
</DescriptionListItemDescription>
<DescriptionListItemTitle>Wiki</DescriptionListItemTitle>

View File

@@ -24,9 +24,13 @@ class UpdateChanges extends Component {
<ul>
{
changes.map((change, index) => {
const checkChange = change.replace(/#\d{4,5}\b/g, (match, contents) => {
return `[${match}](https://github.com/Radarr/Radarr/issues/${match.substring(1)})`;
});
return (
<li key={index}>
<InlineMarkdown data={change} />
<InlineMarkdown data={checkChange} />
</li>
);
})

View File

@@ -7,7 +7,8 @@ function formatTimeSpan(timeSpan) {
}
const duration = moment.duration(timeSpan);
const days = duration.get('days');
const days = Math.floor(duration.asDays());
const hours = padNumber(duration.get('hours'), 2);
const minutes = padNumber(duration.get('minutes'), 2);
const seconds = padNumber(duration.get('seconds'), 2);

View File

@@ -4,6 +4,7 @@ import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday';
import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday';
import translate from 'Utilities/String/translate';
function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
if (!date) {
@@ -21,15 +22,15 @@ function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat,
}
if (isYesterday(date)) {
return 'Yesterday';
return translate('Yesterday');
}
if (isTodayDate) {
return 'Today';
return translate('Today');
}
if (isTomorrow(date)) {
return 'Tomorrow';
return translate('Tomorrow');
}
if (isInNextWeek(date)) {

View File

@@ -1,11 +1,12 @@
import moment from 'moment';
function isToday(date) {
if (!date) {
return false;
}
return moment(date).isSame(moment(), 'day');
const dateObj = (typeof date === 'object') ? date : new Date(date);
const today = new Date();
return dateObj.getDate() === today.getDate() && dateObj.getMonth() === today.getMonth() && dateObj.getFullYear() === today.getFullYear();
}
export default isToday;

View File

@@ -1,11 +1,13 @@
import moment from 'moment';
function isTomorrow(date) {
if (!date) {
return false;
}
return moment(date).isSame(moment().add(1, 'day'), 'day');
const dateObj = (typeof date === 'object') ? date : new Date(date);
const today = new Date();
const tomorrow = new Date((today.setDate(today.getDate() + 1)));
return dateObj.getDate() === tomorrow.getDate() && dateObj.getMonth() === tomorrow.getMonth() && dateObj.getFullYear() === tomorrow.getFullYear();
}
export default isTomorrow;

View File

@@ -1,11 +1,13 @@
import moment from 'moment';
function isYesterday(date) {
if (!date) {
return false;
}
return moment(date).isSame(moment().subtract(1, 'day'), 'day');
const dateObj = (typeof date === 'object') ? date : new Date(date);
const today = new Date();
const yesterday = new Date((today.setDate(today.getDate() - 1)));
return dateObj.getDate() === yesterday.getDate() && dateObj.getMonth() === yesterday.getMonth() && dateObj.getFullYear() === yesterday.getFullYear();
}
export default isYesterday;

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import qs from 'qs';
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils
@@ -10,18 +9,17 @@ export default function parseUrl(url) {
// The `origin`, `password`, and `username` properties are unavailable in
// Opera Presto. We synthesize `origin` if it's not present. While `password`
// and `username` are ignored intentionally.
const properties = _.pick(
anchor,
'hash',
'host',
'hostname',
'href',
'origin',
'pathname',
'port',
'protocol',
'search'
);
const properties = {
hash: anchor.hash,
host: anchor.host,
hostname: anchor.hostname,
href: anchor.href,
origin: anchor.origin,
pathname: anchor.pathname,
port: anchor.port,
protocol: anchor.protocol,
search: anchor.search
};
properties.isAbsolute = (/^[\w:]*\/\//).test(url);

View File

@@ -1,62 +0,0 @@
#!/bin/sh
#get the bundle's MacOS directory full path
DIR=$(cd "$(dirname "$0")"; pwd)
#change these values to match your app
EXE_PATH="$DIR/Radarr.exe"
APPNAME="Radarr"
#set up environment
if [[ -x '/opt/local/bin/mono' ]]; then
# Macports and mono-supplied installer path
export PATH="/opt/local/bin:$PATH"
elif [[ -x '/usr/local/bin/mono' ]]; then
# Homebrew-supplied path to mono
export PATH="/usr/local/bin:$PATH"
fi
export DYLD_FALLBACK_LIBRARY_PATH="$DIR"
if [ -e /Library/Frameworks/Mono.framework ]; then
MONO_FRAMEWORK_PATH=/Library/Frameworks/Mono.framework/Versions/Current
export PATH="$MONO_FRAMEWORK_PATH/bin:$PATH"
export DYLD_FALLBACK_LIBRARY_PATH="$DYLD_FALLBACK_LIBRARY_PATH:$MONO_FRAMEWORK_PATH/lib"
fi
if [[ -f '/opt/local/lib/libsqlite3.0.dylib' ]]; then
export DYLD_FALLBACK_LIBRARY_PATH="/opt/local/lib:$DYLD_FALLBACK_LIBRARY_PATH"
fi
export DYLD_FALLBACK_LIBRARY_PATH="$DYLD_FALLBACK_LIBRARY_PATH:$HOME/lib:/usr/local/lib:/lib:/usr/lib"
#mono version check
REQUIRED_MAJOR=4
REQUIRED_MINOR=6
VERSION_TITLE="Cannot launch $APPNAME"
VERSION_MSG="$APPNAME requires Mono Runtime Environment(MRE) $REQUIRED_MAJOR.$REQUIRED_MINOR or later."
DOWNLOAD_URL="http://www.mono-project.com/download/#download-mac"
MONO_VERSION="$(mono --version | grep 'Mono JIT compiler version ' | cut -f5 -d\ )"
# if [[ -o DEBUG ]]; then osascript -e "display dialog \"MONO_VERSION: $MONO_VERSION\""; fi
MONO_VERSION_MAJOR="$(echo $MONO_VERSION | cut -f1 -d.)"
MONO_VERSION_MINOR="$(echo $MONO_VERSION | cut -f2 -d.)"
if [ -z "$MONO_VERSION" ] \
|| [ $MONO_VERSION_MAJOR -lt $REQUIRED_MAJOR ] \
|| [ $MONO_VERSION_MAJOR -eq $REQUIRED_MAJOR -a $MONO_VERSION_MINOR -lt $REQUIRED_MINOR ]
then
osascript \
-e "set question to display dialog \"$VERSION_MSG\" with title \"$VERSION_TITLE\" buttons {\"Cancel\", \"Download...\"} default button 2" \
-e "if button returned of question is equal to \"Download...\" then open location \"$DOWNLOAD_URL\""
echo "$VERSION_TITLE"
echo "$VERSION_MSG"
exit 1
fi
MONO_EXEC="exec mono --debug"
#run app using mono
$MONO_EXEC "$EXE_PATH"

View File

@@ -17,28 +17,27 @@
"license": "GPL-3.0",
"readmeFilename": "readme.md",
"dependencies": {
"@babel/core": "7.9.6",
"@babel/plugin-proposal-class-properties": "7.8.3",
"@babel/plugin-proposal-decorators": "7.8.3",
"@babel/plugin-proposal-export-default-from": "7.8.3",
"@babel/plugin-proposal-export-namespace-from": "7.8.3",
"@babel/plugin-proposal-function-sent": "7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.3",
"@babel/plugin-proposal-numeric-separator": "7.8.3",
"@babel/plugin-proposal-optional-chaining": "7.9.0",
"@babel/plugin-proposal-throw-expressions": "7.8.3",
"@babel/core": "7.11.6",
"@babel/plugin-proposal-class-properties": "7.10.4",
"@babel/plugin-proposal-decorators": "7.10.5",
"@babel/plugin-proposal-export-default-from": "7.10.4",
"@babel/plugin-proposal-export-namespace-from": "7.10.4",
"@babel/plugin-proposal-function-sent": "7.10.4",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.10.4",
"@babel/plugin-proposal-numeric-separator": "7.10.4",
"@babel/plugin-proposal-optional-chaining": "7.11.0",
"@babel/plugin-proposal-throw-expressions": "7.10.4",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.9.6",
"@babel/preset-react": "7.9.4",
"@fortawesome/fontawesome-free": "5.13.0",
"@fortawesome/fontawesome-svg-core": "1.2.28",
"@fortawesome/free-regular-svg-icons": "5.13.0",
"@fortawesome/free-solid-svg-icons": "5.13.0",
"@fortawesome/react-fontawesome": "0.1.9",
"@babel/preset-env": "7.11.5",
"@babel/preset-react": "7.10.4",
"@fortawesome/fontawesome-free": "5.15.0",
"@fortawesome/fontawesome-svg-core": "1.2.31",
"@fortawesome/free-regular-svg-icons": "5.15.0",
"@fortawesome/free-solid-svg-icons": "5.15.0",
"@fortawesome/react-fontawesome": "0.1.11",
"@microsoft/signalr": "3.1.7",
"@popperjs/core": "2.2.1",
"@sentry/browser": "5.15.5",
"@sentry/integrations": "5.15.5",
"@sentry/browser": "5.24.2",
"@sentry/integrations": "5.24.2",
"ansi-colors": "4.1.1",
"autoprefixer": "9.7.5",
"babel-eslint": "10.1.0",
@@ -48,19 +47,19 @@
"classnames": "2.2.6",
"clipboard": "2.0.6",
"connected-react-router": "6.8.0",
"core-js": "3",
"core-js": "3.6.5",
"css-loader": "3.4.2",
"del": "5.1.0",
"del": "6.0.0",
"element-class": "0.2.2",
"eslint": "7.5.0",
"eslint": "7.10.0",
"eslint-plugin-filenames": "1.3.2",
"eslint-plugin-import": "2.22.0",
"eslint-plugin-react": "7.20.5",
"eslint-plugin-react": "7.21.3",
"eslint-plugin-simple-import-sort": "5.0.3",
"esprint": "0.7.0",
"file-loader": "6.0.0",
"file-loader": "6.1.0",
"filesize": "6.1.0",
"fuse.js": "6.0.4",
"fuse.js": "6.4.1",
"gulp": "4.0.2",
"gulp-cached": "1.1.1",
"gulp-concat": "2.6.1",
@@ -71,14 +70,14 @@
"gulp-watch": "5.0.1",
"gulp-wrap": "0.15.0",
"history": "4.10.1",
"html-webpack-plugin": "3.2.0",
"html-webpack-plugin": "4.5.0",
"jdu": "1.0.0",
"jquery": "3.5.1",
"loader-utils": "^2.0.0",
"lodash": "4.17.20",
"mini-css-extract-plugin": "0.9.0",
"mobile-detect": "1.4.4",
"moment": "2.24.0",
"moment": "2.29.0",
"mousetrap": "1.6.5",
"normalize.css": "8.0.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
@@ -92,22 +91,22 @@
"qs": "6.9.4",
"react": "16.13.1",
"react-addons-shallow-compare": "15.6.2",
"react-async-script": "1.1.1",
"react-async-script": "1.2.0",
"react-autosuggest": "10.0.2",
"react-custom-scrollbars": "4.2.1",
"react-dnd": "10.0.2",
"react-dnd-html5-backend": "10.0.2",
"react-dnd": "11.1.3",
"react-dnd-html5-backend": "11.1.3",
"react-document-title": "2.0.3",
"react-dom": "16.13.1",
"react-focus-lock": "2.3.1",
"react-google-recaptcha": "2.0.1",
"react-lazyload": "2.6.7",
"react-focus-lock": "2.4.1",
"react-google-recaptcha": "2.1.0",
"react-lazyload": "3.0.0",
"react-measure": "1.4.7",
"react-popper": "1.3.7",
"react-redux": "7.2.0",
"react-router": "5.1.2",
"react-router-dom": "5.1.2",
"react-slider": "1.0.7",
"react-redux": "7.2.1",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-slider": "1.0.11",
"react-tabs": "3.1.1",
"react-text-truncate": "0.16.0",
"react-virtualized": "9.21.1",
@@ -121,12 +120,12 @@
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "1.2.1",
"stylelint": "13.6.1",
"stylelint": "13.7.2",
"stylelint-order": "4.1.0",
"url-loader": "4.1.0",
"webpack": "4.42.1",
"webpack-stream": "5.2.1",
"worker-loader": "2.0.0"
"webpack": "4.44.2",
"webpack-stream": "6.1.0",
"worker-loader": "3.0.3"
},
"main": "index.js",
"browserslist": [

View File

@@ -37,13 +37,14 @@ Compression=lzma2/normal
AppContact={#ForumsURL}
VersionInfoVersion={#BaseVersion}.{#BuildNumber}
SetupLogging=yes
OutputDir=output
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}"
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts)"; GroupDescription: "Start automatically"; Flags: exclusive
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
@@ -57,6 +58,9 @@ Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon
Name: "{userstartup}\{#AppName}"; Filename: "{app}\Radarr.exe"; WorkingDir: "{app}"; Tasks: startupShortcut
[InstallDelete]
Name: "{app}"; Type: filesandordirs
[Run]
Filename: "{app}\Radarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u"; Flags: runhidden waituntilterminated;
Filename: "{app}\Radarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
@@ -72,5 +76,6 @@ function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ResultCode: Integer;
begin
Exec(ExpandConstant('{commonappdata}\Radarr\bin\Radarr.Console.exe'), '/u', '', 0, ewWaitUntilTerminated, ResultCode)
Exec('net', 'stop radarr', '', 0, ewWaitUntilTerminated, ResultCode)
Exec('sc', 'delete radarr', '', 0, ewWaitUntilTerminated, ResultCode)
end;

View File

@@ -1,3 +1,4 @@
using System;
using System.IO;
using System.Linq;
using Nancy;
@@ -57,7 +58,7 @@ namespace NzbDrone.Api.FileSystem
if (!_diskProvider.FolderExists(path))
{
return new string[0];
return Array.Empty<string>();
}
return _diskScanService.GetVideoFiles(path).Select(f => new

View File

@@ -56,9 +56,7 @@ namespace NzbDrone.Api.Indexers
public int? Leechers { get; set; }
public DownloadProtocol Protocol { get; set; }
// TODO: Remove in v3
// Used to support the original Release Push implementation
// JsonIgnore so we don't serialize it, but can still parse it
// JsonIgnore so we don't serialize it, but can still parse it, removed in v3
[JsonIgnore]
public DownloadProtocol DownloadProtocol
{

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Api.ImportList
base.MapToResource(resource, definition);
resource.Enabled = definition.Enabled;
resource.EnableAuto = definition.EnableAuto;
resource.EnableAuto = (int)definition.EnableAuto;
resource.ProfileId = definition.ProfileId;
resource.RootFolderPath = definition.RootFolderPath;
resource.ShouldMonitor = definition.ShouldMonitor;
@@ -34,7 +34,7 @@ namespace NzbDrone.Api.ImportList
base.MapToModel(definition, resource);
definition.Enabled = resource.Enabled;
definition.EnableAuto = resource.EnableAuto;
definition.EnableAuto = (ImportListType)resource.EnableAuto;
definition.ProfileId = resource.ProfileId;
definition.RootFolderPath = resource.RootFolderPath;
definition.ShouldMonitor = resource.ShouldMonitor;

View File

@@ -6,7 +6,7 @@ namespace NzbDrone.Api.ImportList
public class ImportListResource : ProviderResource
{
public bool Enabled { get; set; }
public bool EnableAuto { get; set; }
public int EnableAuto { get; set; }
public bool ShouldMonitor { get; set; }
public string RootFolderPath { get; set; }
public int ProfileId { get; set; }

View File

@@ -147,7 +147,7 @@ namespace NzbDrone.Api
{
var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList();
var result = new List<TProviderResource>(defaultDefinitions.Count());
var result = new List<TProviderResource>(defaultDefinitions.Count);
foreach (var providerDefinition in defaultDefinitions)
{

View File

@@ -42,6 +42,8 @@ namespace NzbDrone.Automation.Test
// Timeout as windows automation tests seem to take alot longer to get going
driver = new ChromeDriver(service, options, new TimeSpan(0, 3, 0));
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
_runner.KillAll();
_runner.Start();
@@ -62,6 +64,19 @@ namespace NzbDrone.Automation.Test
.Select(e => e.Text);
}
protected void TakeScreenshot(string name)
{
try
{
Screenshot image = ((ITakesScreenshot)driver).GetScreenshot();
image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to save screenshot {name}, {ex.Message}");
}
}
[OneTimeTearDown]
public void SmokeTestTearDown()
{

View File

@@ -1,4 +1,5 @@
using FluentAssertions;
using System.Reflection;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Automation.Test.PageModel;
using OpenQA.Selenium;
@@ -21,6 +22,10 @@ namespace NzbDrone.Automation.Test
{
_page.MovieNavIcon.Click();
_page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.CssSelector("div[class*='MovieIndex']")).Should().NotBeNull();
}
@@ -30,6 +35,9 @@ namespace NzbDrone.Automation.Test
_page.CalendarNavIcon.Click();
_page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.CssSelector("div[class*='CalendarPage']")).Should().NotBeNull();
}
@@ -39,6 +47,9 @@ namespace NzbDrone.Automation.Test
_page.ActivityNavIcon.Click();
_page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.LinkText("Queue")).Should().NotBeNull();
_page.Find(By.LinkText("History")).Should().NotBeNull();
_page.Find(By.LinkText("Blacklist")).Should().NotBeNull();
@@ -50,6 +61,9 @@ namespace NzbDrone.Automation.Test
_page.SystemNavIcon.Click();
_page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.CssSelector("div[class*='Health']")).Should().NotBeNull();
}
@@ -58,11 +72,12 @@ namespace NzbDrone.Automation.Test
{
_page.MovieNavIcon.Click();
_page.WaitForNoSpinner();
_page.Find(By.LinkText("Add New")).Click();
_page.WaitForNoSpinner();
var imageName = MethodBase.GetCurrentMethod().Name;
TakeScreenshot(imageName);
_page.Find(By.CssSelector("input[class*='AddNewMovie-searchInput']")).Should().NotBeNull();
}
}

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