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

Compare commits

...

214 Commits

Author SHA1 Message Date
Mark McDowall
7b43c2e345 Fixed: Loading movie images after placeholder in Safari
Closes #10474
2024-09-25 06:48:30 +03:00
Bogdan
dc599b6531 Sort allowed sorting keys 2024-09-25 06:47:37 +03:00
Weblate
1421179654 Multiple Translations updated by Weblate
ignore-downstream

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

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






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

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

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

(cherry picked from commit 3c857135c59029635b0972f959f9a8255bcff21f)

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes #10363
Closes #10348
2024-09-03 16:13:43 +03:00
Weblate
b4b5ad9567 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Nota Inutilis <hugo@notainutilis.fr>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: 极染 <poledye@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-09-03 13:56:02 +03:00
Mark McDowall
3ae6347532 Convert Queue to TypeScript
(cherry picked from commit 76650af9fdc7ef06d13ce252986d21574903d293)

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

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

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

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

Fixed: Renaming movie file for a movie

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

Includes:
* Fixed: Subtitle title migration when original title is null
  (cherry picked from commit 8921c5d7a079c58b0c74713355bf82846abc43ab)
* Fixed: Migrating subtitle files with unexpectedly large number at end
  (cherry picked from commit f95dd00b51e61a96a0e6c094ec922c8f5cbc5334)
* Fixed: Parsing of subtitle languages separated by dash
  (cherry picked from commit cb72e752f9e24e5691292d86ac1f46c8fa35a844)
* Fix incorrect subtitle copy regex
  (cherry picked from commit 2ef46e5b902b42cdbafbd80b3154a5d22b090f29)
2024-09-01 15:08:40 +03:00
Bogdan
36d80387c6 Bump version to 5.10.2 2024-09-01 08:06:34 +03:00
Bogdan
9fe4793606 Fixed: Don't show today's relative time for release dates 2024-08-31 11:06:02 +03:00
Mark McDowall
fa80608394 Convert RelativeDateCell to TypeScript
(cherry picked from commit 824ed0a36931ce7aae9aa544a7baf0738dae568c)
2024-08-31 10:47:35 +03:00
Bogdan
6e81d5917e Refactor include time for movie history
(cherry picked from commit 05edd44ed6dfe73c25da021ef2600e609852f7e0)
(cherry picked from commit ca372bee258523339aff2b868f8f9a619d44dbca)
(cherry picked from commit 2a662afaef0bed9f8cd153d60eee5ecbe85508ba)

Closes #10039
Closes #10045
Closes #10046
2024-08-31 10:47:34 +03:00
Bogdan
8d189523c4 Fixed: Forbid empty spaces in Release Profile restrictions
(cherry picked from commit 9136ee4ad934c10eef743e646f8b1c05eff2a2ff)
2024-08-31 07:01:06 +03:00
Bogdan
4d589422e6 Fixed: Importing files without media info available 2024-08-30 01:52:45 +03:00
Bogdan
675612e7c6 Fixed: Existing and Already In Library icons for movie search results 2024-08-29 13:01:40 +03:00
Bogdan
be3916f67d Fixed: Released as default Minimum Availability for add movie modals 2024-08-29 12:50:08 +03:00
Bogdan
453f216e0d New: Add Malayalam and Kannada languages 2024-08-29 09:42:58 +03:00
Bogdan
2d4846e5be New: Add Macedonian and Slovenian languages 2024-08-29 09:42:58 +03:00
Bogdan
2700a6cf8a Fixed: Paths for renamed movie files in Custom Script and Webhook
Closes #10359
2024-08-27 02:11:31 +03:00
Bogdan
674e414111 Don't persist value for SslCertHash when checking for existence 2024-08-27 01:56:03 +03:00
Bogdan
21bd21b70c New: Digital and Physical release dates for Poster options 2024-08-27 01:45:13 +03:00
Bogdan
fde87a38f9 Convert MovieQuality to TypeScript 2024-08-26 08:18:53 +03:00
Bogdan
0d6ba200d3 Fixed: Hide reboot and shutdown UI buttons on docker
Closes #10358
2024-08-26 04:06:40 +03:00
Bogdan
93298645e3 Render with ternary in bulk manage
Closes #10360
2024-08-26 04:01:30 +03:00
Bogdan
58f544e9e0 New: Bypass IP addresses ranges in proxies
(cherry picked from commit 402db9128c214d4c5af6583643cb49d3aa7a28b5)
2024-08-26 03:52:08 +03:00
bakerboy448
cf952d5c0b Fixed: Trim spaces and empty values in Proxy Bypass List
(cherry picked from commit 846333ddf0d9da775c80d004fdb9b41e700ef359)
2024-08-26 03:51:51 +03:00
Treycos
f6d630bdd3 Updated code action fixall value for VSCode
(cherry picked from commit 8af4246ff9baee4c291550102769a1186f65dc29)
2024-08-26 03:51:37 +03:00
Bogdan
657ced4772 Fix disabled style for monitor toggle button
(cherry picked from commit dde28cbd7e16b85f78d38c8dde7cf6bbb6119bb3)
2024-08-26 03:51:23 +03:00
namakeingo
d3a0c83f98 Fixed: False positive HC for MultiSubs (#10024)
* Fixed: Multisubs wrongly detected as hardcoded

As flagged by multiple people before "Multisubs" is a commonly used Tag that indicates that the file has more than 3 subtitle languages. Multisubs never indicate a hardcoded sub as you cannot have a multisubs where you can select between different languages if the subtitles are hardcoded in the video. This minor change excludes "MULTISUBS" from the regex used.
2024-08-26 00:57:52 +03:00
Bogdan
5833d5d4c4 Bump version to 5.10.1 2024-08-25 10:12:39 +03:00
Bogdan
2ba4562f49 Fixed: Improve appearance and sorting by ratings for Discover
Fixes #7271
2024-08-25 09:22:23 +03:00
Bogdan
d79db69644 Hide ratings on movie table in absence of data 2024-08-25 09:10:35 +03:00
Bogdan
7532dfb03c Remove extraneous height from movie posters 2024-08-25 09:08:54 +03:00
Bogdan
a47528aa81 Convert rating component to TypeScript 2024-08-25 09:07:02 +03:00
Bogdan
a812d9f39f Bump core-js
Update UI libs
2024-08-24 05:45:33 +03:00
Mark McDowall
fc97f05850 New: Optionally include Custom Format Score for Discord On File Import notifications
(cherry picked from commit e16ace54a8120cd98007a09fe1e6136be3e699fc)

Closes #10313
2024-08-24 04:32:49 +03:00
Bogdan
644876123d Convert MovieTitleLink to TypeScript
Closes #10322
2024-08-24 04:29:45 +03:00
Treycos
540659a799 Convert First Run to TypeScript
(cherry picked from commit 8484a8bebac38def457d428257888b7824ffb72f)

Closes #10307
2024-08-24 04:24:31 +03:00
Weblate
288668f7e6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Kerk en IT <info@kerkenit.nl>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-08-24 04:09:22 +03:00
Servarr
fcf3be42d5 Automated API Docs update 2024-08-24 04:08:39 +03:00
Mark McDowall
16e218501e Fixed: Limit redirects after login to local paths
(cherry picked from commit 14005d8d1054eafaba808337a109d5812f3e79e6)
2024-08-24 01:56:42 +03:00
Bogdan
caf2d33c11 Fixed: Keeping search results when Missing/Cutoff Unmet repopulates 2024-08-24 01:54:04 +03:00
kephasdev
bc918ed3b5 Fix: Use indexer's Multi Languages setting for pushed releases
(cherry picked from commit 35a2bc940328bf61b39dd0012867bdaa564ee489)

Fixed: Calculating Custom Formats with languages in queue

(cherry picked from commit 8af12cc4e7f71cf169392cd86ccf0eb81f6b375c)

Closes #10273
Closes #10321
2024-08-23 05:57:03 +03:00
Bogdan
df77474314 Fixed: Improve sorting movies by release dates 2024-08-23 03:50:54 +03:00
Servarr
bf84471509 Automated API Docs update 2024-08-22 06:51:51 +03:00
Bogdan
d346d969de Fixed: Validate root folder existence when adding movie 2024-08-22 06:26:55 +03:00
Servarr
14b125ccd9 Automated API Docs update 2024-08-21 15:04:21 -05:00
Bogdan
da5323a08f New: Bulk manage custom formats 2024-08-21 03:57:21 +03:00
Bogdan
672b351497 Bump version to 5.10.0 2024-08-20 21:11:31 +03:00
Servarr
fc4f4ab211 Automated API Docs update 2024-08-20 02:30:14 +03:00
Weblate
333e8281ea Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gabriel Markowski <gmarkowski62@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translation: Servarr/Radarr
2024-08-19 11:21:43 -05:00
shangchenglumetro
c278ffd8a0 Fix typos and improve log messages 2024-08-19 06:40:43 +03:00
Bogdan
5898eea3d0 Bump babel packages
Closes #10320
2024-08-19 06:24:55 +03:00
Bogdan
5b78a1297a Use autoprefixer in UI build
(cherry picked from commit 47a05ecb36e5c960b4f6ca7d279df7c281114611)

Closes #10319
2024-08-19 06:22:54 +03:00
martylukyy
14e3e1fa35 New: Configure log file size limit in UI
(cherry picked from commit 35baebaf7280749d5dfe5440e28b425e45a22d21)

Closes #10318
2024-08-19 06:19:34 +03:00
Bogdan
c0e76544ef Bump version to 5.9.1 2024-08-18 15:04:17 +03:00
Weblate
8c16677875 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-08-18 05:00:07 +03:00
Mark McDowall
401e19547c Cache root folders and improve getting disk space for movie path roots
(cherry picked from commit 63fdf8ca8ff9b22ce4cf8764cc05aad5d1d0ae62)

Closes #10219
2024-08-18 04:30:31 +03:00
Bogdan
c9f28fdc4f Fixed: Validation for Movie Info language 2024-08-18 00:21:04 +03:00
Bogdan
0ad4d7ea9a Fixed: Removing invalid statuses on provider deletion 2024-08-17 18:09:29 +03:00
Bogdan
e8bb3df68e New: Runtime for auto tagging 2024-08-16 23:45:55 +03:00
Bogdan
9442f1fb04 Fixed: Excluded movies on search results 2024-08-16 01:19:19 +03:00
Bogdan
9ad6b3a611 New: Improve status label and progress bar style for deleted movies
Closes #7127
2024-08-16 00:19:36 +03:00
Bogdan
fa1d6ad109 Fixed disabled options for SelectInput 2024-08-15 22:10:48 +03:00
Bogdan
ccbc8f591b Improve status labels for deleted movies 2024-08-15 19:14:22 +03:00
Bogdan
a4301f8db0 Fixed: Parse NEWCAM as CAM
Co-authored-by: bokkoman <bokkoman@gmail.com>

Fixes #10255
2024-08-15 16:05:14 +03:00
Bogdan
fe00825f2b New: Parse DarQ release group
Co-authored-by: Mark McDowall <mark@mcdowall.ca>

Closes #10299
2024-08-15 15:47:40 +03:00
Bogdan
17a9b0f7b0 Exclude movies without year in the missing page
They should not have any release dates, so not quite missing in any case.
2024-08-15 15:41:49 +03:00
Bogdan
62bdb66d0f New: Increase max size limit for quality definitions
Fixes #9822
Closes #10295
2024-08-15 13:26:07 +03:00
Bogdan
7c1fedb8ce Sort quality profiles by name in custom filters 2024-08-15 01:14:59 +03:00
Bogdan
333351da45 Trim trailing slash from trailer link
Convert MovieDetailsLinks to Typescript
2024-08-15 00:50:23 +03:00
Mark McDowall
fbbe7f7b5d Fix typos and improve log messages
(cherry picked from commit 37c4647f242c37f22c7ac455d304055441acf362)

Closes #10277
2024-08-14 23:44:33 +03:00
Mark McDowall
edec201a6c Improve messaging for for Send Notifications setting in Emby / Jellyfin
(cherry picked from commit 4c0b8961741a7dd0cf2aba81cdbcb74c1208a1ff)

Closes #10251
2024-08-14 23:42:51 +03:00
Bogdan
1e783bfe07 Fixed: Dedupe titles to avoid similar search requests
Closes #10278
2024-08-14 23:39:03 +03:00
Bogdan
7d5236de21 Display movie title for interactive search modal 2024-08-14 23:39:03 +03:00
Bogdan
1efe7db5f3 Fixed: Sending Manual Interaction Required notifications for unknown movies 2024-08-14 23:02:04 +03:00
Bogdan
b37cc42805 Fixed: Duplicated changelog lines 2024-08-14 20:57:08 +03:00
Bogdan
fa19f45171 Fixed: Stale formats score after changing quality profile for movies 2024-08-14 20:32:34 +03:00
Bogdan
4ae382cea7 Fixed: Showing multi-languages for movies parsed without languages 2024-08-14 19:26:52 +03:00
Mark McDowall
37c09ba1f8 Fixed: Allow leading/trailing spaces on non-Windows
(cherry picked from commit 9127a91dfc460f442498a00faed98737047098cd)

Closes #10240
2024-08-14 19:21:50 +03:00
Mark McDowall
322df78f5a Fixed: Updating movie path from different OS paths
(cherry picked from commit e791f4b743d9660b0ad1decc4c5ed0e864f3b243)

Closes #10218
2024-08-14 19:21:50 +03:00
Mark McDowall
3a4446cc8e New: Validate that folders in paths don't start or end with a space
(cherry picked from commit 316b5cbf75b45ef9a25f96ce1f2fbed25ad94296)

Closes #9958
2024-08-14 19:21:50 +03:00
Bogdan
6c456e57d8 Convert formatBytes to TypeScript
Closes #10272
2024-08-14 18:48:49 +03:00
Mark McDowall
abc7efabea New: Configurable log file size limit
(cherry picked from commit 813965e6a20edef2772d68eaa7646af33028425a)

Closes #10267
2024-08-14 18:41:00 +03:00
Mark McDowall
ace692aca6 New: Add Compact Log Event Format option for console logging
(cherry picked from commit 0d914f4c53876540ed2df83ad3d71615c013856f)

Closes #10266
2024-08-14 18:36:10 +03:00
Bogdan
882bde713f Upgrade nlog to 5.3.3
Closes #10265
2024-08-14 17:11:53 +03:00
Bogdan
2575e3647f Include available version in update health check
(cherry picked from commit 15e3c3efb18242caf28b9bfc77a72a78296018bf)

Closes #10227
2024-08-14 16:24:23 +03:00
Mark McDowall
5cac5b6068 Update React Lint rules for TSX
(cherry picked from commit 1299a97579bec52ee3d16ab8d05c9e22edd80330)

Closes #10248
2024-08-14 16:20:32 +03:00
Mark McDowall
4628868dfa Fixed: Marking queued item as failed not blocking the correct Torrent Info Hash
(cherry picked from commit 4b186e894e4e229a435c077e00c65b67ca178333)

Closes #10274
2024-08-14 16:09:54 +03:00
Bogdan
25685314bc Fixed: Parsing alternative titles containing "A.K.A." 2024-08-14 15:45:45 +03:00
Qstick
41b1ea553e Log calls to deprecated endpoints
(cherry picked from commit aaaf18aec33dc0ae5075b53ab81812743608d6a6)
2024-08-14 15:45:26 +03:00
Weblate
5d17f8e84d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Wolfy The Broccoly <theproviderofsolace@gmail.com>
Co-authored-by: iMohmmedSA <i.mohmmed.i+1@gmail.com>
Co-authored-by: marudosurdo <marudosurdo@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-08-14 15:42:53 +03:00
jmwallace
7490fc7040 Fix typo in "Import Extra Files" help text
The typo can be found at Settings -> Media Management -> Importing -> Import Extra Files.
2024-08-14 15:41:33 +03:00
Stevie Robinson
f4e1f51a9c Fixed: Interactive Import dropdown width on mobile
(cherry picked from commit f2f4a98eed5bc83224917897642a28381ca648b9)
2024-08-14 15:40:39 +03:00
Mark McDowall
8e1016572b New: Return downloading magnets from Transmission
(cherry picked from commit 11a9dcb3890eaf99602900f37e64007f2fbf9b8e)
2024-08-14 15:40:12 +03:00
Bogdan
caabb032f3 Fixed: Persist selected filter for interactive searches 2024-08-14 15:39:52 +03:00
RaZaSB
ce9c5d4d97 New: Remove all single quote characters from searches
(cherry picked from commit 0877a6718d3df8e217a72cc5b113b8398e495eb1)
2024-08-14 15:39:38 +03:00
Mark McDowall
967bed3161 Align queue action buttons on right
(cherry picked from commit f7a58aab339e2012b6bb91d0b3a38d733ec213c6)
2024-08-14 15:39:11 +03:00
Servarr
8d9f1697ee Automated API Docs update 2024-08-14 15:38:54 +03:00
Bogdan
3be2c6b0be Fixed: Validate uniqueness for import list exclusions 2024-08-13 00:01:24 -05:00
The Dark
b6d9c73a17 New: Import list exclusion pagination and bulk removal
(cherry picked from commit 428569106499b5e3a463f1990ae2996d1ae4ab49)

Persist page size for Import List Exclusions

(cherry picked from commit e81bb3b993adac705fd61dc9e281b040ca2338f5)

Clear pending changes for edit import list exclusions on modal close

(cherry picked from commit 7b87de2e93c2aa499cff224f84253ba944bb58d4)

Fixed actions column width for import list exclusions

(cherry picked from commit d691ad8e12ea4f2bc77f0b551c17d22d91c4ba22)
2024-08-13 00:01:24 -05:00
Bogdan
b1a7652753 Rename ImportExclusion to ImportListExclusion 2024-08-13 00:01:24 -05:00
Bogdan
f76c97c3ce Remove unused ImportExclusions property 2024-08-13 00:01:24 -05:00
Bogdan
1f5a84d202 Convert Import List Options to TypeScript
Co-authored-by: The Dark <12370876+CheAle14@users.noreply.github.com>
2024-08-13 00:01:24 -05:00
Bogdan
d25bcdb043 Rename ImportExclusions to ImportListExclusions 2024-08-13 00:01:24 -05:00
Bogdan
f75497f57d Fixed: Overwriting query params for remove item handler 2024-08-13 00:01:24 -05:00
Bogdan
2f413c68d9 Fixed: Total runtime hours without decimal point 2024-08-13 00:00:37 -05:00
Bogdan
68c20713e5 Validation for bulk movies editor 2024-08-11 19:58:20 -05:00
Bogdan
6eeed96d12 Fix tags height in tag inputs
And other relevant changes missing from #4715
2024-08-06 11:27:55 -05:00
Bogdan
6f306a22e5 Fixed: Persist indexer flags for automatic imports 2024-08-03 12:48:34 -05:00
Bogdan
29ef75960d Fixed: Moving files for torrents when Remove Completed is disabled
(cherry picked from commit 78a0def46a4c8628d9bcf6af2701aa35b3f959b9)

Fixed: Moving files on import for usenet clients

(cherry picked from commit 291d792810d071f28c389d100b9642854d7cd70e)
2024-08-02 22:40:49 -05:00
Wolfy The Broccoly
364a42424a Add translation for MovieIndexSelectAllButton 2024-08-02 22:38:06 -05:00
Mark McDowall
a5b315ba83 Fix height of tags in tag inputs
(cherry picked from commit 5ac6c0e651400aa4d2e7126b0ccf1bcd4c6224b2)
2024-08-02 22:27:45 -05:00
Mark McDowall
e80e96de0e Don't hash files in development builds
(cherry picked from commit bc7799139e52b92956eb595fb87f44d7dda9a320)
2024-08-02 22:27:15 -05:00
Bogdan
44c7c71226 Fixed: Calculate movie availability comparing UTC dates 2024-08-02 22:25:52 -05:00
Mark McDowall
04c5e6c2a6 New: Default file log level changed to debug
(cherry picked from commit 9b528eb82914a05cfc3b67d4d6146ce51e86f68d)
2024-08-02 22:24:13 -05:00
OnTheCliff
5533528b56 Fixed: Runtime value for movies longer than 24 hours
* Update MediaInfoResource.cs

Fixed runtime value for movies longer than 24 hours

* Update src/Radarr.Api.V3/MovieFiles/MediaInfoResource.cs

Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>

* Update src/Radarr.Api.V3/MovieFiles/MediaInfoResource.cs

Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>

---------

Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-08-02 22:23:16 -05:00
Bogdan
74246df881 Fixed: Pagination for TMDb lists containing a mix of media types 2024-07-30 18:59:32 -05:00
Bogdan
88127298ae Improve messaging for renamed movie files progress info 2024-07-26 00:07:35 +03:00
Bogdan
5559fa5fa5 Bump ImageSharp to 3.1.5
https://github.com/advisories/GHSA-63p8-c4ww-9cg7
2024-07-26 00:05:51 +03:00
diamondpete
d503e01747 Fixed: Remove apostrophe, backtick in contractions
(cherry picked from commit 6a4824c02932ee1bd57c1f4f0644f8bc693f6006)

Closes #10178
2024-07-25 21:40:44 +03:00
Mark McDowall
ae89ae175f Fixed: Don't treat SubFrench as French audio language
(cherry picked from commit 5ad3d2efcce7d983bd783b551f32666529086901)

Closes #10209
2024-07-25 21:36:50 +03:00
Bogdan
df35e78e1f New: Display original language on movie details and search results page
Closes #10206
2024-07-25 21:33:19 +03:00
ManiMatter
a3b3fee06b Treat forcedMetaDL from qBit as queued instead of downloading
(cherry picked from commit 9a613afa355fbc8cdf29c4d1b8eb1f1586405eb7)
2024-07-25 07:42:53 +03:00
Bogdan
ae377d97a5 New: Ignore Litestream tables in Database
(cherry picked from commit 2a26c6722afa5c657fde162cbddbe9e8731f3a0c)
2024-07-25 07:37:13 +03:00
Bogdan
270df9d1dd Fixed: Improve filtering performance in Select Movie Modal 2024-07-25 07:23:32 +03:00
Bogdan
6ed3045433 Original Language filter optional for TMDb Popular lists 2024-07-25 03:18:07 +03:00
justin vanderhooft
ddb7d5690b Fixed: Show root folder when path is not available yet on imports
(cherry picked from commit f6fbd3cfee4db891e68f9f15551ea62b02077b5e)
2024-07-25 03:06:26 +03:00
Jendrik Weise
a1104b8263 New: Update matching movie path in Jellyfin/Emby library
(cherry picked from commit ad0dc01cf7ed16ccfa8260717111ad8a44675221)

Closes #8898
2024-07-23 23:39:16 +03:00
Bogdan
358ff0c130 Fix table name for Alternative Titles migrations 2024-07-21 20:16:39 +03:00
Bogdan
ff0a04c331 Remove SQLite specific schema condition from migrations 2024-07-21 20:16:39 +03:00
Bogdan
c12f01f919 Bump version to 5.9.0 2024-07-21 17:35:30 +03:00
Weblate
93d661242a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2024-07-20 16:18:48 +03:00
Bogdan
324dac8db3 New: Bump dotnet to 6.0.32 2024-07-19 23:40:24 +03:00
Weblate
bba69d8b22 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dream <seth.gecko.rr@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Rauniik <raunerjakub@gmail.com>
Co-authored-by: Serhii Matrunchyk <serhii@digitalidea.studio>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: damienmillet <contact@damien-millet.dev>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: quek76 <quek@libertysurf.fr>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-07-19 00:07:13 +03:00
Mark McDowall
1366f6e8b4 New: Show update settings on all platforms
(cherry picked from commit c023fc700896c7f0751c4ac63c4e1a89d6e1a9bb)

Closes #10184
2024-07-18 20:59:42 +03:00
servarr[bot]
f79712951b Fixed: Assume category path from qBittorent starting with '//' is a Windows UNC path
* Fixed: Assume category path from qBittorent starting with '//' is a Windows UNC path

Radarr/Radarr#10162

(cherry picked from commit 19466aa29050e1b13b1db8cc61662b10d76a82e4)

* fixup! Fixed: Assume category path from qBittorent starting with '//' is a Windows UNC path

---------

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-07-18 19:53:55 +03:00
Bogdan
101b046753 Fix custom formats sorting for quality profiles 2024-07-17 20:31:56 +03:00
Bogdan
cd713e7252 New: Sort by tags on movie index 2024-07-17 17:41:46 +03:00
Mark McDowall
a54f54eb6e New: Add option to show tags on movies Poster and Overview
(cherry picked from commit e35b39b4b1c88e13f9b1515c68b4d0942f84fa6d)

Closes #10176
2024-07-17 17:41:46 +03:00
Mark McDowall
f2af7a1b72 New: Use natural sorting for lists of items in the UI
(cherry picked from commit 1a1c8e6c08a6db5fcd2b5d17e65fa1f943d2e746)

Closes #10177
2024-07-17 17:41:46 +03:00
Marc Carbonell
a5b48153a6 New: Add a few spanish release groups to exceptions (#10120)
Co-authored-by: Marc Carbonell Belmonte <mcarbonell@sitel-sa.com>
2024-07-17 16:00:01 +03:00
Stevie Robinson
1804e486d6 New: Wrap specifications in Custom Format and Auto Tagging modals
(cherry picked from commit 7b8d606a1bed6257d7942de47576c1505fd9cb57)
2024-07-17 15:32:47 +03:00
Marc Carbonell
b490177a77 Remove extraneous indentation in RemoveFileExtension
(cherry picked from commit dca5239420e21f91c1d67bc8bbb14cdb13c8d5d9)
2024-07-17 15:21:25 +03:00
Bogdan
7a90b4a6b2 Bump version to 5.8.3 2024-07-14 12:34:19 +03:00
711 changed files with 17340 additions and 12567 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.8.2'
majorVersion: '5.11.0'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.421'
dotnetVersion: '6.0.424'
nodeVersion: '20.X'
innoVersion: '6.2.2'
windowsImage: 'windows-2022'

View File

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

View File

@@ -9,7 +9,7 @@
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll": true
"source.fixAll": "explicit"
},
"typescript.preferences.quoteStyle": "single",

View File

@@ -66,7 +66,7 @@ module.exports = (env) => {
output: {
path: distFolder,
publicPath: '/',
filename: '[name]-[contenthash].js',
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
sourceMapFilename: '[file].map'
},
@@ -91,7 +91,7 @@ module.exports = (env) => {
new MiniCssExtractPlugin({
filename: 'Content/styles.css',
chunkFilename: 'Content/[id]-[chunkhash].css'
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
}),
new HtmlWebpackPlugin({
@@ -201,7 +201,7 @@ module.exports = (env) => {
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
}
}
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,13 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
@@ -113,7 +113,7 @@ class HistoryRow extends Component {
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguage
<MovieLanguages
languages={languages}
/>
</TableRowCell>
@@ -143,7 +143,7 @@ class HistoryRow extends Component {
if (name === 'date') {
return (
<RelativeDateCellConnector
<RelativeDateCell
key={name}
date={date}
/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,4 +26,5 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
text-align: right;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
@@ -36,7 +35,6 @@ const mapDispatchToProps = {
lookupMovie,
clearAddMovie,
fetchRootFolders,
fetchImportExclusions,
fetchQueueDetails,
clearQueueDetails,
fetchMovieFiles,
@@ -56,7 +54,6 @@ class AddNewMovieConnector extends Component {
componentDidMount() {
this.props.fetchRootFolders();
this.props.fetchImportExclusions();
this.props.fetchQueueDetails();
}
@@ -131,7 +128,6 @@ AddNewMovieConnector.propTypes = {
lookupMovie: PropTypes.func.isRequired,
clearAddMovie: PropTypes.func.isRequired,
fetchRootFolders: PropTypes.func.isRequired,
fetchImportExclusions: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired,
fetchMovieFiles: PropTypes.func.isRequired,

View File

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

View File

@@ -85,6 +85,7 @@
margin-top: 20px;
}
.originalLanguage,
.studio,
.genres {
margin-left: 5px;

View File

@@ -8,6 +8,7 @@ interface CssExports {
'genres': string;
'icons': string;
'links': string;
'originalLanguage': string;
'overlay': string;
'overview': string;
'poster': string;

View File

@@ -62,6 +62,7 @@ class AddNewMovieSearchResult extends Component {
titleSlug,
year,
studio,
originalLanguage,
genres,
status,
overview,
@@ -70,7 +71,7 @@ class AddNewMovieSearchResult extends Component {
images,
existingMovieId,
isExistingMovie,
isExclusionMovie,
isExcluded,
isSmallScreen,
colorImpairedMode,
id,
@@ -154,26 +155,27 @@ class AddNewMovieSearchResult extends Component {
</div>
<div className={styles.icons}>
<div>
{
isExistingMovie &&
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title={translate('AlreadyInYourLibrary')}
/>
}
{
isExistingMovie &&
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title={translate('AlreadyInYourLibrary')}
/>
}
{
isExclusionMovie &&
<Icon
className={styles.exclusionIcon}
name={icons.DANGER}
size={36}
title={translate('MovieIsOnImportExclusionList')}
/>
}
{
isExcluded &&
<Icon
className={styles.exclusionIcon}
name={icons.DANGER}
size={36}
title={translate('MovieIsOnImportExclusionList')}
/>
}
</div>
</div>
</div>
@@ -213,17 +215,31 @@ class AddNewMovieSearchResult extends Component {
}
{
!!studio &&
originalLanguage?.name ?
<Label size={sizes.LARGE}>
<Icon
name={icons.LANGUAGE}
size={13}
/>
<span className={styles.originalLanguage}>
{originalLanguage.name}
</span>
</Label> :
null
}
{
studio ?
<Label size={sizes.LARGE}>
<Icon
name={icons.STUDIO}
size={13}
/>
<span className={styles.studio}>
{studio}
</span>
</Label>
</Label> :
null
}
{
@@ -233,7 +249,6 @@ class AddNewMovieSearchResult extends Component {
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.slice(0, 3).join(', ')}
</span>
@@ -271,6 +286,7 @@ class AddNewMovieSearchResult extends Component {
{
isExistingMovie && isSmallScreen &&
<MovieStatusLabel
status={status}
hasMovieFiles={hasMovieFile}
monitored={monitored}
isAvailable={isAvailable}
@@ -311,6 +327,7 @@ AddNewMovieSearchResult.propTypes = {
titleSlug: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
studio: PropTypes.string,
originalLanguage: PropTypes.object,
genres: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string.isRequired,
overview: PropTypes.string,
@@ -319,7 +336,7 @@ AddNewMovieSearchResult.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
existingMovieId: PropTypes.number,
isExistingMovie: PropTypes.bool.isRequired,
isExclusionMovie: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool,
isSmallScreen: PropTypes.bool.isRequired,
id: PropTypes.number,
monitored: PropTypes.bool.isRequired,
@@ -333,7 +350,8 @@ AddNewMovieSearchResult.propTypes = {
};
AddNewMovieSearchResult.defaultProps = {
genres: []
genres: [],
isExcluded: false
};
export default AddNewMovieSearchResult;

View File

@@ -1,27 +1,24 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector';
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
function createMapStateToProps() {
return createSelector(
createExistingMovieSelector(),
createExclusionMovieSelector(),
createDimensionsSelector(),
(state) => state.queue.details.items,
(state) => state.movieFiles.items,
(state, { internalId }) => internalId,
(state) => state.settings.ui.item.movieRuntimeFormat,
(isExistingMovie, isExclusionMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
return {
existingMovieId: internalId,
isExistingMovie,
isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen,
queueItem,
movieFile,

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
import Blocklist from 'Activity/Blocklist/Blocklist';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import Queue from 'Activity/Queue/Queue';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
@@ -115,12 +115,12 @@ function AppRoutes(props) {
<Route
path="/activity/queue"
component={QueueConnector}
component={Queue}
/>
<Route
path="/activity/blocklist"
component={BlocklistConnector}
component={Blocklist}
/>
{/*

View File

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

View File

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

View File

@@ -1,9 +1,10 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import BlocklistAppState from './BlocklistAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import MovieCollectionAppState from './MovieCollectionAppState';
import MovieCreditAppState from './MovieCreditAppState';
import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
import ParseAppState from './ParseAppState';
@@ -46,6 +47,9 @@ export interface CustomFilter {
}
export interface AppSectionState {
isConnected: boolean;
isReconnecting: boolean;
version: string;
dimensions: {
isSmallScreen: boolean;
width: number;
@@ -61,6 +65,7 @@ interface AppState {
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState;
movieCredits: MovieCreditAppState;
movieFiles: MovieFilesAppState;
movieIndex: MovieIndexAppState;
movies: MoviesAppState;

View File

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

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import MovieCredit from 'typings/MovieCredit';
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
export default MovieCreditAppState;

View File

@@ -20,11 +20,15 @@ export interface MovieIndexAppState {
showTitle: boolean;
showMonitored: boolean;
showQualityProfile: boolean;
showReleaseDate: boolean;
showCinemaRelease: boolean;
showDigitalRelease: boolean;
showPhysicalRelease: boolean;
showReleaseDate: boolean;
showTmdbRating: boolean;
showImdbRating: boolean;
showRottenTomatoesRating: boolean;
showTraktRating: boolean;
showTags: boolean;
showSearchAction: boolean;
};
@@ -37,6 +41,7 @@ export interface MovieIndexAppState {
showAdded: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
showTags: boolean;
showSearchAction: boolean;
};

View File

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

View File

@@ -3,19 +3,30 @@ import AppSectionState, {
AppSectionItemState,
AppSectionSaveState,
AppSectionSchemaState,
PagedAppSectionState,
} from 'App/State/AppSectionState';
import Language from 'Language/Language';
import CustomFormat from 'typings/CustomFormat';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import ImportListExclusion from 'typings/ImportListExclusion';
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings';
import General from 'typings/Settings/General';
import UiSettings from 'typings/Settings/UiSettings';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {
isTestingAll: boolean;
}
export interface GeneralAppState
extends AppSectionItemState<General>,
AppSectionSaveState {}
export interface ImportListAppState
@@ -26,7 +37,9 @@ export interface ImportListAppState
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {}
AppSectionSaveState {
isTestingAll: boolean;
}
export interface NotificationAppState
extends AppSectionState<Notification>,
@@ -36,12 +49,34 @@ export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface ImportListOptionsSettingsAppState
extends AppSectionItemState<ImportListOptionsSettings>,
AppSectionSaveState {}
export interface ImportListExclusionsSettingsAppState
extends AppSectionState<ImportListExclusion>,
AppSectionSaveState,
PagedAppSectionState,
AppSectionDeleteState {
pendingChanges: Partial<ImportListExclusion>;
}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
advancedSettings: boolean;
customFormats: CustomFormatAppState;
downloadClients: DownloadClientAppState;
general: GeneralAppState;
importListExclusions: ImportListExclusionsSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;

View File

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

View File

@@ -3,10 +3,10 @@ import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate';
import styles from './AgendaEvent.css';
@@ -82,7 +82,7 @@ class AgendaEvent extends Component {
startTime = moment(startTime);
const downloading = !!(queueItem || grabbed);
const isMonitored = monitored;
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', downloading);
const statusStyle = getStatusStyle(hasFile, downloading, isMonitored, isAvailable);
const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`;

View File

@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
if (view === calendarViews.WEEK) {
formatedDate = momentDate.format(calendarWeekColumnHeader);
} else if (view === calendarViews.FORECAST) {
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
}
return (

View File

@@ -2,10 +2,10 @@ import classNames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate';
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
import styles from './CalendarEvent.css';
@@ -39,7 +39,7 @@ class CalendarEvent extends Component {
const isDownloading = !!(queueItem || grabbed);
const isMonitored = monitored;
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', isDownloading);
const statusStyle = getStatusStyle(hasFile, isDownloading, isMonitored, isAvailable);
const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`;
const eventType = [];

View File

@@ -0,0 +1,25 @@
function getStatusStyle(hasFile, downloading, isMonitored, isAvailable) {
if (downloading) {
return 'queue';
}
if (hasFile && isMonitored) {
return 'downloaded';
}
if (hasFile && !isMonitored) {
return 'unmonitored';
}
if (isAvailable && isMonitored) {
return 'missingMonitored';
}
if (!isMonitored) {
return 'missingUnmonitored';
}
return 'continuing';
}
export default getStatusStyle;

View File

@@ -2,7 +2,7 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import getProgressBarKind from 'Utilities/Movie/getProgressBarKind';
import translate from 'Utilities/String/translate';
import styles from './CollectionMovieLabel.css';
@@ -45,7 +45,7 @@ class CollectionMovieLabel extends Component {
<div
className={classNames(
styles.movieStatus,
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
styles[getProgressBarKind(status, monitored, hasFile, isAvailable)]
)}
>
{

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import SelectInput from 'Components/Form/SelectInput';
import IconButton from 'Components/Link/IconButton';
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
import sortByProp from 'Utilities/Array/sortByProp';
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
@@ -14,7 +15,7 @@ import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilte
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
import styles from './FilterBuilderRow.css';
@@ -77,7 +78,7 @@ function getRowValueConnector(selectedFilterBuilderProp) {
return QualityFilterBuilderRowValueConnector;
case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValueConnector;
return QualityProfileFilterBuilderRowValue;
case filterBuilderValueTypes.MOVIE:
return MovieFilterBuilderRowValue;
@@ -228,7 +229,7 @@ class FilterBuilderRow extends Component {
key: name,
value: typeof label === 'function' ? label() : label
};
}).sort((a, b) => a.value.localeCompare(b.value));
}).sort(sortByProp('value'));
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);

View File

@@ -56,7 +56,9 @@ function getValue(input, selectedFilterBuilderProp) {
}
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
return parseInt(input);
const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
return Number(input).toFixed(numberFractionDigits);
}
return input;

View File

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

View File

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

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
import sortByProp from 'Utilities/Array/sortByProp';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createQualityProfilesSelector() {
return createSelector(
(state: AppState) => state.settings.qualityProfiles.items,
(qualityProfiles) => {
return qualityProfiles;
}
);
}
function QualityProfileFilterBuilderRowValue(
props: FilterBuilderRowValueProps
) {
const qualityProfiles = useSelector(createQualityProfilesSelector());
const tagList = qualityProfiles
.map(({ id, name }) => ({ id, name }))
.sort(sortByProp('name'));
return <FilterBuilderRowValue {...props} tagList={tagList} />;
}
export default QualityProfileFilterBuilderRowValue;

View File

@@ -1,28 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.qualityProfiles,
(qualityProfiles) => {
const tagList = qualityProfiles.items.map((qualityProfile) => {
const {
id,
name
} = qualityProfile;
return {
id,
name
};
});
return {
tagList
};
}
);
}
export default connect(createMapStateToProps)(FilterBuilderRowValue);

View File

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

View File

@@ -1,69 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
const availabilityOptions = [
{
key: 'announced',
get value() {
return translate('Announced');
}
},
{
key: 'inCinemas',
get value() {
return translate('InCinemas');
}
},
{
key: 'released',
get value() {
return translate('Released');
}
}
];
function AvailabilitySelectInput(props) {
const values = [...availabilityOptions];
const {
includeNoChange,
includeMixed
} = props;
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
isDisabled: true
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
isDisabled: true
});
}
return (
<EnhancedSelectInput
{...props}
values={values}
/>
);
}
AvailabilitySelectInput.propTypes = {
includeNoChange: PropTypes.bool.isRequired,
includeMixed: PropTypes.bool.isRequired
};
AvailabilitySelectInput.defaultProps = {
includeNoChange: false,
includeMixed: false
};
export default AvailabilitySelectInput;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
interface AvailabilitySelectInputProps {
includeNoChange: boolean;
includeNoChangeDisabled?: boolean;
includeMixed?: boolean;
}
interface IMovieAvailabilityOption {
key: string;
value: string;
format?: string;
isDisabled?: boolean;
}
const movieAvailabilityOptions: IMovieAvailabilityOption[] = [
{
key: 'announced',
get value() {
return translate('Announced');
},
},
{
key: 'inCinemas',
get value() {
return translate('InCinemas');
},
},
{
key: 'released',
get value() {
return translate('Released');
},
},
];
function AvailabilitySelectInput(props: AvailabilitySelectInputProps) {
const values = [...movieAvailabilityOptions];
const {
includeNoChange = false,
includeNoChangeDisabled = true,
includeMixed = false,
} = props;
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
isDisabled: includeNoChangeDisabled,
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
value: `(${translate('Mixed')})`,
isDisabled: true,
});
}
return <EnhancedSelectInput {...props} values={values} />;
}
export default AvailabilitySelectInput;

View File

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

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexers } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import sortByProp from 'Utilities/Array/sortByProp';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
@@ -18,7 +18,7 @@ function createMapStateToProps() {
items
} = indexers;
const values = items.sort(sortByName).map((indexer) => ({
const values = items.sort(sortByProp('name')).map((indexer) => ({
key: indexer.id,
value: indexer.name
}));

View File

@@ -5,17 +5,20 @@ import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
function MovieMonitoredSelectInput(props) {
const values = [...monitorOptions];
const {
includeNoChange,
includeMixed
includeMixed,
...otherProps
} = props;
const values = [...monitorOptions];
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
get value() {
return translate('NoChange');
},
isDisabled: true
});
}
@@ -23,14 +26,16 @@ function MovieMonitoredSelectInput(props) {
if (includeMixed) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
get value() {
return `(${translate('Mixed')})`;
},
isDisabled: true
});
}
return (
<EnhancedSelectInput
{...props}
{...otherProps}
values={values}
/>
);

View File

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

View File

@@ -52,6 +52,7 @@ class SelectInput extends Component {
const {
key,
value: optionValue,
isDisabled: optionIsDisabled = false,
...otherOptionProps
} = option;
@@ -59,6 +60,7 @@ class SelectInput extends Component {
<option
key={key}
value={key}
disabled={optionIsDisabled}
{...otherOptionProps}
>
{typeof optionValue === 'function' ? optionValue() : optionValue}

View File

@@ -12,6 +12,14 @@
}
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from '~Components/Form/Input.css';
}
.internalInput {
flex: 1 1 0%;
margin-left: 3px;

View File

@@ -1,6 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'hasError': string;
'hasWarning': string;
'input': string;
'internalInput': string;
'isFocused': string;

View File

@@ -225,6 +225,8 @@ class TagInput extends Component {
const {
className,
inputContainerClassName,
hasError,
hasWarning,
...otherProps
} = this.props;
@@ -241,7 +243,9 @@ class TagInput extends Component {
className={className}
inputContainerClassName={classNames(
inputContainerClassName,
isFocused && styles.isFocused
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
value={value}
suggestions={suggestions}

View File

@@ -7,17 +7,11 @@
}
.link {
composes: link from '~Components/Link/Link.css';
max-width: 100%;
line-height: 1px;
}
.linkWithEdit {
composes: link from '~Components/Link/Link.css';
max-width: calc(100% - 9px - 4px - 2px);
line-height: 1px;
}
.editContainer {
@@ -36,5 +30,6 @@
.label {
composes: label from '~Components/Label.css';
display: flex;
max-width: 100%;
}

View File

@@ -49,7 +49,11 @@ class TextTagInputConnector extends Component {
const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
newTags.forEach((newTag) => {
newValue.push(newTag.trim());
const newTagValue = newTag.trim();
if (newTagValue) {
newValue.push(newTagValue);
}
});
onChange({ name, value: newValue });
@@ -80,7 +84,12 @@ class TextTagInputConnector extends Component {
const newValue = [...valueArray];
newValue.splice(tagToReplace.index, 1);
newValue.push(newTag.name.trim());
const newTagValue = newTag.name.trim();
if (newTagValue) {
newValue.push(newTagValue);
}
onChange({ name, value: newValue });
};

View File

@@ -1,73 +0,0 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import { kinds } from 'Helpers/Props';
import styles from './Icon.css';
class Icon extends PureComponent {
//
// Render
render() {
const {
containerClassName,
className,
name,
kind,
size,
title,
isSpinning,
...otherProps
} = this.props;
const icon = (
<FontAwesomeIcon
className={classNames(
className,
styles[kind]
)}
icon={name}
spin={isSpinning}
style={{
fontSize: `${size}px`
}}
{...otherProps}
/>
);
if (title) {
return (
<span
className={containerClassName}
title={typeof title === 'function' ? title() : title}
>
{icon}
</span>
);
}
return icon;
}
}
Icon.propTypes = {
containerClassName: PropTypes.string,
className: PropTypes.string,
name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};
Icon.defaultProps = {
kind: kinds.DEFAULT,
size: 14,
isSpinning: false,
fixedWidth: false
};
export default Icon;

View File

@@ -0,0 +1,60 @@
import {
FontAwesomeIcon,
FontAwesomeIconProps,
} from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import React, { ComponentProps } from 'react';
import { kinds } from 'Helpers/Props';
import { Kind } from 'Helpers/Props/kinds';
import styles from './Icon.css';
export interface IconProps
extends Omit<
FontAwesomeIconProps,
'icon' | 'spin' | 'name' | 'title' | 'size'
> {
containerClassName?: ComponentProps<'span'>['className'];
name: FontAwesomeIconProps['icon'];
kind?: Extract<Kind, keyof typeof styles>;
size?: number;
isSpinning?: FontAwesomeIconProps['spin'];
title?: string | (() => string);
}
export default function Icon({
containerClassName,
className,
name,
kind = kinds.DEFAULT,
size = 14,
title,
isSpinning = false,
fixedWidth = false,
...otherProps
}: IconProps) {
const icon = (
<FontAwesomeIcon
className={classNames(className, styles[kind])}
icon={name}
spin={isSpinning}
fixedWidth={fixedWidth}
style={{
fontSize: `${size}px`,
}}
{...otherProps}
/>
);
if (title) {
return (
<span
className={containerClassName}
title={typeof title === 'function' ? title() : title}
>
{icon}
</span>
);
}
return icon;
}

View File

@@ -1,3 +1,7 @@
.wrapper {
display: inline-block;
}
.image {
align-content: center;
margin-right: 5px;

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@
cursor: default;
}
.title {
.name {
margin-bottom: 2px;
color: var(--helpTextColor);
font-size: 10px;

View File

@@ -4,9 +4,9 @@ interface CssExports {
'label': string;
'large': string;
'medium': string;
'name': string;
'outline': string;
'small': string;
'title': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -7,7 +7,7 @@ import styles from './InfoLabel.css';
function InfoLabel(props) {
const {
className,
title,
name,
kind,
size,
outline,
@@ -25,8 +25,8 @@ function InfoLabel(props) {
)}
{...otherProps}
>
<div className={styles.title}>
{title}
<div className={styles.name}>
{name}
</div>
<div>
{children}
@@ -37,7 +37,7 @@ function InfoLabel(props) {
InfoLabel.propTypes = {
className: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired,
size: PropTypes.oneOf(sizes.all).isRequired,
outline: PropTypes.bool.isRequired,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,59 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import translate from 'Utilities/String/translate';
import Link from './Link';
import styles from './IconButton.css';
function IconButton(props) {
const {
className,
iconClassName,
name,
kind,
size,
isSpinning,
isDisabled,
...otherProps
} = props;
return (
<Link
className={classNames(
className,
isDisabled && styles.isDisabled
)}
aria-label={translate('TableOptionsButton')}
isDisabled={isDisabled}
{...otherProps}
>
<Icon
className={iconClassName}
name={name}
kind={kind}
size={size}
isSpinning={isSpinning}
/>
</Link>
);
}
IconButton.propTypes = {
...Link.propTypes,
className: PropTypes.string.isRequired,
iconClassName: PropTypes.string,
kind: PropTypes.string,
name: PropTypes.object.isRequired,
size: PropTypes.number,
title: PropTypes.string,
isSpinning: PropTypes.bool,
isDisabled: PropTypes.bool
};
IconButton.defaultProps = {
className: styles.button,
size: 12
};
export default IconButton;

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