Compare commits

...

90 Commits

Author SHA1 Message Date
Mark McDowall
48b9f0a07a Fixed: Reprocessing items that were previously blocked during importing
(cherry picked from commit bce848facf8aeaeac6a1d59c92941d00589034a4)
2024-06-26 16:48:48 +00:00
Mark McDowall
a6db8bfe0e New: Ignore Deluge torrents without a title
(cherry picked from commit a0d29331341320268552660658b949179c963793)
2024-06-26 02:46:53 +03:00
Bogdan
2033d7e411 Fixed: Exclude invalid releases from Newznab and Torznab parsers
(cherry picked from commit fb060730c7d52cd342484dc68595698a9430df7b)
2024-06-26 02:46:39 +03:00
dependabot[bot]
4a04e54ceb Bump ws from 7.5.9 to 7.5.10
Bumps [ws](https://github.com/websockets/ws) from 7.5.9 to 7.5.10.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.9...7.5.10)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-23 22:41:14 +03:00
Bogdan
d57a9ab9b0 Bump version to 0.3.30 2024-06-23 21:53:58 +03:00
dependabot[bot]
d333204194 Bump braces from 3.0.2 to 3.0.3
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 15:07:54 +03:00
Servarr
c3676f8d33 Automated API Docs update 2024-06-11 15:03:22 +03:00
Bogdan
932356be61 Ignore Grabbed from API docs
Run application in docs.sh specific to platform

(cherry picked from commit c331c8bd119fa9f85a53e96db04f541b2d90bbd3)

Closes #3515
2024-06-11 14:14:57 +03:00
servarr[bot]
5b1b2a2d67 Fixed: Improve error messaging if config file isn't formatted correctly 2024-06-11 14:05:24 +03:00
Bogdan
c362e8c467 Fixed: Ignore case when resolving indexer by name in release push
(cherry picked from commit a90ab1a8fd50126d7f60eaa684eac1e0cd98e2b7)
2024-06-11 14:02:24 +03:00
Bogdan
67c00a8cc7 Fixed: Ignore case for name validation in providers
(cherry picked from commit 0edc5ba99a15c5f80305b387a053f35fc3f6e51b)
2024-06-11 14:02:04 +03:00
Bogdan
27a086dfff Bump version to 0.3.29 2024-06-09 12:58:59 +03:00
sillock1
8ee0df9c65 Fixed: Trimming slashes from UrlBase when using environment variable 2024-06-06 14:47:25 +03:00
sillock1
da30b55902 New: Optionally use Environment Variables for settings in config.xml 2024-06-06 14:47:25 +03:00
Servarr
c7226fc85f Automated API Docs update 2024-06-06 12:27:57 +03:00
Mark McDowall
84f22dbadc New: Require password confirmation when setting or changing password
(cherry picked from commit b248163df598dc611ee919d525eb7357256d73d5)

Closes #3089
2024-06-06 11:46:54 +03:00
Mark McDowall
06a53ef9ca New: Authentication is now required
(cherry picked from commit d3018fb5015af26a897281f0e892b706cdb6e821)

Closes #1807
Closes #2878
Closes #2873
2024-06-06 11:46:48 +03:00
Mark McDowall
b5ef0cda1e New: Setting to disable authentication for local addresses
(cherry picked from commit b154b00c6156512e7fbd0a2c4833c116a74f23ca)

Closes #1804
Closes #2077
2024-06-06 11:44:37 +03:00
Weblate
1b1290efac Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yi Cao <caoyi06@qq.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Co-authored-by: r0bertreh <Robert.reh@live.de>
Co-authored-by: topnew <sznetim@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2024-06-03 09:15:48 +03:00
Bogdan
dcbc3ea3f8 Update the wanted section for missing and cutoff unmet
(cherry picked from commit 9b4ff657af41e67aeb5866ee3056f1a8f2a901ea)
2024-05-22 03:20:10 +03:00
Bogdan
9a7b2cb818 Fix Goodreads test 2024-05-19 19:33:19 +03:00
Bogdan
f9cba39f0a Ignore metadata tests temporarily 2024-05-19 19:33:19 +03:00
Bogdan
6b6ff4fe76 New: Link to "Why Searches Could be Failing" wiki section 2024-05-19 19:33:19 +03:00
Bogdan
05d0fe2da6 Bump version to 0.3.28 2024-05-13 15:31:57 +03:00
Bogdan
7aab2b49e2 Bump Npgsql to 7.0.7 2024-05-13 15:26:43 +03:00
Weblate
8887df92ed Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-05-10 14:05:03 +03:00
Bogdan
9ee651d6c0 Refactor PasswordInput to use type password
(cherry picked from commit c7c1e3ac9e5bffd4d92298fed70916e3808613fd)
2024-05-10 14:04:07 +03:00
Bogdan
5544e169a6 Use number input for seed ratio
(cherry picked from commit 1eddf3a152fae04142263c02a3e3b317ff2feeb2)

Plus translations

Closes #3470
2024-05-10 01:37:15 +03:00
Bogdan
11d83165e5 Fixed: Notifications with only On Rename enabled 2024-05-10 01:34:57 +03:00
Bogdan
9e6d1c581c Fixed: Parsing long downloading/seeding values from Transmission
(cherry picked from commit 8360dd7a7bab1dfb49a40aae382b47e9253d9fd1)
2024-05-09 05:50:16 +03:00
Weblate
66e20a0aec Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-05-05 12:35:40 +03:00
Bogdan
e639b36283 Fixed: Indexer flags for torrent release pushes
(cherry picked from commit 47ba002806fe2c2004a649aa193ae318343a84e4)
2024-05-05 12:35:12 +03:00
Mark McDowall
c9f4fb141f Forward X-Forwarded-Host header
(cherry picked from commit 3fbe4361386e9fb8dafdf82ad9f00f02bec746cc)
2024-05-05 12:35:02 +03:00
Bogdan
29a43fc2fd Bump version to 0.3.27 2024-05-05 12:34:33 +03:00
Bogdan
f9454b5b5a Fixed: Initialize databases after app folder migrations
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-05-05 01:06:47 +03:00
Bogdan
9aa6d47349 Use newer Node.js task for in pipelines 2024-04-29 14:42:15 +03:00
Bogdan
e09946d946 Fixed: Limit titles in task name to 10 authors
(cherry picked from commit c81ae6546118e954e481894d0b3fa6e9a20359c7)

Closes #3449
2024-04-28 13:55:18 +03:00
Stevie Robinson
c9c5429120 New: Don't initially select 0 byte files in Interactive Import
(cherry picked from commit 04bd535cfca5e25c6a2d5417c6f18d5bf5180f67)

Closes #3448
2024-04-28 13:54:19 +03:00
Mark McDowall
ed7bd6c66d Fixed: Improve paths longer than 256 on Windows failing to hardlink
(cherry picked from commit a97fbcc40a6247bf59678425cf460588fd4dbecd)
2024-04-28 13:52:17 +03:00
Christopher
c88fe7cae8 New: Remove qBitorrent torrents that reach inactive seeding time
(cherry picked from commit d738035fed859eb475051f3df494b9c975a42e82)
2024-04-28 13:52:05 +03:00
Bogdan
68642579d0 Bump version to 0.3.26 2024-04-28 12:58:25 +03:00
Weblate
f061d70d38 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fara <faraindahhh@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Mailme Dashite <mailmedashite@protonmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: maodun96 <435795439@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2024-04-27 21:13:42 +03:00
Bogdan
fd4a609f51 Fixed: Retrying download on not suppressed HTTP errors 2024-04-27 21:09:36 +03:00
Bogdan
9957f734a5 Database corruption message linking to wiki 2024-04-25 11:30:34 +03:00
Bogdan
695b8b2ae1 Bump dotnet to 6.0.29 2024-04-22 09:32:41 +03:00
Bogdan
420824b279 Convert createTagsSelector to typescript 2024-04-21 12:48:48 +03:00
Servarr
badc2567c3 Automated API Docs update 2024-04-21 12:44:49 +03:00
Bogdan
c8c81927d9 Fixed: Re-testing edited providers will forcibly test them
(cherry picked from commit e9662544621b2d1fb133ff9d96d0eb20b8198725)

Closes #3432
2024-04-21 12:36:52 +03:00
Josh McKinney
f9df843789 Add dev container workspace
Allows the linting and style settings for the frontend to be applied even when you load the main repo as a workspace

(cherry picked from commit d6278fced49b26be975c3a6039b38a94f700864b)

Closes #3428
2024-04-21 12:34:01 +03:00
Bogdan
3cd39d4ee8 Bump frontend dependencies 2024-04-21 12:30:55 +03:00
Bogdan
8a39ef4c56 Bump version to 0.3.25 2024-04-21 09:17:25 +03:00
Bogdan
ba1195fc1b Fixed: Skip move when source and destination are the same
Co-authored-by: Qstick <qstick@gmail.com>
2024-04-20 18:00:00 +03:00
Bogdan
7656142db4 Bump SixLabors.ImageSharp to 3.1.4 2024-04-19 08:03:01 +03:00
Weblate
74c3b45ef8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Altair <villagermd@outlook.com>
Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Gandrushka <andrew.pyndyk@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Jacopo Luca Maria Latrofa <jacopo.latrofa@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: toeiazarothis <patrickdealmeida89000@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-04-18 20:00:38 +03:00
Josh McKinney
f7368d3d09 Add DevContainer, VSCode config and extensions.json
(cherry picked from commit 5061dc4b5e5ea9925740496a5939a1762788b793)

Closes #3414
2024-04-14 08:35:27 +03:00
Bogdan
5d8e2300f2 Bump version to 0.3.24 2024-04-14 08:26:42 +03:00
Weblate
1fb54c0da5 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: fordas <fordas15@gmail.com>
Co-authored-by: myrad2267 <myrad2267@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translation: Servarr/Readarr
2024-04-10 02:22:54 +03:00
Bogdan
5a9a6e593b New: Detect shfs mounts in disk space
(cherry picked from commit 1aef91041e404f76f278f430e4e53140fb125792)
2024-04-10 02:22:17 +03:00
Qstick
2d5fc655c0 Added table identifier to OrderBy to avoid column ambiguity on joins
Co-Authored-By: Richard <1252123+kharenis@users.noreply.github.com>

(cherry picked from commit c57ceac4debf7419be84096f997ba7b75c906586)
2024-04-08 18:47:02 +03:00
Bogdan
cfcc9a5856 Bump version to 0.3.23 2024-04-07 07:58:18 +03:00
Stevie Robinson
8c9555f82e New: Informational text on Custom Formats modal
(cherry picked from commit 238ba85f0a2639608d9890292dfe0b96c0212f10)

Closes #3405
2024-04-06 16:47:46 +03:00
Cuki
ee20ba1811 Fixed: Use widely supported display mode for PWA
(cherry picked from commit 1562d3bae3002947f9e428321d2b162ad69c3309)
2024-04-06 16:45:17 +03:00
Mark McDowall
4cf1215cfa Fixed: Cleanse BHD RSS key in log files
(cherry picked from commit 60ee7cc716d344fc904fa6fb28f7be0386ae710d)
2024-04-06 16:45:00 +03:00
Mark McDowall
a8ab099177 Fixed: Sending ntfy.sh notifications with unicode characters
(cherry picked from commit a169ebff2adda5c8585c6aae6249b1c1f7c12264)
2024-04-06 16:44:45 +03:00
Servarr
50af8a12d4 Automated API Docs update 2024-04-06 06:56:02 +03:00
Weblate
510c39c5d8 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-04-06 06:19:30 +03:00
Bogdan
dd4a0121f2 Fixed: Migrations running on newer versions of SQLite 2024-04-06 06:18:29 +03:00
Mark McDowall
4fb62c072a Fixed: Task with removed author causing error
(cherry picked from commit fc6494c569324c839debdb1d08dde23b8f1b8d76)

Closes #3376
2024-03-28 11:22:01 +02:00
Louis R
2b100d0f72 Fixed: Exceptions when checking for routable IPv4 addresses
(cherry picked from commit 060b789bc6f10f667795697eb536d4bd3851da49)
2024-03-28 11:19:16 +02:00
Weblate
abfdc44f92 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Casselluu <jack10193@163.com>
Co-authored-by: CharlesEnard <charles.enard@gmail.com>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Stanislav <prekop3@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: shimmyx <shimmygodx@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2024-03-28 11:06:09 +02:00
Bogdan
6e76f9966a New: Allow HEAD requests to ping endpoint
(cherry picked from commit 7353fe479dbb8d0dab76993ebed92d48e1b05524)
2024-03-28 11:05:17 +02:00
Stevie Robinson
2b6ceab9d4 Fixed: Handling torrents with relative path in rTorrent
(cherry picked from commit 35d0e6a6f806c68756450a7d199600d7fb49d6c5)
2024-03-28 11:05:05 +02:00
Bogdan
b636729960 New: Advanced settings toggle in import list, notification and download client modals
(cherry picked from commit 13c925b3418d1d48ec041e3d97ab51aaf2b8977a)
2024-03-28 11:04:53 +02:00
Carlos Gustavo Sarmiento
8af8366575 Fixed: qBittorrent not correctly handling retention during testing
(cherry picked from commit 588372fd950fc85f5e9a4275fbcb423b247ed0ee)
2024-03-28 11:04:41 +02:00
Bogdan
1d31e9b9d9 Bump version to 0.3.22 2024-03-24 17:46:11 +02:00
Mark McDowall
37a9f670dd Fixed: Task progress messages in the UI
(cherry picked from commit c6417337812f3578a27f9dc1e44fdad80f557271)

Closes #3370
2024-03-22 11:45:46 +02:00
Bogdan
11eda3b11b Fix BookInfo tests 2024-03-17 00:36:02 +02:00
Weblate
04682c9d91 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dennis Langthjem <dennis@langthjem.dk>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ihor Mudryi <mudryy33@gmail.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com>
Co-authored-by: Mark Martines <mark-martines@hotmail.com>
Co-authored-by: Maxence Winandy <maxence.winandy@gmail.com>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: infoaitek24 <info@aitekph.com>
Co-authored-by: linkin931 <931linkin@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2024-03-16 18:19:11 +02:00
Mina Gaid
50fdc449ac Fixed icons for macOS application 2024-03-16 17:52:10 +02:00
Bogdan
b8c295727a Link to author from book details
Co-authored-by: plmcgrn <889547+plmcgrn@users.noreply.github.com>

Closes #3356
2024-03-16 00:46:31 +02:00
Mark McDowall
93ee466780 New: Show author names after task name when applicable
(cherry picked from commit 6d552f2a60f44052079b5e8944f5e1bbabac56e0)

Closes #3361
2024-03-15 23:50:22 +02:00
Mark McDowall
77f1e8f8c9 Fixed: Disabled select option still selectable
(cherry picked from commit 063dba22a803295adee4fdcbe42718af3e85ca78)

Closes #3362
2024-03-15 23:32:38 +02:00
Bogdan
1aa746bea1 Ensure authors are populated in PageConnector 2024-03-15 23:28:18 +02:00
Bogdan
490041d77c Ensure not allowed cursor is shown for disabled select inputs 2024-03-15 23:26:02 +02:00
Stevie Robinson
5dc5592c17 Fixed: Wrapping of naming tokens with alternate separators
(cherry picked from commit 80630bf97f5bb3b49d4824dc039d2edfc74e4797)

Closes #3294
Closes #3360
2024-03-15 23:25:16 +02:00
Servarr
8fb1aff68a Automated API Docs update 2024-03-14 07:58:15 +02:00
Mark McDowall
a397a19034 Fixed: Release push with only Magnet URL
(cherry picked from commit 9f705e4161af3f4dd55b399d56b0b9c5a36e181b)
2024-03-14 07:13:17 +02:00
Bogdan
d0df761422 New: Indexer flags
(cherry picked from commit 7a768b5d0faf9aa57e78aee19cefee8fb19a42d5)
2024-03-10 19:11:25 +02:00
Bogdan
4781675c1a Bump ImageSharp, Polly 2024-03-10 13:14:58 +02:00
Bogdan
0361262bb4 Bump version to 0.3.21 2024-03-10 09:06:37 +02:00
246 changed files with 6965 additions and 3101 deletions

View File

@@ -0,0 +1,13 @@
// This file is used to open the backend and frontend in the same workspace, which is necessary as
// the frontend has vscode settings that are distinct from the backend
{
"folders": [
{
"path": ".."
},
{
"path": "../frontend"
}
],
"settings": {}
}

View File

@@ -0,0 +1,19 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "Readarr",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"nodeGypDependencies": true,
"version": "16",
"nvmVersion": "latest"
}
},
"forwardPorts": [8787],
"customizations": {
"vscode": {
"extensions": ["esbenp.prettier-vscode"]
}
}
}

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot
version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly

1
.gitignore vendored
View File

@@ -125,6 +125,7 @@ coverage*.xml
coverage*.json
setup/Output/
*.~is
.mono
# .NET Core
project.lock.json

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"ms-dotnettools.csdevkit",
"ms-vscode-remote.remote-containers"
]
}

26
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
"name": "Run Readarr",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build dotnet",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/_output/net6.0/Readarr",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "integratedTerminal",
"stopAtEntry": false
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

44
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,44 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build dotnet",
"command": "dotnet",
"type": "process",
"args": [
"msbuild",
"-restore",
"${workspaceFolder}/src/Readarr.sln",
"-p:GenerateFullPaths=true",
"-p:Configuration=Debug",
"-p:Platform=Posix",
"-consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/Readarr.sln",
"-property:GenerateFullPaths=true",
"-consoleloggerparameters:NoSummary;ForceNoAlign"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/src/Readarr.sln"
],
"problemMatcher": "$msCompile"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.3.20'
majorVersion: '0.3.30'
minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.417'
dotnetVersion: '6.0.421'
nodeVersion: '20.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
@@ -166,10 +166,10 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: NodeTool@0
- task: UseNode@1
displayName: Set Node.js version
inputs:
versionSpec: $(nodeVersion)
version: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1
@@ -1075,10 +1075,10 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: NodeTool@0
- task: UseNode@1
displayName: Set Node.js version
inputs:
versionSpec: $(nodeVersion)
version: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1

14
docs.sh
View File

@@ -1,3 +1,7 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
PLATFORM=$1
if [ "$PLATFORM" = "Windows" ]; then
@@ -21,15 +25,21 @@ slnFile=src/Readarr.sln
platform=Posix
if [ "$PLATFORM" = "Windows" ]; then
application=Readarr.Console.dll
else
application=Readarr.dll
fi
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Readarr.Api.V1/openapi.json "$outputFolder/net6.0/$RUNTIME/Readarr.console.dll" v1 &
dotnet tool run swagger tofile --output ./src/Readarr.Api.V1/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v1 &
sleep 45

View File

@@ -1,3 +1,5 @@
import AuthorsAppState from './AuthorsAppState';
import CommandAppState from './CommandAppState';
import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState';
@@ -34,6 +36,8 @@ export interface CustomFilter {
}
interface AppState {
authors: AuthorsAppState;
commands: CommandAppState;
settings: SettingsAppState;
tags: TagsAppState;
}

View File

@@ -0,0 +1,18 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import Author from 'Author/Author';
interface AuthorsAppState
extends AppSectionState<Author>,
AppSectionDeleteState,
AppSectionSaveState {
itemMap: Record<number, number>;
deleteOptions: {
addImportListExclusion: boolean;
};
}
export default AuthorsAppState;

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;

View File

@@ -5,6 +5,7 @@ import AppSectionState, {
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings';
@@ -27,11 +28,13 @@ export interface NotificationAppState
extends AppSectionState<Notification>,
AppSectionDeleteState {}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState {
downloadClients: DownloadClientAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
notifications: NotificationAppState;
uiSettings: UiSettingsAppState;

View File

@@ -0,0 +1,18 @@
import ModelBase from 'App/ModelBase';
interface Author extends ModelBase {
added: string;
genres: string[];
monitored: boolean;
overview: string;
path: string;
qualityProfileId: number;
metadataProfileId: number;
rootFolderPath: string;
sortName: string;
tags: number[];
authorName: string;
isSaving?: boolean;
}
export default Author;

View File

@@ -2,11 +2,11 @@ import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
function AuthorNameLink({ titleSlug, authorName }) {
function AuthorNameLink({ titleSlug, authorName, ...otherProps }) {
const link = `/author/${titleSlug}`;
return (
<Link to={link}>
<Link to={link} {...otherProps}>
{authorName}
</Link>
);

View File

@@ -27,3 +27,9 @@
width: 80px;
}
.indexerFlags {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}

View File

@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'indexerFlags': string;
'monitored': string;
'pageCount': string;
'position': string;

View File

@@ -2,12 +2,17 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import BookSearchCellConnector from 'Book/BookSearchCellConnector';
import BookTitleLink from 'Book/BookTitleLink';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import StarRating from 'Components/StarRating';
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 Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BookStatus from './BookStatus';
import styles from './BookRow.css';
@@ -67,6 +72,7 @@ class BookRow extends Component {
authorMonitored,
titleSlug,
bookFiles,
indexerFlags,
isEditorActive,
isSelected,
onSelectedChange,
@@ -190,6 +196,24 @@ class BookRow extends Component {
);
}
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell
@@ -235,6 +259,7 @@ BookRow.propTypes = {
position: PropTypes.string,
pageCount: PropTypes.number,
ratings: PropTypes.object.isRequired,
indexerFlags: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired,
isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired,
@@ -246,4 +271,8 @@ BookRow.propTypes = {
onMonitorBookPress: PropTypes.func.isRequired
};
BookRow.defaultProps = {
indexerFlags: 0
};
export default BookRow;

View File

@@ -7,21 +7,18 @@ import BookRow from './BookRow';
const selectBookFiles = createSelector(
(state) => state.bookFiles,
(bookFiles) => {
const {
items
} = bookFiles;
const { items } = bookFiles;
const bookFileDict = items.reduce((acc, file) => {
return items.reduce((acc, file) => {
const bookId = file.bookId;
if (!acc.hasOwnProperty(bookId)) {
acc[bookId] = [];
}
acc[bookId].push(file);
return acc;
}, {});
return bookFileDict;
}
);
@@ -31,10 +28,14 @@ function createMapStateToProps() {
selectBookFiles,
(state, { id }) => id,
(author = {}, bookFiles, bookId) => {
const files = bookFiles[bookId] ?? [];
const bookFile = files[0];
return {
authorMonitored: author.monitored,
authorName: author.authorName,
bookFiles: bookFiles[bookId] ?? []
bookFiles: files,
indexerFlags: bookFile ? bookFile.indexerFlags : 0
};
}
);

View File

@@ -173,7 +173,7 @@ class AuthorEditorFooter extends Component {
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];

View File

@@ -84,9 +84,15 @@
font-size: 20px;
}
.authorLink {
composes: link from '~Components/Link/Link.css';
margin-right: 15px;
color: var(--white);
}
.duration {
margin-right: 15px;
margin-left: 10px;
}
.detailsLabel {

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'alternateTitlesIconContainer': string;
'authorLink': string;
'backdrop': string;
'backdropOverlay': string;
'cover': string;

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import AuthorNameLink from 'Author/AuthorNameLink';
import BookCover from 'Book/BookCover';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
@@ -113,7 +114,7 @@ class BookDetailsHeader extends Component {
className={styles.monitorToggleButton}
monitored={monitored}
isSaving={isSaving}
size={isSmallScreen ? 30: 40}
size={isSmallScreen ? 30 : 40}
onPress={onMonitorTogglePress}
/>
</div>
@@ -131,7 +132,12 @@ class BookDetailsHeader extends Component {
</div>
<div>
{author.authorName}
<AuthorNameLink
className={styles.authorLink}
titleSlug={author.titleSlug}
authorName={author.authorName}
/>
{
!!pageCount &&
<span className={styles.duration}>

View File

@@ -89,7 +89,7 @@ class BookEditorFooter extends Component {
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
interface IndexerFlagsProps {
indexerFlags: number;
}
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
const flags = allIndexerFlags.items.filter(
// eslint-disable-next-line no-bitwise
(item) => (indexerFlags & item.id) === item.id
);
return flags.length ? (
<ul>
{flags.map((flag, index) => {
return <li key={index}>{flag.name}</li>;
})}
</ul>
) : null;
}
export default IndexerFlags;

View File

@@ -116,7 +116,7 @@ class BookFileEditorTableContent extends Component {
});
return acc;
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]);
}, [{ key: 'selectQuality', value: translate('SelectQuality'), isDisabled: true }]);
const hasSelectedFiles = this.getSelectedIds().length > 0;

View File

@@ -88,7 +88,7 @@ class BookshelfFooter extends Component {
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: NO_CHANGE, value: translate('NoChange'), isDisabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];

View File

@@ -0,0 +1,38 @@
import ModelBase from 'App/ModelBase';
export interface CommandBody {
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
completionMessage: string;
requiresDiskAccess: boolean;
isExclusive: boolean;
isLongRunning: boolean;
name: string;
lastExecutionTime: string;
lastStartTime: string;
trigger: string;
suppressMessages: boolean;
authorId?: number;
authorIds?: number[];
}
interface Command extends ModelBase {
name: string;
commandName: string;
message: string;
body: CommandBody;
priority: string;
status: string;
result: string;
queued: string;
started: string;
ended: string;
duration: string;
trigger: string;
stateChangeTime: string;
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
lastExecutionTime: string;
}
export default Command;

View File

@@ -19,7 +19,7 @@
.isDisabled {
opacity: 0.7;
cursor: not-allowed;
cursor: not-allowed !important;
}
.dropdownArrowContainer {

View File

@@ -14,6 +14,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
@@ -83,6 +84,9 @@ function getComponent(type) {
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInput;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
@@ -288,6 +292,7 @@ FormInputGroup.propTypes = {
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number,
pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),

View File

@@ -0,0 +1,62 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import EnhancedSelectInput from './EnhancedSelectInput';
const selectIndexerFlagsValues = (selectedFlags: number) =>
createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => {
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
// eslint-disable-next-line no-bitwise
if ((selectedFlags & id) === id) {
acc.push(id);
}
return acc;
}, []);
const values = indexerFlags.items.map(({ id, name }) => ({
key: id,
value: name,
}));
return {
value,
values,
};
}
);
interface IndexerFlagsSelectInputProps {
name: string;
indexerFlags: number;
onChange(payload: object): void;
}
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
const { indexerFlags, onChange } = props;
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
const onChangeWrapper = useCallback(
({ name, value }: { name: string; value: number[] }) => {
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
onChange({ name, value: indexerFlags });
},
[onChange]
);
return (
<EnhancedSelectInput
{...props}
value={value}
values={values}
onChange={onChangeWrapper}
/>
);
}
export default IndexerFlagsSelectInput;

View File

@@ -39,7 +39,7 @@ function createMapStateToProps() {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: includeNoChangeDisabled
isDisabled: includeNoChangeDisabled
});
}
@@ -47,7 +47,7 @@ function createMapStateToProps() {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -18,7 +18,7 @@ function MonitorBooksSelectInput(props) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: true
isDisabled: true
});
}
@@ -26,7 +26,7 @@ function MonitorBooksSelectInput(props) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -17,7 +17,7 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: true
isDisabled: true
});
}
@@ -25,7 +25,7 @@ function MonitorNewItemsSelectInput(props) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -1,5 +0,0 @@
.input {
composes: input from '~Components/Form/TextInput.css';
font-family: $passwordFamily;
}

View File

@@ -1,7 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import TextInput from './TextInput';
import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input
function onCopy(e) {
@@ -13,17 +11,14 @@ function PasswordInput(props) {
return (
<TextInput
{...props}
type="password"
onCopy={onCopy}
/>
);
}
PasswordInput.propTypes = {
className: PropTypes.string.isRequired
};
PasswordInput.defaultProps = {
className: styles.input
...TextInput.props
};
export default PasswordInput;

View File

@@ -26,7 +26,7 @@ function createMapStateToProps() {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: includeNoChangeDisabled
isDisabled: includeNoChangeDisabled
});
}
@@ -34,7 +34,7 @@ function createMapStateToProps() {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@@ -2,3 +2,7 @@
margin-right: 5px;
color: var(--themeRed);
}
.rating {
margin-right: 15px;
}

View File

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

View File

@@ -6,7 +6,7 @@ import styles from './HeartRating.css';
function HeartRating({ rating, iconSize }) {
return (
<span>
<span className={styles.rating}>
<Icon
className={styles.heart}
name={icons.HEART}

View File

@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
import ColorImpairedContext from 'App/ColorImpairedContext';
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
import SignalRConnector from 'Components/SignalRConnector';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import locationShape from 'Helpers/Props/Shapes/locationShape';
import PageHeader from './Header/PageHeader';
import PageSidebar from './Sidebar/PageSidebar';
@@ -75,6 +76,7 @@ class Page extends Component {
isSmallScreen,
isSidebarVisible,
enableColorImpairedMode,
authenticationEnabled,
onSidebarToggle,
onSidebarVisibleChange
} = this.props;
@@ -108,6 +110,10 @@ class Page extends Component {
isOpen={this.state.isConnectionLostModalOpen}
onModalClose={this.onConnectionLostModalClose}
/>
<AuthenticationRequiredModal
isOpen={!authenticationEnabled}
/>
</div>
</ColorImpairedContext.Provider>
);
@@ -123,6 +129,7 @@ Page.propTypes = {
isUpdated: PropTypes.bool.isRequired,
isDisconnected: PropTypes.bool.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
authenticationEnabled: PropTypes.bool.isRequired,
onResize: PropTypes.func.isRequired,
onSidebarToggle: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired

View File

@@ -7,10 +7,18 @@ import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Ac
import { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
import {
fetchImportLists,
fetchIndexerFlags,
fetchLanguages,
fetchMetadataProfiles,
fetchQualityProfiles,
fetchUISettings
} from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ErrorPage from './ErrorPage';
import LoadingPage from './LoadingPage';
import Page from './Page';
@@ -44,6 +52,7 @@ const selectAppProps = createSelector(
);
const selectIsPopulated = createSelector(
(state) => state.authors.isPopulated,
(state) => state.customFilters.isPopulated,
(state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated,
@@ -51,9 +60,11 @@ const selectIsPopulated = createSelector(
(state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
(
authorsIsPopulated,
customFiltersIsPopulated,
tagsIsPopulated,
uiSettingsIsPopulated,
@@ -61,10 +72,12 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated,
metadataProfilesIsPopulated,
importListsIsPopulated,
indexerFlagsIsPopulated,
systemStatusIsPopulated,
translationsIsPopulated
) => {
return (
authorsIsPopulated &&
customFiltersIsPopulated &&
tagsIsPopulated &&
uiSettingsIsPopulated &&
@@ -72,6 +85,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated &&
metadataProfilesIsPopulated &&
importListsIsPopulated &&
indexerFlagsIsPopulated &&
systemStatusIsPopulated &&
translationsIsPopulated
);
@@ -79,6 +93,7 @@ const selectIsPopulated = createSelector(
);
const selectErrors = createSelector(
(state) => state.authors.error,
(state) => state.customFilters.error,
(state) => state.tags.error,
(state) => state.settings.ui.error,
@@ -86,9 +101,11 @@ const selectErrors = createSelector(
(state) => state.settings.qualityProfiles.error,
(state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error,
(state) => state.settings.indexerFlags.error,
(state) => state.system.status.error,
(state) => state.app.translations.error,
(
authorsError,
customFiltersError,
tagsError,
uiSettingsError,
@@ -96,10 +113,12 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
indexerFlagsError,
systemStatusError,
translationsError
) => {
const hasError = !!(
authorsError ||
customFiltersError ||
tagsError ||
uiSettingsError ||
@@ -107,6 +126,7 @@ const selectErrors = createSelector(
qualityProfilesError ||
metadataProfilesError ||
importListsError ||
indexerFlagsError ||
systemStatusError ||
translationsError
);
@@ -120,6 +140,7 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
indexerFlagsError,
systemStatusError,
translationsError
};
@@ -133,18 +154,21 @@ function createMapStateToProps() {
selectErrors,
selectAppProps,
createDimensionsSelector(),
createSystemStatusSelector(),
(
enableColorImpairedMode,
isPopulated,
errors,
app,
dimensions
dimensions,
systemStatus
) => {
return {
...app,
...errors,
isPopulated,
isSmallScreen: dimensions.isSmallScreen,
authenticationEnabled: systemStatus.authentication !== 'none',
enableColorImpairedMode
};
}
@@ -177,6 +201,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchImportLists() {
dispatch(fetchImportLists());
},
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchUISettings() {
dispatch(fetchUISettings());
},
@@ -218,6 +245,7 @@ class PageConnector extends Component {
this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchMetadataProfiles();
this.props.dispatchFetchImportLists();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
@@ -245,6 +273,7 @@ class PageConnector extends Component {
dispatchFetchQualityProfiles,
dispatchFetchMetadataProfiles,
dispatchFetchImportLists,
dispatchFetchIndexerFlags,
dispatchFetchUISettings,
dispatchFetchStatus,
dispatchFetchTranslations,
@@ -287,6 +316,7 @@ PageConnector.propTypes = {
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,

View File

@@ -253,7 +253,7 @@ class SignalRConnector extends Component {
handleWantedCutoff = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'cutoffUnmet',
section: 'wanted.cutoffUnmet',
updateOnly: true,
...body.resource
});
@@ -263,7 +263,7 @@ class SignalRConnector extends Component {
handleWantedMissing = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'missing',
section: 'wanted.missing',
updateOnly: true,
...body.resource
});

View File

@@ -25,14 +25,3 @@
font-family: 'Ubuntu Mono';
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
}
/*
* text-security-disc
*/
@font-face {
font-weight: normal;
font-style: normal;
font-family: 'text-security-disc';
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
}

View File

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

View File

@@ -0,0 +1,34 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModal(props) {
const {
isOpen
} = props;
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<AuthenticationRequiredModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
AuthenticationRequiredModal.propTypes = {
isOpen: PropTypes.bool.isRequired
};
export default AuthenticationRequiredModal;

View File

@@ -0,0 +1,5 @@
.authRequiredAlert {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'authRequiredAlert': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,170 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef } from 'react';
import Alert from 'Components/Alert';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
import translate from 'Utilities/String/translate';
import styles from './AuthenticationRequiredModalContent.css';
function onModalClose() {
// No-op
}
function AuthenticationRequiredModalContent(props) {
const {
isPopulated,
error,
isSaving,
settings,
onInputChange,
onSavePress,
dispatchFetchStatus
} = props;
const {
authenticationMethod,
authenticationRequired,
username,
password,
passwordConfirmation
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
const didMount = useRef(false);
useEffect(() => {
if (!isSaving && didMount.current) {
dispatchFetchStatus();
}
didMount.current = true;
}, [isSaving, dispatchFetchStatus]);
return (
<ModalContent
showCloseButton={false}
onModalClose={onModalClose}
>
<ModalHeader>
{translate('AuthenticationRequired')}
</ModalHeader>
<ModalBody>
<Alert
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{translate('AuthenticationRequiredWarning')}
</Alert>
{
isPopulated && !error ?
<div>
<FormGroup>
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
helpLink="https://wiki.servarr.com/readarr/faq#forced-authentication"
onChange={onInputChange}
{...authenticationMethod}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
{...username}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
{...password}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
{...passwordConfirmation}
/>
</FormGroup>
</div> :
null
}
{
!isPopulated && !error ? <LoadingIndicator /> : null
}
</ModalBody>
<ModalFooter>
<SpinnerButton
kind={kinds.PRIMARY}
isSpinning={isSaving}
isDisabled={!authenticationEnabled}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
AuthenticationRequiredModalContent.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
settings: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired
};
export default AuthenticationRequiredModalContent;

View File

@@ -0,0 +1,86 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
const SECTION = 'general';
function createMapStateToProps() {
return createSelector(
createSettingsSectionSelector(SECTION),
(sectionSettings) => {
return {
...sectionSettings
};
}
);
}
const mapDispatchToProps = {
dispatchClearPendingChanges: clearPendingChanges,
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
dispatchSaveGeneralSettings: saveGeneralSettings,
dispatchFetchGeneralSettings: fetchGeneralSettings,
dispatchFetchStatus: fetchStatus
};
class AuthenticationRequiredModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchGeneralSettings();
}
componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetGeneralSettingsValue({ name, value });
};
onSavePress = () => {
this.props.dispatchSaveGeneralSettings();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchFetchGeneralSettings,
dispatchSetGeneralSettingsValue,
dispatchSaveGeneralSettings,
...otherProps
} = this.props;
return (
<AuthenticationRequiredModalContent
{...otherProps}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
/>
);
}
}
AuthenticationRequiredModalContentConnector.propTypes = {
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);

View File

@@ -0,0 +1,17 @@
import { useCallback, useState } from 'react';
export default function useModalOpenState(
initialState: boolean
): [boolean, () => void, () => void] {
const [isOpen, setOpen] = useState(initialState);
const setModalOpen = useCallback(() => {
setOpen(true);
}, [setOpen]);
const setModalClosed = useCallback(() => {
setOpen(false);
}, [setOpen]);
return [isOpen, setModalOpen, setModalClosed];
}

View File

@@ -60,6 +60,7 @@ import {
faFileImport as fasFileImport,
faFileInvoice as farFileInvoice,
faFilter as fasFilter,
faFlag as fasFlag,
faFolderOpen as fasFolderOpen,
faForward as fasForward,
faHeart as fasHeart,
@@ -155,6 +156,7 @@ export const FATAL = fasTimesCircle;
export const FILE = farFile;
export const FILEIMPORT = fasFileImport;
export const FILTER = fasFilter;
export const FLAG = fasFlag;
export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen;
export const GROUP = farObjectGroup;

View File

@@ -15,6 +15,7 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
export const BOOK_EDITION_SELECT = 'bookEditionSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select';

View File

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

View File

@@ -0,0 +1,7 @@
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}

View File

@@ -1,7 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'input': string;
'modalBody': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,106 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import 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, scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './SelectIndexerFlagsModalContent.css';
class SelectIndexerFlagsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
indexerFlags
} = props;
this.state = {
indexerFlags
};
}
//
// Listeners
onIndexerFlagsChange = ({ value }) => {
this.setState({ indexerFlags: value });
};
onIndexerFlagsSelect = () => {
this.props.onIndexerFlagsSelect(this.state);
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
indexerFlags
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Set indexer Flags
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>
{translate('IndexerFlags')}
</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_FLAGS_SELECT}
name="indexerFlags"
indexerFlags={indexerFlags}
autoFocus={true}
onChange={this.onIndexerFlagsChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onIndexerFlagsSelect}
>
{translate('SetIndexerFlags')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectIndexerFlagsModalContent.propTypes = {
indexerFlags: PropTypes.number.isRequired,
onIndexerFlagsSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectIndexerFlagsModalContent;

View File

@@ -0,0 +1,54 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
const mapDispatchToProps = {
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchSaveInteractiveImportItems: saveInteractiveImportItem
};
class SelectIndexerFlagsModalContentConnector extends Component {
//
// Listeners
onIndexerFlagsSelect = ({ indexerFlags }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchSaveInteractiveImportItems
} = this.props;
dispatchUpdateInteractiveImportItems({
ids,
indexerFlags
});
dispatchSaveInteractiveImportItems({ ids });
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<SelectIndexerFlagsModalContent
{...this.props}
onIndexerFlagsSelect={this.onIndexerFlagsSelect}
/>
);
}
}
SelectIndexerFlagsModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchSaveInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(SelectIndexerFlagsModalContentConnector);

View File

@@ -20,6 +20,7 @@ import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
@@ -30,7 +31,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
const columns = [
const COLUMNS = [
{
name: 'path',
label: 'Path',
@@ -74,11 +75,21 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
kind: kinds.DANGER
kind: kinds.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
isVisible: true
@@ -102,6 +113,7 @@ const BOOK = 'book';
const EDITION = 'edition';
const RELEASE_GROUP = 'releaseGroup';
const QUALITY = 'quality';
const INDEXER_FLAGS = 'indexerFlags';
const replaceExistingFilesOptions = {
COMBINE: 'combine',
@@ -288,6 +300,21 @@ class InteractiveImportModalContent extends Component {
inconsistentBookReleases
} = this.state;
const allColumns = _.cloneDeep(COLUMNS);
const columns = allColumns.map((column) => {
const showIndexerFlags = items.some((item) => item.indexerFlags);
if (!showIndexerFlags) {
const indexerFlagsColumn = allColumns.find((c) => c.name === 'indexerFlags');
if (indexerFlagsColumn) {
indexerFlagsColumn.isVisible = false;
}
}
return column;
});
const selectedIds = this.getSelectedIds();
const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null;
const importIdsByBook = _.chain(items).filter((x) => x.book).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value();
@@ -299,7 +326,8 @@ class InteractiveImportModalContent extends Component {
{ key: BOOK, value: translate('SelectBook') },
{ key: EDITION, value: translate('SelectEdition') },
{ key: QUALITY, value: translate('SelectQuality') },
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') }
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') },
{ key: INDEXER_FLAGS, value: translate('SelectIndexerFlags') }
];
if (allowAuthorChange) {
@@ -422,6 +450,7 @@ class InteractiveImportModalContent extends Component {
isSaving={isSaving}
{...item}
allowAuthorChange={allowAuthorChange}
columns={columns}
onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange}
/>
@@ -518,6 +547,13 @@ class InteractiveImportModalContent extends Component {
onModalClose={this.onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === INDEXER_FLAGS}
ids={selectedIds}
indexerFlags={0}
onModalClose={this.onSelectModalClose}
/>
<ConfirmImportModal
isOpen={isConfirmImportModalOpen}
books={booksImported}

View File

@@ -134,6 +134,7 @@ class InteractiveImportModalContentConnector extends Component {
book,
foreignEditionId,
quality,
indexerFlags,
disableReleaseSwitching
} = item;
@@ -158,6 +159,7 @@ class InteractiveImportModalContentConnector extends Component {
bookId: book.id,
foreignEditionId,
quality,
indexerFlags,
downloadId: this.props.downloadId,
disableReleaseSwitching
});

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality';
import IndexerFlags from 'Book/IndexerFlags';
import FileDetails from 'BookFile/FileDetails';
import Icon from 'Components/Icon';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -14,6 +15,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -34,7 +36,8 @@ class InteractiveImportRow extends Component {
isSelectAuthorModalOpen: false,
isSelectBookModalOpen: false,
isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false
isSelectQualityModalOpen: false,
isSelectIndexerFlagsModalOpen: false
};
}
@@ -44,14 +47,16 @@ class InteractiveImportRow extends Component {
author,
book,
foreignEditionId,
quality
quality,
size
} = this.props;
if (
author &&
book != null &&
foreignEditionId &&
quality
quality &&
size > 0
) {
this.props.onSelectedChange({ id, value: true });
}
@@ -133,6 +138,10 @@ class InteractiveImportRow extends Component {
this.setState({ isSelectQualityModalOpen: true });
};
onSelectIndexerFlagsPress = () => {
this.setState({ isSelectIndexerFlagsModalOpen: true });
};
onSelectAuthorModalClose = (changed) => {
this.setState({ isSelectAuthorModalOpen: false });
this.selectRowAfterChange(changed);
@@ -153,6 +162,11 @@ class InteractiveImportRow extends Component {
this.selectRowAfterChange(changed);
};
onSelectIndexerFlagsModalClose = (changed) => {
this.setState({ isSelectIndexerFlagsModalOpen: false });
this.selectRowAfterChange(changed);
};
//
// Render
@@ -167,7 +181,9 @@ class InteractiveImportRow extends Component {
releaseGroup,
size,
customFormats,
indexerFlags,
rejections,
columns,
additionalFile,
isSelected,
isReprocessing,
@@ -180,7 +196,8 @@ class InteractiveImportRow extends Component {
isSelectAuthorModalOpen,
isSelectBookModalOpen,
isSelectReleaseGroupModalOpen,
isSelectQualityModalOpen
isSelectQualityModalOpen,
isSelectIndexerFlagsModalOpen
} = this.state;
const authorName = author ? author.authorName : '';
@@ -193,6 +210,7 @@ class InteractiveImportRow extends Component {
const showBookNumberPlaceholder = !isReprocessing && isSelected && !!author && !book;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality;
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
const pathCellContents = (
<div onClick={this.onDetailsPress}>
@@ -215,6 +233,8 @@ class InteractiveImportRow extends Component {
/>
);
const isIndexerFlagsColumnVisible = columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false;
return (
<TableRow
className={additionalFile ? styles.additionalFile : undefined}
@@ -307,6 +327,28 @@ class InteractiveImportRow extends Component {
}
</TableRowCell>
{isIndexerFlagsColumnVisible ? (
<TableRowCellButton
title={translate('ClickToChangeIndexerFlags')}
onPress={this.onSelectIndexerFlagsPress}
>
{showIndexerFlagsPlaceholder ? (
<InteractiveImportRowCellPlaceholder isOptional={true} />
) : (
<>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</>
)}
</TableRowCellButton>
) : null}
<TableRowCell>
{
rejections.length ?
@@ -378,6 +420,13 @@ class InteractiveImportRow extends Component {
real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose}
/>
<SelectIndexerFlagsModal
isOpen={isSelectIndexerFlagsModalOpen}
ids={[id]}
indexerFlags={indexerFlags ?? 0}
onModalClose={this.onSelectIndexerFlagsModalClose}
/>
</TableRow>
);
}
@@ -395,7 +444,9 @@ InteractiveImportRow.propTypes = {
quality: PropTypes.object,
size: PropTypes.number.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
indexerFlags: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
audioTags: PropTypes.object.isRequired,
additionalFile: PropTypes.bool.isRequired,
isReprocessing: PropTypes.bool,

View File

@@ -62,6 +62,15 @@ const columns = [
isSortable: true,
isVisible: true
},
{
name: 'indexerFlags',
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {

View File

@@ -40,6 +40,7 @@
}
.rejected,
.indexerFlags,
.download {
composes: cell;

View File

@@ -6,6 +6,7 @@ interface CssExports {
'customFormatScore': string;
'download': string;
'indexer': string;
'indexerFlags': string;
'peers': string;
'protocol': string;
'quality': string;

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality';
import IndexerFlags from 'Book/IndexerFlags';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -129,6 +130,7 @@ class InteractiveSearchRow extends Component {
quality,
customFormatScore,
customFormats,
indexerFlags = 0,
rejections,
downloadAllowed,
isGrabbing,
@@ -189,10 +191,21 @@ class InteractiveSearchRow extends Component {
formatCustomFormatScore(customFormatScore, customFormats.length)
}
tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
position={tooltipPositions.LEFT}
/>
</TableRowCell>
<TableRowCell className={styles.indexerFlags}>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.rejected}>
{
!!rejections.length &&
@@ -265,6 +278,7 @@ InteractiveSearchRow.propTypes = {
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
downloadAllowed: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired,
@@ -277,6 +291,7 @@ InteractiveSearchRow.propTypes = {
};
InteractiveSearchRow.defaultProps = {
indexerFlags: 0,
rejections: [],
isGrabbing: false,
isGrabbed: false

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
@@ -7,7 +8,7 @@ import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import AddNewAuthorSearchResultConnector from './Author/AddNewAuthorSearchResultConnector';
@@ -127,9 +128,16 @@ class AddNewItem extends Component {
!isFetching && !!error ?
<div className={styles.message}>
<div className={styles.helpText}>
Failed to load search results, please try again.
{translate('FailedLoadingSearchResults')}
</div>
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
<div>
<Link to="https://wiki.servarr.com/readarr/troubleshooting#invalid-response-received-from-metadata-api">
{translate('WhySearchesCouldBeFailing')}
</Link>
</div>
<div>{getErrorMessage(error)}</div>
</div> : null
}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
@@ -150,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
</Form>
<FieldSet legend={translate('Conditions')}>
<Alert kind={kinds.INFO}>
<div>
{translate('CustomFormatsSettingsTriggerInfo')}
</div>
</Alert>
<div className={styles.customFormats}>
{
specifications.map((tag) => {

View File

@@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import styles from './EditDownloadClientModalContent.css';
@@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteDownloadClientPress,
...otherProps
} = this.props;
@@ -205,6 +207,12 @@ class EditDownloadClientModalContent extends Component {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -245,6 +253,7 @@ EditDownloadClientModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteDownloadClientPress: PropTypes.func
};

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
import {
saveDownloadClient,
setDownloadClientFieldValue,
setDownloadClientValue,
testDownloadClient,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setDownloadClientValue,
setDownloadClientFieldValue,
saveDownloadClient,
testDownloadClient
testDownloadClient,
toggleAdvancedSettings
};
class EditDownloadClientModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
this.props.testDownloadClient({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
setDownloadClientFieldValue: PropTypes.func.isRequired,
saveDownloadClient: PropTypes.func.isRequired,
testDownloadClient: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -32,7 +32,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -11,16 +11,69 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import { icons, inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const authenticationMethodOptions = [
{ key: 'none', value: 'None' },
{ key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' }
export const authenticationMethodOptions = [
{
key: 'none',
get value() {
return translate('None');
},
isDisabled: true
},
{
key: 'external',
get value() {
return translate('External');
},
isHidden: true
},
{
key: 'basic',
get value() {
return translate('AuthBasic');
}
},
{
key: 'forms',
get value() {
return translate('AuthForm');
}
}
];
export const authenticationRequiredOptions = [
{
key: 'enabled',
get value() {
return translate('Enabled');
}
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
}
}
];
const certificateValidationOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
{ key: 'disabled', value: 'Disabled' }
{
key: 'enabled',
get value() {
return translate('Enabled');
}
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
}
},
{
key: 'disabled',
get value() {
return translate('Disabled');
}
}
];
class SecuritySettings extends Component {
@@ -68,8 +121,10 @@ class SecuritySettings extends Component {
const {
authenticationMethod,
authenticationRequired,
username,
password,
passwordConfirmation,
apiKey,
certificateValidation
} = settings;
@@ -79,26 +134,40 @@ class SecuritySettings extends Component {
return (
<FieldSet legend={translate('Security')}>
<FormGroup>
<FormLabel>
{translate('Authentication')}
</FormLabel>
<FormLabel>{translate('Authentication')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={translate('AuthenticationRequiredWarning')}
onChange={onInputChange}
{...authenticationMethod}
/>
</FormGroup>
{
authenticationEnabled &&
authenticationEnabled ?
<FormGroup>
<FormLabel>
{translate('Username')}
</FormLabel>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
@@ -106,15 +175,14 @@ class SecuritySettings extends Component {
onChange={onInputChange}
{...username}
/>
</FormGroup>
</FormGroup> :
null
}
{
authenticationEnabled &&
authenticationEnabled ?
<FormGroup>
<FormLabel>
{translate('Password')}
</FormLabel>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
@@ -122,19 +190,33 @@ class SecuritySettings extends Component {
onChange={onInputChange}
{...password}
/>
</FormGroup>
</FormGroup> :
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
{...passwordConfirmation}
/>
</FormGroup> :
null
}
<FormGroup>
<FormLabel>
{translate('APIKey')}
</FormLabel>
<FormLabel>{translate('ApiKey')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="apiKey"
readOnly={true}
helpTextWarning={translate('ApiKeyHelpTextWarning')}
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
buttons={[
<ClipboardButton
key="copy"
@@ -160,9 +242,7 @@ class SecuritySettings extends Component {
</FormGroup>
<FormGroup>
<FormLabel>
{translate('CertificateValidation')}
</FormLabel>
<FormLabel>{translate('CertificateValidation')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}

View File

@@ -20,6 +20,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import styles from './EditImportListModalContent.css';
@@ -66,6 +67,7 @@ function EditImportListModalContent(props) {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteImportListPress,
showMetadataProfile,
...otherProps
@@ -332,6 +334,12 @@ function EditImportListModalContent(props) {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -372,6 +380,7 @@ EditImportListModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteImportListPress: PropTypes.func
};

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions';
import {
saveImportList,
setImportListFieldValue,
setImportListValue,
testImportList,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditImportListModalContent from './EditImportListModalContent';
@@ -25,7 +31,8 @@ const mapDispatchToProps = {
setImportListValue,
setImportListFieldValue,
saveImportList,
testImportList
testImportList,
toggleAdvancedSettings
};
class EditImportListModalContentConnector extends Component {
@@ -58,6 +65,10 @@ class EditImportListModalContentConnector extends Component {
this.props.testImportList({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -67,6 +78,7 @@ class EditImportListModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -84,6 +96,7 @@ EditImportListModalContentConnector.propTypes = {
setImportListFieldValue: PropTypes.func.isRequired,
saveImportList: PropTypes.func.isRequired,
testImportList: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -32,7 +32,7 @@ const autoAddOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -32,7 +32,7 @@ const enableOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'enabled',

View File

@@ -1,6 +1,6 @@
.option {
display: flex;
align-items: center;
align-items: stretch;
flex-wrap: wrap;
margin: 3px;
border: 1px solid var(--borderColor);
@@ -17,7 +17,7 @@
}
.small {
width: 460px;
width: 490px;
}
.large {
@@ -26,7 +26,7 @@
.token {
flex: 0 0 50%;
padding: 6px 16px;
padding: 6px;
background-color: var(--popoverTitleBackgroundColor);
font-family: $monoSpaceFontFamily;
}
@@ -34,9 +34,9 @@
.example {
display: flex;
align-items: center;
align-self: stretch;
justify-content: space-between;
flex: 0 0 50%;
padding: 6px 16px;
padding: 6px;
background-color: var(--popoverBodyBackgroundColor);
.footNote {

View File

@@ -14,6 +14,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import NotificationEventItems from './NotificationEventItems';
import styles from './EditNotificationModalContent.css';
@@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteNotificationPress,
...otherProps
} = props;
@@ -140,6 +142,12 @@ function EditNotificationModalContent(props) {
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
@@ -179,6 +187,7 @@ EditNotificationModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteNotificationPress: PropTypes.func
};

View File

@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
import {
saveNotification,
setNotificationFieldValue,
setNotificationValue,
testNotification,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditNotificationModalContent from './EditNotificationModalContent';
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
setNotificationValue,
setNotificationFieldValue,
saveNotification,
testNotification
testNotification,
toggleAdvancedSettings
};
class EditNotificationModalContentConnector extends Component {
@@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
this.props.testNotification({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
setNotificationFieldValue: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -1,8 +1,11 @@
import $ from 'jquery';
import _ from 'lodash';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getProviderState from 'Utilities/State/getProviderState';
import { set } from '../baseActions';
const abortCurrentRequests = {};
let lastTestData = null;
export function createCancelTestProviderHandler(section) {
return function(getState, payload, dispatch) {
@@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
return function(getState, payload, dispatch) {
dispatch(set({ section, isTesting: true }));
const testData = getProviderState(payload, getState, section);
const {
queryParams = {},
...otherPayload
} = payload;
const testData = getProviderState({ ...otherPayload }, getState, section);
const params = { ...queryParams };
// If the user is re-testing the same provider without changes
// force it to be tested.
if (_.isEqual(testData, lastTestData)) {
params.forceTest = true;
}
lastTestData = testData;
const ajaxOptions = {
url: `${url}/test`,
url: `${url}/test?${$.param(params, true)}`,
method: 'POST',
contentType: 'application/json',
dataType: 'json',
@@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
abortCurrentRequests[section] = abortRequest;
request.done((data) => {
lastTestData = null;
dispatch(set({
section,
isTesting: false,

View File

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

View File

@@ -1,9 +1,11 @@
import _ from 'lodash';
import moment from 'moment';
import React from 'react';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import bookEntities from 'Book/bookEntities';
import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import { filterTypePredicates, filterTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
@@ -243,6 +245,15 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
{
name: 'indexerFlags',
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isVisible: false
},
{
name: 'status',
label: 'Status',

View File

@@ -207,6 +207,7 @@ export const actionHandlers = handleThunks({
foreignEditionId: item.foreignEditionId ? item.ForeignEditionId : undefined,
quality: item.quality,
releaseGroup: item.releaseGroup,
indexerFlags: item.indexerFlags,
downloadId: item.downloadId,
additionalFile: item.additionalFile,
replaceExistingFiles: item.replaceExistingFiles,

View File

@@ -10,6 +10,7 @@ import downloadClients from './Settings/downloadClients';
import general from './Settings/general';
import importListExclusions from './Settings/importListExclusions';
import importLists from './Settings/importLists';
import indexerFlags from './Settings/indexerFlags';
import indexerOptions from './Settings/indexerOptions';
import indexers from './Settings/indexers';
import languages from './Settings/languages';
@@ -35,6 +36,7 @@ export * from './Settings/downloadClientOptions';
export * from './Settings/general';
export * from './Settings/importLists';
export * from './Settings/importListExclusions';
export * from './Settings/indexerFlags';
export * from './Settings/indexerOptions';
export * from './Settings/indexers';
export * from './Settings/languages';
@@ -70,6 +72,7 @@ export const defaultState = {
downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState,
general: general.defaultState,
indexerFlags: indexerFlags.defaultState,
indexerOptions: indexerOptions.defaultState,
indexers: indexers.defaultState,
importLists: importLists.defaultState,
@@ -115,6 +118,7 @@ export const actionHandlers = handleThunks({
...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers,
...general.actionHandlers,
...indexerFlags.actionHandlers,
...indexerOptions.actionHandlers,
...indexers.actionHandlers,
...importLists.actionHandlers,
@@ -151,6 +155,7 @@ export const reducers = createHandleActions({
...downloadClients.reducers,
...downloadClientOptions.reducers,
...general.reducers,
...indexerFlags.reducers,
...indexerOptions.reducers,
...indexers.reducers,
...importLists.reducers,

View File

@@ -0,0 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
const createIndexerFlagsSelector = createSelector(
(state: AppState) => state.settings.indexerFlags,
(indexerFlags) => indexerFlags
);
export default createIndexerFlagsSelector;

View File

@@ -0,0 +1,23 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Author from 'Author/Author';
function createMultiAuthorsSelector(authorIds: number[]) {
return createSelector(
(state: AppState) => state.authors.itemMap,
(state: AppState) => state.authors.items,
(itemMap, allAuthors) => {
return authorIds.reduce((acc: Author[], authorId) => {
const author = allAuthors[itemMap[authorId]];
if (author) {
acc.push(author);
}
return acc;
}, []);
}
);
}
export default createMultiAuthorsSelector;

View File

@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createTagsSelector() {
return createSelector(
(state) => state.tags.items,
(state: AppState) => state.tags.items,
(tags) => {
return tags;
}

View File

@@ -2,7 +2,6 @@ module.exports = {
// Families
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
passwordFamily: 'text-security-disc',
// Sizes
extraSmallFontSize: '11px',

View File

@@ -10,15 +10,6 @@
width: 100%;
}
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}
.queued,
.started,
.ended {

View File

@@ -2,14 +2,12 @@
// Please do not change this file!
interface CssExports {
'actions': string;
'commandName': string;
'duration': string;
'ended': string;
'queued': string;
'started': string;
'trigger': string;
'triggerContent': string;
'userAgent': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,279 +0,0 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRow.css';
function getStatusIconProps(status, message) {
const title = titleCase(status);
switch (status) {
case 'queued':
return {
name: icons.PENDING,
title
};
case 'started':
return {
name: icons.REFRESH,
isSpinning: true,
title
};
case 'completed':
return {
name: icons.CHECK,
kind: kinds.SUCCESS,
title: message === 'Completed' ? title : `${title}: ${message}`
};
case 'failed':
return {
name: icons.FATAL,
kind: kinds.DANGER,
title: `${title}: ${message}`
};
default:
return {
name: icons.UNKNOWN,
title
};
}
}
function getFormattedDates(props) {
const {
queued,
started,
ended,
showRelativeDates,
shortDateFormat
} = props;
if (showRelativeDates) {
return {
queuedAt: moment(queued).fromNow(),
startedAt: started ? moment(started).fromNow() : '-',
endedAt: ended ? moment(ended).fromNow() : '-'
};
}
return {
queuedAt: formatDate(queued, shortDateFormat),
startedAt: started ? formatDate(started, shortDateFormat) : '-',
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
};
}
class QueuedTaskRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
...getFormattedDates(props),
isCancelConfirmModalOpen: false
};
this._updateTimeoutId = null;
}
componentDidMount() {
this.setUpdateTimer();
}
componentDidUpdate(prevProps) {
const {
queued,
started,
ended
} = this.props;
if (
queued !== prevProps.queued ||
started !== prevProps.started ||
ended !== prevProps.ended
) {
this.setState(getFormattedDates(this.props));
}
}
componentWillUnmount() {
if (this._updateTimeoutId) {
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
}
}
//
// Control
setUpdateTimer() {
this._updateTimeoutId = setTimeout(() => {
this.setState(getFormattedDates(this.props));
this.setUpdateTimer();
}, 30000);
}
//
// Listeners
onCancelPress = () => {
this.setState({
isCancelConfirmModalOpen: true
});
};
onAbortCancel = () => {
this.setState({
isCancelConfirmModalOpen: false
});
};
//
// Render
render() {
const {
trigger,
commandName,
queued,
started,
ended,
status,
duration,
message,
clientUserAgent,
longDateFormat,
timeFormat,
onCancelPress
} = this.props;
const {
queuedAt,
startedAt,
endedAt,
isCancelConfirmModalOpen
} = this.state;
let triggerIcon = icons.QUICK;
if (trigger === 'manual') {
triggerIcon = icons.INTERACTIVE;
} else if (trigger === 'scheduled') {
triggerIcon = icons.SCHEDULED;
}
return (
<TableRow>
<TableRowCell className={styles.trigger}>
<span className={styles.triggerContent}>
<Icon
name={triggerIcon}
title={titleCase(trigger)}
/>
<Icon
{...getStatusIconProps(status, message)}
/>
</span>
</TableRowCell>
<TableRowCell>
<span className={styles.commandName}>
{commandName}
</span>
{
clientUserAgent ?
<span className={styles.userAgent} title={translate('UserAgentProvidedByTheAppThatCalledTheAPI')}>
from: {clientUserAgent}
</span> :
null
}
</TableRowCell>
<TableRowCell
className={styles.queued}
title={formatDateTime(queued, longDateFormat, timeFormat)}
>
{queuedAt}
</TableRowCell>
<TableRowCell
className={styles.started}
title={formatDateTime(started, longDateFormat, timeFormat)}
>
{startedAt}
</TableRowCell>
<TableRowCell
className={styles.ended}
title={formatDateTime(ended, longDateFormat, timeFormat)}
>
{endedAt}
</TableRowCell>
<TableRowCell className={styles.duration}>
{formatTimeSpan(duration)}
</TableRowCell>
<TableRowCell
className={styles.actions}
>
{
status === 'queued' &&
<IconButton
title={translate('RemovedFromTaskQueue')}
name={icons.REMOVE}
onPress={this.onCancelPress}
/>
}
</TableRowCell>
<ConfirmModal
isOpen={isCancelConfirmModalOpen}
kind={kinds.DANGER}
title={translate('Cancel')}
message={translate('CancelMessageText')}
confirmLabel={translate('YesCancel')}
cancelLabel={translate('NoLeaveIt')}
onConfirm={onCancelPress}
onCancel={this.onAbortCancel}
/>
</TableRow>
);
}
}
QueuedTaskRow.propTypes = {
trigger: PropTypes.string.isRequired,
commandName: PropTypes.string.isRequired,
queued: PropTypes.string.isRequired,
started: PropTypes.string,
ended: PropTypes.string,
status: PropTypes.string.isRequired,
duration: PropTypes.string,
message: PropTypes.string,
clientUserAgent: PropTypes.string,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onCancelPress: PropTypes.func.isRequired
};
export default QueuedTaskRow;

View File

@@ -0,0 +1,238 @@
import moment from 'moment';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds } from 'Helpers/Props';
import { cancelCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
import styles from './QueuedTaskRow.css';
function getStatusIconProps(status: string, message: string | undefined) {
const title = titleCase(status);
switch (status) {
case 'queued':
return {
name: icons.PENDING,
title,
};
case 'started':
return {
name: icons.REFRESH,
isSpinning: true,
title,
};
case 'completed':
return {
name: icons.CHECK,
kind: kinds.SUCCESS,
title: message === 'Completed' ? title : `${title}: ${message}`,
};
case 'failed':
return {
name: icons.FATAL,
kind: kinds.DANGER,
title: `${title}: ${message}`,
};
default:
return {
name: icons.UNKNOWN,
title,
};
}
}
function getFormattedDates(
queued: string,
started: string | undefined,
ended: string | undefined,
showRelativeDates: boolean,
shortDateFormat: string
) {
if (showRelativeDates) {
return {
queuedAt: moment(queued).fromNow(),
startedAt: started ? moment(started).fromNow() : '-',
endedAt: ended ? moment(ended).fromNow() : '-',
};
}
return {
queuedAt: formatDate(queued, shortDateFormat),
startedAt: started ? formatDate(started, shortDateFormat) : '-',
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
};
}
interface QueuedTimes {
queuedAt: string;
startedAt: string;
endedAt: string;
}
export interface QueuedTaskRowProps {
id: number;
trigger: string;
commandName: string;
queued: string;
started?: string;
ended?: string;
status: string;
duration?: string;
message?: string;
body: CommandBody;
clientUserAgent?: string;
}
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
const {
id,
trigger,
commandName,
queued,
started,
ended,
status,
duration,
message,
body,
clientUserAgent,
} = props;
const dispatch = useDispatch();
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
useSelector(createUISettingsSelector());
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
null
);
const [times, setTimes] = useState<QueuedTimes>(
getFormattedDates(
queued,
started,
ended,
showRelativeDates,
shortDateFormat
)
);
const [
isCancelConfirmModalOpen,
openCancelConfirmModal,
closeCancelConfirmModal,
] = useModalOpenState(false);
const handleCancelPress = useCallback(() => {
dispatch(cancelCommand({ id }));
}, [id, dispatch]);
useEffect(() => {
updateTimeTimeoutId.current = setTimeout(() => {
setTimes(
getFormattedDates(
queued,
started,
ended,
showRelativeDates,
shortDateFormat
)
);
}, 30000);
return () => {
if (updateTimeTimeoutId.current) {
clearTimeout(updateTimeTimeoutId.current);
}
};
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
const { queuedAt, startedAt, endedAt } = times;
let triggerIcon = icons.QUICK;
if (trigger === 'manual') {
triggerIcon = icons.INTERACTIVE;
} else if (trigger === 'scheduled') {
triggerIcon = icons.SCHEDULED;
}
return (
<TableRow>
<TableRowCell className={styles.trigger}>
<span className={styles.triggerContent}>
<Icon name={triggerIcon} title={titleCase(trigger)} />
<Icon {...getStatusIconProps(status, message)} />
</span>
</TableRowCell>
<QueuedTaskRowNameCell
commandName={commandName}
body={body}
clientUserAgent={clientUserAgent}
/>
<TableRowCell
className={styles.queued}
title={formatDateTime(queued, longDateFormat, timeFormat)}
>
{queuedAt}
</TableRowCell>
<TableRowCell
className={styles.started}
title={formatDateTime(started, longDateFormat, timeFormat)}
>
{startedAt}
</TableRowCell>
<TableRowCell
className={styles.ended}
title={formatDateTime(ended, longDateFormat, timeFormat)}
>
{endedAt}
</TableRowCell>
<TableRowCell className={styles.duration}>
{formatTimeSpan(duration)}
</TableRowCell>
<TableRowCell className={styles.actions}>
{status === 'queued' && (
<IconButton
title={translate('RemovedFromTaskQueue')}
name={icons.REMOVE}
onPress={openCancelConfirmModal}
/>
)}
</TableRowCell>
<ConfirmModal
isOpen={isCancelConfirmModalOpen}
kind={kinds.DANGER}
title={translate('Cancel')}
message={translate('CancelPendingTask')}
confirmLabel={translate('YesCancel')}
cancelLabel={translate('NoLeaveIt')}
onConfirm={handleCancelPress}
onCancel={closeCancelConfirmModal}
/>
</TableRow>
);
}

View File

@@ -1,31 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { cancelCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import QueuedTaskRow from './QueuedTaskRow';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onCancelPress() {
dispatch(cancelCommand({
id: props.id
}));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);

View File

@@ -0,0 +1,8 @@
.commandName {
display: inline-block;
min-width: 220px;
}
.userAgent {
color: #b0b0b0;
}

View File

@@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'commandName': string;
'userAgent': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -0,0 +1,65 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import createMultiAuthorsSelector from 'Store/Selectors/createMultiAuthorsSelector';
import translate from 'Utilities/String/translate';
import styles from './QueuedTaskRowNameCell.css';
function formatTitles(titles: string[]) {
if (!titles) {
return null;
}
if (titles.length > 11) {
return (
<span title={titles.join(', ')}>
{titles.slice(0, 10).join(', ')}, {titles.length - 10} more
</span>
);
}
return <span>{titles.join(', ')}</span>;
}
export interface QueuedTaskRowNameCellProps {
commandName: string;
body: CommandBody;
clientUserAgent?: string;
}
export default function QueuedTaskRowNameCell(
props: QueuedTaskRowNameCellProps
) {
const { commandName, body, clientUserAgent } = props;
const movieIds = [...(body.authorIds ?? [])];
if (body.authorId) {
movieIds.push(body.authorId);
}
const authors = useSelector(createMultiAuthorsSelector(movieIds));
const sortedAuthors = authors.sort((a, b) =>
a.sortName.localeCompare(b.sortName)
);
return (
<TableRowCell>
<span className={styles.commandName}>
{commandName}
{sortedAuthors.length ? (
<span> - {formatTitles(sortedAuthors.map((a) => a.authorName))}</span>
) : null}
</span>
{clientUserAgent ? (
<span
className={styles.userAgent}
title={translate('TaskUserAgentTooltip')}
>
{translate('From')}: {clientUserAgent}
</span>
) : null}
</TableRowCell>
);
}

View File

@@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import translate from 'Utilities/String/translate';
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
const columns = [
{
name: 'trigger',
label: '',
isVisible: true
},
{
name: 'commandName',
label: () => translate('Name'),
isVisible: true
},
{
name: 'queued',
label: () => translate('Queued'),
isVisible: true
},
{
name: 'started',
label: () => translate('Started'),
isVisible: true
},
{
name: 'ended',
label: () => translate('Ended'),
isVisible: true
},
{
name: 'duration',
label: () => translate('Duration'),
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
function QueuedTasks(props) {
const {
isFetching,
isPopulated,
items
} = props;
return (
<FieldSet legend={translate('Queue')}>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
isPopulated &&
<Table
columns={columns}
>
<TableBody>
{
items.map((item) => {
return (
<QueuedTaskRowConnector
key={item.id}
{...item}
/>
);
})
}
</TableBody>
</Table>
}
</FieldSet>
);
}
QueuedTasks.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
items: PropTypes.array.isRequired
};
export default QueuedTasks;

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