Compare commits

..

326 Commits

Author SHA1 Message Date
Qstick
aa52f9fc6d Simplify X-Forwarded-For handling
This happens in asp.net middleware now

Co-Authored-By: ta264 <ta264@users.noreply.github.com>

(cherry picked from commit 16e2d130e6a2e7239bcfe92187a7f990f93eff00)
2022-12-08 05:09:36 +00:00
Qstick
da5e35fc25 Unbork UI Build 2022-11-28 22:11:13 -06:00
Mark McDowall
a6a2219bc4 Fixed: Handle Flood reporting errors for completed and stopped downloads
(cherry picked from commit f2b2eb69a3e8b271535bd92dc2f5cbfd45664daf)
2022-11-28 21:25:03 -06:00
Qstick
aa2855a62b Re-saving edited providers will forcibly save them
Fixes #533

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-27 19:32:14 -06:00
Qstick
599f52e72f Fixed: Validation when testing indexers, import lists, connections and download clients
Fixes #1612

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-27 19:10:12 -06:00
Qstick
c5c2b94b9a Fixup Repack Tests 2022-11-25 22:16:34 -06:00
Qstick
b016b36435 Fixed: Validate if equals or child for startup folder
Fixes #1712
Close #1713

(cherry picked from commit 0991cfe27efd6ddb533227b25754661e18d7e9ad)
2022-11-25 22:07:42 -06:00
Qstick
d93a0c27e9 Sentry logging exceptions
Fixes #855

Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
2022-11-25 21:58:58 -06:00
Qstick
eecf08e063 Updated NLog Version
Co-Authored-By: Robin Dadswell <19610103+RobinDadswell@users.noreply.github.com>
2022-11-25 21:52:52 -06:00
Qstick
fb1643f630 Lint Fixes 2022-11-25 21:39:44 -06:00
bakerboy448
a061179f6b Fixed: Repack Preference Ignored
Fixes #1867

(cherry picked from commit 04447d9d4db8cc3d54baf0a721f4430cf77908c4)
2022-11-25 21:39:44 -06:00
Stevie Robinson
b441f6c05b Fixed: updated rTorrent download client note
Fixes #1882

(cherry picked from commit 743d28b93a55553ee25381570d0daa04ed2117af)
2022-11-25 21:33:35 -06:00
Chromo-residuum-opec
0904eac300 Update help text for rTorrent download client options
Fixes #1868

(cherry picked from commit d2a23f7bcdf71800f019644d7b6b5d712e311d7f)
2022-11-25 21:31:57 -06:00
Qstick
29e3d8f477 Fixed: Fall back to sorting by release title if artist is not matched
Fixes #1870
2022-11-25 21:29:38 -06:00
Qstick
71bd88e531 New: Add Release group to history for all events
Fixes #1499

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-25 21:28:05 -06:00
Stevie Robinson
0689bec779 New: Add optional Source Title column to history
(cherry picked from commit 581fb2cb3d47d62fe16b840081647056ec77043d)
2022-11-25 21:16:37 -06:00
Devin Buhl
117a5c8010 New: Add application URL to host configuration settings
Fixes #1816
Closes #1817

(cherry picked from commit 762042ba97c2ae689cee32d8e66a458f6d7a8adc)
2022-11-25 21:16:37 -06:00
Qstick
d10d91439f New: Add indexer name to the download report log
Fixes #1904
2022-11-25 21:16:37 -06:00
Mark McDowall
ed0722bae4 New: Validate that naming formats don't contain illegal characters
Fixes #620

(cherry picked from commit 145c644c9d8f1636027da8a782a7e74f3182c678)
2022-11-25 21:16:37 -06:00
psylenced
e69371deca Fix: Trace logging postgres cleanse for large json files.
(cherry picked from commit 2ce9d099e1001eb4fccd61edcb0597782da872d4)
2022-11-25 20:30:54 -06:00
Chris
5ae8deb9d6 Fixed: Postgres secret regex now less greedy
[common]

(cherry picked from commit 812e5ac5a3d76b69c172c5611b315ea809ab4b48)
2022-11-25 20:30:54 -06:00
Chris
291674fc18 Fixed: Regex in log cleanser taking 10+ minutes on messages longer than 100k.
(cherry picked from commit d01bae92bfd68b04c54ab19bafe8af16c68ce711)
2022-11-25 20:30:54 -06:00
Qstick
774cd04d32 Fixed SeedConfigProvider cache refresh after indexer settings change
Fixes #1036

Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
2022-11-25 20:24:00 -06:00
Qstick
3eb0533b17 New: Disable autocomplete of port number
Fixes #1249

Co-Authored-By: Stevie Robinson <stevie.robinson@gmail.com>
2022-11-25 20:21:10 -06:00
Qstick
2fe11ca1a9 Rename NzbSearchService to ReleaseSearchService
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-25 20:18:28 -06:00
Qstick
c8ae6b0299 Fixed: Custom Script Health Issue Level
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2022-11-25 20:14:47 -06:00
Qstick
2086ae1e2c Ensure .Mono and .Windows projects have all dependencies in build output
Co-Authored-By: ta264 <ta264@users.noreply.github.com>
2022-11-25 20:10:23 -06:00
Chris
2aeec97d5e Added: Ntfy provider for notifications
(cherry picked from commit f6e6bc98f7846328d158ab2f804ca65b49632912)
2022-11-25 20:02:23 -06:00
Qstick
45e9d14916 New: Base API info endpoint
(cherry picked from commit 5e57ffbcf9ac3a346d4bf2b692248393215bad89)
2022-11-25 20:01:21 -06:00
Qstick
ded45a53f3 Improve page scrollbar
Fixed: Scrolling in Firefox in small window (requires refresh)
New: Style scrollbar in Firefox
Fixed: Scrolling with click and drag
Fixes #3088
Fixes #2706
2022-11-25 20:00:56 -06:00
Mark McDowall
d5cbb8f84f Replace react-custom-scrollbars
(cherry picked from commit 1c6c9a7960865d4bf8f58d5d04bef1aa1409594a)
2022-11-25 20:00:56 -06:00
Mark McDowall
cac0fca0d2 Added react-hooks lint rules
(cherry picked from commit 381d64259396582de8d63ada99333e42cf5e3189)
2022-11-25 19:55:55 -06:00
Mark McDowall
9b5a050c40 Publish ApplicationStartingEvent during startup
(cherry picked from commit 5400bce1295bdc4198d2cfe0b9258bbb7ccf0852)
2022-11-25 19:54:05 -06:00
Weblate
9fa67dbe5f Translated using Weblate (German) [skip ci]
Currently translated at 100.0% (879 of 879 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 62.4% (549 of 879 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.8% (702 of 879 strings)

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: marapavelka <mara.pavelka@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translation: Servarr/Readarr
2022-11-25 19:25:06 -06:00
Mark McDowall
505ef5ee63 Fixed: Testing SABnzbd when no categories are configured
(cherry picked from commit 0e31281828c737e3f6eecbb870960194888a970a)
2022-11-25 19:24:38 -06:00
Mark McDowall
87a2d7382e Added SECURITY.md
(cherry picked from commit 80af164385d9087a627142ca2281ae74ac0572af)

(cherry picked from commit aa8e886dab3ffedc835900db39787d47dc12465c)
2022-11-21 19:25:11 -06:00
Qstick
7c74a8df01 Fixed: UI Errors in Add Book page
Fixes #1906
2022-11-03 16:30:54 -05:00
Qstick
f8324b3c92 Create CODE_OF_CONDUCT.md 2022-11-03 15:58:16 -05:00
Weblate
cb03f3bf6b Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 100.0% (879 of 879 strings)

Update translation files [skip ci]

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 69.0% (607 of 879 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.7% (675 of 879 strings)

Co-authored-by: Sincejunly <qq943384135@gmail.com>
Co-authored-by: Tiago Ribeiro <tiago.err@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: libsu <libsu@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2022-11-03 10:42:08 -05:00
ta264
d6f85ec4f9 Use wildcard for FreeBSD itemPattern 2022-11-02 21:22:34 +00:00
Qstick
dd0dd921b0 Ignore brotli test on osx
(cherry picked from commit 2afe6af5a6dac26fe9d2c20e806a93b621e18bbf)
2022-10-30 19:03:10 -05:00
Mark McDowall
1f02423148 Fixed: Series list jump bar click issues
(cherry picked from commit 9c7378625112088d022239fdbdb90c0dc941d61d)
2022-10-20 20:42:53 -05:00
Qstick
7a4fba851f fixup! 2022-10-20 20:39:36 -05:00
Qstick
2d5efc268f New: Retry Postgres connection 3 times (with 5 second sleep) on Startup
(cherry picked from commit 3e700b63c26b247fcac83428ba79e53c88f797ff)

(cherry picked from commit dbca393772d7f558b45a780a6767187bf5900a23)
2022-10-20 20:39:36 -05:00
Mark McDowall
cad5762f31 Sliding expiration for auth cookie and a little clean up
Fixes #1802

(cherry picked from commit 05ee4e644907b7f1e84589465ac9ab1848f5a766)
2022-10-20 20:23:15 -05:00
Qstick
bcc8370d05 Cleanup dual target and mono code
Fixes #1893
Fixes #1808
2022-10-20 20:19:52 -05:00
Qstick
3481168df5 Fixed: qBittorrent Updates
Fixes #1658
Fixes #1536
2022-10-20 20:03:55 -05:00
Qstick
337a30ac0f Add thread to discord notifications 2022-10-19 22:00:13 -05:00
Weblate
eaad4de4dc Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 69.0% (607 of 879 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 65.4% (575 of 879 strings)

Translated using Weblate (Slovak) [skip ci]

Currently translated at 7.3% (65 of 879 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.3% (671 of 879 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.3% (671 of 879 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.6% (6 of 879 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.8% (702 of 879 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (614 of 879 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 63.8% (561 of 879 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 75.9% (668 of 879 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.8% (702 of 879 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 62.3% (548 of 879 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 62.4% (549 of 879 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 67.2% (591 of 879 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 65.8% (579 of 879 strings)

Added translation using Weblate (Latvian) [skip ci]

Translated using Weblate (German) [skip ci]

Currently translated at 92.0% (809 of 879 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (879 of 879 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (879 of 879 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 67.9% (597 of 879 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.5% (875 of 879 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 75.0% (660 of 879 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 92.0% (809 of 879 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 67.1% (590 of 879 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dainel Amendoeira <daniel@amendoeira.eu>
Co-authored-by: Gian Klug <gian.klug@ict-scouts.ch>
Co-authored-by: Gylesie <github-anon.dasheens@aleeas.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: HiNesslio <chi.lio@shms-mail.ch>
Co-authored-by: Marcin <ml.cichy@gmail.com>
Co-authored-by: Mijail Todorovich <mijailtodorovich+git@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Thomas Schwery <thomas@schwery.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: cikyw <cikyw@vomoto.com>
Co-authored-by: fyu0h <biiigback@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: 麋鹿先生 <y.qiuyu@outlook.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/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/is/
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/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/sk/
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
2022-10-16 10:06:07 -05:00
Mark McDowall
b98ae37abf Handle redirects for 308 redirects
(cherry picked from commit 6eed7c8fed096fa1762448bc57876440f542be98)
2022-10-16 10:03:35 -05:00
Stevie Robinson
5dac84f9d8 New: Torrent Seed Ratio no longer advance settings
(cherry picked from commit c98fac65ed8e669c5e97ed3255c424b61fe0e8b3)
2022-10-16 10:03:06 -05:00
bakerboy448
ef21c73619 update feature request template
[skip ci]

(cherry picked from commit 362e664ce6d301e8283d489b3e0d945db46d22d3)
2022-10-16 10:02:34 -05:00
Joe Milazzo
14d74f2eca New: Kavita Connection (#1880)
* Added ability for Readarr to inform Kavita when a change occurs for rescan.

* Use the existing API with a POST rather than a new API.

* Updated some wording

* Fixed PR comments
2022-10-02 18:41:38 -05:00
servarr[bot]
c3cbbb7627 Update Bug Report Template [skip ci] (#1842)
(cherry picked from commit 99e0d42b717b279be0f9005039025cd730f90f6c)

Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2022-09-26 21:05:36 -05:00
servarr[bot]
ad3a58c422 New: Parse version with a space before 'v''
* New: Parse anime version with a space before 'v'

(cherry picked from commit e9123982f33ab35ca022f91f345da05fef23d6dc)

* Delete AnimeVersionFixture.cs

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
Co-authored-by: Qstick <qstick@gmail.com>
2022-09-26 16:55:34 -05:00
Robin Dadswell
347b154882 Update Bug Report Template
[skip ci]

(cherry picked from commit b9185574f3761e125151907e3d31511689a4513e)
2022-09-26 16:42:52 -05:00
Weblate
63ccc155d6 Translated using Weblate (Russian) [skip ci]
Currently translated at 66.6% (586 of 879 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 60.5% (532 of 879 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (879 of 879 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.6% (700 of 879 strings)

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: emanuelsipos <emanuelsipos1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translation: Servarr/Readarr
2022-08-18 07:12:51 -05:00
ta264
76613316fa Fixed: Book filters on author properties
Fixes #1755
2022-08-15 14:04:29 +01:00
ta264
f33e9f2bbc Fixed: Selecting edition for books in manual import 2022-08-15 14:04:29 +01:00
Robin Dadswell
e4a3d7b273 Fixed: Postgres Timezone Issues
Co-authored-by: ta264 <ta264@users.noreply.github.com>
2022-08-13 19:55:48 +01:00
Robin Dadswell
46c2e0ba82 New: Postgres Support
Co-Authored-By: Qstick <376117+Qstick@users.noreply.github.com>
Co-authored-by: ta264 <ta264@users.noreply.github.com>

(cherry picked from commit 80b1aa9a2c81617bdda7ef551c19a2f114e49204)
2022-08-13 19:55:48 +01:00
Robin Dadswell
8616373f96 Bump FluentMigrator to 3.3.2 2022-08-13 19:55:48 +01:00
ta264
b3f99d8c20 Fixed: Add missing author when a book's author changes in metadata
Fixes READARR-VH
2022-08-05 11:30:13 +01:00
ta264
c0c0847963 Update packages 2022-08-05 11:30:13 +01:00
Qstick
c92f9d03c0 Fixed: Releases Size filter has incorrect value type
(cherry picked from commit 46bc711558d9f3ab278125a4292eb7851e51d308)
2022-07-29 23:26:22 -04:00
bakerboy448
f7bf1e243d Fixed: List Import additions when book / author lookup required 2022-07-29 12:06:53 -05:00
Weblate
20b1f41ac1 Translated using Weblate (Catalan) [skip ci]
Currently translated at 49.4% (433 of 876 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.5% (5 of 876 strings)

Co-authored-by: beefnoodle <acer.wang@protonmail.com>
Co-authored-by: dtalens <databio@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2022-07-26 09:22:12 +01:00
bakerboy448
f703db1e00 Fixed: Better Cleansing of Tracker Announce Keys
Fixed: Cleanse Notifiarr secret from URL in logs

Fixes: #4623
(cherry picked from commit e6210aede6f7ead197e82572976bc0267d910d46)

(cherry picked from commit ec866082d44d299096112a6c7c232384b1f74505)
2022-07-26 09:21:28 +01:00
bakerboy448
20f67c8035 Fixed: Importing Readarr Lists
Fixes #1747
2022-07-26 09:20:38 +01:00
Qstick
56ae497bfa Fixed: Don't call for server notifications on event driven check 2022-07-17 13:48:12 -05:00
Weblate
e760dc56c6 Translated using Weblate (French) [skip ci]
Currently translated at 75.1% (658 of 876 strings)

Co-authored-by: Sytha <tharaud.sylvain@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translation: Servarr/Readarr
2022-07-17 13:42:13 -05:00
Weblate
7ad26b386c Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (612 of 876 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 75.1% (658 of 876 strings)

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 10.9% (96 of 876 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.1% (1 of 876 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.3% (669 of 876 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 62.5% (548 of 876 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 89.8% (787 of 876 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 66.0% (579 of 876 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 65.6% (575 of 876 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 62.6% (549 of 876 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (612 of 876 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 63.8% (559 of 876 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 67.1% (588 of 876 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.9% (700 of 876 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 89.1% (781 of 876 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 62.4% (547 of 876 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 62.5% (548 of 876 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 67.0% (587 of 876 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 65.8% (577 of 876 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.9% (700 of 876 strings)

Added translation using Weblate (Lithuanian) [skip ci]

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Giorgio <sannagiorgio1997@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Maxent <rouaultmaxent@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: loksum213108 <lok3222003@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/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/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
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/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
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_TW/
Translation: Servarr/Readarr
2022-07-13 11:01:02 -05:00
Weblate
199085249f Translated using Weblate (Portuguese) [skip ci]
Currently translated at 75.9% (665 of 876 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 88.0% (771 of 876 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 64.2% (563 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 87.7% (769 of 876 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Marcin <ml.cichy@gmail.com>
Co-authored-by: Vitor Brito <main@vitorbrito.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: ηg <jonas.konrath@icloud.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
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/
Translation: Servarr/Readarr
2022-06-25 20:30:02 -05:00
ta264
b84041d95f Use DryIoc for Automoqer, drop Unity dependency
(cherry picked from commit e3468daba04b52fbf41ce3004934a26b0220ec4f)
(cherry picked from commit cf4103d73d74896daa12e7a0237e8923eaa6c095)
2022-06-22 10:13:37 +01:00
Robin Dadswell
615facb3c4 fixed test due to GoodReads BookId change 2022-06-15 23:28:43 +01:00
Weblate
b4fd1340c2 Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 68.1% (597 of 876 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 78.9% (692 of 876 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 64.2% (563 of 876 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 99.8% (875 of 876 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 97.6% (855 of 876 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 68.0% (596 of 876 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 68.0% (596 of 876 strings)

Translated using Weblate (Norwegian Bokmål) [skip ci]

Currently translated at 8.5% (75 of 876 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 67.9% (595 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (876 of 876 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (851 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (594 of 851 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 81.3% (692 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.8% (594 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.6% (593 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 69.6% (593 of 851 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Francesco <francy.ammirati@hotmail.com>
Co-authored-by: Giorgio <sannagiorgio1997@gmail.com>
Co-authored-by: Gjermund Wiggen <gjermundwiggen@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Matrixlee <matrix.alax@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Pan Jarek <jsawiuk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: carreyli <laddie1987@qq.com>
Co-authored-by: un110 <13735581@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2022-06-11 15:13:19 -05:00
bakerboy448
1da27b0978 Fixed: Cleanse GoodReads oauth from logs 2022-05-30 09:26:47 +01:00
ta264
b0a3ddef9c Fixed: Speed up book api 2022-05-30 09:05:25 +01:00
ta264
a59706ceb4 Fixed: Removed unnecessary author data from book endpoint 2022-05-30 09:05:25 +01:00
nitsua
ce58e6ecdb Fixed: Mark as Failed errors
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>

(cherry picked from commit a9792973eef960e8310a611cf1da2d2cada57532)
2022-05-25 17:02:44 +01:00
bakerboy448
d01ce8b908 Fixed: Added new and missing translations 2022-05-25 14:08:08 +01:00
ta264
575f995916 Reinstate tests 2022-05-24 21:16:00 +01:00
ta264
c17c8815ef New: Add linux-x86 builds
(cherry picked from commit 8907b06899483d7cb3ecb9546d82af00d129f8e1)
2022-05-25 13:06:39 +01:00
ta264
8445941510 New: .NET 6.0.5 2022-05-25 13:06:39 +01:00
ta264
32ae18ae6f Fix bad translation 2022-05-25 13:06:39 +01:00
PearsonFlyer
b91ec241a6 Fixed: Add translation for RestartRequiredHelpTextWarning 2022-05-15 20:18:29 +01:00
Qstick
17ce8187fd New: Instance name in System/Status API endpoint 2022-05-15 13:05:44 -05:00
Qstick
41615fe026 New: Instance name for Page Title 2022-05-15 13:05:44 -05:00
Robin Dadswell
9cff8f31e9 New: Instance Name used for Syslog 2022-05-15 13:05:44 -05:00
Robin Dadswell
21538b972d New: Set Instance Name 2022-05-15 13:05:44 -05:00
Robin Dadswell
5a7b4d41d8 New: Added UDP syslog support
(cherry picked from commit 8d856b2edb8bf46a2b516d5f7644ae3fa1151323)
2022-05-15 13:05:44 -05:00
Weblate
16f67265c9 Translated using Weblate (Finnish) [skip ci]
Currently translated at 79.7% (679 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (851 of 851 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 67.3% (573 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 66.3% (565 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 66.3% (565 of 851 strings)

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 11.1% (95 of 851 strings)

Translated using Weblate (Slovak) [skip ci]

Currently translated at 5.5% (47 of 851 strings)

Translated using Weblate (Norwegian Bokmål) [skip ci]

Currently translated at 7.1% (61 of 851 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 1.4% (12 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 69.8% (594 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 69.8% (594 of 851 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 78.0% (664 of 851 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 63.9% (544 of 851 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 91.5% (779 of 851 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 66.2% (564 of 851 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 64.0% (545 of 851 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 66.3% (565 of 851 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 68.6% (584 of 851 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 79.7% (679 of 851 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 90.2% (768 of 851 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 63.8% (543 of 851 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 63.9% (544 of 851 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 68.5% (583 of 851 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 67.3% (573 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 69.4% (591 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 69.4% (591 of 851 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: DarkFighterLuke <luca-consoli@live.it>
Co-authored-by: Giorgio <sannagiorgio1997@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ZakiZtraki <vovanjudo@gmail.com>
Co-authored-by: lhquark <lhquark@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/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/is/
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/nb_NO/
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/ro/
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/
Translation: Servarr/Readarr
2022-05-09 17:32:50 -05:00
Qstick
03d2a85821 Fixed: Correct User-Agent api logging
(cherry picked from commit 5824ba963b480c920cee9d8ae6594c820167cd07)
2022-05-06 20:15:01 -05:00
Qstick
fa68a9559f Deleted translation using Weblate (zh_HANS (generated) (zh_HANS)) [skip ci] 2022-05-01 22:35:19 -05:00
Qstick
c4a2a432d2 Deleted translation using Weblate (zh_HANS (generated) (zh_HANS)) [skip ci] 2022-05-01 22:34:16 -05:00
Weblate
0b3eda4de3 Added translation using Weblate (zh_HANS (generated) (zh_HANS)) [skip ci]
Deleted translation using Weblate (Chinese (Min Nan)) [skip ci]

Translated using Weblate (Chinese (Simplified)) [skip ci]

Currently translated at 22.6% (193 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 58.5% (498 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (851 of 851 strings)

Translated using Weblate (Chinese (Simplified)) [skip ci]

Currently translated at 3.2% (28 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 58.5% (498 of 851 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 56.9% (485 of 851 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 56.8% (484 of 851 strings)

Update translation files [skip ci]

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Chinese (Simplified)) [skip ci]

Currently translated at 3.1% (27 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (851 of 851 strings)

Translated using Weblate (Chinese (Simplified)) [skip ci]

Currently translated at 1.6% (14 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (851 of 851 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 58.7% (500 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 98.8% (841 of 851 strings)

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 6.8% (58 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 58.5% (498 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 58.5% (498 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 95.1% (810 of 851 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (851 of 851 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 70.7% (602 of 851 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 82.1% (699 of 851 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 59.3% (505 of 851 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 59.3% (505 of 851 strings)

Co-authored-by: Ana <phampyk@gmail.com>
Co-authored-by: AnlakHui <AnlakHui@gmail.com>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: M1C <webnar@gmail.com>
Co-authored-by: Mateo Periago Serrano <mateoperi@pm.me>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Qstick <qstick@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: andrey4korop <andrey999@i.ua>
Co-authored-by: jianjam <jianjam@qq.com>
Co-authored-by: killsover <w904202822@163.com>
Co-authored-by: lhquark <lhquark@gmail.com>
Co-authored-by: marcosteam <wdy71608161@gmail.com>
Co-authored-by: minermartijn <minermartijn@gmail.com>
Co-authored-by: neoestremi <remidu34070@hotmail.fr>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
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/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_Hans/
Translation: Servarr/Readarr
2022-05-01 22:34:16 -05:00
Mark McDowall
a1d4b3a0cf Fixed: UI hiding search results with duplicate GUIDs
(cherry picked from commit 458c5cd0b3c8efe6fc7cd852c357138468f8446e)
2022-04-24 13:54:39 -05:00
Douglas R Andreani
28f055e838 New: Add date picker for custom filter dates
(cherry picked from commit 5a08d5dc248bf1dbaa43264a2a470149cf716a3c)
2022-04-18 21:08:44 -05:00
Mark McDowall
8d91fb81bd Fixed: Interactive Search Filter not filtering multiple qualities in the same filter row
(cherry picked from commit c93f63cd204bf62dab3dffef6e29c8dd4c408cab)
2022-04-18 21:08:30 -05:00
bakerboy448
085913a852 Fixed: Clarify Qbit Content Path Error
(cherry picked from commit bba4a5636ed07277d82531c35cfc996bd17870eb)
2022-04-06 22:09:34 -05:00
ta264
8599fc4ee6 Fix .editorconfig to disallow this
(cherry picked from commit be29fc6adb2c8d8e52afea301118cf413bdc2d57)
2022-04-06 15:28:32 -05:00
Marcelo Castagna
68f017cd84 Fixed: Properly handle 119 error code from Synology Download Station
(cherry picked from commit 3be5d6c258bd947ae4c4d895b2f54faa5a7a222b)
2022-04-05 20:40:56 -05:00
bakerboy448
f34171444e Fixed: API error when sending payload without optional parameters
Co-authored-by: Qstick <qstick@gmail.com>

(cherry picked from commit 8d83b1d8d66ef24a25f312bdeac1e5625a630c00)
2022-04-05 20:40:36 -05:00
Qstick
10760471e0 Fixed: Cleanup Temp files after backup creation
(cherry picked from commit 7f0b708cb9f16a0116199625a2a5b4e67049be6a)
2022-04-05 20:40:13 -05:00
ta264
4ecabcd3d6 Fixed: Loading old commands from database
(cherry picked from commit 0f87cb72e559a19bddc6c9d4387ec7d9428291f8)
2022-04-02 20:05:00 +01:00
Weblate
1af2e14474 Translated using Weblate (Chinese (Simplified)) [skip ci]
Currently translated at 0.7% (6 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 58.6% (499 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 94.7% (806 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 58.6% (499 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 57.5% (490 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 94.4% (804 of 851 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: killsover <w904202822@163.com>
Co-authored-by: libsu <libsu@qq.com>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_Hans/
Translation: Servarr/Readarr
2022-04-01 07:32:31 -05:00
Ajax
fe836c56ee New: Update Cert Validation Help Text
(cherry picked from commit 66be23a7c4ae717cf060f5e89b4df7a08bdff5ac)

Closes #1541
Closes #1546
2022-03-31 20:46:08 -05:00
bakerboy448
ae611cca74 Fix Release Profile Typos 2022-03-31 20:46:08 -05:00
bakerboy448
c446f9835e Fixed: Clarify Indexer Priority Helptext
(cherrypicked from f4dbda1318564463b23056b866cb87a7c0e50393)

Closes #1481
2022-03-31 20:46:08 -05:00
bakerboy448
2ff8bf204f Fixed: Improve help text for download client Category
(cherry picked from commit d18751eff2c684a72e4c698a1e0f6c282c8da0da)

Closes #1535
2022-03-31 20:46:08 -05:00
Ajax
5aa17e7a67 Fixed: IPv4 instead of IP4
(cherry picked from commit c7427f8df828733a81d83ea28393a18d10875769)

Closes #1532
2022-03-31 20:46:08 -05:00
bakerboy448
312cf2d364 remove unnecessary usings 2022-03-31 20:44:49 -05:00
Taloth Saldono
a9852ded2f Add response size to http responses
(cherry picked from commit d899225509f04a9b6c72da19c7d63ff53498de22)
2022-03-23 19:58:08 -05:00
Weblate
4286f546e9 Translated using Weblate (Spanish) [skip ci]
Currently translated at 59.6% (508 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 57.5% (490 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 57.3% (488 of 851 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 58.5% (498 of 851 strings)

Co-authored-by: EthanChoy <ethanchoy@163.com>
Co-authored-by: Kakise <sam.taa@icloud.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: westay1984 <westjay@qq.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/zh_CN/
Translation: Servarr/Readarr
2022-03-23 19:57:48 -05:00
Qstick
482fe04161 New: Add Validations for Recycle Bin Folder 2022-03-23 19:26:11 -05:00
Qstick
cbdc2c51c4 Improve path validation for Custom Script notifications 2022-03-23 19:21:12 -05:00
Weblate
f55fda9bac Translated using Weblate (Spanish) [skip ci]
Currently translated at 57.2% (487 of 851 strings)

Translated using Weblate (Chinese (Simplified)) [skip ci]

Currently translated at 0.5% (5 of 851 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 57.5% (490 of 851 strings)

Co-authored-by: Ayi <4ayixd@gmail.com>
Co-authored-by: Mateo Periago Serrano <mateoperi@pm.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: liuliping <306166677@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_Hans/
Translation: Servarr/Readarr
2022-03-18 14:41:35 -05:00
Weblate
3841f7708e Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (851 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 91.8% (782 of 851 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 68.7% (585 of 851 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: José Eduardo Veiga <vitruxpt@vitruxbot.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2022-03-11 01:44:35 -06:00
ta264
f2bfed0252 New: .NET 6.0.3 2022-03-09 21:47:57 +00:00
PearsonFlyer
bc7e4ea622 Fixed: Healthcheck warning message used incorrect variable 2022-03-09 14:14:54 -06:00
Marcelo Castagna
38f16e6b85 Update Synology error codes
(cherry picked from commit a8cb7784f2a6f833bda9e24ca2146701df8da75c)
2022-03-08 16:10:53 -06:00
Weblate
af57024c63 Translated using Weblate (French) [skip ci]
Currently translated at 57.5% (490 of 851 strings)

Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]

Currently translated at 0.1% (1 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 90.7% (772 of 851 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 90.7% (772 of 851 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 66.8% (569 of 851 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 56.0% (477 of 851 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (851 of 851 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 71.3% (607 of 851 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 82.4% (702 of 851 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 57.3% (488 of 851 strings)

Update translation files [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Update translation files [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: KevoM <lilmarsu@gmail.com>
Co-authored-by: Nuno Filipe de Vilhena Santos <nunovilhenasantos@msn.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Renan da Mota Ciciliato <renanciciliato@gmail.com>
Co-authored-by: Rico_Walker <ricardo.walker1203@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zalhera <tobias.bechen@gmail.com>
Co-authored-by: nopetw <lubduphaur@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
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/hu/
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/zh_TW/
Translation: Servarr/Readarr
2022-03-07 13:14:37 +00:00
bakerboy448
54faa58a4d Fixed: Assume SABnzbd develop version is 3.0.0 if not specified
(cherry picked from commit 77412f23766e8bb553e83d87f1e599e795a67f8b)
2022-03-06 21:27:33 -06:00
servarr[bot]
8ca35a709c Fixed: Updater version number logging
(cherry picked from commit b34f4fde1bbe611bb5c886890e3be7cec2b96e6b)
2022-03-06 21:26:52 -06:00
bakerboy448
72a0e38b36 Fixed: Update from version in logs
Closes #1574

(cherry picked from commit bc3e3714b97dec19091e45324d9f2b550f1dbbd5)
2022-03-04 20:46:06 -06:00
bakerboy448
b25f8449a2 New: Add more information about Windows service to installer
Closes #520
2022-02-22 20:54:53 +00:00
Mark McDowall
cf0a15a308 Fixed: Recycle bin log message
(cherry picked from commit 9e1b799fb74c341510d6da98f9f658d642b25c02)
2022-02-22 12:46:43 -06:00
ta264
a677098f0f Fixed: Make authentication cookie name unique to Readarr 2022-02-20 21:32:38 +00:00
ta264
a40cce9c71 Bump to 0.1.1 2022-02-20 21:31:57 +00:00
ta264
6d94136273 Report runtime identifier to sentry
[common]
2022-02-20 13:32:21 +00:00
ta264
f6d620f969 Fixed: Crash on startup on arm32 due to ImageSharp
See https://github.com/SixLabors/ImageSharp/issues/2001
2022-02-20 12:06:06 +00:00
ta264
8d9302da77 Fixed: No longer require first run as admin on windows 2022-02-16 21:07:58 +00:00
ta264
f5cbab6ac9 Fixed: Exception tagging audio files on import
Fixes #1523
2022-02-15 21:51:23 +00:00
ta264
cb0ae36033 Fixed: Enable response compression over https
[common]
2022-02-15 21:11:39 +00:00
ta264
93a42e2fe9 Fix duplicates in author search 2022-02-11 21:32:09 +00:00
ta264
0f9ab46dd6 New: Upgrade to .NET 6.0.2 2022-02-09 20:59:34 +00:00
ta264
3a360d7a1e Fixed: Contain memory usage during library import 2022-02-09 20:59:34 +00:00
ta264
8e37aa2e78 Fixed: Take publisher and format into account when choosing best edition 2022-02-09 20:59:34 +00:00
ta264
000022a927 Fixed: Return sensible value from Levenshtein when distance too large 2022-02-09 20:59:34 +00:00
ta264
2bfc50b59b Fixed: Don't forget about existing match when trying remote candidates 2022-02-09 20:59:34 +00:00
ta264
4f46ab9a9a Speed up FuzzyContains a bit more 2022-02-04 19:57:24 +00:00
ta264
e7410959fe Fixed: Correct and speed up FuzzyContains
Fixes READARR-C1
2022-02-04 17:28:33 +00:00
PearsonFlyer
ee1112026a Fixed: Updated ruTorrent stopped state helptext 2022-01-26 10:52:43 -06:00
ta264
65681cad10 Fixed: Catch any exceptions thrown identifying books 2022-01-25 21:02:33 +00:00
ta264
9653f9bbca Fixed: Error reading PDF file
Fixes READARR-6P
2022-01-24 21:16:55 +00:00
ta264
6f97ca9a55 Fixed: Handle missing category when getting Qbittorrent download path
[common]

Fixes READARR-2VR
Fixes READARR-1GP
2022-01-24 21:13:20 +00:00
ta264
c0e193dd1f Fixed: Improve fuzzy matching algorithm to match around word boundaries
Fixes READARR-C1
2022-01-24 20:58:00 +00:00
ta264
ecf1e1a130 Fixed: Error splitting subtitle from book title
Fixes #1473
Fixes READARR-39N
Fixes READARR-3ES
Fixes READARR-6CM
2022-01-24 20:58:00 +00:00
PearsonFlyer
967dae5132 Fixed: Email attachments of ebooks 2022-01-24 18:04:36 -06:00
PearsonFlyer
f4b96dc45e Fixed: Avoid download path check false positives for Flood
(cherry picked from commit d4d4bf8784fedbf76f938685229a700a2e552e79)
2022-01-23 16:22:26 -06:00
bakerboy448
8181749629 Fixed: Improved Indexer test failure message when no results are returned
(cherry picked from commit 05b1581b7dfa5bc8a2b38a60179e3472c8134f74)
2022-01-23 16:19:18 -06:00
PearsonFlyer
9bca258541 Fixed: Changed movie references to book 2022-01-22 15:04:08 -06:00
bakerboy448
77d2db3c47 Fixed: More Translations 2022-01-22 19:23:24 +00:00
ta264
1dd61e826b Fixed: SourceTitle if Book Title null in Discord ImportFailed event
Fixes READARR-FG
2022-01-20 22:08:00 +00:00
ta264
857a5f5005 Fix remote path mapping health check interface 2022-01-20 22:21:41 +00:00
bakerboy448
a321288a71 Fixed: Various Translations
Fixes #1209
2022-01-21 13:45:54 +00:00
ta264
62a3355546 Remove unused code 2022-01-21 13:28:04 +00:00
ta264
6db135877a Build installer from build.sh
[common]
2022-01-21 13:28:04 +00:00
ta264
fa1985509d Centralise image choice, update to latest images
[common]
2022-01-21 13:28:04 +00:00
ta264
01a5c456c4 Fixed: Set part numbers using alphabetical file ordering if not set in tags 2022-01-19 21:29:50 +00:00
ta264
3ec451e85e Fixed: Correct the 'Ignored' history filter
Closes #1463
2022-01-19 21:29:50 +00:00
Weblate
bf7fc6fa19 Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 96.4% (763 of 791 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (791 of 791 strings)

Update translation files [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Update translation files [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (787 of 787 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2022-01-20 11:17:28 +00:00
Mark McDowall
cbf2f0f4fb Fixed: Jump bar on series page not showing when window is made wider
(cherry picked from commit 0cb8d93069d6310abd39ee2fe73219e17aa83fe6)
2022-01-20 11:16:37 +00:00
ta264
565fb77713 Fixed: Attempt to fix uncaught exceptions fetching book data
Closes #1465
Closes #1464
2022-01-20 10:50:51 +00:00
Robin Dadswell
3ea0c8e5fa New: End Jackett 'all' endpoint support
(cherry picked from commit 54c914d48fefa730728518d50fc9e49032d0947b)
2022-01-20 10:50:51 +00:00
Qstick
61454955be New: Support server notifications
(cherry picked from commit f5f0dd6fae5bc9f308506d56be42ac9a4be908e7)
2022-01-20 10:50:51 +00:00
ta264
34506025ac Fixed: Better searching for local candidate editions 2022-01-20 10:50:51 +00:00
ta264
ef35efb127 Fixed: Make sure exactly one edition is monitored when using remote candidates 2022-01-20 10:50:51 +00:00
ta264
2a4f681b17 Fixed: Speed up RSS sync 2022-01-20 10:50:51 +00:00
bakerboy448
24e2ff56dd New: Various Healthcheck enhancements
Various pulls from Radarr;  Completed at the file - not commit - level
2022-01-19 18:47:38 +00:00
Qstick
c5787b0c87 New: Add AppName to system status response 2022-01-18 23:26:09 -06:00
Qstick
d6ba6bbfd1 Fixed: Parse endpoint response when title failed to parse
Fixes #1175

Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
2022-01-18 18:35:10 -06:00
ta264
de72cfcaaa Update DSN 2022-01-16 22:37:06 +00:00
ta264
90d671d68d Fixed: Error fetching metadata when redirected to work endpoint 2022-01-16 22:09:21 +00:00
ta264
b4b14a5359 Fixed: Error parsing books with no contributors
Fixes #1454
2022-01-16 22:09:21 +00:00
ta264
759c132797 Fixed: Allow scrolling in Select Edition modal
Fixes #1442
2022-01-16 22:09:21 +00:00
ta264
dc1fbb3a7e Fixed: Support nested file naming formats 2022-01-16 22:09:21 +00:00
ta264
eb431f09fd Fixed: Close all database connections on shutdown to remove shm/wal files
[common]
2022-01-16 22:00:47 +00:00
ta264
0031214fb3 Fixed: Check language of book file matches edition language if possible 2022-01-16 21:57:09 +00:00
bakerboy448
94adb4d582 Fixed: Added Various Missing Translations
cleanup en.json of unused translates

Fixes #1397

Closes Sonarr PR # 4791 'Fixed: Better wording of MissingFromDisk'
2022-01-16 21:53:43 +00:00
ta264
bc3764dcda Fixed: Error getting search results when there is only one result 2022-01-16 21:47:52 +00:00
bakerboy448
64fafc599c fix calibre validation typo 2022-01-17 09:17:01 +00:00
ta264
53f66606bc New: Upgrade to .NET 6.0.1 2022-01-17 09:14:28 +00:00
Qstick
706f8310bb Fixed: Use general settings cert validation for email 2022-01-17 09:14:28 +00:00
ta264
ae4994571a Fixed: Use our own HttpClient for Aria2 requests 2022-01-17 09:14:28 +00:00
ta264
258847a83b Fixed: Use our own HttpClient for rTorrent RPC requests 2022-01-17 09:14:28 +00:00
Qstick
43f1d77b9f Fixed: Twitter connect not sending messages after http rework 2022-01-17 09:14:28 +00:00
ta264
f451796cf5 Fixed: Better check for write access in Calibre 2022-01-16 13:33:16 +00:00
Weblate
620fb57b30 Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 99.7% (785 of 787 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (787 of 787 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (787 of 787 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 71.2% (561 of 787 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 63.0% (496 of 787 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 71.1% (560 of 787 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 70.3% (554 of 787 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.7% (785 of 787 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 63.0% (496 of 787 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.8% (769 of 770 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 64.2% (495 of 770 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 71.9% (554 of 770 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 64.2% (495 of 770 strings)

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Gian Klug <gian.klug@ict-scouts.ch>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: diemade <spamkill@posteo.ch>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translation: Servarr/Readarr
2022-01-15 13:25:44 +00:00
PearsonFlyer
bca70e931f Fixed: Translation warning for search all 2022-01-15 13:12:24 +00:00
ta264
c7f0a8578a Fixed: Calibre auth check when library missing 2022-01-15 06:04:15 +00:00
ta264
c92563ea10 New: Validate remote calibre servers have authentication enabled 2022-01-14 19:54:18 +00:00
ta264
66e7ce6f27 Fixed: Queue items hanging in 'Downloaded - Importing' state 2022-01-14 19:54:18 +00:00
ta264
b1da9a1934 Fixed: Null reference setting download history for unmapped downloads 2022-01-14 19:54:17 +00:00
BookInfo
1491788081 Move all data fetching to BookInfo v2 2022-01-14 19:54:06 +00:00
ta264
33e1c4a537 Fixed: Books must match the edition title, not the work title 2022-01-03 21:23:58 +00:00
ta264
8c3dd3730a Fixed: Determine book author as primary author of most popular edition 2022-01-03 21:17:52 +00:00
bakerboy448
02927dc37d Fixed: Various Wiki Links 2021-12-30 16:39:18 -06:00
Robin Dadswell
be46d5ae8f New: On delete notifications 2021-12-30 16:38:49 -06:00
bakerboy448
e4fdf71eee New: Display Unknown Items in Activity Queue by Default 2021-12-27 17:46:48 -06:00
ta264
9a2260a00f Revert "Move all data fetching to BookInfo"
This reverts commit f6ff53ca31.
2021-12-26 19:01:35 +00:00
Qstick
399a370e7a Fix Index creation on migration 20 2021-12-25 15:20:58 -06:00
Weblate
ff27ad2096 Translated using Weblate (Slovak) [skip ci]
Currently translated at 6.2% (48 of 770 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 63.8% (492 of 770 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 64.2% (495 of 770 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 65.9% (508 of 770 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 65.7% (506 of 770 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 63.8% (492 of 770 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 64.6% (498 of 770 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 63.8% (492 of 770 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 63.8% (492 of 770 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 63.8% (492 of 770 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 63.8% (492 of 770 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 64.2% (495 of 770 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 62.9% (485 of 770 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 66.1% (509 of 770 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 66.2% (510 of 770 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 71.6% (552 of 770 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 64.0% (493 of 770 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 64.2% (495 of 770 strings)

Translated using Weblate (Norwegian Bokmål) [skip ci]

Currently translated at 8.1% (63 of 770 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.7% (591 of 770 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 64.0% (493 of 770 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 96.4% (743 of 770 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 72.7% (560 of 770 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 63.7% (491 of 770 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 63.8% (492 of 770 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 63.8% (492 of 770 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 63.8% (492 of 770 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 72.7% (560 of 770 strings)

Added translation using Weblate (Ukrainian) [skip ci]

Added translation using Weblate (Persian) [skip ci]

Added translation using Weblate (Bengali) [skip ci]

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 62.0% (478 of 770 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 74.8% (575 of 768 strings)

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Nuno Filipe de Vilhena Santos <nunovilhenasantos@msn.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
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/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/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
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/nb_NO/
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/ro/
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/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2021-12-24 10:12:41 -06:00
ta264
76bcf94f1d Fixed: Added missing sort token details for poster views
Fixes #1424
2021-12-23 21:41:43 +00:00
ta264
472da10149 Fixed: Fix book index footer stats
Fixes #1405
Fixes #1407
2021-12-23 21:41:43 +00:00
ta264
8011112919 Fixed: Error opening manual import for unmatched items
Fixes #1425
2021-12-23 21:41:43 +00:00
ta264
6d8adec7dc Fixed: Frontend error sorting book index by status
Fixes #1426
2021-12-23 21:41:43 +00:00
ta264
29c7404185 Fixed: Send primary series to Calibre
Fixes #1419
2021-12-23 21:41:43 +00:00
BookInfo
f6ff53ca31 Move all data fetching to BookInfo 2021-12-24 15:41:29 +00:00
Davide Z
2dff18490e remove movies reference from readme.MD (#1423)[skip ci] 2021-12-21 09:45:32 -06:00
Qstick
6d7657e8b3 Bump RestSharp to 106.15.0
Fixes #1400
2021-12-18 10:30:48 -06:00
ta264
0db6cf272f New: Add release date sort option to book index view
Fixes #1403
2021-12-16 21:22:54 +00:00
ta264
5827644300 Fixed: Error getting seed config for rtorrent 2021-12-16 21:50:07 +00:00
ta264
d1f1052d7d Fixed: Missing / Wanted filter on book index view
Fixes #1366
2021-12-16 20:12:04 +00:00
ta264
341d64ebf7 Fixed: Don't try to read audio tags from a book file 2021-12-15 11:55:55 +00:00
ta264
c817d6c0d8 Fixed: Error 'Book with Id not found' in manual import 2021-12-15 11:55:55 +00:00
ta264
735fceb074 Fixed: Prevent duplicate searches on list add 2021-12-15 11:55:55 +00:00
ta264
0e43f67a9f Fixed: Track when calibre changes files to prevent unnecessary re-scans 2021-12-15 11:55:55 +00:00
ta264
eb95fe265a New: Send genres to calibre as tags 2021-12-15 11:55:55 +00:00
Taloth Saldono
3e32f7d49f Clarified Aria2 RPC Path field.
closes #4738
2021-12-15 11:55:55 +00:00
Mark McDowall
4b8c739b5c Aria2 fixes
Fixed: Removing completed downloads from Aria2
Fixed: Return correct path for Aria2 downloads in a job folder
Fixed: Seeding torrents in Aria2 are treated as finished downloading
2021-12-15 11:55:55 +00:00
Qstick
eeb172d3bf Aria2 formatting cleanup 2021-12-15 11:55:55 +00:00
Mark McDowall
e2745c5956 Fixed: Blocklisting pending releases 2021-12-15 11:55:55 +00:00
LLeny
ede981f737 New: Aria2 Torrent Client 2021-12-15 11:55:55 +00:00
Jesse Chan
e2c1d76516 Flood: explicitly cast DateFinished long? to long 2021-12-15 11:55:55 +00:00
Jesse Chan
05a8a3d764 New: Removing Flood downloads when seeding criteria have been met 2021-12-15 11:55:55 +00:00
leaty
da262f3d95 New: Removing rtorrent downloads when seeding criteria have been met
(cherry picked from commit 411be4d0116f0739bb9c71235312d0c5a26dd3a2)
2021-12-15 11:55:55 +00:00
Taloth Saldono
a5b1711827 Fixed: Refreshing Plex Server series in high volume systems
(cherry picked from commit 3fb55e9defdb90ab807fcacff249193a4d6114d5)
2021-12-15 11:55:55 +00:00
Mark McDowall
7b0802cfd6 Fixed: Errors loading queue after episodes in series are removed
Closes #3565
2021-12-15 11:55:55 +00:00
Mark McDowall
16fcba02ba Fixed: Update path before importing to ensure it hasn't changed
Closes #684
2021-12-15 11:55:55 +00:00
Mark McDowall
b3c217d713 Some cleanup of things marked for removal in v3 2021-12-15 11:55:55 +00:00
Mark McDowall
975ff0baf3 Mark completed imports based on history as imported to remove from queue 2021-12-15 11:55:55 +00:00
Mark McDowall
0bb8672119 Fixed: Mark "BAD" Nzbget Downloads as Failed 2021-12-15 11:55:55 +00:00
Mark McDowall
9ebeee8b4f Fixed: Exception thrown when marking download as complete
Closes #326

(cherry picked from commit 27a9edf33d6ddb5f028f19b308245a067f1d8f2a)
2021-12-15 11:55:55 +00:00
Mark McDowall
894adbe91e Fixed: Error when processing release with files Sonarr is unable to parse 2021-12-15 11:55:55 +00:00
Mark McDowall
e7628476f7 Additional logging when trying to complete tracked downloads
(cherry picked from commit 2df6b5370067635fdc9196451e09e4c2e6427eac)
2021-12-15 11:55:55 +00:00
Mark McDowall
3e5045e496 Fixed: Not removing seeded download if it was manual imported in some cases 2021-12-15 11:55:55 +00:00
Mark McDowall
95d93dfa09 Fixed: Manual import for unknown series items will properly mark as imported
Fixes: #277
(cherry picked from commit 3ffcf114682f9b1730f54706ecbf1bf237206bb1)
2021-12-15 11:55:55 +00:00
Mark McDowall
842f80d567 Fixed: Imports triggered through API not being marked as imported/removed from client
Fixes #258

(cherry picked from commit 382bcbcbeddcf04d5bc361744b6b57ca3c2c54e6)
2021-12-15 11:55:55 +00:00
Mark McDowall
fc8b6014ec Fixed: Imported downloads not being removed when seeding goals are met
Fixes #3693
2021-12-15 11:55:55 +00:00
Mark McDowall
6fb24128f8 Fixed: Episodes removed from queue re-appearing on refresh
Fixes #249

(cherry picked from commit 1a921c09e5180e283f52411dd2d0344a96015c0c)
2021-12-15 11:55:55 +00:00
Mark McDowall
8dbd006978 Fixed: Remove seeded downloads if they've finished seeding after import 2021-12-15 11:55:55 +00:00
Mark McDowall
fed67044f0 Fixed: Don't process downloads removed from the client 2021-12-15 11:55:55 +00:00
Mark McDowall
f7c05d98a9 Don't re-trigger completed event 2021-12-15 11:55:55 +00:00
Mark McDowall
25b37ace34 Track fully imported downloads in separate history table
New: Improved detection of already imported downloads
Closes #232
2021-12-15 11:55:55 +00:00
Servarr
e1eb9a0ba7 Translations from Weblate 2021-12-12 11:05:34 -06:00
ta264
b6055c3339 Fixed: Correctly set MonitorNewItems for new list authors 2021-12-08 21:30:24 +00:00
bakerboy448
0e78d8d535 Fixed: Support older glibc in libMonoPosixHelper
(based on Prowlarr 9e7af8369e165c19a2f181071e63bef6961cd64e)
2021-12-08 16:11:06 -06:00
ta264
0cf7953cab Fixed: Correctly force add duplicate books 2021-12-06 21:09:05 +00:00
Weblate
0d3cd24fb2 Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 98.4% (756 of 768 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (768 of 768 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 64.7% (495 of 764 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 70.5% (539 of 764 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: hidaba <nag@hidaba.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2021-12-05 10:53:12 -06:00
bakerboy448
41d7a1f355 update contributing [skip ci] 2021-12-03 11:11:54 +00:00
bakerboy448
608c0f268f fixup! readme updates [skip ci] 2021-12-03 11:11:54 +00:00
bakerboy448
8c38e4ede8 readme updates [skip ci] 2021-12-03 11:11:54 +00:00
bakerboy448
4f791f965a Update Github Templates [skip ci]
(cherry picked from commit beb22844c960db8e0f42a32809ad74cffce06e09)
2021-12-03 11:11:38 +00:00
PearsonFlyer
0b0bebf3aa Fixed: Improved root folder setup page
(cherry picked from commit eb4f55de85)
2021-12-02 21:59:59 +00:00
ta264
05a0508d78 Fixed: Add author/book fails unless you change Monitor New Items value 2021-12-02 21:15:49 +00:00
ta264
6cecf23b88 Fixed: Removing from Goodreads shelf when different edition imported 2021-12-02 21:40:28 +00:00
Weblate
fb0128e3be Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 100.0% (757 of 757 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 64.4% (488 of 757 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.7% (755 of 757 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 98.1% (743 of 757 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 68.6% (520 of 757 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 68.6% (520 of 757 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (757 of 757 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 67.6% (512 of 757 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.7% (743 of 745 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (745 of 745 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 66.8% (498 of 745 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2021-12-02 20:06:24 +00:00
ta264
6947e6e34c Fixed: Speed up initial author load 2021-12-01 21:56:48 +00:00
ta264
c51ae664aa New: Option to control which new author books get monitored 2021-12-01 21:23:23 +00:00
ta264
1d694af98e Workaround .net error serializing new object()
See https://github.com/dotnet/runtime/issues/61995
2021-12-01 21:23:22 +00:00
ta264
bc678e1976 Fixed: Books added via import list default to 'Automatically Switch Edition'
Fixes #565
2021-12-01 21:13:59 +00:00
Taloth Saldono
c0db20a860 Fixed: OSX version detection
ref sonarr/4113 (not a fix, just partially)

(cherry picked from commit 026af222297d1d90d824e077e989ffd146bb5016)
2021-12-01 22:04:33 +00:00
Mark McDowall
621bb20756 Improve default path for Synology Download Station
Fixes: Missing default path for Download Station
Fixes: Error when getting destination path for Synology Download Station in health check

(cherry picked from commit 4bf3ab1511b4ea25642476bf9df13f91b6f73d76)
2021-12-01 21:18:46 +00:00
ta264
326d395da4 Fixed: Don't allow interactive import while book match is calculating
Fixes #1238
2021-11-29 21:49:12 +00:00
ta264
7be2effa05 Fixed: Sort by path in book index view
Fixes #1233
2021-11-28 19:24:39 +00:00
ta264
8f965a7cd5 New: Add book custom filters for author and automatic release switching
Fixes #1355
2021-11-28 19:19:39 +00:00
ta264
15dee29057 Fixed: Missing Book column in cutoff unmet 2021-11-28 19:11:18 +00:00
ta264
61617ff5bc Bump System.Data.Sqlite to fix osx-arm64 builds 2021-11-28 18:18:03 +00:00
ta264
27e3b5e630 Fixed: Windows installer and adding/removing services
Fixes #1361
2021-11-28 18:18:03 +00:00
ta264
1b6694739e Fixed: Sorting by Series, Rating and Status on author details page
Fixes #1315
2021-11-23 21:55:33 +00:00
ta264
a7c0eabb56 New: Series file naming tokens
Closes #25
Closes #26
2021-11-23 21:05:40 +00:00
Qstick
7c5188638f Fixed: Combine content headers with response headers
(cherry picked from commit c4cf38255e422b5c0d4e5c3909810cc596b0510b)
2021-11-24 01:59:19 +00:00
ta264
4ce405728a New: Add osx-arm64 and linux-musl-arm builds 2021-11-22 19:06:02 +00:00
ta264
615acdaebe New: Combine mass editor and author editor, enable book editor 2021-11-22 19:05:39 +00:00
Weblate
d460cbf319 merge 2021-11-20 16:51:49 +00:00
ta264
4887ed0d2f New: Book editor on author details page 2021-11-19 20:03:32 +00:00
ta264
bf852cadbe Fixed: Prevent frontend errors when many books added to Readarr 2021-11-19 20:03:32 +00:00
ta264
05d24821f7 Fixed: Restarting windows service from UI
(cherry picked from commit 3ae1ccc5e25eb16420c1f4ab627f42e3f478b22e)
2021-11-19 20:03:32 +00:00
ta264
04e575903f Fixed: Tray app restart
(cherry picked from commit 5fce3bbedb48edc547d1e6a1db3af06b5542886d)
2021-11-19 20:03:32 +00:00
ta264
dfb9558868 Fixed: Broken SSL certificate validation option
(cherry picked from commit a6a761cb32a268c4447071dc692c428789063fce)
2021-11-19 20:03:32 +00:00
Weblate
808fb5f2aa Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 65.7% (488 of 742 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 65.0% (483 of 742 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 66.3% (492 of 742 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (742 of 742 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 67.2% (499 of 742 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 73.1% (543 of 742 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 73.1% (543 of 742 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 65.2% (484 of 742 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 65.4% (486 of 742 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 65.7% (488 of 742 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.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/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/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
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/zh_CN/
Translation: Servarr/Readarr
2021-11-18 02:00:01 -06:00
Weblate
9f62ee34f1 Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (743 of 743 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 65.7% (488 of 742 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 65.0% (483 of 742 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 66.3% (492 of 742 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (742 of 742 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 67.2% (499 of 742 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 73.1% (543 of 742 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 73.1% (543 of 742 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 65.3% (485 of 742 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 65.2% (484 of 742 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 65.4% (486 of 742 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 65.7% (488 of 742 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mm519897405 <baiya@vip.qq.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/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/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
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/zh_CN/
Translation: Servarr/Readarr
2021-11-17 14:30:07 +00:00
ta264
847a9eae93 New: Improve match by adding small penalty for missing isbn/asin 2021-11-17 14:30:01 +00:00
ta264
a9e181425b Fixed: Only send supported image data to calibre 2021-11-17 14:30:01 +00:00
ta264
357245a9eb Fixed: None metadata profile no longer grabs future books
Fixes #1234
2021-11-17 14:30:01 +00:00
ta264
8280908c39 Fixed: Import list exclusion display overflow
Fixes #1140
2021-11-17 14:30:01 +00:00
ta264
b05bd685bc New: Make monitoring existing books on an import list optional 2021-11-17 14:30:01 +00:00
ta264
33e5351add New: Add option to only monitor selected book when adding single book 2021-11-17 14:30:01 +00:00
ta264
a1c2986af8 Fixed: UI updates when new author book monitor state set
Fixes #1298
2021-11-17 14:30:01 +00:00
bakerboy448
2983b60026 Fixed: Cleanse APIKey from Signalr logging
[common]
2021-11-15 07:38:57 -06:00
bakerboy448
0d9e98cf51 various fixups of confusing UI translates 2021-11-15 07:38:32 -06:00
ta264
5d63ef01b6 Fixed: Calibre authentication with Basic auth 2021-11-14 21:06:02 +00:00
Qstick
0bb391235f Translation sync 2021-11-14 11:00:55 -06:00
ta264
c4d02c1666 Fixed: Gracefully fall back to ipv4 if ipv6 is broken 2021-11-14 15:38:11 +00:00
ta264
7c6adb0da7 New: Use native .NET socks proxy
(cherry picked from commit d93110336fea31565129b356c90043761f8c2c5b)
(cherry picked from commit 6a0bd0e8207d7e21240a91d219f279a9a7a1bf30)
2021-11-13 08:45:25 +00:00
ta264
8f2ae2beff New: Upgrade to .NET 6 2021-11-13 08:45:25 +00:00
ta264
f53a1d7942 Package updates 2021-11-13 08:45:25 +00:00
ta264
cef15887a4 Use modern HttpClient
(cherry picked from commit 402f8b296f17bf161824ec5ff40d67d036d00d94)
2021-11-13 08:45:25 +00:00
Qstick
5f946c0aa3 New: HealthCheck for valid Branch value
(cherry picked from commit ef2f954b814b10094de25d691812e699a587e0f4)
2021-11-11 18:04:26 +00:00
bakerboy448
7528ed4084 Fixed: Default Branch to Develop 2021-11-11 13:24:51 +00:00
Robin Dadswell
e8ddaf6ccf Fixed: Audiobooks will not be attached to email notifications 2021-11-11 13:23:42 +00:00
ta264
c5281d04f1 Bump macos build agent 2021-11-11 11:08:43 +00:00
ta264
ec0e930a54 New: Goodreads listopia import list 2021-11-11 11:08:43 +00:00
ta264
fb7ec5c61e New: Goodreads series import list 2021-11-11 11:08:43 +00:00
ta264
cb2bd0273f Fixed: Null reference error importing some books 2021-11-11 11:08:43 +00:00
ta264
d6e55e2913 Fixed: Deleted books removed from UI 2021-11-11 11:08:43 +00:00
ta264
a3a914adfe Fixed: Forms login persists across restarts in docker
Closes #1307

(cherry picked from commit a219b4a1b869863b2ef47d4bdf33d308cb261ba3)
2021-11-11 11:08:43 +00:00
ta264
1835f15460 Improve ErrorHandler and quieten logging
Closes #1301

(cherry picked from commit 5fb6b449507af0b238afe6b699d8f55cc5deb790)
2021-11-11 11:08:43 +00:00
ta264
d1caed5c7d Fiddle release date 2021-11-11 11:08:43 +00:00
BookInfo
6bdfe01fbc Alternative metadata source 2021-11-11 11:08:43 +00:00
Robin Dadswell
225f0b310a Fixed: Time column is first column on events page
(cherry picked from commit c14ef7bee7477ad5d29498f1cba94267eb11daf0)
2021-11-09 18:54:33 -06:00
Weblate
6a0c99d32b Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 66.1% (488 of 738 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 66.1% (488 of 738 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (738 of 738 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 66.1% (488 of 738 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.8% (737 of 738 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Nackophilz <clement.wigy@gmail.com>
Co-authored-by: Stéphane Dupont <aleistor@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: zpengcom <z.peng.com@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2021-11-06 18:08:28 -05:00
Robin Dadswell
04cf0594f6 Fixed: Prowl notification priority 2021-11-06 18:08:05 -05:00
ta264
2001035257 Fixed: Correctly detect mounts in FreeBSD jails
(cherry picked from commit 67b1fd9bc5c3808ef7227a40ceb5304eefd50fa7)
2021-10-27 17:22:28 +01:00
ta264
355dcaed6c Fixed: Calender .ics feed
(cherry picked from commit 147cfe538a3ac1a54ae9e84460e37f744e69668f)
2021-10-26 19:14:26 +01:00
ta264
3d3292fd3d Fixed: Add missing index to improve book import performance 2021-10-23 14:10:00 +01:00
ta264
78d073a0a8 Fixed: Filtering editions by language 2021-10-23 14:09:54 +01:00
ta264
ca8f768f84 Fixed: Error adding book to existing author in incognito session 2021-10-23 13:59:48 +01:00
Mark McDowall
b57ba3d6e2 New: Add logging is release is rejected because no download URL is available
(cherry picked from commit a83ed3bcce4b0a5ed5279029eed1b2ccfb4e8b85)
2021-10-18 06:20:49 -05:00
Mark McDowall
d48f2a1cb6 Fixed: Qbit torrents treated as failed after error
(cherry picked from commit 7e175bf95ed0a7b624b04a397b953f27043211ae)
2021-10-17 14:23:26 -05:00
Weblate
d8e4d388dc Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (738 of 738 strings)

Update translation files [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Update translation files [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (737 of 737 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (737 of 737 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (737 of 737 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (737 of 737 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (737 of 737 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (737 of 737 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: zsigus <zsigus.attila@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2021-10-17 14:22:54 -05:00
bakerboy448
47ae2032e5 update contributing [skip ci] 2021-10-10 17:09:17 -05:00
Robin Dadswell
89319f9833 New: Renamed Blacklist to Blocklist
(cherry picked from commit ead1371846b1f19cd49928052be0128bf7ccd41f)
2021-10-10 17:02:48 -05:00
ta264
07829a19b8 Fixed: Account for ISO 639 B vs T 2021-10-07 21:01:45 +01:00
Sean A
27259c1ebd Fixed: Ensure AudioTag years have default values (#1272)
* Fixed: Ensure AudioTagService years have default values

* Fix AudioTagServiceFixture.cs tests

Co-authored-by: Sean Anderson <sean.anderson@gaiaresources.com.au>
2021-10-08 09:58:20 +01:00
860 changed files with 22939 additions and 10492 deletions

View File

@@ -19,10 +19,10 @@ indent_size = 4
dotnet_sort_system_directives_first = true
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:refactoring
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
# Indentation preferences
csharp_indent_block_contents = true
@@ -32,10 +32,6 @@ csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _

View File

@@ -1,14 +1,13 @@
name: Bug Report
title: "[BUG]: "
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
options:
- label: I have searched the existing issues
- label: I have searched the existing open and closed issues
required: true
- type: textarea
attributes:
@@ -43,12 +42,14 @@ body:
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
- **Database**: Sqlite 3.36.0
value: |
- OS:
- Readarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
- Database:
render: markdown
validations:
required: true
@@ -64,12 +65,11 @@ body:
required: true
- type: textarea
attributes:
label: Anything else?
label: Trace Logs?
description: |
Trace Logs (https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files)
Links? References? Anything that will give us more context about the issue you are encountering!
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations:
required: true

View File

@@ -1,14 +1,13 @@
name: Feature Request
title: "[FEAT]: "
description: 'Suggest an idea for Readarr'
labels: ['Type: Feature Request', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
options:
- label: I have searched the existing issues
- label: I have searched the existing open and closed issues
required: true
- type: textarea
attributes:

View File

@@ -1,5 +0,0 @@
{
"files.associations": {
"*.yaml": "home-assistant"
}
}

132
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,132 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<development@readarr.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,51 +1,13 @@
# How to Contribute #
# How to Contribute
We're always looking for people to help make Readarr even better, there are a number of ways to contribute.
This file is updated on an ad-hoc basis, for the latest details please see the [contributing wiki page](https://wiki.servarr.com/readarr/contributing).
This file has been moved to the wiki for the latest details please see the [contributing wiki page](https://wiki.servarr.com/readarr/contributing).
## Documentation ##
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/readarr) the better.
## Documentation
## Development ##
Setup guides, [FAQ](https://wiki.servarr.com/readarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/readarr) the better.
### Tools required ###
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download/) (Node 12.X.X or higher)
- [Yarn](https://yarnpkg.com/)
- .NET Core 5.0.
## Development
### Getting started ###
1. Fork Readarr
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
3. Install the required Node Packages `yarn install`
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
5. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `net5.0`
6. Debug the project in Visual Studio
7. Open http://localhost:8787
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Readarr/Readarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
- Rebase from Readarr's develop branch, don't merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on the discord if you have any questions
- Add tests (unit/integration)
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
- One feature/bug fix per pull request to keep things clean and easy to understand
- Use 4 spaces instead of tabs, this is the default for VS 2019 and WebStorm (to my knowledge)
### Pull Requesting ###
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
- new-feature (Good)
- fix-bug (Good)
- patch (Bad)
- develop (Bad)
If you have any questions about any of this, please let us know.
See the [Wiki Page](https://wiki.servarr.com/readarr/contributing)

View File

@@ -1,56 +1,68 @@
# Readarr
[![Build Status](https://dev.azure.com/Readarr/Readarr/_apis/build/status/Readarr.Readarr?branchName=develop)](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/readarr)](https://hub.docker.com/r/hotio/readarr)
[![Backers on Open Collective](https://opencollective.com/readarr/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/readarr/sponsors/badge.svg)](#sponsors)
[![Translated](https://translate.servarr.com/widgets/servarr/-/readarr/svg-badge.svg)](https://translate.servarr.com/engage/readarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/readarr)](https://wiki.servarr.com/readarr/installation#docker)
[![Donors on Open Collective](https://opencollective.com/Readarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Readarr/sponsors/badge.svg)](#sponsors)
[![Mega Sponsors on Open Collective](https://opencollective.com/Readarr/megasponsors/badge.svg)](#mega-sponsors)
### Readarr is currently in beta testing and is generally still in a work in progress. Features may be broken, incomplete, or cause spontaneous combustion.
### Readarr is currently in beta testing and is generally still in a work in progress. Features may be broken, incomplete, or cause spontaneous combustion
Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort and rename them.
Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort, and rename them.
Note that only one type of a given book is supported. If you want both an audiobook and ebook of a given book you will need multiple instances.
## Major Features Include:
## Major Features Include
* Can watch for better quality of the ebooks and audiobooks you have and do an automatic upgrade. *e.g. from PDF to AZW3*
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Automatically detects new books
* Can scan your existing library and download any missing books
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Advanced customization for profiles, such that Readarr will always download the copy you want
* Fully configurable book renaming
* Full integration with SABnzbd and NZBGet
* Full integration with Calibre (add to library, conversion)
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
* Full integration with Calibre (add to library, conversion) (Requires Calibre Content Server)
* And a beautiful UI
## Support
Note: GitHub Issues are for Bugs and Feature Requests Only
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/readarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://readarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/readarr)
Note: GitHub Issues are for Bugs and Feature Requests Only
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Readarr/Readarr/issues)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/readarr)
## Contributors
## Contributors & Developers
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Readarr/Readarr/graphs/contributors"><img src="https://opencollective.com/Readarr/contributors.svg?width=890&button=false" /></a>
[API Documentation](https://readarr.com/docs/api/)
This project exists thanks to all the people who contribute.
- [Contribute (GitHub)](CONTRIBUTING.md)
- [Contribution (Wiki Article)](https://wiki.servarr.com/readarr/contributing)
[![Contributors List](https://opencollective.com/Readarr/contributors.svg?width=890&button=false)](https://github.com/Readarr/Readarr/graphs/contributors)
## Backers
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/readarr#backer)
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Readarr#backer)
<img src="https://opencollective.com/readarr/backers.svg?width=890"></a>
[![Backers List](https://opencollective.com/Readarr/backers.svg?width=890)](https://opencollective.com/Readarr#backer)
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/readarr#sponsor)
<img src="https://opencollective.com/readarr/sponsors.svg?width=890"></a>
[![Sponsors List](https://opencollective.com/Readarr/sponsors.svg?width=890)](https://opencollective.com/readarr#sponsor)
## Mega Sponsors
<img src="https://opencollective.com/readarr/tiers/mega-sponsor.svg?width=890"></a>
[![Mega Sponsors List](https://opencollective.com/Readarr/tiers/mega-sponsor.svg?width=890)](https://opencollective.com/readarr#mega-sponsor)
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2021
* Copyright 2010-2022

8
SECURITY.md Normal file
View File

@@ -0,0 +1,8 @@
# Security Policy
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities on Discord (preferred) to
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
us within 72 hours. If the issue is confirmed, we will release a patch as soon
as possible depending on complexity/severity.

View File

@@ -7,15 +7,19 @@ variables:
outputFolder: './_output'
artifactsFolder: './_artifacts'
testsFolder: './_tests'
majorVersion: '0.1.0'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.1.1'
minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '5.0.302'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
dotnetVersion: '6.0.302'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11'
trigger:
branches:
@@ -42,7 +46,7 @@ stages:
matrix:
Windows:
osName: 'Windows'
imageName: 'windows-2019'
imageName: ${{ variables.windowsImage }}
enableAnalysis: 'false'
pool:
@@ -62,16 +66,13 @@ stages:
inputs:
version: $(dotnetVersion)
- bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
displayName: Enable FreeBSD Support
displayName: Extra Platform Support
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
@@ -81,31 +82,29 @@ stages:
displayName: Build Readarr Backend
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- powershell: Get-ChildItem _output\net5.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
- powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
displayName: Clean up intermediate output
- task: PublishPipelineArtifact@1
inputs:
path: $(outputFolder)
artifact: '$(osName)Backend'
artifactType: 'pipeline'
parallel: true
parallelCount: 100
- publish: $(outputFolder)
artifact: '$(osName)Backend'
displayName: Publish Backend
- publish: '$(testsFolder)/net5.0/win-x64/publish'
artifact: WindowsCoreTests
displayName: Publish Windows Test Package
- publish: '$(testsFolder)/net5.0/linux-x64/publish'
artifact: LinuxCoreTests
displayName: Publish Linux Test Package
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests
displayName: Publish Linux Musl Test Package
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
artifact: FreebsdCoreTests
displayName: Publish FreeBSD Test Package
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
artifact: MacCoreTests
displayName: Publish MacOS Test Package
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
- stage: Build_Backend_Other
displayName: Build Backend (Other OS)
@@ -116,11 +115,11 @@ stages:
matrix:
Linux:
osName: 'Linux'
imageName: 'ubuntu-18.04'
imageName: ${{ variables.linuxImage }}
enableAnalysis: 'true'
Mac:
osName: 'Mac'
imageName: 'macos-10.14'
imageName: ${{ variables.macImage }}
enableAnalysis: 'false'
pool:
@@ -137,25 +136,29 @@ stages:
inputs:
version: $(dotnetVersion)
- bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "BSD already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
displayName: Enable FreeBSD Support
displayName: Extra Platform Support
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder)
displayName: Cache NuGet packages
- bash: ./build.sh --backend --enable-bsd
- bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Readarr Backend
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
find ${TESTSFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
displayName: Clean up intermediate output
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- stage: Build_Frontend
displayName: Frontend
@@ -166,13 +169,13 @@ stages:
matrix:
Linux:
osName: 'Linux'
imageName: 'ubuntu-18.04'
imageName: ${{ variables.linuxImage }}
Mac:
osName: 'Mac'
imageName: 'macos-10.14'
imageName: ${{ variables.macImage }}
Windows:
osName: 'Windows'
imageName: 'windows-2019'
imageName: ${{ variables.windowsImage }}
pool:
vmImage: $(imageName)
steps:
@@ -209,7 +212,7 @@ stages:
- job: Windows_Installer
displayName: Create Installer
pool:
vmImage: 'windows-2019'
vmImage: ${{ variables.windowsImage }}
steps:
- checkout: self
fetchDepth: 1
@@ -225,16 +228,11 @@ stages:
artifactName: WindowsFrontend
targetPath: _output
displayName: Fetch Frontend
- bash: ./build.sh --packages
displayName: Create Packages
- bash: |
setup/inno/ISCC.exe setup/readarr.iss //DFramework=net5.0 //DRuntime=win-x86
cp setup/output/Readarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create .NET Core Windows installer
- bash: |
setup/inno/ISCC.exe setup/readarr.iss //DFramework=net5.0 //DRuntime=win-x64
cp setup/output/Readarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
displayName: Create .NET Core Windows installer
./build.sh --packages --installer
cp setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
cp setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create Installers
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller'
displayName: Publish Installer
@@ -247,7 +245,7 @@ stages:
- job: Other_Packages
displayName: Create Standard Packages
pool:
vmImage: 'ubuntu-18.04'
vmImage: ${{ variables.linuxImage }}
steps:
- checkout: self
fetchDepth: 1
@@ -263,90 +261,120 @@ stages:
artifactName: WindowsFrontend
targetPath: _output
displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-bsd
- bash: ./build.sh --packages --enable-extra-platforms
displayName: Create Packages
- bash: |
find . -name "fpcalc" -exec chmod a+x {} \;
find . -name "Readarr" -exec chmod a+x {} \;
find . -name "Readarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits
- task: ArchiveFiles@2
displayName: Create Windows Core zip
displayName: Create win-x64 zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2
displayName: Create Windows x86 Core zip
displayName: Create win-x86 zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS Core app
displayName: Create osx-x64 app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS Core tar
displayName: Create osx-x64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/macos/net5.0
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2
displayName: Create Linux Core tar
displayName: Create osx-arm64 app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-x64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2
displayName: Create Linux Musl Core tar
displayName: Create linux-musl-x64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2
displayName: Create ARM32 Linux Core tar
displayName: Create linux-x86 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2
displayName: Create Linux Core tar
displayName: Create linux-musl-arm tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create ARM64 Linux Musl Core tar
displayName: Create linux-musl-arm64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create FreeBSD Core Core tar
displayName: Create freebsd-x64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'Packages'
displayName: Publish Packages
@@ -392,22 +420,22 @@ stages:
matrix:
MacCore:
osName: 'Mac'
testName: 'MacCore'
testName: 'osx-x64'
poolName: 'Azure Pipelines'
imageName: 'macos-10.14'
imageName: ${{ variables.macImage }}
WindowsCore:
osName: 'Windows'
testName: 'WindowsCore'
testName: 'win-x64'
poolName: 'Azure Pipelines'
imageName: 'windows-2019'
imageName: ${{ variables.windowsImage }}
LinuxCore:
osName: 'Linux'
testName: 'LinuxCore'
testName: 'linux-x64'
poolName: 'Azure Pipelines'
imageName: 'ubuntu-18.04'
imageName: ${{ variables.linuxImage }}
FreebsdCore:
osName: 'Linux'
testName: 'FreebsdCore'
testName: 'freebsd-x64'
poolName: 'FreeBSD'
imageName:
@@ -426,15 +454,11 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)Tests'
artifactName: '$(testName)-tests'
targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- bash: |
chmod a+x _tests/fpcalc
displayName: Make fpcalc Executable
condition: and(succeeded(), or(eq(variables['osName'], 'Mac'), eq(variables['testName'], 'LinuxCore')))
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
@@ -458,11 +482,15 @@ stages:
matrix:
alpine:
testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests
artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool:
vmImage: 'ubuntu-18.04'
vmImage: ${{ variables.linuxImage }}
container: $[ variables['containerImage'] ]
@@ -470,9 +498,15 @@ stages:
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
displayName: 'Install .NET'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -495,6 +529,57 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-Tests'
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
failTaskOnFailedTests: true
- stage: Integration
displayName: Integration
@@ -506,18 +591,18 @@ stages:
matrix:
MacCore:
osName: 'Mac'
testName: 'MacCore'
imageName: 'macos-10.14'
testName: 'osx-x64'
imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz'
WindowsCore:
osName: 'Windows'
testName: 'WindowsCore'
imageName: 'windows-2019'
testName: 'win-x64'
imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip'
LinuxCore:
osName: 'Linux'
testName: 'LinuxCore'
imageName: 'ubuntu-18.04'
testName: 'linux-x64'
imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz'
pool:
@@ -533,7 +618,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)Tests'
artifactName: '$(testName)-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -563,6 +648,66 @@ stages:
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Readarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_FreeBSD
displayName: Integration Native FreeBSD
workspace:
@@ -578,14 +723,14 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'FreebsdCoreTests'
artifactName: 'freebsd-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '/$(pattern)'
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -612,13 +757,17 @@ stages:
strategy:
matrix:
alpine:
testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests
testName: 'linux-musl-x64'
artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Readarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Readarr.*.linux-core-x86.tar.gz'
pool:
vmImage: 'ubuntu-18.04'
vmImage: ${{ variables.linuxImage }}
container: $[ variables['containerImage'] ]
@@ -626,9 +775,15 @@ stages:
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
displayName: 'Install .NET'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -674,15 +829,18 @@ stages:
matrix:
Linux:
osName: 'Linux'
imageName: 'ubuntu-18.04'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz'
Mac:
osName: 'Mac'
imageName: 'macos-10.14'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz'
Windows:
osName: 'Windows'
imageName: 'windows-2019'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip'
pool:
@@ -698,7 +856,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(osName)CoreTests'
artifactName: '$(artifactName)-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -749,10 +907,10 @@ stages:
matrix:
Linux:
osName: 'Linux'
imageName: 'ubuntu-18.04'
imageName: ${{ variables.linuxImage }}
Windows:
osName: 'Windows'
imageName: 'windows-2019'
imageName: ${{ variables.windowsImage }}
pool:
vmImage: $(imageName)
steps:
@@ -781,7 +939,7 @@ stages:
displayName: Frontend
condition: eq(variables['System.PullRequest.IsFork'], 'False')
pool:
vmImage: windows-2019
vmImage: ${{ variables.windowsImage }}
steps:
- checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@1
@@ -804,7 +962,7 @@ stages:
variables:
disable.coverage.autogenerate: 'true'
pool:
vmImage: ubuntu-18.04
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
@@ -842,8 +1000,8 @@ stages:
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: |
./build.sh --backend -f net5.0 -r linux-x64
TEST_DIR=_tests/net5.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
./build.sh --backend -f net6.0 -r linux-x64
TEST_DIR=_tests/net6.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
displayName: Coverage Unit Tests
env:
NUGET_PACKAGES: $(nugetCacheFolder)
@@ -876,7 +1034,7 @@ stages:
- job:
displayName: Discord Notification
pool:
vmImage: 'ubuntu-18.04'
vmImage: ${{ variables.linuxImage }}
steps:
- task: DownloadPipelineArtifact@2
continueOnError: true
@@ -892,3 +1050,4 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)

138
build.sh
View File

@@ -27,15 +27,22 @@ UpdateVersionNumber()
fi
}
EnableBsdSupport()
EnableExtraPlatformsInSDK()
{
#todo enable sdk with
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
fi
}
@@ -129,7 +136,7 @@ PackageLinux()
echo "Adding Readarr.Mono to UpdatePackage"
cp $folder/Readarr.Mono.* $folder/Readarr.Update
if [ "$framework" = "net5.0" ]; then
if [ "$framework" = "net6.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
fi
@@ -140,12 +147,13 @@ PackageLinux()
PackageMacOS()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating MacOS Package for $framework"
ProgressStart "Creating MacOS Package for $framework $runtime"
local folder=$artifactsFolder/macos/$framework/Readarr
local folder=$artifactsFolder/$runtime/$framework/Readarr
PackageFiles "$folder" "$framework" "osx-x64"
PackageFiles "$folder" "$framework" "$runtime"
echo "Removing Service helpers"
rm -f $folder/ServiceUninstall.*
@@ -156,7 +164,7 @@ PackageMacOS()
echo "Adding Readarr.Mono to UpdatePackage"
cp $folder/Readarr.Mono.* $folder/Readarr.Update
if [ "$framework" = "net5.0" ]; then
if [ "$framework" = "net6.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
fi
@@ -167,10 +175,11 @@ PackageMacOS()
PackageMacOSApp()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating macOS App Package for $framework"
ProgressStart "Creating macOS App Package for $framework $runtime"
local folder=$artifactsFolder/macos-app/$framework
local folder="$artifactsFolder/$runtime-app/$framework"
rm -rf $folder
mkdir -p $folder
@@ -178,7 +187,7 @@ PackageMacOSApp()
mkdir -p $folder/Readarr.app/Contents/MacOS
echo "Copying Binaries"
cp -r $artifactsFolder/macos/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
cp -r $artifactsFolder/$runtime/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
echo "Removing Update Folder"
rm -r $folder/Readarr.app/Contents/MacOS/Readarr.Update
@@ -225,12 +234,38 @@ Package()
PackageWindows "$framework" "$runtime"
;;
osx)
PackageMacOS "$framework"
PackageMacOSApp "$framework"
PackageMacOS "$framework" "$runtime"
PackageMacOSApp "$framework" "$runtime"
;;
esac
}
BuildInstaller()
{
local framework="$1"
local runtime="$2"
./_inno/ISCC.exe setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
}
InstallInno()
{
ProgressStart "Installing portable Inno Setup"
rm -rf _inno
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
mkdir _inno
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
rm innosetup.exe
ProgressEnd "Installed portable Inno Setup"
}
RemoveInno()
{
rm -rf _inno
}
PackageTests()
{
local framework="$1"
@@ -262,8 +297,10 @@ if [ $# -eq 0 ]; then
BACKEND=YES
FRONTEND=YES
PACKAGES=YES
INSTALLER=NO
LINT=YES
ENABLE_BSD=NO
ENABLE_EXTRA_PLATFORMS=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi
while [[ $# -gt 0 ]]
@@ -275,8 +312,12 @@ case $key in
BACKEND=YES
shift # past argument
;;
--enable-bsd)
ENABLE_BSD=YES
--enable-bsd|--enable-extra-platforms)
ENABLE_EXTRA_PLATFORMS=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument
;;
-r|--runtime)
@@ -297,6 +338,10 @@ case $key in
PACKAGES=YES
shift # past argument
;;
--installer)
INSTALLER=YES
shift # past argument
;;
--lint)
LINT=YES
shift # past argument
@@ -316,24 +361,30 @@ esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ];
then
UpdateVersionNumber
if [ "$ENABLE_BSD" = "YES" ];
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
EnableBsdSupport
EnableExtraPlatforms
fi
Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
PackageTests "net5.0" "win-x64"
PackageTests "net5.0" "win-x86"
PackageTests "net5.0" "linux-x64"
PackageTests "net5.0" "linux-musl-x64"
PackageTests "net5.0" "osx-x64"
if [ "$ENABLE_BSD" = "YES" ];
PackageTests "net6.0" "win-x64"
PackageTests "net6.0" "win-x86"
PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
PackageTests "net5.0" "freebsd-x64"
PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi
else
PackageTests "$FRAMEWORK" "$RID"
@@ -362,19 +413,30 @@ then
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
Package "net5.0" "win-x64"
Package "net5.0" "win-x86"
Package "net5.0" "linux-x64"
Package "net5.0" "linux-musl-x64"
Package "net5.0" "linux-arm64"
Package "net5.0" "linux-musl-arm64"
Package "net5.0" "linux-arm"
Package "net5.0" "osx-x64"
if [ "$ENABLE_BSD" = "YES" ];
Package "net6.0" "win-x64"
Package "net6.0" "win-x86"
Package "net6.0" "linux-x64"
Package "net6.0" "linux-musl-x64"
Package "net6.0" "linux-arm64"
Package "net6.0" "linux-musl-arm64"
Package "net6.0" "linux-arm"
Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
Package "net5.0" "freebsd-x64"
Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi
else
Package "$FRAMEWORK" "$RID"
fi
fi
if [ "$INSTALLER" = "YES" ];
then
InstallInno
BuildInstaller "net6.0" "win-x64"
BuildInstaller "net6.0" "win-x86"
RemoveInno
fi

5
debian/changelog vendored
View File

@@ -1,5 +0,0 @@
nzbdrone {version} {branch}; urgency=low
* Automatic Release.
-- NzbDrone <contact@nzbdrone.com> Mon, 26 Aug 2013 00:00:00 -0700

1
debian/compat vendored
View File

@@ -1 +0,0 @@
8

12
debian/control vendored
View File

@@ -1,12 +0,0 @@
Section: web
Priority: optional
Maintainer: Sonarr <contact@nzbdrone.com>
Source: nzbdrone
Homepage: https://readarr.com
Vcs-Git: git@github.com:readarr/Readarr.git
Vcs-Browser: https://github.com/readarr/Readarr
Package: nzbdrone
Architecture: all
Depends: libmono-cil-dev (>= 3.2), sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
Description: Readarr is a music collection manager

24
debian/copyright vendored
View File

@@ -1,24 +0,0 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: nzbdrone
Source: https://github.com/readarr/Readarr
Files: *
Copyright: 2010-2016 Readarr <hello@readarr.com>
License: GPL-3.0+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
.
On Debian systems, the complete text of the GNU General
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".

1
debian/install vendored
View File

@@ -1 +0,0 @@
nzbdrone_bin/* opt/NzbDrone

13
debian/rules vendored
View File

@@ -1,13 +0,0 @@
#!/usr/bin/make -f
# -*- makefile -*-
# Sample debian/rules that uses debhelper.
# This file was originally written by Joey Hess and Craig Small.
# As a special exception, when this file is copied by dh-make into a
# dh-make output file, you may use that output file without restriction.
# This special exception was added by Craig Small in version 0.37 of dh-make.
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
%:
dh $@

View File

@@ -39,6 +39,7 @@ module.exports = {
plugins: [
'filenames',
'react',
'react-hooks',
'simple-import-sort',
'import'
],
@@ -308,7 +309,9 @@ module.exports = {
'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2,
'react/sort-comp': 2,
'react/jsx-wrap-multilines': 2
'react/jsx-wrap-multilines': 2,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error'
},
overrides: [
{

View File

@@ -19,9 +19,9 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BlacklistRowConnector from './BlacklistRowConnector';
import BlocklistRowConnector from './BlocklistRowConnector';
class Blacklist extends Component {
class Blocklist extends Component {
//
// Lifecycle
@@ -103,8 +103,8 @@ class Blacklist extends Component {
columns,
totalRecords,
isRemoving,
isClearingBlacklistExecuting,
onClearBlacklistPress,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
@@ -121,7 +121,7 @@ class Blacklist extends Component {
const selectedIds = this.getSelectedIds();
return (
<PageContent title={translate('Blacklist')}>
<PageContent title={translate('Blocklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
@@ -135,8 +135,8 @@ class Blacklist extends Component {
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlacklistPress}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
/>
</PageToolbarSection>
@@ -162,14 +162,14 @@ class Blacklist extends Component {
{
!isAnyFetching && !!error &&
<div>
{translate('UnableToLoadBlacklist')}
{translate('UnableToLoadBlocklist')}
</div>
}
{
isAllPopulated && !error && !items.length &&
<div>
No history blacklist
{translate('NoHistoryBlocklist')}
</div>
}
@@ -188,7 +188,7 @@ class Blacklist extends Component {
{
items.map((item) => {
return (
<BlacklistRowConnector
<BlocklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
@@ -224,7 +224,7 @@ class Blacklist extends Component {
}
}
Blacklist.propTypes = {
Blocklist.propTypes = {
isAuthorFetching: PropTypes.bool.isRequired,
isAuthorPopulated: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
@@ -234,9 +234,9 @@ Blacklist.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
onClearBlocklistPress: PropTypes.func.isRequired
};
export default Blacklist;
export default Blocklist;

View File

@@ -4,34 +4,34 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import * as blacklistActions from 'Store/Actions/blacklistActions';
import * as blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blacklist from './Blacklist';
import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blacklist,
(state) => state.blocklist,
(state) => state.authors,
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
(blacklist, authors, isClearingBlacklistExecuting) => {
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, authors, isClearingBlocklistExecuting) => {
return {
isAuthorFetching: authors.isFetching,
isAuthorPopulated: authors.isPopulated,
isClearingBlacklistExecuting,
...blacklist
isClearingBlocklistExecuting,
...blocklist
};
}
);
}
const mapDispatchToProps = {
...blacklistActions,
...blocklistActions,
executeCommand
};
class BlacklistConnector extends Component {
class BlocklistConnector extends Component {
//
// Lifecycle
@@ -39,27 +39,27 @@ class BlacklistConnector extends Component {
componentDidMount() {
const {
useCurrentPage,
fetchBlacklist,
gotoBlacklistFirstPage
fetchBlocklist,
gotoBlocklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlacklist();
fetchBlocklist();
} else {
gotoBlacklistFirstPage();
gotoBlocklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
this.props.gotoBlacklistFirstPage();
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
this.props.gotoBlocklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlacklist();
this.props.clearBlocklist();
unregisterPagePopulator(this.repopulate);
}
@@ -67,49 +67,49 @@ class BlacklistConnector extends Component {
// Control
repopulate = () => {
this.props.fetchBlacklist();
this.props.fetchBlocklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlacklistFirstPage();
this.props.gotoBlocklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlacklistPreviousPage();
this.props.gotoBlocklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlacklistNextPage();
this.props.gotoBlocklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlacklistLastPage();
this.props.gotoBlocklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlacklistPage({ page });
this.props.gotoBlocklistPage({ page });
}
onRemoveSelected = (ids) => {
this.props.removeBlacklistItems({ ids });
this.props.removeBlocklistItems({ ids });
}
onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey });
this.props.setBlocklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
this.props.gotoBlocklistFirstPage();
}
}
onClearBlacklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
}
//
@@ -117,7 +117,7 @@ class BlacklistConnector extends Component {
render() {
return (
<Blacklist
<Blocklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
@@ -126,30 +126,30 @@ class BlacklistConnector extends Component {
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
/>
);
}
}
BlacklistConnector.propTypes = {
BlocklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlacklist: PropTypes.func.isRequired,
gotoBlacklistFirstPage: PropTypes.func.isRequired,
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
removeBlacklistItems: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired,
fetchBlocklist: PropTypes.func.isRequired,
gotoBlocklistFirstPage: PropTypes.func.isRequired,
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
gotoBlocklistNextPage: PropTypes.func.isRequired,
gotoBlocklistLastPage: PropTypes.func.isRequired,
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
);

View File

@@ -10,7 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import translate from 'Utilities/String/translate';
class BlacklistDetailsModal extends Component {
class BlocklistDetailsModal extends Component {
//
// Render
@@ -78,7 +78,7 @@ class BlacklistDetailsModal extends Component {
}
}
BlacklistDetailsModal.propTypes = {
BlocklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
@@ -87,4 +87,4 @@ BlacklistDetailsModal.propTypes = {
onModalClose: PropTypes.func.isRequired
};
export default BlacklistDetailsModal;
export default BlocklistDetailsModal;

View File

@@ -9,10 +9,10 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BlacklistDetailsModal from './BlacklistDetailsModal';
import styles from './BlacklistRow.css';
import BlocklistDetailsModal from './BlocklistDetailsModal';
import styles from './BlocklistRow.css';
class BlacklistRow extends Component {
class BlocklistRow extends Component {
//
// Lifecycle
@@ -142,7 +142,7 @@ class BlacklistRow extends Component {
/>
<IconButton
title={translate('RemoveFromBlacklist')}
title={translate('RemoveFromBlocklist')}
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
@@ -155,7 +155,7 @@ class BlacklistRow extends Component {
})
}
<BlacklistDetailsModal
<BlocklistDetailsModal
isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
@@ -169,7 +169,7 @@ class BlacklistRow extends Component {
}
BlacklistRow.propTypes = {
BlocklistRow.propTypes = {
id: PropTypes.number.isRequired,
author: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired,
@@ -184,4 +184,4 @@ BlacklistRow.propTypes = {
onRemovePress: PropTypes.func.isRequired
};
export default BlacklistRow;
export default BlocklistRow;

View File

@@ -1,8 +1,8 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import BlacklistRow from './BlacklistRow';
import BlocklistRow from './BlocklistRow';
function createMapStateToProps() {
return createSelector(
@@ -18,9 +18,9 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeBlacklistItem({ id: props.id }));
dispatch(removeBlocklistItem({ id: props.id }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);

View File

@@ -215,13 +215,13 @@ function HistoryDetails(props) {
switch (reason) {
case 'Manual':
reasonMessage = 'File was deleted by via UI';
reasonMessage = translate('FileWasDeletedByViaUI');
break;
case 'MissingFromDisk':
reasonMessage = 'Readarr was unable to find the file on disk so it was removed';
reasonMessage = translate('MissingFromDisk');
break;
case 'Upgrade':
reasonMessage = 'File was deleted to import an upgrade';
reasonMessage = translate('FileWasDeletedByUpgrade');
break;
default:
reasonMessage = '';

View File

@@ -23,8 +23,6 @@ function getIconName(eventType) {
return icons.RETAG;
case 'bookImportIncomplete':
return icons.DOWNLOADED;
case 'downloadImported':
return icons.DOWNLOADED;
case 'downloadIgnored':
return icons.IGNORE;
default:
@@ -61,8 +59,6 @@ function getTooltip(eventType, data) {
return 'Book file tags updated';
case 'bookImportIncomplete':
return 'Files downloaded but not all could be imported';
case 'downloadImported':
return 'Download completed and successfully imported';
case 'downloadIgnored':
return 'Book Download Ignored';
default:

View File

@@ -169,6 +169,16 @@ class HistoryRow extends Component {
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') {
return (
<TableRowCell

View File

@@ -223,14 +223,14 @@ class Queue extends Component {
{
!isRefreshing && hasError &&
<div>
Failed to load Queue
{translate('FailedToLoadQueue')}
</div>
}
{
isAllPopulated && !hasError && !items.length &&
<div>
Queue is empty
{translate('QueueIsEmpty')}
</div>
}
@@ -284,6 +284,17 @@ class Queue extends Component {
return !!(item && item.authorId && item.bookId);
})
)}
allPending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>

View File

@@ -43,14 +43,14 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true });
}
onRemoveQueueItemModalConfirmed = (blacklist, skipredownload) => {
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blacklist, skipredownload);
onRemoveQueueItemPress(blocklist, skipredownload);
this.setState({ isRemoveQueueItemModalOpen: false });
}
@@ -357,6 +357,7 @@ class QueueRow extends Component {
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!author}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>

View File

@@ -22,7 +22,7 @@ class RemoveQueueItemModal extends Component {
this.state = {
remove: true,
blacklist: false,
blocklist: false,
skipredownload: false
};
}
@@ -33,7 +33,7 @@ class RemoveQueueItemModal extends Component {
resetState = function() {
this.setState({
remove: true,
blacklist: false,
blocklist: false,
skipredownload: false
});
}
@@ -45,8 +45,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
}
onSkipReDownloadChange = ({ value }) => {
@@ -72,10 +72,11 @@ class RemoveQueueItemModal extends Component {
const {
isOpen,
sourceTitle,
canIgnore
canIgnore,
isPending
} = this.props;
const { remove, blacklist, skipredownload } = this.state;
const { remove, blocklist, skipredownload } = this.state;
return (
<Modal
@@ -95,37 +96,41 @@ class RemoveQueueItemModal extends Component {
Are you sure you want to remove '{sourceTitle}' from the queue?
</div>
{
isPending ?
null :
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
{translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('BlacklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText={translate('BlacklistHelpText')}
onChange={this.onBlacklistChange}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blacklist &&
blocklist &&
<FormGroup>
<FormLabel>
{translate('SkipRedownload')}
@@ -164,6 +169,7 @@ RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -23,7 +23,7 @@ class RemoveQueueItemsModal extends Component {
this.state = {
remove: true,
blacklist: false,
blocklist: false,
skipredownload: false
};
}
@@ -34,7 +34,7 @@ class RemoveQueueItemsModal extends Component {
resetState = function() {
this.setState({
remove: true,
blacklist: false,
blocklist: false,
skipredownload: false
});
}
@@ -46,8 +46,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
}
onSkipReDownloadChange = ({ value }) => {
@@ -73,10 +73,11 @@ class RemoveQueueItemsModal extends Component {
const {
isOpen,
selectedCount,
canIgnore
canIgnore,
allPending
} = this.props;
const { remove, blacklist, skipredownload } = this.state;
const { remove, blocklist, skipredownload } = this.state;
return (
<Modal
@@ -96,37 +97,41 @@ class RemoveQueueItemsModal extends Component {
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</div>
{
allPending ?
null :
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
Blacklist Release{selectedCount > 1 ? 's' : ''}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText={translate('BlacklistHelpText')}
onChange={this.onBlacklistChange}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blacklist &&
blocklist &&
<FormGroup>
<FormLabel>
{translate('SkipRedownload')}
@@ -165,6 +170,7 @@ RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -0,0 +1,27 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate';
function AuthorMonitorNewItemsOptionsPopoverContent() {
return (
<DescriptionList>
<DescriptionListItem
title={translate('AllBooks')}
data={translate('DataNewAllBooks')}
/>
<DescriptionListItem
title={translate('NewBooks')}
data={translate('DataNewBooks')}
/>
<DescriptionListItem
title={translate('None')}
data={translate('DataNewNone')}
/>
</DescriptionList>
);
}
export default AuthorMonitorNewItemsOptionsPopoverContent;

View File

@@ -1,46 +1,52 @@
import React from 'react';
import Alert from 'Components/Alert';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate';
function AuthorMonitoringOptionsPopoverContent() {
return (
<DescriptionList>
<DescriptionListItem
title={translate('AllBooks')}
data="Monitor all books"
/>
<>
<Alert>
{translate('MonitoringOptionsHelpText')}
</Alert>
<DescriptionList>
<DescriptionListItem
title={translate('AllBooks')}
data={translate('DataAllBooks')}
/>
<DescriptionListItem
title={translate('FutureBooks')}
data="Monitor books that have not released yet"
/>
<DescriptionListItem
title={translate('FutureBooks')}
data={translate('DataFutureBooks')}
/>
<DescriptionListItem
title={translate('MissingBooks')}
data="Monitor books that do not have files or have not released yet"
/>
<DescriptionListItem
title={translate('MissingBooks')}
data={translate('DataMissingBooks')}
/>
<DescriptionListItem
title={translate('ExistingBooks')}
data="Monitor books that have files or have not released yet"
/>
<DescriptionListItem
title={translate('ExistingBooks')}
data={translate('DataExistingBooks')}
/>
<DescriptionListItem
title={translate('FirstBook')}
data="Monitor the first book. All other books will be ignored"
/>
<DescriptionListItem
title={translate('FirstBook')}
data={translate('DataFirstBook')}
/>
<DescriptionListItem
title={translate('LatestBook')}
data="Monitor the latest book and future books"
/>
<DescriptionListItem
title={translate('LatestBook')}
data={translate('DataLatestBook')}
/>
<DescriptionListItem
title={translate('None')}
data="No books will be monitored"
/>
</DescriptionList>
<DescriptionListItem
title={translate('None')}
data={translate('DataNone')}
/>
</DescriptionList>
</>
);
}

View File

@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
function App({ store, history }) {
return (
<DocumentTitle title="Readarr">
<DocumentTitle title={window.Readarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<PageConnector>

View File

@@ -1,11 +1,10 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import AuthorDetailsPageConnector from 'Author/Details/AuthorDetailsPageConnector';
import AuthorEditorConnector from 'Author/Editor/AuthorEditorConnector';
import AuthorIndexConnector from 'Author/Index/AuthorIndexConnector';
import BookDetailsPageConnector from 'Book/Details/BookDetailsPageConnector';
import BookIndexConnector from 'Book/Index/BookIndexConnector';
@@ -82,11 +81,6 @@ function AppRoutes(props) {
component={AddNewItemConnector}
/>
<Route
path="/authoreditor"
component={AuthorEditorConnector}
/>
<Route
exact={true}
path="/shelf"
@@ -138,8 +132,8 @@ function AppRoutes(props) {
/>
<Route
path="/activity/blacklist"
component={BlacklistConnector}
path="/activity/blocklist"
component={BlocklistConnector}
/>
{/*

View File

@@ -39,7 +39,9 @@ function AppUpdatedModalContent(props) {
<div>
{
!update.changes &&
<div className={styles.maintenance}>Maintenance release</div>
<div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
}
{

View File

@@ -5,6 +5,7 @@ import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
@@ -22,6 +23,7 @@ import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
@@ -53,13 +55,56 @@ class AuthorDetails extends Component {
isDeleteAuthorModalOpen: false,
isInteractiveImportModalOpen: false,
isMonitorOptionsModalOpen: false,
isEditorActive: false,
allExpanded: false,
allCollapsed: false,
expandedState: {},
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
selectedTabIndex: 0
};
}
//
// Control
setSelectedState = (items) => {
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((item) => {
const isItemSelected = selectedState[item.id];
if (isItemSelected) {
newSelectedState[item.id] = isItemSelected;
} else {
newSelectedState[item.id] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
}
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
@@ -114,6 +159,10 @@ class AuthorDetails extends Component {
this.setState({ isMonitorOptionsModalOpen: false });
}
onBookEditorTogglePress = () => {
this.setState({ isEditorActive: !this.state.isEditorActive });
}
onExpandAllPress = () => {
const {
allExpanded,
@@ -137,6 +186,27 @@ class AuthorDetails extends Component {
});
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
}
onSelectedChange = (items, id, value, shiftKey = false) => {
this.setState((state) => {
return toggleSelected(state, items, id, value, shiftKey);
});
}
onSaveSelected = (changes) => {
this.props.onSaveSelected({
bookIds: this.getSelectedIds(),
...changes
});
}
onTabSelect = (index, lastIndex) => {
this.setState({ selectedTabIndex: index });
}
@@ -165,6 +235,10 @@ class AuthorDetails extends Component {
nextAuthor,
onRefreshPress,
onSearchPress,
isSaving,
saveError,
isDeleting,
deleteError,
statistics
} = this.props;
@@ -175,6 +249,9 @@ class AuthorDetails extends Component {
isDeleteAuthorModalOpen,
isInteractiveImportModalOpen,
isMonitorOptionsModalOpen,
isEditorActive,
allSelected,
selectedState,
allExpanded,
allCollapsed,
expandedState,
@@ -189,12 +266,14 @@ class AuthorDetails extends Component {
expandIcon = icons.EXPAND;
}
const selectedBookIds = this.getSelectedIds();
return (
<PageContent title={authorName}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RefreshScan')}
label={translate('RefreshAndScan')}
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
title={translate('RefreshInformationAndScanDisk')}
@@ -252,6 +331,33 @@ class AuthorDetails extends Component {
iconName={icons.DELETE}
onPress={this.onDeleteAuthorPress}
/>
<PageToolbarSeparator />
{
isEditorActive ?
<PageToolbarButton
label={translate('BookList')}
iconName={icons.AUTHOR_CONTINUING}
onPress={this.onBookEditorTogglePress}
/> :
<PageToolbarButton
label={translate('BookEditor')}
iconName={icons.EDIT}
onPress={this.onBookEditorTogglePress}
/>
}
{
isEditorActive ?
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
onPress={this.onSelectAllPress}
/> :
null
}
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
@@ -377,7 +483,11 @@ class AuthorDetails extends Component {
<AuthorDetailsSeasonConnector
authorId={id}
isExpanded={true}
selectedState={selectedState}
onExpandPress={this.onExpandPress}
setSelectedState={this.setSelectedState}
onSelectedChange={this.onSelectedChange}
isEditorActive={isEditorActive}
/>
</TabPanel>
@@ -422,7 +532,6 @@ class AuthorDetails extends Component {
</TabPanel>
</Tabs>
}
</div>
<div className={styles.metadataMessage}>
@@ -474,6 +583,19 @@ class AuthorDetails extends Component {
onModalClose={this.onMonitorOptionsClose}
/>
</PageContentBody>
{
isEditorActive &&
<BookEditorFooter
bookIds={selectedBookIds}
selectedCount={selectedBookIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
onSaveSelected={this.onSaveSelected}
/>
}
</PageContent>
);
}
@@ -493,7 +615,6 @@ AuthorDetails.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
isSaving: PropTypes.bool.isRequired,
isRefreshing: PropTypes.bool.isRequired,
isSearching: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
@@ -510,13 +631,17 @@ AuthorDetails.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
onMonitorTogglePress: PropTypes.func.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired
onSearchPress: PropTypes.func.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSaveSelected: PropTypes.func.isRequired
};
AuthorDetails.defaultProps = {
statistics: {},
tags: [],
isSaving: false
tags: []
};
export default AuthorDetails;

View File

@@ -7,6 +7,7 @@ import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { toggleAuthorMonitored } from 'Store/Actions/authorActions';
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
import { saveBookEditor } from 'Store/Actions/bookIndexActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
@@ -21,7 +22,8 @@ import AuthorDetails from './AuthorDetails';
const selectBooks = createSelector(
(state) => state.books,
(books) => {
(state) => state.bookIndex,
(books, index) => {
const {
items,
isFetching,
@@ -29,6 +31,13 @@ const selectBooks = createSelector(
error
} = books;
const {
isSaving,
saveError,
isDeleting,
deleteError
} = index;
const hasBooks = !!items.length;
const hasMonitoredBooks = items.some((e) => e.monitored);
@@ -37,7 +46,11 @@ const selectBooks = createSelector(
isBooksPopulated: isPopulated,
booksError: error,
hasBooks,
hasMonitoredBooks
hasMonitoredBooks,
isSaving,
saveError,
isDeleting,
deleteError
};
}
);
@@ -109,7 +122,11 @@ function createMapStateToProps() {
isBooksPopulated,
booksError,
hasBooks,
hasMonitoredBooks
hasMonitoredBooks,
isSaving,
saveError,
isDeleting,
deleteError
} = books;
const {
@@ -169,6 +186,10 @@ function createMapStateToProps() {
isFetching,
isPopulated,
booksError,
isSaving,
saveError,
isDeleting,
deleteError,
seriesError,
bookFilesError,
hasBooks,
@@ -187,6 +208,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
fetchSeries,
clearSeries,
saveBookEditor,
fetchBookFiles,
clearBookFiles,
toggleAuthorMonitored,
@@ -282,6 +304,10 @@ class AuthorDetailsConnector extends Component {
});
}
onSaveSelected = (payload) => {
this.props.saveBookEditor(payload);
}
//
// Render
@@ -292,6 +318,7 @@ class AuthorDetailsConnector extends Component {
onMonitorTogglePress={this.onMonitorTogglePress}
onRefreshPress={this.onRefreshPress}
onSearchPress={this.onSearchPress}
onSaveSelected={this.onSaveSelected}
/>
);
}
@@ -307,6 +334,7 @@ AuthorDetailsConnector.propTypes = {
isRenamingAuthor: PropTypes.bool.isRequired,
fetchSeries: PropTypes.func.isRequired,
clearSeries: PropTypes.func.isRequired,
saveBookEditor: PropTypes.func.isRequired,
fetchBookFiles: PropTypes.func.isRequired,
clearBookFiles: PropTypes.func.isRequired,
toggleAuthorMonitored: PropTypes.func.isRequired,

View File

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { sortDirections } from 'Helpers/Props';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import getToggledRange from 'Utilities/Table/getToggledRange';
import BookRowConnector from './BookRowConnector';
import styles from './AuthorDetailsSeason.css';
@@ -21,6 +22,26 @@ class AuthorDetailsSeason extends Component {
};
}
componentDidMount() {
this.props.setSelectedState(this.props.items);
}
componentDidUpdate(prevProps) {
const {
items,
sortKey,
sortDirection,
setSelectedState
} = this.props;
if (sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection ||
hasDifferentItemsOrOrder(prevProps.items, items)
) {
setSelectedState(items);
}
}
//
// Listeners
@@ -42,26 +63,42 @@ class AuthorDetailsSeason extends Component {
this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
const {
onSelectedChange,
items
} = this.props;
return onSelectedChange(items, id, value, shiftKey);
}
//
// Render
render() {
const {
items,
isEditorActive,
columns,
sortKey,
sortDirection,
onSortPress,
onTableOptionChange
onTableOptionChange,
selectedState
} = this.props;
let titleColumns = columns;
if (!isEditorActive) {
titleColumns = columns.filter((x) => x.name !== 'select');
}
return (
<div
className={styles.bookType}
>
<div className={styles.books}>
<Table
columns={columns}
columns={titleColumns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
@@ -76,6 +113,9 @@ class AuthorDetailsSeason extends Component {
columns={columns}
{...item}
onMonitorBookPress={this.onMonitorBookPress}
isEditorActive={isEditorActive}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
);
})
@@ -92,9 +132,13 @@ AuthorDetailsSeason.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isEditorActive: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired,
setSelectedState: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onMonitorBookPress: PropTypes.func.isRequired,
uiSettings: PropTypes.object.isRequired

View File

@@ -6,6 +6,7 @@ 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 BookStatus from './BookStatus';
import styles from './BookRow.css';
@@ -65,6 +66,9 @@ class BookRow extends Component {
authorMonitored,
titleSlug,
bookFiles,
isEditorActive,
isSelected,
onSelectedChange,
columns
} = this.props;
@@ -84,6 +88,18 @@ class BookRow extends Component {
return null;
}
if (isEditorActive && name === 'select') {
return (
<TableSelectCell
key={name}
id={id}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'monitored') {
return (
<TableRowCell
@@ -220,6 +236,9 @@ BookRow.propTypes = {
isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired,
bookFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onMonitorBookPress: PropTypes.func.isRequired
};

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent';
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -73,6 +74,7 @@ class EditAuthorModalContent extends Component {
const {
monitored,
monitorNewItems,
qualityProfileId,
metadataProfileId,
path,
@@ -101,6 +103,31 @@ class EditAuthorModalContent extends Component {
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MonitorNewItems')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MonitorNewItems')}
body={<AuthorMonitorNewItemsOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
name="monitorNewItems"
helpText={translate('MonitorNewItemsHelpText')}
{...monitorNewItems}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('QualityProfile')}

View File

@@ -39,6 +39,7 @@ function createMapStateToProps() {
const authorSettings = _.pick(author, [
'monitored',
'monitorNewItems',
'qualityProfileId',
'metadataProfileId',
'path',

View File

@@ -1,280 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import NoAuthor from 'Author/NoAuthor';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import RetagAuthorModal from './AudioTags/RetagAuthorModal';
import AuthorEditorFilterModalConnector from './AuthorEditorFilterModalConnector';
import AuthorEditorFooter from './AuthorEditorFooter';
import AuthorEditorRowConnector from './AuthorEditorRowConnector';
import OrganizeAuthorModal from './Organize/OrganizeAuthorModal';
class AuthorEditor extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isOrganizingAuthorModalOpen: false,
isRetaggingAuthorModalOpen: false
};
}
componentDidUpdate(prevProps) {
const {
isDeleting,
deleteError
} = this.props;
const hasFinishedDeleting = prevProps.isDeleting &&
!isDeleting &&
!deleteError;
if (hasFinishedDeleting) {
this.onSelectAllChange({ value: false });
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onSaveSelected = (changes) => {
this.props.onSaveSelected({
authorIds: this.getSelectedIds(),
...changes
});
}
onOrganizeAuthorPress = () => {
this.setState({ isOrganizingAuthorModalOpen: true });
}
onOrganizeAuthorModalClose = (organized) => {
this.setState({ isOrganizingAuthorModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
}
onRetagAuthorPress = () => {
this.setState({ isRetaggingAuthorModalOpen: true });
}
onRetagAuthorModalClose = (organized) => {
this.setState({ isRetaggingAuthorModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
totalItems,
items,
columns,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
isSaving,
saveError,
isDeleting,
deleteError,
isOrganizingAuthor,
isRetaggingAuthor,
onTableOptionChange,
onSortPress,
onFilterSelect
} = this.props;
const {
allSelected,
allUnselected,
selectedState
} = this.state;
const selectedAuthorIds = this.getSelectedIds();
return (
<PageContent title={translate('AuthorEditor')}>
<PageToolbar>
<PageToolbarSection />
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<PageToolbarSeparator />
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={AuthorEditorFilterModalConnector}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>{getErrorMessage(error, 'Failed to load author from API')}</div>
}
{
!error && isPopulated && !!items.length &&
<div>
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSortPress={onSortPress}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<AuthorEditorRowConnector
key={item.id}
{...item}
columns={columns}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
</div>
}
{
!error && isPopulated && !items.length &&
<NoAuthor totalItems={totalItems} />
}
</PageContentBody>
<AuthorEditorFooter
authorIds={selectedAuthorIds}
selectedCount={selectedAuthorIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
isOrganizingAuthor={isOrganizingAuthor}
isRetaggingAuthor={isRetaggingAuthor}
columns={columns}
showMetadataProfile={columns.find((column) => column.name === 'metadataProfileId').isVisible}
onSaveSelected={this.onSaveSelected}
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
onRetagAuthorPress={this.onRetagAuthorPress}
/>
<OrganizeAuthorModal
isOpen={this.state.isOrganizingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onOrganizeAuthorModalClose}
/>
<RetagAuthorModal
isOpen={this.state.isRetaggingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onRetagAuthorModalClose}
/>
</PageContent>
);
}
}
AuthorEditor.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};
export default AuthorEditor;

View File

@@ -1,97 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort, setAuthorEditorTableOption } from 'Store/Actions/authorEditorActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchRootFolders } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import AuthorEditor from './AuthorEditor';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('authors', 'authorEditor'),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
(author, isOrganizingAuthor, isRetaggingAuthor) => {
return {
isOrganizingAuthor,
isRetaggingAuthor,
...author
};
}
);
}
const mapDispatchToProps = {
dispatchSetAuthorEditorSort: setAuthorEditorSort,
dispatchSetAuthorEditorFilter: setAuthorEditorFilter,
dispatchSetAuthorEditorTableOption: setAuthorEditorTableOption,
dispatchSaveAuthorEditor: saveAuthorEditor,
dispatchFetchRootFolders: fetchRootFolders,
dispatchExecuteCommand: executeCommand
};
class AuthorEditorConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchRootFolders();
}
//
// Listeners
onSortPress = (sortKey) => {
this.props.dispatchSetAuthorEditorSort({ sortKey });
}
onFilterSelect = (selectedFilterKey) => {
this.props.dispatchSetAuthorEditorFilter({ selectedFilterKey });
}
onTableOptionChange = (payload) => {
this.props.dispatchSetAuthorEditorTableOption(payload);
}
onSaveSelected = (payload) => {
this.props.dispatchSaveAuthorEditor(payload);
}
onMoveSelected = (payload) => {
this.props.dispatchExecuteCommand({
name: commandNames.MOVE_AUTHOR,
...payload
});
}
//
// Render
render() {
return (
<AuthorEditor
{...this.props}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onSaveSelected={this.onSaveSelected}
onTableOptionChange={this.onTableOptionChange}
/>
);
}
}
AuthorEditorConnector.propTypes = {
dispatchSetAuthorEditorSort: PropTypes.func.isRequired,
dispatchSetAuthorEditorFilter: PropTypes.func.isRequired,
dispatchSetAuthorEditorTableOption: PropTypes.func.isRequired,
dispatchSaveAuthorEditor: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AuthorEditorConnector);

View File

@@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setAuthorEditorFilter } from 'Store/Actions/authorEditorActions';
function createMapStateToProps() {
return createSelector(
(state) => state.authors.items,
(state) => state.authorEditor.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'authorEditor'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setAuthorEditorFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);

View File

@@ -1,11 +1,23 @@
.footer {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.dropdownContainer {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
}
.inputContainer {
flex: 1;
margin-right: 20px;
min-width: 150px;
}
.buttonContainer {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
@@ -24,12 +36,14 @@
composes: button from '~Components/Link/SpinnerButton.css';
margin-right: 10px;
margin-bottom: 10px;
height: 35px;
}
.deleteSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-bottom: 10px;
margin-left: 50px;
height: 35px;
}
@@ -48,6 +62,10 @@
}
@media only screen and (max-width: $breakpointSmall) {
.dropdownContainer {
display: block;
}
.inputContainer {
margin-right: 0;
}
@@ -61,6 +79,7 @@
}
.buttons {
display: block;
justify-content: space-between;
}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SelectInput from 'Components/Form/SelectInput';
@@ -26,6 +27,7 @@ class AuthorEditorFooter extends Component {
this.state = {
monitored: NO_CHANGE,
monitorNewItems: NO_CHANGE,
qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE,
@@ -46,6 +48,7 @@ class AuthorEditorFooter extends Component {
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
monitorNewItems: NO_CHANGE,
qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE,
@@ -139,13 +142,13 @@ class AuthorEditorFooter extends Component {
isDeleting,
isOrganizingAuthor,
isRetaggingAuthor,
columns,
onOrganizeAuthorPress,
onRetagAuthorPress
} = this.props;
const {
monitored,
monitorNewItems,
qualityProfileId,
metadataProfileId,
rootFolderPath,
@@ -164,112 +167,99 @@ class AuthorEditorFooter extends Component {
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label={translate('MonitorAuthor')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<div className={styles.footer}>
<div className={styles.dropdownContainer}>
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label={translate('MonitorAuthor')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label={translate('MonitorNewItems')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
if (!isVisible) {
return null;
}
<MonitorNewItemsSelectInput
name="monitorNewItems"
value={monitorNewItems}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
if (name === 'qualityProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label={translate('QualityProfile')}
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label={translate('QualityProfile')}
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
if (name === 'metadataProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label={translate('MetadataProfile')}
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/>
<div
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label={translate('MetadataProfile')}
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/>
<MetadataProfileSelectInputConnector
name="metadataProfileId"
value={metadataProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
<MetadataProfileSelectInputConnector
name="metadataProfileId"
value={metadataProfileId}
includeNoChange={true}
includeNone={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
if (name === 'path') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label={translate('RootFolder')}
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<div
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label={translate('RootFolder')}
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
);
}
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
</div>
return null;
})
}
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<div className={styles.buttons}>
<div>
<SpinnerButton
className={styles.organizeSelectedButton}
kind={kinds.WARNING}
@@ -277,7 +267,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onOrganizeAuthorPress}
>
Rename Files
{translate('RenameFiles')}
</SpinnerButton>
<SpinnerButton
@@ -287,7 +277,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onRetagAuthorPress}
>
Write Metadata Tags
{translate('WriteMetadataTags')}
</SpinnerButton>
<SpinnerButton
@@ -296,19 +286,20 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={this.onTagsPress}
>
Set Readarr Tags
{translate('SetReadarrTags')}
</SpinnerButton>
</div>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
Delete
</SpinnerButton>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
{translate('Delete')}
</SpinnerButton>
</div>
</div>
</div>
</div>
@@ -348,7 +339,6 @@ AuthorEditorFooter.propTypes = {
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired

View File

@@ -1,7 +1,7 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { bulkDeleteAuthor } from 'Store/Actions/authorEditorActions';
import { bulkDeleteAuthor } from 'Store/Actions/authorIndexActions';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import DeleteAuthorModalContent from './DeleteAuthorModalContent';

View File

@@ -1,6 +1,9 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import RetagAuthorModal from 'Author/Editor/AudioTags/RetagAuthorModal';
import AuthorEditorFooter from 'Author/Editor/AuthorEditorFooter';
import OrganizeAuthorModal from 'Author/Editor/Organize/OrganizeAuthorModal';
import NoAuthor from 'Author/NoAuthor';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@@ -15,6 +18,9 @@ import { align, icons, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import AuthorIndexFooterConnector from './AuthorIndexFooterConnector';
import AuthorIndexFilterMenu from './Menus/AuthorIndexFilterMenu';
import AuthorIndexSortMenu from './Menus/AuthorIndexSortMenu';
@@ -52,12 +58,20 @@ class AuthorIndex extends Component {
jumpBarItems: { order: [] },
jumpToCharacter: null,
isPosterOptionsModalOpen: false,
isOverviewOptionsModalOpen: false
isOverviewOptionsModalOpen: false,
isEditorActive: false,
isOrganizingAuthorModalOpen: false,
isRetaggingAuthorModalOpen: false,
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
};
}
componentDidMount() {
this.setJumpBarItems();
this.setSelectedState();
}
componentDidUpdate(prevProps) {
@@ -72,6 +86,7 @@ class AuthorIndex extends Component {
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setJumpBarItems();
this.setSelectedState();
}
if (this.state.jumpToCharacter != null) {
@@ -86,6 +101,48 @@ class AuthorIndex extends Component {
this.setState({ scroller: ref });
}
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState);
}
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((author) => {
const isItemSelected = selectedState[author.id];
if (isItemSelected) {
newSelectedState[author.id] = isItemSelected;
} else {
newSelectedState[author.id] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
}
setJumpBarItems() {
const {
items,
@@ -149,10 +206,72 @@ class AuthorIndex extends Component {
this.setState({ isOverviewOptionsModalOpen: false });
}
onEditorTogglePress = () => {
if (this.state.isEditorActive) {
this.setState({ isEditorActive: false });
} else {
const newState = selectAll(this.state.selectedState, false);
newState.isEditorActive = true;
this.setState(newState);
}
}
onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter });
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onSaveSelected = (changes) => {
this.props.onSaveSelected({
authorIds: this.getSelectedIds(),
...changes
});
}
onOrganizeAuthorPress = () => {
this.setState({ isOrganizingAuthorModalOpen: true });
}
onOrganizeAuthorModalClose = (organized) => {
this.setState({ isOrganizingAuthorModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
}
onRetagAuthorPress = () => {
this.setState({ isRetaggingAuthorModalOpen: true });
}
onRetagAuthorModalClose = (organized) => {
this.setState({ isRetaggingAuthorModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
}
onRefreshAuthorPress = () => {
const selectedIds = this.getSelectedIds();
const refreshIds = this.state.isEditorActive && selectedIds.length > 0 ? selectedIds : [];
this.props.onRefreshAuthorPress(refreshIds);
}
//
// Render
@@ -172,11 +291,16 @@ class AuthorIndex extends Component {
view,
isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor,
isRetaggingAuthor,
isSaving,
saveError,
isDeleting,
deleteError,
onScroll,
onSortSelect,
onFilterSelect,
onViewSelect,
onRefreshAuthorPress,
onRssSyncPress,
...otherProps
} = this.props;
@@ -186,23 +310,31 @@ class AuthorIndex extends Component {
jumpBarItems,
jumpToCharacter,
isPosterOptionsModalOpen,
isOverviewOptionsModalOpen
isOverviewOptionsModalOpen,
isEditorActive,
selectedState,
allSelected,
allUnselected
} = this.state;
const selectedAuthorIds = this.getSelectedIds();
const ViewComponent = getViewComponent(view);
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoAuthor = !totalItems;
const refreshLabel = isEditorActive && selectedAuthorIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll');
return (
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('UpdateAll')}
label={refreshLabel}
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isRefreshingAuthor}
onPress={onRefreshAuthorPress}
onPress={this.onRefreshAuthorPress}
/>
<PageToolbarButton
@@ -213,6 +345,35 @@ class AuthorIndex extends Component {
onPress={onRssSyncPress}
/>
<PageToolbarSeparator />
{
isEditorActive ?
<PageToolbarButton
label={translate('AuthorIndex')}
iconName={icons.AUTHOR_CONTINUING}
isDisabled={hasNoAuthor}
onPress={this.onEditorTogglePress}
/> :
<PageToolbarButton
label={translate('AuthorEditor')}
iconName={icons.EDIT}
isDisabled={hasNoAuthor}
onPress={this.onEditorTogglePress}
/>
}
{
isEditorActive ?
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
isDisabled={hasNoAuthor}
onPress={this.onSelectAllPress}
/> :
null
}
</PageToolbarSection>
<PageToolbarSection
@@ -310,6 +471,12 @@ class AuthorIndex extends Component {
sortKey={sortKey}
sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter}
isEditorActive={isEditorActive}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps}
/>
@@ -332,6 +499,24 @@ class AuthorIndex extends Component {
}
</div>
{
isLoaded && isEditorActive &&
<AuthorEditorFooter
authorIds={selectedAuthorIds}
selectedCount={selectedAuthorIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
isOrganizingAuthor={isOrganizingAuthor}
isRetaggingAuthor={isRetaggingAuthor}
showMetadataProfile={true}
onSaveSelected={this.onSaveSelected}
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
onRetagAuthorPress={this.onRetagAuthorPress}
/>
}
<AuthorIndexPosterOptionsModal
isOpen={isPosterOptionsModalOpen}
onModalClose={this.onPosterOptionsModalClose}
@@ -340,8 +525,20 @@ class AuthorIndex extends Component {
<AuthorIndexOverviewOptionsModal
isOpen={isOverviewOptionsModalOpen}
onModalClose={this.onOverviewOptionsModalClose}
/>
<OrganizeAuthorModal
isOpen={this.state.isOrganizingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onOrganizeAuthorModalClose}
/>
<RetagAuthorModal
isOpen={this.state.isRetaggingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onRetagAuthorModalClose}
/>
</PageContent>
);
}
@@ -361,14 +558,21 @@ AuthorIndex.propTypes = {
sortDirection: PropTypes.oneOf(sortDirections.all),
view: PropTypes.string.isRequired,
isRefreshingAuthor: PropTypes.bool.isRequired,
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onViewSelect: PropTypes.func.isRequired,
onRefreshAuthorPress: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired
onScroll: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};
export default AuthorIndex;

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
import { saveAuthorEditor, setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
import { executeCommand } from 'Store/Actions/commandActions';
import scrollPositions from 'Store/scrollPositions';
import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
@@ -18,16 +18,22 @@ function createMapStateToProps() {
createAuthorClientSideCollectionItemsSelector('authorIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
createDimensionsSelector(),
(
author,
isRefreshingAuthor,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting,
dimensionsState
) => {
return {
...author,
isRefreshingAuthor,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen
};
@@ -53,9 +59,14 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(setAuthorView({ view }));
},
onRefreshAuthorPress() {
dispatchSaveAuthorEditor(payload) {
dispatch(saveAuthorEditor(payload));
},
onRefreshAuthorPress(items) {
dispatch(executeCommand({
name: commandNames.REFRESH_AUTHOR
name: commandNames.BULK_REFRESH_AUTHOR,
authorIds: items
}));
},
@@ -76,6 +87,10 @@ class AuthorIndexConnector extends Component {
this.props.dispatchSetAuthorView(view);
}
onSaveSelected = (payload) => {
this.props.dispatchSaveAuthorEditor(payload);
}
onScroll = ({ scrollTop }) => {
scrollPositions.authorIndex = scrollTop;
}
@@ -89,6 +104,7 @@ class AuthorIndexConnector extends Component {
{...this.props}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
/>
);
}
@@ -97,7 +113,8 @@ class AuthorIndexConnector extends Component {
AuthorIndexConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
dispatchSetAuthorView: PropTypes.func.isRequired
dispatchSetAuthorView: PropTypes.func.isRequired,
dispatchSaveAuthorEditor: PropTypes.func.isRequired
};
export default withScrollPosition(

View File

@@ -19,6 +19,13 @@ $hoverScale: 1.05;
left: 0;
}
.editorSelect {
position: absolute;
top: 0;
left: 5px;
z-index: 3;
}
.posterContainer {
position: relative;
overflow: hidden;

View File

@@ -5,6 +5,7 @@ import AuthorPoster from 'Author/AuthorPoster';
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar';
import CheckInput from 'Components/Form/CheckInput';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -67,6 +68,15 @@ class AuthorIndexOverview extends Component {
this.setState({ isDeleteAuthorModalOpen: false });
}
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
}
//
// Render
@@ -97,6 +107,8 @@ class AuthorIndexOverview extends Component {
isSearchingAuthor,
onRefreshAuthorPress,
onSearchPress,
isEditorActive,
isSelected,
...otherProps
} = this.props;
@@ -127,6 +139,18 @@ class AuthorIndexOverview extends Component {
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.posterContainer}>
{
isEditorActive &&
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
}
{
status === 'ended' &&
<div
@@ -270,7 +294,10 @@ AuthorIndexOverview.propTypes = {
isRefreshingAuthor: PropTypes.bool.isRequired,
isSearchingAuthor: PropTypes.bool.isRequired,
onRefreshAuthorPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired
onSearchPress: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
AuthorIndexOverview.defaultProps = {

View File

@@ -73,7 +73,9 @@ class AuthorIndexOverviews extends Component {
sortKey,
overviewOptions,
jumpToCharacter,
scrollTop
scrollTop,
isEditorActive,
selectedState
} = this.props;
const {
@@ -91,6 +93,8 @@ class AuthorIndexOverviews extends Component {
(prevState.width !== width ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items) ||
prevProps.isEditorActive !== isEditorActive ||
prevProps.selectedState !== selectedState ||
prevProps.overviewOptions.showTitle !== overviewOptions.showTitle)) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
@@ -148,7 +152,10 @@ class AuthorIndexOverviews extends Component {
shortDateFormat,
longDateFormat,
timeFormat,
isSmallScreen
isSmallScreen,
selectedState,
isEditorActive,
onSelectedChange
} = this.props;
const {
@@ -184,6 +191,9 @@ class AuthorIndexOverviews extends Component {
authorId={author.id}
qualityProfileId={author.qualityProfileId}
metadataProfileId={author.metadataProfileId}
isSelected={selectedState[author.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/>
</div>
);
@@ -264,7 +274,10 @@ AuthorIndexOverviews.propTypes = {
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired
timeFormat: PropTypes.string.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
};
export default AuthorIndexOverviews;

View File

@@ -78,6 +78,13 @@ $hoverScale: 1.05;
color: $white;
}
.editorSelect {
position: absolute;
top: 10px;
left: 10px;
z-index: 4;
}
.controls {
position: absolute;
bottom: 10px;

View File

@@ -4,6 +4,7 @@ import AuthorPoster from 'Author/AuthorPoster';
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar';
import CheckInput from 'Components/Form/CheckInput';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
@@ -63,6 +64,15 @@ class AuthorIndexPoster extends Component {
}
}
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
}
//
// Render
@@ -84,6 +94,7 @@ class AuthorIndexPoster extends Component {
showMonitored,
showQualityProfile,
qualityProfile,
metadataProfile,
showSearchAction,
showRelativeDates,
shortDateFormat,
@@ -92,6 +103,9 @@ class AuthorIndexPoster extends Component {
isSearchingAuthor,
onRefreshAuthorPress,
onSearchPress,
isEditorActive,
isSelected,
onSelectedChange,
...otherProps
} = this.props;
@@ -120,6 +134,18 @@ class AuthorIndexPoster extends Component {
<div>
<div className={styles.content}>
<div className={styles.posterContainer}>
{
isEditorActive &&
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
}
<Label className={styles.controls}>
<SpinnerIconButton
className={styles.action}
@@ -234,6 +260,7 @@ class AuthorIndexPoster extends Component {
sizeOnDisk={sizeOnDisk}
qualityProfile={qualityProfile}
showQualityProfile={showQualityProfile}
metadataProfile={metadataProfile}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
@@ -275,6 +302,7 @@ AuthorIndexPoster.propTypes = {
showMonitored: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired,
qualityProfile: PropTypes.object.isRequired,
metadataProfile: PropTypes.object.isRequired,
showSearchAction: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
@@ -282,7 +310,10 @@ AuthorIndexPoster.propTypes = {
isRefreshingAuthor: PropTypes.bool.isRequired,
isSearchingAuthor: PropTypes.bool.isRequired,
onRefreshAuthorPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired
onSearchPress: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
AuthorIndexPoster.defaultProps = {

View File

@@ -8,8 +8,10 @@ function AuthorIndexPosterInfo(props) {
const {
qualityProfile,
showQualityProfile,
previousAiring,
metadataProfile,
added,
nextBook,
lastBook,
bookCount,
path,
sizeOnDisk,
@@ -27,20 +29,10 @@ function AuthorIndexPosterInfo(props) {
);
}
if (sortKey === 'previousAiring' && previousAiring) {
if (sortKey === 'metadataProfileId') {
return (
<div className={styles.info}>
{
getRelativeDate(
previousAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true
}
)
}
{metadataProfile.name}
</div>
);
}
@@ -63,6 +55,42 @@ function AuthorIndexPosterInfo(props) {
);
}
if (sortKey === 'nextBook' && nextBook) {
const date = getRelativeDate(
nextBook.releaseDate,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false
}
);
return (
<div className={styles.info}>
{`Next Book ${date}`}
</div>
);
}
if (sortKey === 'lastBook' && lastBook) {
const date = getRelativeDate(
lastBook.releaseDate,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false
}
);
return (
<div className={styles.info}>
{`Last Book ${date}`}
</div>
);
}
if (sortKey === 'bookCount') {
let books = '1 book';
@@ -101,8 +129,10 @@ function AuthorIndexPosterInfo(props) {
AuthorIndexPosterInfo.propTypes = {
qualityProfile: PropTypes.object.isRequired,
showQualityProfile: PropTypes.bool.isRequired,
previousAiring: PropTypes.string,
metadataProfile: PropTypes.object.isRequired,
added: PropTypes.string,
nextBook: PropTypes.object,
lastBook: PropTypes.object,
bookCount: PropTypes.number.isRequired,
path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number,

View File

@@ -116,7 +116,9 @@ class AuthorIndexPosters extends Component {
posterOptions,
jumpToCharacter,
isSmallScreen,
scrollTop
isEditorActive,
scrollTop,
selectedState
} = this.props;
const {
@@ -138,6 +140,8 @@ class AuthorIndexPosters extends Component {
prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items)) ||
prevProps.isEditorActive !== isEditorActive ||
prevProps.selectedState !== selectedState ||
prevProps.posterOptions.showTitle !== posterOptions.showTitle) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
@@ -198,7 +202,10 @@ class AuthorIndexPosters extends Component {
posterOptions,
showRelativeDates,
shortDateFormat,
timeFormat
timeFormat,
selectedState,
isEditorActive,
onSelectedChange
} = this.props;
const {
@@ -246,6 +253,9 @@ class AuthorIndexPosters extends Component {
authorId={author.id}
qualityProfileId={author.qualityProfileId}
metadataProfileId={author.metadataProfileId}
isSelected={selectedState[author.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/>
</div>
);
@@ -328,7 +338,10 @@ AuthorIndexPosters.propTypes = {
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired
timeFormat: PropTypes.string.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
};
export default AuthorIndexPosters;

View File

@@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import AuthorIndexTableOptionsConnector from './AuthorIndexTableOptionsConnector';
import hasGrowableColumns from './hasGrowableColumns';
@@ -15,6 +16,10 @@ function AuthorIndexHeader(props) {
showBanners,
columns,
onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
isEditorActive,
...otherProps
} = props;
@@ -33,6 +38,21 @@ function AuthorIndexHeader(props) {
return null;
}
if (name === 'select') {
if (isEditorActive) {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
return null;
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
@@ -80,6 +100,10 @@ function AuthorIndexHeader(props) {
AuthorIndexHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired,
showBanners: PropTypes.bool.isRequired
};

View File

@@ -13,6 +13,7 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import getProgressBarKind from 'Utilities/Author/getProgressBarKind';
@@ -101,8 +102,11 @@ class AuthorIndexRow extends Component {
columns,
isRefreshingAuthor,
isSearchingAuthor,
isEditorActive,
isSelected,
onRefreshAuthorPress,
onSearchPress
onSearchPress,
onSelectedChange
} = this.props;
const {
@@ -131,6 +135,19 @@ class AuthorIndexRow extends Component {
return null;
}
if (isEditorActive && name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={id}
key={name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'status') {
return (
<AuthorStatusCell
@@ -431,7 +448,10 @@ AuthorIndexRow.propTypes = {
isRefreshingAuthor: PropTypes.bool.isRequired,
isSearchingAuthor: PropTypes.bool.isRequired,
onRefreshAuthorPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired
onSearchPress: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
AuthorIndexRow.defaultProps = {

View File

@@ -48,6 +48,9 @@ class AuthorIndexTable extends Component {
const {
items,
columns,
selectedState,
onSelectedChange,
isEditorActive,
showBanners,
showTitle
} = this.props;
@@ -67,6 +70,9 @@ class AuthorIndexTable extends Component {
authorId={author.id}
qualityProfileId={author.qualityProfileId}
metadataProfileId={author.metadataProfileId}
isSelected={selectedState[author.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
showBanners={showBanners}
showTitle={showTitle}
/>
@@ -87,7 +93,12 @@ class AuthorIndexTable extends Component {
isSmallScreen,
onSortPress,
scroller,
scrollTop
scrollTop,
allSelected,
allUnselected,
onSelectAllChange,
isEditorActive,
selectedState
} = this.props;
return (
@@ -108,8 +119,13 @@ class AuthorIndexTable extends Component {
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
isEditorActive={isEditorActive}
/>
}
selectedState={selectedState}
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
@@ -129,7 +145,13 @@ AuthorIndexTable.propTypes = {
scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onSortPress: PropTypes.func.isRequired
onSortPress: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
};
export default AuthorIndexTable;

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -10,7 +11,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const NO_CHANGE = 'noChange';
@@ -92,6 +93,12 @@ class MonitoringOptionsModalContent extends Component {
</ModalHeader>
<ModalBody>
<Alert kind={kinds.INFO}>
<div>
{translate('MonitorBookExistingOnlyWarning')}
</div>
</Alert>
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Monitoring')}</FormLabel>

View File

@@ -8,6 +8,7 @@ import * as commandNames from 'Commands/commandNames';
import { toggleBooksMonitored } from 'Store/Actions/bookActions';
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
@@ -43,11 +44,12 @@ function createMapStateToProps() {
(state, { titleSlug }) => titleSlug,
selectBookFiles,
(state) => state.books,
(state) => state.editions,
createAllAuthorSelector(),
createCommandsSelector(),
createUISettingsSelector(),
createDimensionsSelector(),
(titleSlug, bookFiles, books, authors, commands, uiSettings, dimensions) => {
(titleSlug, bookFiles, books, editions, authors, commands, uiSettings, dimensions) => {
const book = books.items.find((b) => b.titleSlug === titleSlug);
const author = authors.find((a) => a.id === book.authorId);
const sortedBooks = books.items.filter((b) => b.authorId === book.authorId);
@@ -79,8 +81,8 @@ function createMapStateToProps() {
isRefreshingCommand.body.bookId === book.id
);
const isFetching = isBookFilesFetching;
const isPopulated = isBookFilesPopulated;
const isFetching = isBookFilesFetching || editions.isFetching;
const isPopulated = isBookFilesPopulated && editions.isPopulated;
return {
...book,
@@ -104,6 +106,8 @@ const mapDispatchToProps = {
executeCommand,
fetchBookFiles,
clearBookFiles,
fetchEditions,
clearEditions,
clearReleases,
cancelFetchReleases,
toggleBooksMonitored
@@ -121,7 +125,8 @@ class BookDetailsConnector extends Component {
}
componentDidUpdate(prevProps) {
if (!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
if (prevProps.id !== this.props.id ||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
this.unpopulate();
this.populate();
@@ -140,12 +145,14 @@ class BookDetailsConnector extends Component {
const bookId = this.props.id;
this.props.fetchBookFiles({ bookId });
this.props.fetchEditions({ bookId });
}
unpopulate = () => {
this.props.cancelFetchReleases();
this.props.clearReleases();
this.props.clearBookFiles();
this.props.clearEditions();
}
//
@@ -195,6 +202,8 @@ BookDetailsConnector.propTypes = {
titleSlug: PropTypes.string.isRequired,
fetchBookFiles: PropTypes.func.isRequired,
clearBookFiles: PropTypes.func.isRequired,
fetchEditions: PropTypes.func.isRequired,
clearEditions: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired,
cancelFetchReleases: PropTypes.func.isRequired,
toggleBooksMonitored: PropTypes.func.isRequired,

View File

@@ -149,21 +149,24 @@ class BookDetailsHeader extends Component {
</div>
<div className={styles.detailsLabels}>
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.CALENDAR}
size={17}
/>
{
releaseDate &&
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.CALENDAR}
size={17}
/>
<span className={styles.sizeOnDisk}>
{
moment(releaseDate).format(shortDateFormat)
}
</span>
</Label>
<span className={styles.sizeOnDisk}>
{
moment(releaseDate).format(shortDateFormat)
}
</span>
</Label>
}
<Label
className={styles.detailsLabel}

View File

@@ -8,15 +8,25 @@ import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import BookDetailsHeader from './BookDetailsHeader';
const selectOverview = createSelector(
(state) => state.editions,
(editions) => {
const monitored = editions.items.find((e) => e.monitored === true);
return monitored?.overview;
}
);
function createMapStateToProps() {
return createSelector(
createBookSelector(),
selectOverview,
createUISettingsSelector(),
createDimensionsSelector(),
(book, uiSettings, dimensions) => {
(book, overview, uiSettings, dimensions) => {
return {
...book,
overview,
shortDateFormat: uiSettings.shortDateFormat,
isSmallScreen: dimensions.isSmallScreen
};

View File

@@ -6,11 +6,13 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
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 } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
class EditBookModalContent extends Component {
@@ -36,6 +38,9 @@ class EditBookModalContent extends Component {
authorName,
statistics,
item,
isFetching,
isPopulated,
error,
isSaving,
onInputChange,
onModalClose,
@@ -48,7 +53,8 @@ class EditBookModalContent extends Component {
editions
} = item;
const hasFile = statistics ? statistics.bookFileCount : 0;
const hasFile = statistics ? statistics.bookFileCount > 0 : false;
const errorMessage = getErrorMessage(error, 'Unable to load editions');
return (
<ModalContent onModalClose={onModalClose}>
@@ -88,20 +94,33 @@ class EditBookModalContent extends Component {
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('Edition')}
</FormLabel>
{
isFetching &&
<LoadingIndicator />
}
<FormInputGroup
type={inputTypes.BOOK_EDITION_SELECT}
name="editions"
helpText={translate('EditionsHelpText')}
isDisabled={anyEditionOk.value && hasFile}
bookEditions={editions}
onChange={onInputChange}
/>
</FormGroup>
{
error &&
<div>{errorMessage}</div>
}
{
isPopulated && !isFetching && !!editions.value.length &&
<FormGroup>
<FormLabel>
{translate('Edition')}
</FormLabel>
<FormInputGroup
type={inputTypes.BOOK_EDITION_SELECT}
name="editions"
helpText={translate('EditionsHelpText')}
isDisabled={anyEditionOk.value && hasFile}
bookEditions={editions}
onChange={onInputChange}
/>
</FormGroup>
}
</Form>
</ModalBody>
@@ -131,6 +150,9 @@ EditBookModalContent.propTypes = {
authorName: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
item: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,

View File

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveBook, setBookValue } from 'Store/Actions/bookActions';
import { saveEditions } from 'Store/Actions/editionActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createBookSelector from 'Store/Selectors/createBookSelector';
import selectSettings from 'Store/Selectors/selectSettings';
@@ -12,20 +13,27 @@ import EditBookModalContent from './EditBookModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.books,
(state) => state.editions,
createBookSelector(),
createAuthorSelector(),
(bookState, book, author) => {
(bookState, editionState, book, author) => {
const {
isSaving,
saveError,
pendingChanges
} = bookState;
const {
isFetching,
isPopulated,
error
} = editionState;
const bookSettings = _.pick(book, [
'monitored',
'anyEditionOk',
'editions'
'anyEditionOk'
]);
bookSettings.editions = editionState.items;
const settings = selectSettings(bookSettings, pendingChanges, saveError);
@@ -34,6 +42,9 @@ function createMapStateToProps() {
authorName: author.authorName,
bookType: book.bookType,
statistics: book.statistics,
isFetching,
isPopulated,
error,
isSaving,
saveError,
item: settings.settings,
@@ -45,7 +56,8 @@ function createMapStateToProps() {
const mapDispatchToProps = {
dispatchSetBookValue: setBookValue,
dispatchSaveBook: saveBook
dispatchSaveBook: saveBook,
dispatchSaveEditions: saveEditions
};
class EditBookModalContentConnector extends Component {
@@ -70,6 +82,9 @@ class EditBookModalContentConnector extends Component {
this.props.dispatchSaveBook({
id: this.props.bookId
});
this.props.dispatchSaveEditions({
id: this.props.bookId
});
}
//
@@ -92,6 +107,7 @@ EditBookModalContentConnector.propTypes = {
saveError: PropTypes.object,
dispatchSetBookValue: PropTypes.func.isRequired,
dispatchSaveBook: PropTypes.func.isRequired,
dispatchSaveEditions: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -0,0 +1,70 @@
.inputContainer {
margin-right: 20px;
min-width: 150px;
}
.buttonContainer {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.buttonContainerContent {
flex-grow: 0;
}
.buttons {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.organizeSelectedButton,
.tagsButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-right: 10px;
height: 35px;
}
.deleteSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-left: 50px;
height: 35px;
}
@media only screen and (max-width: $breakpointExtraLarge) {
.deleteSelectedButton {
margin-left: 0;
}
}
@media only screen and (max-width: $breakpointLarge) {
.buttonContainer {
justify-content: flex-start;
margin-top: 10px;
}
}
@media only screen and (max-width: $breakpointSmall) {
.inputContainer {
margin-right: 0;
}
.buttonContainer {
justify-content: flex-start;
}
.buttonContainerContent {
flex-grow: 1;
}
.buttons {
justify-content: space-between;
}
.selectedAuthorLabel {
text-align: left;
}
}

View File

@@ -0,0 +1,156 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BookEditorFooterLabel from './BookEditorFooterLabel';
import DeleteBookModal from './Delete/DeleteBookModal';
import styles from './BookEditorFooter.css';
const NO_CHANGE = 'noChange';
class BookEditorFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
monitored: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
isDeleteBookModalOpen: false,
isTagsModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
};
}
componentDidUpdate(prevProps) {
const {
isSaving,
saveError
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false
});
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
if (value === NO_CHANGE) {
return;
}
switch (name) {
case 'monitored':
this.props.onSaveSelected({ [name]: value === 'monitored' });
break;
default:
this.props.onSaveSelected({ [name]: value });
}
}
onDeleteSelectedPress = () => {
this.setState({ isDeleteBookModalOpen: true });
}
onDeleteBookModalClose = () => {
this.setState({ isDeleteBookModalOpen: false });
}
//
// Render
render() {
const {
bookIds,
selectedCount,
isSaving,
isDeleting
} = this.props;
const {
monitored,
isDeleteBookModalOpen
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'monitored', value: 'Monitored' },
{ key: 'unmonitored', value: 'Unmonitored' }
];
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<BookEditorFooterLabel
label={translate('MonitorBook')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<BookEditorFooterLabel
label={translate('SelectedCountBooksSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
Delete
</SpinnerButton>
</div>
</div>
</div>
<DeleteBookModal
isOpen={isDeleteBookModalOpen}
bookIds={bookIds}
onModalClose={this.onDeleteBookModalClose}
/>
</PageContentFooter>
);
}
}
BookEditorFooter.propTypes = {
bookIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSaveSelected: PropTypes.func.isRequired
};
export default BookEditorFooter;

View File

@@ -0,0 +1,8 @@
.label {
margin-bottom: 3px;
font-weight: bold;
}
.savingIcon {
margin-left: 8px;
}

View File

@@ -0,0 +1,40 @@
import PropTypes from 'prop-types';
import React from 'react';
import SpinnerIcon from 'Components/SpinnerIcon';
import { icons } from 'Helpers/Props';
import styles from './BookEditorFooterLabel.css';
function BookEditorFooterLabel(props) {
const {
className,
label,
isSaving
} = props;
return (
<div className={className}>
{label}
{
isSaving &&
<SpinnerIcon
className={styles.savingIcon}
name={icons.SPINNER}
isSpinning={true}
/>
}
</div>
);
}
BookEditorFooterLabel.propTypes = {
className: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired
};
BookEditorFooterLabel.defaultProps = {
className: styles.label
};
export default BookEditorFooterLabel;

View File

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

View File

@@ -0,0 +1,9 @@
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.deleteFilesMessage {
margin-top: 20px;
color: $dangerColor;
}

View File

@@ -0,0 +1,172 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import 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 } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './DeleteBookModalContent.css';
class DeleteBookModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false,
addImportListExclusion: true
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
}
onAddImportListExclusionChange = ({ value }) => {
this.setState({ addImportListExclusion: value });
}
onDeleteBookConfirmed = () => {
const {
deleteFiles,
addImportListExclusion
} = this.state;
this.setState({ deleteFiles: false });
this.props.onDeleteSelectedPress(deleteFiles, addImportListExclusion);
}
//
// Render
render() {
const {
book,
files,
onModalClose
} = this.props;
const {
deleteFiles,
addImportListExclusion
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Delete Selected Book
</ModalHeader>
<ModalBody>
<div>
<FormGroup>
<FormLabel>{`Delete File${book.length > 1 ? 's' : ''}`}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="deleteFiles"
value={deleteFiles}
helpText={translate('DeleteFilesHelpText')}
kind={kinds.DANGER}
isDisabled={files.length === 0}
onChange={this.onDeleteFilesChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AddListExclusion')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="addImportListExclusion"
value={addImportListExclusion}
helpText={translate('AddImportListExclusionHelpText')}
kind={kinds.DANGER}
onChange={this.onAddImportListExclusionChange}
/>
</FormGroup>
{
!addImportListExclusion &&
<div className={styles.deleteFilesMessage}>
<div>
{translate('IfYouDontAddAnImportListExclusionAndTheAuthorHasAMetadataProfileOtherThanNoneThenThisBookMayBeReaddedDuringTheNextAuthorRefresh')}
</div>
</div>
}
</div>
<div className={styles.message}>
{`Are you sure you want to delete ${book.length} selected book${book.length > 1 ? 's' : ''}${deleteFiles ? ' and their files' : ''}?`}
</div>
<ul>
{
book.map((s) => {
return (
<li key={s.title}>
<span>{s.title}</span>
</li>
);
})
}
</ul>
{
deleteFiles &&
<div>
<div className={styles.deleteFilesMessage}>
{translate('TheFollowingFilesWillBeDeleted')}
</div>
<ul>
{
files.map((s) => {
return (
<li key={s.path}>
<span>{s.path}</span>
</li>
);
})
}
</ul>
</div>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteBookConfirmed}
>
Delete
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteBookModalContent.propTypes = {
book: PropTypes.arrayOf(PropTypes.object).isRequired,
files: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteSelectedPress: PropTypes.func.isRequired
};
export default DeleteBookModalContent;

View File

@@ -0,0 +1,54 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { bulkDeleteBook } from 'Store/Actions/bookIndexActions';
import DeleteBookModalContent from './DeleteBookModalContent';
function createMapStateToProps() {
return createSelector(
(state, { bookIds }) => bookIds,
(state) => state.books.items,
(state) => state.bookFiles.items,
(bookIds, allBooks, allBookFiles) => {
const selectedBook = _.intersectionWith(allBooks, bookIds, (s, id) => {
return s.id === id;
});
const sortedBook = _.orderBy(selectedBook, 'title');
const selectedFiles = _.intersectionWith(allBookFiles, bookIds, (s, id) => {
return s.bookId === id;
});
const files = _.orderBy(selectedFiles, ['bookId', 'path']);
const book = _.map(sortedBook, (s) => {
return {
title: s.title,
path: s.path
};
});
return {
book,
files
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onDeleteSelectedPress(deleteFiles, addImportListExclusion) {
dispatch(bulkDeleteBook({
bookIds: props.bookIds,
deleteFiles,
addImportListExclusion
}));
props.onModalClose();
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteBookModalContent);

View File

@@ -2,7 +2,9 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import NoAuthor from 'Author/NoAuthor';
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageJumpBar from 'Components/Page/PageJumpBar';
@@ -11,10 +13,13 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BookIndexFooterConnector from './BookIndexFooterConnector';
import BookIndexFilterMenu from './Menus/BookIndexFilterMenu';
import BookIndexSortMenu from './Menus/BookIndexSortMenu';
@@ -52,12 +57,19 @@ class BookIndex extends Component {
jumpBarItems: { order: [] },
jumpToCharacter: null,
isPosterOptionsModalOpen: false,
isOverviewOptionsModalOpen: false
isOverviewOptionsModalOpen: false,
isConfirmSearchModalOpen: false,
isEditorActive: false,
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
};
}
componentDidMount() {
this.setJumpBarItems();
this.setSelectedState();
}
componentDidUpdate(prevProps) {
@@ -72,6 +84,7 @@ class BookIndex extends Component {
hasDifferentItemsOrOrder(prevProps.items, items)
) {
this.setJumpBarItems();
this.setSelectedState();
}
if (this.state.jumpToCharacter != null) {
@@ -86,6 +99,48 @@ class BookIndex extends Component {
this.setState({ scroller: ref });
}
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState);
}
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((book) => {
const isItemSelected = selectedState[book.id];
if (isItemSelected) {
newSelectedState[book.id] = isItemSelected;
} else {
newSelectedState[book.id] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
}
setJumpBarItems() {
const {
items,
@@ -150,10 +205,64 @@ class BookIndex extends Component {
this.setState({ isOverviewOptionsModalOpen: false });
}
onEditorTogglePress = () => {
if (this.state.isEditorActive) {
this.setState({ isEditorActive: false });
} else {
const newState = selectAll(this.state.selectedState, false);
newState.isEditorActive = true;
this.setState(newState);
}
}
onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter });
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onSaveSelected = (changes) => {
this.props.onSaveSelected({
bookIds: this.getSelectedIds(),
...changes
});
}
onSearchPress = () => {
this.setState({ isConfirmSearchModalOpen: true });
}
onRefreshBookPress = () => {
const selectedIds = this.getSelectedIds();
const refreshIds = this.state.isEditorActive && selectedIds.length > 0 ? selectedIds : [];
this.props.onRefreshBookPress(refreshIds);
}
onSearchConfirmed = () => {
const selectedMovieIds = this.getSelectedIds();
const searchIds = this.state.isMovieEditorActive && selectedMovieIds.length > 0 ? selectedMovieIds : this.props.items.map((m) => m.id);
this.props.onSearchPress(searchIds);
this.setState({ isConfirmSearchModalOpen: false });
}
onConfirmSearchModalClose = () => {
this.setState({ isConfirmSearchModalOpen: false });
}
//
// Render
@@ -173,11 +282,15 @@ class BookIndex extends Component {
view,
isRefreshingBook,
isRssSyncExecuting,
isSearching,
isSaving,
saveError,
isDeleting,
deleteError,
onScroll,
onSortSelect,
onFilterSelect,
onViewSelect,
onRefreshAuthorPress,
onRssSyncPress,
...otherProps
} = this.props;
@@ -187,23 +300,35 @@ class BookIndex extends Component {
jumpBarItems,
jumpToCharacter,
isPosterOptionsModalOpen,
isOverviewOptionsModalOpen
isOverviewOptionsModalOpen,
isConfirmSearchModalOpen,
isEditorActive,
selectedState,
allSelected,
allUnselected
} = this.state;
const selectedBookIds = this.getSelectedIds();
const ViewComponent = getViewComponent(view);
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoAuthor = !totalItems;
const refreshLabel = isEditorActive && selectedBookIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll');
const searchIndexLabel = selectedFilterKey === 'all' ? translate('SearchAll') : translate('SearchFiltered');
const searchEditorLabel = selectedBookIds.length > 0 ? translate('SearchSelected') : translate('SearchAll');
const searchWarningCount = isEditorActive && selectedBookIds.length > 0 ? selectedBookIds.length : items.length;
return (
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('UpdateAll')}
label={refreshLabel}
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
isSpinning={isRefreshingBook}
onPress={onRefreshAuthorPress}
onPress={this.onRefreshBookPress}
/>
<PageToolbarButton
@@ -214,6 +339,44 @@ class BookIndex extends Component {
onPress={onRssSyncPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={isEditorActive ? searchEditorLabel : searchIndexLabel}
iconName={icons.SEARCH}
isDisabled={isSearching || !items.length}
onPress={this.onSearchPress}
/>
<PageToolbarSeparator />
{
isEditorActive ?
<PageToolbarButton
label={translate('BookIndex')}
iconName={icons.AUTHOR_CONTINUING}
isDisabled={hasNoAuthor}
onPress={this.onEditorTogglePress}
/> :
<PageToolbarButton
label={translate('BookEditor')}
iconName={icons.EDIT}
isDisabled={hasNoAuthor}
onPress={this.onEditorTogglePress}
/>
}
{
isEditorActive ?
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
isDisabled={hasNoAuthor}
onPress={this.onSelectAllPress}
/> :
null
}
</PageToolbarSection>
<PageToolbarSection
@@ -311,6 +474,12 @@ class BookIndex extends Component {
sortKey={sortKey}
sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter}
isEditorActive={isEditorActive}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps}
/>
@@ -336,6 +505,19 @@ class BookIndex extends Component {
}
</div>
{
isLoaded && isEditorActive &&
<BookEditorFooter
bookIds={selectedBookIds}
selectedCount={selectedBookIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
onSaveSelected={this.onSaveSelected}
/>
}
<BookIndexPosterOptionsModal
isOpen={isPosterOptionsModalOpen}
onModalClose={this.onPosterOptionsModalClose}
@@ -346,6 +528,25 @@ class BookIndex extends Component {
onModalClose={this.onOverviewOptionsModalClose}
/>
<ConfirmModal
isOpen={isConfirmSearchModalOpen}
kind={kinds.DANGER}
title={translate('MassBookSearch')}
message={
<div>
<div>
{translate('MassBookSearchWarning', [searchWarningCount])}
</div>
<div>
{translate('ThisCannotBeCancelled')}
</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={this.onSearchConfirmed}
onCancel={this.onConfirmSearchModalClose}
/>
</PageContent>
);
}
@@ -365,14 +566,21 @@ BookIndex.propTypes = {
sortDirection: PropTypes.oneOf(sortDirections.all),
view: PropTypes.string.isRequired,
isRefreshingBook: PropTypes.bool.isRequired,
isSearching: PropTypes.bool.isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onViewSelect: PropTypes.func.isRequired,
onRefreshAuthorPress: PropTypes.func.isRequired,
onRefreshBookPress: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired
onSearchPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};
export default BookIndex;

View File

@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { setBookFilter, setBookSort, setBookTableOption, setBookView } from 'Store/Actions/bookIndexActions';
import { saveBookEditor, setBookFilter, setBookSort, setBookTableOption, setBookView } from 'Store/Actions/bookIndexActions';
import { executeCommand } from 'Store/Actions/commandActions';
import scrollPositions from 'Store/scrollPositions';
import createBookClientSideCollectionItemsSelector from 'Store/Selectors/createBookClientSideCollectionItemsSelector';
@@ -19,12 +19,16 @@ function createMapStateToProps() {
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
createDimensionsSelector(),
(
book,
isRefreshingAuthorCommand,
isRefreshingBookCommand,
isRssSyncExecuting,
isCutoffBooksSearch,
isMissingBooksSearch,
dimensionsState
) => {
const isRefreshingBook = isRefreshingBookCommand || isRefreshingAuthorCommand;
@@ -32,6 +36,7 @@ function createMapStateToProps() {
...book,
isRefreshingBook,
isRssSyncExecuting,
isSearching: isCutoffBooksSearch || isMissingBooksSearch,
isSmallScreen: dimensionsState.isSmallScreen
};
}
@@ -56,9 +61,14 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(setBookView({ view }));
},
onRefreshAuthorPress() {
dispatchSaveBookEditor(payload) {
dispatch(saveBookEditor(payload));
},
onRefreshBookPress(items) {
dispatch(executeCommand({
name: commandNames.REFRESH_AUTHOR
name: commandNames.BULK_REFRESH_BOOK,
bookIds: items
}));
},
@@ -66,6 +76,13 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(executeCommand({
name: commandNames.RSS_SYNC
}));
},
onSearchPress(items) {
dispatch(executeCommand({
name: commandNames.BOOK_SEARCH,
bookIds: items
}));
}
};
}
@@ -79,6 +96,10 @@ class BookIndexConnector extends Component {
this.props.dispatchSetBookView(view);
}
onSaveSelected = (payload) => {
this.props.dispatchSaveBookEditor(payload);
}
onScroll = ({ scrollTop }) => {
scrollPositions.bookIndex = scrollTop;
}
@@ -92,6 +113,7 @@ class BookIndexConnector extends Component {
{...this.props}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
/>
);
}
@@ -100,7 +122,8 @@ class BookIndexConnector extends Component {
BookIndexConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
dispatchSetBookView: PropTypes.func.isRequired
dispatchSetBookView: PropTypes.func.isRequired,
dispatchSaveBookEditor: PropTypes.func.isRequired
};
export default withScrollPosition(

View File

@@ -14,16 +14,18 @@ class BookIndexFooter extends PureComponent {
// Render
render() {
const { author } = this.props;
const count = author.length;
const { book } = this.props;
const count = book.length;
let books = 0;
let bookFiles = 0;
let ended = 0;
let continuing = 0;
let monitored = 0;
let totalFileSize = 0;
author.forEach((s) => {
const authors = new Set();
book.forEach((s) => {
authors.add(s.authorId);
const { statistics = {} } = s;
const {
@@ -35,12 +37,6 @@ class BookIndexFooter extends PureComponent {
books += bookCount;
bookFiles += bookFileCount;
if (s.status === 'ended') {
ended++;
} else {
continuing++;
}
if (s.monitored) {
monitored++;
}
@@ -104,23 +100,6 @@ class BookIndexFooter extends PureComponent {
</div>
<div className={styles.statistics}>
<DescriptionList>
<DescriptionListItem
title={translate('Authors')}
data={count}
/>
<DescriptionListItem
title={translate('Ended')}
data={ended}
/>
<DescriptionListItem
title={translate('Continuing')}
data={continuing}
/>
</DescriptionList>
<DescriptionList>
<DescriptionListItem
title={translate('Monitored')}
@@ -134,6 +113,11 @@ class BookIndexFooter extends PureComponent {
</DescriptionList>
<DescriptionList>
<DescriptionListItem
title={translate('Authors')}
data={authors.size}
/>
<DescriptionListItem
title={translate('Books')}
data={books}
@@ -161,7 +145,7 @@ class BookIndexFooter extends PureComponent {
}
BookIndexFooter.propTypes = {
author: PropTypes.arrayOf(PropTypes.object).isRequired
book: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default BookIndexFooter;

View File

@@ -6,16 +6,18 @@ import BookIndexFooter from './BookIndexFooter';
function createUnoptimizedSelector() {
return createSelector(
createClientSideCollectionSelector('authors', 'authorIndex'),
(authors) => {
return authors.items.map((s) => {
createClientSideCollectionSelector('books', 'bookIndex'),
(books) => {
return books.items.map((s) => {
const {
authorId,
monitored,
status,
statistics
} = s;
return {
authorId,
monitored,
status,
statistics
@@ -25,19 +27,19 @@ function createUnoptimizedSelector() {
);
}
function createAuthorSelector() {
function createBookSelector() {
return createDeepEqualSelector(
createUnoptimizedSelector(),
(author) => author
(book) => book
);
}
function createMapStateToProps() {
return createSelector(
createAuthorSelector(),
(author) => {
createBookSelector(),
(book) => {
return {
author
book
};
}
);

View File

@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import createBookAuthorSelector from 'Store/Selectors/createBookAuthorSelector';
import createBookQualityProfileSelector from 'Store/Selectors/createBookQualityProfileSelector';
import createBookSelector from 'Store/Selectors/createBookSelector';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
@@ -32,17 +33,19 @@ function selectShowSearchAction() {
function createMapStateToProps() {
return createSelector(
createBookSelector(),
createBookAuthorSelector(),
createBookQualityProfileSelector(),
selectShowSearchAction(),
createExecutingCommandsSelector(),
(
book,
author,
qualityProfile,
showSearchAction,
executingCommands
) => {
// If an book is deleted this selector may fire before the parent
// If a book is deleted this selector may fire before the parent
// selectors, which will result in an undefined book, if that happens
// we want to return early here and again in the render function to avoid
// trying to show an book that has no information available.
@@ -54,7 +57,7 @@ function createMapStateToProps() {
const isRefreshingBook = executingCommands.some((command) => {
return (
(command.name === commandNames.REFRESH_AUTHOR &&
command.body.authorId === book.author.id) ||
command.body.authorId === book.authorId) ||
(command.name === commandNames.REFRESH_BOOK &&
command.body.bookId === book.id)
);
@@ -63,7 +66,7 @@ function createMapStateToProps() {
const isSearchingBook = executingCommands.some((command) => {
return (
(command.name === commandNames.AUTHOR_SEARCH &&
command.body.authorId === book.author.id) ||
command.body.authorId === book.authorId) ||
(command.name === commandNames.BOOK_SEARCH &&
command.body.bookIds.includes(book.id))
);
@@ -71,6 +74,7 @@ function createMapStateToProps() {
return {
...book,
author,
qualityProfile,
showSearchAction,
isRefreshingBook,

View File

@@ -46,6 +46,15 @@ function BookIndexSortMenu(props) {
Author, Title
</SortMenuItem>
<SortMenuItem
name="releaseDate"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Release Date
</SortMenuItem>
<SortMenuItem
name="qualityProfileId"
sortKey={sortKey}

View File

@@ -19,6 +19,13 @@ $hoverScale: 1.05;
left: 0;
}
.editorSelect {
position: absolute;
top: 0;
left: 5px;
z-index: 3;
}
.posterContainer {
position: relative;
overflow: hidden;

View File

@@ -5,12 +5,14 @@ import AuthorPoster from 'Author/AuthorPoster';
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import BookIndexProgressBar from 'Book/Index/ProgressBar/BookIndexProgressBar';
import CheckInput from 'Components/Form/CheckInput';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate';
import BookIndexOverviewInfo from './BookIndexOverviewInfo';
@@ -41,10 +43,26 @@ class BookIndexOverview extends Component {
this.state = {
isEditAuthorModalOpen: false,
isDeleteAuthorModalOpen: false
isDeleteAuthorModalOpen: false,
overview: ''
};
}
componentDidMount() {
const { id } = this.props;
// Note that this component is lazy loaded by the virtualised view.
// We want to avoid storing overviews for *all* books which is
// why it's not put into the redux store
const promise = createAjaxRequest({
url: `/book/${id}/overview`
}).request;
promise.done((data) => {
this.setState({ overview: data.overview });
});
}
//
// Listeners
@@ -67,6 +85,15 @@ class BookIndexOverview extends Component {
this.setState({ isDeleteAuthorModalOpen: false });
}
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
}
//
// Render
@@ -74,7 +101,6 @@ class BookIndexOverview extends Component {
const {
id,
title,
overview,
monitored,
titleSlug,
nextAiring,
@@ -95,6 +121,8 @@ class BookIndexOverview extends Component {
isSearchingBook,
onRefreshBookPress,
onSearchPress,
isEditorActive,
isSelected,
...otherProps
} = this.props;
@@ -106,6 +134,7 @@ class BookIndexOverview extends Component {
} = statistics;
const {
overview,
isEditAuthorModalOpen,
isDeleteAuthorModalOpen
} = this.state;
@@ -125,6 +154,17 @@ class BookIndexOverview extends Component {
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.posterContainer}>
{
isEditorActive &&
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
}
{
status === 'ended' &&
<div
@@ -244,7 +284,6 @@ class BookIndexOverview extends Component {
BookIndexOverview.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
titleSlug: PropTypes.string.isRequired,
nextAiring: PropTypes.string,
@@ -264,7 +303,10 @@ BookIndexOverview.propTypes = {
isRefreshingBook: PropTypes.bool.isRequired,
isSearchingBook: PropTypes.bool.isRequired,
onRefreshBookPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired
onSearchPress: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
BookIndexOverview.defaultProps = {

View File

@@ -73,7 +73,9 @@ class BookIndexOverviews extends Component {
sortKey,
overviewOptions,
jumpToCharacter,
scrollTop
scrollTop,
isEditorActive,
selectedState
} = this.props;
const {
@@ -91,6 +93,8 @@ class BookIndexOverviews extends Component {
(prevState.width !== width ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items) ||
prevProps.isEditorActive !== isEditorActive ||
prevProps.selectedState !== selectedState ||
prevProps.overviewOptions !== overviewOptions)) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
@@ -148,7 +152,10 @@ class BookIndexOverviews extends Component {
shortDateFormat,
longDateFormat,
timeFormat,
isSmallScreen
isSmallScreen,
selectedState,
isEditorActive,
onSelectedChange
} = this.props;
const {
@@ -183,6 +190,9 @@ class BookIndexOverviews extends Component {
isSmallScreen={isSmallScreen}
bookId={book.id}
authorId={book.authorId}
isSelected={selectedState[book.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/>
</div>
);
@@ -263,7 +273,10 @@ BookIndexOverviews.propTypes = {
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired
timeFormat: PropTypes.string.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
};
export default BookIndexOverviews;

View File

@@ -78,6 +78,13 @@ $hoverScale: 1.05;
color: $white;
}
.editorSelect {
position: absolute;
top: 10px;
left: 10px;
z-index: 4;
}
.controls {
position: absolute;
bottom: 10px;

View File

@@ -5,6 +5,7 @@ import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import EditBookModalConnector from 'Book/Edit/EditBookModalConnector';
import BookIndexProgressBar from 'Book/Index/ProgressBar/BookIndexProgressBar';
import CheckInput from 'Components/Form/CheckInput';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
@@ -73,6 +74,15 @@ class BookIndexPoster extends Component {
}
}
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
}
//
// Render
@@ -103,6 +113,9 @@ class BookIndexPoster extends Component {
isSearchingBook,
onRefreshBookPress,
onSearchPress,
isEditorActive,
isSelected,
onSelectedChange,
...otherProps
} = this.props;
@@ -132,6 +145,18 @@ class BookIndexPoster extends Component {
<div>
<div className={styles.content}>
<div className={styles.posterContainer}>
{
isEditorActive &&
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
}
<Label className={styles.controls}>
<SpinnerIconButton
className={styles.action}
@@ -309,7 +334,10 @@ BookIndexPoster.propTypes = {
isRefreshingBook: PropTypes.bool.isRequired,
isSearchingBook: PropTypes.bool.isRequired,
onRefreshBookPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired
onSearchPress: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
BookIndexPoster.defaultProps = {

View File

@@ -8,8 +8,8 @@ function BookIndexPosterInfo(props) {
const {
qualityProfile,
showQualityProfile,
previousAiring,
added,
releaseDate,
author,
bookFileCount,
sizeOnDisk,
@@ -27,24 +27,6 @@ function BookIndexPosterInfo(props) {
);
}
if (sortKey === 'previousAiring' && previousAiring) {
return (
<div className={styles.info}>
{
getRelativeDate(
previousAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true
}
)
}
</div>
);
}
if (sortKey === 'added' && added) {
const addedDate = getRelativeDate(
added,
@@ -63,6 +45,24 @@ function BookIndexPosterInfo(props) {
);
}
if (sortKey === 'releaseDate' && added) {
const date = getRelativeDate(
releaseDate,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false
}
);
return (
<div className={styles.info}>
{`Released ${date}`}
</div>
);
}
if (sortKey === 'bookFileCount') {
let books = '1 file';
@@ -101,9 +101,9 @@ function BookIndexPosterInfo(props) {
BookIndexPosterInfo.propTypes = {
qualityProfile: PropTypes.object.isRequired,
showQualityProfile: PropTypes.bool.isRequired,
previousAiring: PropTypes.string,
author: PropTypes.object.isRequired,
added: PropTypes.string,
releaseDate: PropTypes.string,
bookFileCount: PropTypes.number.isRequired,
sizeOnDisk: PropTypes.number,
sortKey: PropTypes.string.isRequired,

View File

@@ -121,7 +121,9 @@ class BookIndexPosters extends Component {
posterOptions,
jumpToCharacter,
isSmallScreen,
scrollTop
isEditorActive,
scrollTop,
selectedState
} = this.props;
const {
@@ -142,7 +144,9 @@ class BookIndexPosters extends Component {
prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items))) {
hasDifferentItemsOrOrder(prevProps.items, items)) ||
prevProps.isEditorActive !== isEditorActive ||
prevProps.selectedState !== selectedState) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
@@ -202,7 +206,10 @@ class BookIndexPosters extends Component {
posterOptions,
showRelativeDates,
shortDateFormat,
timeFormat
timeFormat,
selectedState,
isEditorActive,
onSelectedChange
} = this.props;
const {
@@ -251,6 +258,9 @@ class BookIndexPosters extends Component {
style={style}
bookId={book.id}
authorId={book.authorId}
isSelected={selectedState[book.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/>
</div>
);
@@ -333,7 +343,10 @@ BookIndexPosters.propTypes = {
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired
timeFormat: PropTypes.string.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
};
export default BookIndexPosters;

View File

@@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props';
import BookIndexTableOptionsConnector from './BookIndexTableOptionsConnector';
import styles from './BookIndexHeader.css';
@@ -13,6 +14,10 @@ function BookIndexHeader(props) {
const {
columns,
onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
isEditorActive,
...otherProps
} = props;
@@ -31,6 +36,21 @@ function BookIndexHeader(props) {
return null;
}
if (name === 'select') {
if (isEditorActive) {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
return null;
}
if (name === 'actions') {
return (
<VirtualTableHeaderCell
@@ -75,7 +95,11 @@ function BookIndexHeader(props) {
BookIndexHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired
onTableOptionChange: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
};
export default BookIndexHeader;

View File

@@ -11,6 +11,7 @@ import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -100,8 +101,11 @@ class BookIndexRow extends Component {
columns,
isRefreshingBook,
isSearchingBook,
isEditorActive,
isSelected,
onRefreshBookPress,
onSearchPress
onSearchPress,
onSelectedChange
} = this.props;
const {
@@ -128,6 +132,19 @@ class BookIndexRow extends Component {
return null;
}
if (isEditorActive && name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={id}
key={name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'status') {
return (
<BookStatusCell
@@ -368,7 +385,10 @@ BookIndexRow.propTypes = {
isRefreshingBook: PropTypes.bool.isRequired,
isSearchingBook: PropTypes.bool.isRequired,
onRefreshBookPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired
onSearchPress: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
BookIndexRow.defaultProps = {

View File

@@ -47,7 +47,10 @@ class BookIndexTable extends Component {
rowRenderer = ({ key, rowIndex, style }) => {
const {
items,
columns
columns,
selectedState,
onSelectedChange,
isEditorActive
} = this.props;
const book = items[rowIndex];
@@ -64,6 +67,9 @@ class BookIndexTable extends Component {
columns={columns}
authorId={book.authorId}
bookId={book.id}
isSelected={selectedState[book.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/>
</VirtualTableRow>
);
@@ -81,7 +87,12 @@ class BookIndexTable extends Component {
isSmallScreen,
onSortPress,
scroller,
scrollTop
scrollTop,
allSelected,
allUnselected,
onSelectAllChange,
isEditorActive,
selectedState
} = this.props;
return (
@@ -101,8 +112,13 @@ class BookIndexTable extends Component {
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
isEditorActive={isEditorActive}
/>
}
selectedState={selectedState}
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
@@ -120,7 +136,13 @@ BookIndexTable.propTypes = {
scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onSortPress: PropTypes.func.isRequired
onSortPress: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
};
export default BookIndexTable;

View File

@@ -1,10 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MonitorBooksSelectInput from 'Components/Form/MonitorBooksSelectInput';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './BookshelfFooter.css';
const NO_CHANGE = 'noChange';
@@ -19,7 +21,8 @@ class BookshelfFooter extends Component {
this.state = {
monitored: NO_CHANGE,
monitor: NO_CHANGE
monitor: NO_CHANGE,
monitorNewItems: NO_CHANGE
};
}
@@ -32,7 +35,8 @@ class BookshelfFooter extends Component {
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
monitor: NO_CHANGE
monitor: NO_CHANGE,
monitorNewItems: NO_CHANGE
});
}
}
@@ -47,7 +51,8 @@ class BookshelfFooter extends Component {
onUpdateSelectedPress = () => {
const {
monitor,
monitored
monitored,
monitorNewItems
} = this.state;
const changes = {};
@@ -60,6 +65,10 @@ class BookshelfFooter extends Component {
changes.monitor = monitor;
}
if (monitorNewItems !== NO_CHANGE) {
changes.monitorNewItems = monitorNewItems;
}
this.props.onUpdateSelectedPress(changes);
}
@@ -74,7 +83,8 @@ class BookshelfFooter extends Component {
const {
monitored,
monitor
monitor,
monitorNewItems
} = this.state;
const monitoredOptions = [
@@ -83,7 +93,9 @@ class BookshelfFooter extends Component {
{ key: 'unmonitored', value: 'Unmonitored' }
];
const noChanges = monitored === NO_CHANGE && monitor === NO_CHANGE;
const noChanges = monitored === NO_CHANGE &&
monitor === NO_CHANGE &&
monitorNewItems === NO_CHANGE;
return (
<PageContentFooter>
@@ -103,7 +115,7 @@ class BookshelfFooter extends Component {
<div className={styles.inputContainer}>
<div className={styles.label}>
Monitor Books
{translate('MonitorExistingBooks')}
</div>
<MonitorBooksSelectInput
@@ -115,6 +127,20 @@ class BookshelfFooter extends Component {
/>
</div>
<div className={styles.inputContainer}>
<div className={styles.label}>
{translate('MonitorNewBooks')}
</div>
<MonitorNewItemsSelectInput
name="monitorNewItems"
value={monitorNewItems}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div>
<div className={styles.label}>
{selectedCount} Author(s) Selected
@@ -127,7 +153,7 @@ class BookshelfFooter extends Component {
isDisabled={!selectedCount || noChanges}
onPress={this.onUpdateSelectedPress}
>
Update Selected
{translate('UpdateSelected')}
</SpinnerButton>
</div>
</PageContentFooter>

View File

@@ -1,7 +1,7 @@
export const APPLICATION_UPDATE = 'ApplicationUpdate';
export const BACKUP = 'Backup';
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
export const CLEAR_BLACKLIST = 'ClearBlacklist';
export const CLEAR_BLOCKLIST = 'ClearBlocklist';
export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_BOOK_SEARCH = 'CutoffUnmetBookSearch';
export const DELETE_LOG_FILES = 'DeleteLogFiles';
@@ -12,7 +12,9 @@ export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_BOOK_SEARCH = 'MissingBookSearch';
export const MOVE_AUTHOR = 'MoveAuthor';
export const REFRESH_AUTHOR = 'RefreshAuthor';
export const BULK_REFRESH_AUTHOR = 'BulkRefreshAuthor';
export const REFRESH_BOOK = 'RefreshBook';
export const BULK_REFRESH_BOOK = 'BulkRefreshBook';
export const RENAME_FILES = 'RenameFiles';
export const RENAME_AUTHOR = 'RenameAuthor';
export const RESCAN_FOLDERS = 'RescanFolders';

View File

@@ -160,6 +160,7 @@ class DateFilterBuilderRowValue extends Component {
<TextInput
name={NAME}
value={filterValue}
type="date"
placeholder="yyyy-mm-dd"
onChange={this.onValueChange}
/>

View File

@@ -8,6 +8,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import FilterBuilderRow from './FilterBuilderRow';
import styles from './FilterBuilderModalContent.css';
@@ -165,7 +166,9 @@ class FilterBuilderModalContent extends Component {
</div>
</div>
<div className={styles.label}>Filters</div>
<div className={styles.label}>
{translate('Filters')}
</div>
<div className={styles.rows}>
{

View File

@@ -12,9 +12,9 @@ import ModalBody from 'Components/Modal/ModalBody';
import Portal from 'Components/Portal';
import Scroller from 'Components/Scroller/Scroller';
import { icons, scrollDirections, sizes } from 'Helpers/Props';
import { isMobile as isMobileUtil } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import HintedSelectInputOption from './HintedSelectInputOption';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput';

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
// import translate from 'Utilities/String/translate';
import AutoCompleteInput from './AutoCompleteInput';
import BookEditionSelectInputConnector from './BookEditionSelectInputConnector';
@@ -16,6 +17,7 @@ import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
import MonitorBooksSelectInput from './MonitorBooksSelectInput';
import MonitorNewItemsSelectInput from './MonitorNewItemsSelectInput';
import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput';
@@ -51,6 +53,9 @@ function getComponent(type) {
case inputTypes.MONITOR_BOOKS_SELECT:
return MonitorBooksSelectInput;
case inputTypes.MONITOR_NEW_ITEMS_SELECT:
return MonitorNewItemsSelectInput;
case inputTypes.NUMBER:
return NumberInput;
@@ -212,7 +217,7 @@ function FormInputGroup(props) {
<Link
to={helpLink}
>
More Info
{translate('MoreInfo')}
</Link>
}

View File

@@ -7,6 +7,7 @@ function MonitorBooksSelectInput(props) {
const {
includeNoChange,
includeMixed,
includeSpecificBook,
...otherProps
} = props;
@@ -28,6 +29,13 @@ function MonitorBooksSelectInput(props) {
});
}
if (includeSpecificBook) {
values.push({
key: 'specificBook',
value: 'Only This Book'
});
}
return (
<SelectInput
values={values}
@@ -39,12 +47,14 @@ function MonitorBooksSelectInput(props) {
MonitorBooksSelectInput.propTypes = {
includeNoChange: PropTypes.bool.isRequired,
includeMixed: PropTypes.bool.isRequired,
includeSpecificBook: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
};
MonitorBooksSelectInput.defaultProps = {
includeNoChange: false,
includeMixed: false
includeMixed: false,
includeSpecificBook: false
};
export default MonitorBooksSelectInput;

View File

@@ -0,0 +1,50 @@
import PropTypes from 'prop-types';
import React from 'react';
import monitorNewItemsOptions from 'Utilities/Author/monitorNewItemsOptions';
import SelectInput from './SelectInput';
function MonitorNewItemsSelectInput(props) {
const {
includeNoChange,
includeMixed,
...otherProps
} = props;
const values = [...monitorNewItemsOptions];
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
disabled: true
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
});
}
return (
<SelectInput
values={values}
{...otherProps}
/>
);
}
MonitorNewItemsSelectInput.propTypes = {
includeNoChange: PropTypes.bool.isRequired,
includeMixed: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
};
MonitorNewItemsSelectInput.defaultProps = {
includeNoChange: false,
includeMixed: false
};
export default MonitorNewItemsSelectInput;

View File

@@ -33,7 +33,7 @@ function ConfirmModal(props) {
return () => unbindShortcut('enter', onConfirm);
}
}, [isOpen, onConfirm]);
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
return (
<Modal

View File

@@ -6,9 +6,9 @@ import ReactDOM from 'react-dom';
import FocusLock from 'react-focus-lock';
import ErrorBoundary from 'Components/Error/ErrorBoundary';
import { sizes } from 'Helpers/Props';
import { isIOS } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isIOS } from 'Utilities/mobile';
import { setScrollLock } from 'Utilities/scrollLock';
import ModalError from './ModalError';
import styles from './Modal.css';

View File

@@ -14,7 +14,7 @@ function PageContent(props) {
return (
<ErrorBoundary errorComponent={PageContentError}>
<DocumentTitle title={title ? `${title} - Readarr` : 'Readarr'}>
<DocumentTitle title={title ? `${title} - ${window.Readarr.instanceName}` : window.Readarr.instanceName}>
<div className={className}>
{children}
</div>

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