Compare commits

..

135 Commits

Author SHA1 Message Date
Michon van Dooren 6a556a89aa Fixed: Include preferred size in quality definition reset
(cherry picked from commit 8e925ac76d2f46cf5fef1ea62a20ae5e85d3000e)
2023-07-30 16:46:46 +00:00
Bogdan 4ead5186ae Bump version to 0.3.1 2023-07-30 09:12:56 +03:00
Bogdan dea797c375 Fixed: (UI) Ensure root folders are populated in Author Editor 2023-07-30 05:10:21 +03:00
PearsonFlyer 58ba24762b Fixed: Correctly calculate books count on Author page
Closes #1931
2023-07-30 05:10:07 +03:00
Servarr fbd7b4fe33 Automated API Docs update [skip ci] 2023-07-30 05:09:25 +03:00
Mark McDowall fee7fbbff6 New: Add result to commands to report commands that did not complete successfully
(cherry picked from commit 103ce3def4636ef891e72bd687ef8f46b5125233)
2023-07-30 03:44:14 +03:00
Taloth Saldono 18253a298e Log Goodreads connection failures with more info.
(cherry picked from commit 6672650b6b5e152e82fb3ad38a0a158d66c0b83d)
2023-07-30 03:44:14 +03:00
Bogdan 22f92150c3 Ensure original data is shown when no matches are made 2023-07-29 18:32:54 +03:00
Bogdan 4d7a762ee8 Fix book tests 2023-07-29 14:59:47 +03:00
Stevie Robinson b11517e2ac Extend InlineMarkdown to handle code blocks in backticks
(cherry picked from commit e1c5533efa397632becc606c17232f97055e371b)
2023-07-29 09:59:50 +03:00
Bogdan d5af254f47 Fix AuthorLookupFixture 2023-07-29 09:04:22 +03:00
Bogdan f09da06f80 More test fixes 2023-07-29 07:48:47 +03:00
Bogdan d3443510b4 Rename formatPreferredWordScore to formatCustomFormatScore
Closes #2731
2023-07-29 06:35:19 +03:00
Bogdan d73eb1b5f9 Validation for Custom Format specifications
Co-authored-by: Qstick <qstick@gmail.com>
(cherry picked from commit 3d6cf24d7c91f8ff697c34264c249f7450894106)

Closes #2726
2023-07-29 06:32:29 +03:00
Bogdan 39778a95bf Dedupe releases based on indexer priority
(cherry picked from commit 38c717bcef6fa5fcd2ff1c7901639eb888a94a8a)

Closes #2727
2023-07-29 06:28:23 +03:00
Taloth Saldono 9fccca1154 Fixed up some errors and do the guid cache fix on the module instead of backend coz that would cause other issues.
(cherry picked from commit 8eaab46488f00a74197c517c6ef773626aec5173)
2023-07-29 06:27:34 +03:00
Mark McDowall e165663616 Fixed: Sorting in Interactive search duplicates results
(cherry picked from commit a6637b2911f7818e596c1518e94bd111cff0120b)

Closes #739
Closes #743
2023-07-29 06:27:31 +03:00
Bogdan b49d2312ab Fixed: Check only enabled Jackett indexers for '/all' endpoint
(cherry picked from commit ae3dd5730e05c5229e7f7092f15c33859524863b)

Closes #2730
2023-07-29 05:38:15 +03:00
Bogdan 52221c7cf4 Fixed: Ensure failing indexers are marked as failed when testing all
(cherry picked from commit b407eba61284d5fb855df6a2868805853aa6f448)

Closes #2735
2023-07-29 05:36:23 +03:00
bakerboy448 ad7b110a0b New: Use better page size for Newznab/Torznab (up to 100) when supported by the indexer
(cherry picked from commit ddb25b109575cc378462a1c3a64705f2003f01f0)

Closes #2181
2023-07-29 05:33:52 +03:00
Weblate b04b483f86 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translation: Servarr/Readarr
2023-07-28 12:55:31 +03:00
Bogdan b79941e0a1 Fix tests 2023-07-26 14:00:10 +03:00
Weblate 84d47b1f23 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translation: Servarr/Readarr
2023-07-26 07:53:53 +03:00
Weblate 17df4d47fb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: SHUAI.W <x@ousui.org>
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
2023-07-25 01:04:44 +03:00
Bogdan b9f89dddc9 Bump version to 0.3.0 2023-07-23 09:38:17 +03:00
Weblate e3fc469cd3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translation: Servarr/Readarr
2023-07-23 05:05:02 +03:00
Bogdan 4304685a65 Add support for deprecated values in field select options
(cherry picked from commit d9786887f3fe30ef60ad9c50b3272bf60dfef309)

Closes #2718
2023-07-23 05:03:40 +03:00
Bogdan 7d77b1fbe5 Trim spaces from a split list in GetValueConverter 2023-07-23 05:02:18 +03:00
Bogdan 1989174801 Fix typo in SkipRedownload
Closes #2711
2023-07-23 05:01:08 +03:00
Bogdan ac4ae9bb4d Fixed: Ensure Monitoring Options resets to No Change
(cherry picked from commit 180153cd8440df88c9aa5694c67c6cae537dc595)
2023-07-23 04:52:30 +03:00
Bogdan f399d27470 Cache busting for CSS files
(cherry picked from commit 38f263931ff8faba050762abe5fb692a5bc0d515)
2023-07-23 03:03:51 +03:00
Weblate c5fd2e3aa0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-07-21 13:37:45 +03:00
bakerboy448 e971d68d67 New: Log when testing for matching Remote Path Mapping
(cherry picked from commit 360d989cb047d0f752dd71b806aa0a746e3b5f3d)
2023-07-21 07:51:17 +03:00
Mark McDowall af858ac4aa Fix chunk IDs and source map file names
(cherry picked from commit bb8fed94eb2c44040031643e8c20ff72de759535)
2023-07-20 00:56:17 +03:00
Weblate 63ea253a6b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Qstick <qstick@gmail.com>
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/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/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
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/th/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/uk/
Translation: Servarr/Readarr
2023-07-19 15:11:56 +03:00
Bogdan 484f2eb3ec Fixed: Error when selecting different Quality Profile
(cherry picked from commit 5e19478266b33905e88b2e769269e44e5dd98e4b)

Closes #2694
2023-07-18 07:31:27 +03:00
Bogdan 15190aa61a Use 2 spaces indentation for ts/tsx files
(cherry picked from commit bc374f07cebc8463c6416630d0fb06f011f21c31)
2023-07-18 07:29:00 +03:00
Bogdan a3aac90bf7 Fixed: (ImportLists) Removed minimum refresh interval for FetchSingleList 2023-07-18 02:17:48 +03:00
Servarr dd9cbc4f54 Automated API Docs update [skip ci] 2023-07-17 06:37:31 +03:00
Bogdan 4bca0d77b7 New: Show tooltips with Custom Formats in History and Queue
Closes #2676
2023-07-17 05:18:03 +03:00
Weblate 1316b388ad Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translation: Servarr/Readarr
2023-07-17 04:39:11 +03:00
Servarr 243c88ce56 Automated API Docs update [skip ci] 2023-07-17 04:37:01 +03:00
Bogdan 921f170234 Use named keys for apply tags help text
Closes #2673
2023-07-17 04:36:24 +03:00
Weblate 3e102627f5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translation: Servarr/Readarr
2023-07-17 04:36:19 +03:00
Weblate f3b5f0c5cb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translation: Servarr/Readarr
2023-07-17 04:35:09 +03:00
Weblate a53516e821 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Godwhitelight <godwhitelight1@gmail.com>
Co-authored-by: Guy Porat <guyporatmail@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: liimee <git.taaa@fedora.email>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-07-17 04:34:08 +03:00
Qstick f0f95be57f New: Download Client Tags
(cherry picked from commit f6ae9fd6c5173cbf1540341fa99d2f120be1d28e)
2023-07-17 04:30:26 +03:00
Bogdan f436d730fe New: Bulk Manage Applications, Download Clients
Co-authored-by: Qstick <qstick@gmail.com>
2023-07-17 04:30:26 +03:00
Mark McDowall f7c135faaf Fixed: Ensure translations are fetched before loading app
(cherry picked from commit ad2721dc55f3233e4c299babe5744418bc530418)

Closes #2670
Closes #2674
Closes #2679
2023-07-17 03:41:06 +03:00
Taloth Saldono 8bb52105fd New: Per download client setting to Remove Completed/Failed downloads instead of global setting
(cherry picked from commit 2dba5ef4b431bee0a061be67354c9a7a612a03c8)
2023-07-16 23:45:15 +03:00
Bogdan e5a1b7a72e Add missing seed criteria validation 2023-07-16 21:25:14 +03:00
Bogdan 2f2a521391 Fixed: (Nyaa) Update default filtered category 2023-07-16 21:03:40 +03:00
Qstick 304d1e3462 TagSelect field type
(cherry picked from commit 09347f79c5c486ccb88d732c1bac1cacc668536c)

Closes #558
2023-07-16 19:58:00 +03:00
Bogdan 1d1cc6526d Bump version to 0.2.4 2023-07-16 08:19:33 +03:00
Weblate 690e0b5d96 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Leliene <lhena.gardien@gmail.com>
Co-authored-by: 君禹渊 <taoxu2870@outlook.com>
Co-authored-by: 無情天 <kofzhanganguo@126.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
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
2023-07-15 18:58:53 +03:00
Servarr 212eedd345 Automated API Docs update [skip ci] 2023-07-14 06:29:56 +03:00
Bogdan 0b38743292 Update webpack, eslint and core-js 2023-07-14 06:25:01 +03:00
jack-mil 1def54f246 New: Custom Format Score column in queue
(cherry picked from commit a6f2db9139c4a6b01d162ccf8884fc02c874b4cf)

Closes #2647
Closes #2658
2023-07-14 05:11:28 +03:00
bakerboy448 0eeaa1e443 Import list logging improvements
(cherry picked from commit d2be869d9cee2ee6452a53dcabe20c1598fd115a)

Closes #2654
2023-07-14 05:03:13 +03:00
Zak Saunders 7beee07a2c New: Make Release Group Outline Not Show as Required
(cherry picked from commit 89e363fd145ae6c531e6b3fa4fde258af05afca0)

Closes #2277
2023-07-14 04:56:05 +03:00
dependabot[bot] 924f739d1f Bump semver from 5.7.1 to 5.7.2
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-14 04:53:23 +03:00
Weblate b187fb23e3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translation: Servarr/Readarr
2023-07-12 00:58:12 +03:00
Bogdan ca043b3820 Bump version to 0.2.3 2023-07-09 15:02:39 +03:00
Bogdan c3c9b9afbb Add db migration for Metadata Profile's Ignored to List 2023-07-09 03:17:21 +03:00
Bogdan f225a742cc New: (UI) Minor improvements to metadata profiles listing 2023-07-08 22:39:39 +03:00
Servarr f4fd36061c Automated API Docs update [skip ci] 2023-07-08 22:15:39 +03:00
Bogdan 38e39449aa Fixed: (MetadataProfile) Allow usage of Must Not Contain 2023-07-08 22:10:57 +03:00
Bogdan 484c255fd4 Add validation for MinPopularity and MinPages in metadata profiles 2023-07-08 21:19:40 +03:00
dependabot[bot] f341b5f449 Bump tough-cookie from 4.1.2 to 4.1.3
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-08 20:10:05 +03:00
Weblate eb5654c634 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Kevin Orel Edry <techg9@gmail.com>
Co-authored-by: Tacit <1750630216@qq.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: 阿卡林刘 <ScottLiu_NonWin@outlook.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-07-08 20:09:49 +03:00
dependabot[bot] e843046d76 Bump stylelint from 15.6.1 to 15.10.1
Bumps [stylelint](https://github.com/stylelint/stylelint) from 15.6.1 to 15.10.1.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/15.6.1...15.10.1)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-08 01:50:20 +03:00
Qstick ef57545221 Add package to Sentry release to ensure apps don't mix 2023-07-04 12:16:37 -05:00
Bogdan 09d44726a4 New: Speed up API add by reworking AuthorExistsValidator
Co-authored-by: Qstick <qstick@gmail.com>
2023-07-03 17:19:19 +03:00
Bogdan 0e2d39f580 Create overload for ToJson() with Formatting param
(cherry picked from commit aa2b0031671b6846eaa75e0914cd03ae7bbb0da7)
2023-07-03 11:19:05 +03:00
Qstick dbcb0e77a8 Fixed: Allow restore to process backups up to ~500MB
(cherry picked from commit 551edb9e655d2a541a2232f85e79a5e3f7b433aa)
2023-07-03 11:18:52 +03:00
Bogdan 0186900a54 Bump version to 0.2.2 2023-07-02 12:03:41 +03:00
Weblate 941b30edac Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: BeardedWatermelon <periklis.karantonis@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
Translation: Servarr/Readarr
2023-06-30 13:56:01 +03:00
Weblate 5c61b6ceb3 Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 100.0% (945 of 945 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 61.2% (579 of 945 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 61.4% (581 of 945 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 61.3% (580 of 945 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 63.9% (604 of 945 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 87.9% (831 of 945 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 61.9% (585 of 945 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 61.5% (582 of 945 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 61.4% (581 of 945 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 61.5% (582 of 945 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 68.1% (644 of 945 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/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/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/th/
Translation: Servarr/Readarr
2023-06-28 13:56:13 +03:00
Bogdan 55959e1112 New: Improve empty list messaging
(cherry picked from commit ee843259bca2d9764a3919b123524a51a3a16cce)

Closes #2621
2023-06-27 06:43:45 +03:00
Bogdan 07451cbcde Fixed: Invalid image URL if Author/Book is missing background image
(cherry picked from commit dd096e0fda71b3afa9b09c2900abbf226d8a5204)

Closes #2625
2023-06-27 06:36:21 +03:00
Bogdan 1ebdffcd26 Allow array of string as value in EnhancedSelectInput
(cherry picked from commit 6816767fad9e4e839e77c5fe40aece97033cd052)
2023-06-27 04:00:24 +03:00
Weblate 75119ce9df Translated using Weblate (Hungarian) [skip ci]
Currently translated at 97.2% (916 of 942 strings)

Co-authored-by: sutoramon <sutoramon@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translation: Servarr/Readarr
2023-06-26 02:26:01 +03:00
Bogdan 668dc6dfde Fix translation in RemoveQueueItemModal 2023-06-26 02:08:41 +03:00
Bogdan ee989c9c67 Bump version to 0.2.1 2023-06-25 09:04:44 +03:00
servarr[bot] acac3bd680 Add more trace logs related info to bug_report.yml [skip ci] 2023-06-22 16:40:17 +03:00
Servarr 3b18f3206d Automated API Docs update [skip ci] 2023-06-21 08:16:49 +03:00
Bogdan fcf057a019 Remove not implemented endpoints from API docs 2023-06-21 08:00:04 +03:00
Servarr c7399cdd2b Automated API Docs update [skip ci] 2023-06-21 07:45:48 +03:00
Bogdan 08a3682b89 Limit search input to first character matching when only one character is typed 2023-06-21 07:25:09 +03:00
Bogdan 3da00f75dc Remove not implemented endpoints from API docs
Closes #2613
2023-06-21 07:09:01 +03:00
Bogdan 60abb298b2 Convert to 'using' declaration in Housekeeping Tasks
Closes #2612
2023-06-21 07:03:13 +03:00
Bogdan c710b117ab Prevent NullRef when deleting missing backups
(cherry picked from commit 0ff0fe2e68f3abf7b8e4d6bf0c1e9dee4eb68227)

Closes #2610
2023-06-21 06:37:09 +03:00
Weblate 816f53b36b Translated using Weblate (Chinese (Traditional) (zh_TW)) [skip ci]
Currently translated at 3.5% (33 of 942 strings)

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

Currently translated at 100.0% (942 of 942 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 60.6% (571 of 942 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 97.5% (919 of 942 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: KHng0284 <giakhang021109@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: ted09080037 <ted09080037@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_TW/
Translation: Servarr/Readarr
2023-06-21 02:28:40 +03:00
bakerboy448 749684e24a New: Indexer Messaging and Error Improvements
(cherry picked from commit 3b505d8734dcbe3fa53acba7f94f1361151e6a44)
2023-06-19 16:12:15 +03:00
Bogdan 3a0ca45aa9 Fix sorting queue items by size 2023-06-18 15:00:49 +03:00
Bogdan 595efd498e Close database connections in housekeeping tasks
Co-authored-by: ferencmarkizay <ferencmarkizay@gmail.com>
2023-06-18 15:00:11 +03:00
Bogdan dea1060d61 Bump version to 0.2.0 2023-06-18 07:18:42 +03:00
Weblate f6049b8bf2 Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 100.0% (942 of 942 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-06-18 04:53:21 +03:00
Bogdan 53ced38221 New: Improved page loading errors
Closes #2605
2023-06-17 00:51:10 +03:00
Tristan Kennedy 3a3cf8511e Added padding to search tab to maintain visual consistancy
(cherry picked from commit 55ef505d740a9aadc7f161274006e150b0d9cf8f)
2023-06-17 00:50:55 +03:00
Servarr 9ec913337d Automated API Docs update [skip ci] 2023-06-15 13:26:07 +03:00
Weblate 9a2120ae92 Update translation files [skip ci]
Updated by "Cleanup translation files" hook in Weblate.

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

Currently translated at 16.0% (151 of 942 strings)

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

Currently translated at 100.0% (942 of 942 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 61.6% (581 of 942 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 59.9% (565 of 942 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 65.9% (621 of 942 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 60.9% (574 of 942 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 61.4% (579 of 942 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 61.5% (580 of 942 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 61.4% (579 of 942 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 64.0% (603 of 942 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 78.5% (740 of 942 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 88.1% (830 of 942 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 61.9% (584 of 942 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 61.6% (581 of 942 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 61.5% (580 of 942 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 61.6% (581 of 942 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 68.2% (643 of 942 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fatalicus <fatalicus@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
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/pl/
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/tr/
Translation: Servarr/Readarr
2023-06-15 13:16:52 +03:00
Bogdan 818d3a94d5 Add skip ci to API docs update commit 2023-06-15 13:15:37 +03:00
Bogdan 4e493b74e6 Update cleansing rules for RSS TL feed and homedir for Mac
(cherry picked from commit e5ff4aafa3f0b855fec332788e9fc490a03dfce3)

Closes #2593
2023-06-15 13:06:26 +03:00
Bogdan c7eaf1e85c Update translations
(cherry picked from commit 26031389757f6b5270bbe5591101b08e58debb73)

Closes #2599
2023-06-15 13:02:50 +03:00
Bogdan 31fe15c911 Add HelpTextWarning support in FieldDefinition
(cherry picked from commit 0e07d54ee77d5f83716e17b6757e23f38ff73694)

Closes #2595
2023-06-15 13:00:42 +03:00
Bogdan 2c36a6c25f Require ApiKey for all actions in SonarrImport
(cherry picked from commit 19b8fbe13bf584b915a05fe9fc87622adbaee0b7)

Closes #2600
2023-06-15 12:58:43 +03:00
Bogdan 6af56f7a15 Fixed: Treat redirects as errors in Readarr Import List
(cherry picked from commit 059a156f4a34c6b9cbe139fa1973b814e8a534ae)

Closes #2601
2023-06-15 12:57:09 +03:00
Qstick 6e13191c25 Fixed: Correctly handle 302 and 303 redirects in HttpClient
(cherry picked from commit ed7c5a937f4b50fcdf819e8fe347c8c0bc6bd2e7)

(cherry picked from commit 11bd764a75d3b97117098738d3489c4b3329738f)
2023-06-14 08:13:20 +03:00
bakerboy448 921ddfc962 Fixed: Handle checkingResumeData state form qBittorrent
(cherry picked from commit 8d8a16225ff7772ccb57784f272ca31e28bb8455)
2023-06-14 08:12:58 +03:00
Weblate 22f977401a Update translation files [skip ci]
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translation: Servarr/Readarr
2023-06-13 01:38:53 +03:00
Bogdan 113d9a07ef Update translations 2023-06-13 01:35:03 +03:00
Qstick 0560d65ea1 Update Remote Path Mapping delete modal title
(cherry picked from commit 18716a00516a971f7f2eb369b920266bea24fe08)

Closes #2588
Fixes #2587
2023-06-12 22:31:04 +03:00
Weblate 94ff105104 Translated using Weblate (Indonesian) [skip ci]
Currently translated at 3.7% (35 of 928 strings)

Co-authored-by: liimee <git.taaa@fedora.email>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translation: Servarr/Readarr
2023-06-12 21:58:09 +03:00
Bogdan 9bcf258aa9 Bump version to 0.1.9 2023-06-11 09:38:16 +03:00
Bogdan 54985bd4ca Use more specific styling for kinds in ProgressBar
(cherry picked from commit dd31c913d2a974d95f3be251714ce749cfd99a72)
2023-06-10 02:07:40 +03:00
Weblate 9e4d551f08 Translated using Weblate (Russian) [skip ci]
Currently translated at 67.7% (629 of 928 strings)

Co-authored-by: Андрей <andryfly7@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translation: Servarr/Readarr
2023-06-10 02:02:58 +03:00
Weblate 8390da1c2a Translated using Weblate (Greek) [skip ci]
Currently translated at 99.8% (927 of 928 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 78.4% (728 of 928 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 77.0% (715 of 928 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 98.3% (913 of 928 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 98.3% (913 of 928 strings)

Translated using Weblate (Indonesian) [skip ci]

Currently translated at 3.5% (33 of 928 strings)

Translated using Weblate (Croatian) [skip ci]

Currently translated at 15.7% (146 of 928 strings)

Translated using Weblate (Ukrainian) [skip ci]

Currently translated at 66.7% (619 of 928 strings)

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

Currently translated at 15.7% (146 of 928 strings)

Translated using Weblate (Catalan) [skip ci]

Currently translated at 67.7% (629 of 928 strings)

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

Currently translated at 100.0% (928 of 928 strings)

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

Currently translated at 100.0% (928 of 928 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 76.9% (714 of 928 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 61.5% (571 of 928 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 61.6% (572 of 928 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 86.7% (805 of 928 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 67.6% (628 of 928 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 65.7% (610 of 928 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 61.5% (571 of 928 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 73.0% (678 of 928 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 98.0% (910 of 928 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 78.3% (727 of 928 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 98.5% (915 of 928 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 98.1% (911 of 928 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 98.1% (911 of 928 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 68.2% (633 of 928 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 68.2% (633 of 928 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 78.3% (727 of 928 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 61.5% (571 of 928 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 60.0% (557 of 928 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 61.5% (571 of 928 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 61.5% (571 of 928 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 64.0% (594 of 928 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 88.2% (819 of 928 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 61.9% (575 of 928 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 61.5% (571 of 928 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 67.9% (631 of 928 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 61.6% (572 of 928 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 61.6% (572 of 928 strings)

Update translation files [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Jens <jensmahnke@me.com>
Co-authored-by: MoowGlax <matthieu.derouet.pro@gmail.com>
Co-authored-by: Thijs Waalen <contact@thijswaalen.com>
Co-authored-by: Thodoris Kalatzis <teo.kal@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: emacsdias <emacs.dias@gmail.com>
Co-authored-by: foXaCe <foxace66@gmail.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: splifter <a.strahlke@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
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/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/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/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/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/
Translation: Servarr/Readarr
2023-06-07 19:21:06 +03:00
Qstick 44ae043c58 Bump version to 0.1.8 2023-06-04 14:48:35 -05:00
Bogdan bb7e2fc70c Fixed: Don't log handled exceptions in API 2023-05-31 06:51:57 +03:00
Bogdan b05938a9a8 Revert "Fixed: Don't log handled exceptions in API"
This reverts commit fecb3895ed.
2023-05-31 06:51:56 +03:00
Weblate 1e42ac572e Update translation files [skip ci]
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
Translation: Servarr/Readarr
2023-05-28 22:28:24 +03:00
Servarr 649dd0bda0 Automated API Docs update 2023-05-28 22:27:52 +03:00
Bogdan de24aef059 Deserialize asynchronously in LocalizationService
(cherry picked from commit 86a7f7bd54aa733b0e3abd3ec9463a85dd348118)

Closes #2561
2023-05-28 22:15:01 +03:00
Qstick 10766dd227 Faster tag view in UI for large libraries
(cherry picked from commit b050e1d2eb3bff9e28e7a1545d121be091789308)

Closes #2571
2023-05-28 22:08:34 +03:00
Bogdan 257d279e43 Fixed: Enforce validation warnings
(cherry picked from commit 48ee1158ad4213fd0690842e2672f52d08f7ad26)

Closes #2570
2023-05-28 22:02:41 +03:00
Bogdan 1db333088a Add minimum length as const in ApiKeyValidationCheck
(cherry picked from commit b06269544cfa11015f3fb938fd8c2ef07d9cac4a)

Closes #2565
2023-05-28 22:00:31 +03:00
Bogdan d1aff31593 Use 'var' instead of explicit type
(cherry picked from commit 12374f7f0038e5b25548f5ab3f71122410832393)

Closes #2559
2023-05-28 21:59:25 +03:00
Bogdan 89dd4d3271 Inline 'out' variable declarations
(cherry picked from commit 281add47de1d3940990156c841362125dea9cc7d)

Closes #2558
2023-05-28 21:55:43 +03:00
Bogdan fc6c78a54e Standardize variable declaration
(cherry picked from commit 909f2ded6b75998fa8e1addd0dcf849279e7b120)

Closes #2556
Closes #2557
2023-05-28 21:47:48 +03:00
Bogdan c98f4512df Enforce rule IDE0005 on build
(cherry picked from commit 6b1e4ef81938d264a2ddc8b626b0502f799aa640)

Closes #2555
2023-05-28 21:46:59 +03:00
Matthew Strapp 0a43481aed Fixed: Use relative paths instead of absolute paths for webmanifest
(cherry picked from commit 8e771f95ade919a8f1ed7b48675f032a6c508cb2)
2023-05-28 21:42:27 +03:00
Bogdan df6c142250 Simplify ShouldHaveApiKey and HasErrors
(cherry picked from commit 7343616a47cd538bba4c9128d2c1094561f9b3a5)
2023-05-28 21:41:07 +03:00
Weblate 58cf93e360 Update translation files [skip ci]
Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 61.4% (567 of 922 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 61.4% (567 of 922 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 63.9% (590 of 922 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 88.3% (815 of 922 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 61.9% (571 of 922 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 61.6% (568 of 922 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 61.4% (567 of 922 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 61.6% (568 of 922 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 68.0% (627 of 922 strings)

Update translation files [skip ci]

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Anonymous <noreply@weblate.org>
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/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/is/
Translation: Servarr/Readarr
2023-05-28 10:25:24 -05:00
Qstick 7be282ad12 Bump version to 0.1.7 2023-05-28 09:27:02 -05:00
476 changed files with 9467 additions and 4242 deletions
+10 -4
View File
@@ -36,12 +36,18 @@ dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _ dotnet_naming_style.instance_field_style.required_prefix = _
# Prefer "var" everywhere # Prefer "var" everywhere
csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_for_built_in_types = true
csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_when_type_is_apparent = true
csharp_style_var_elsewhere = true:suggestion csharp_style_var_elsewhere = true
# Prefer "out" variables to be declared inline
csharp_style_inlined_variable_declaration = true
# Using directive is unnecessary. # Using directive is unnecessary.
dotnet_diagnostic.IDE0005.severity = error dotnet_diagnostic.IDE0005.severity = error
# Use var instead of explicit type
dotnet_diagnostic.IDE0007.severity = error
# Inline variable declaration
dotnet_diagnostic.IDE0018.severity = error
# Stylecop Rules # Stylecop Rules
dotnet_diagnostic.SA0001.severity = none dotnet_diagnostic.SA0001.severity = none
@@ -269,7 +275,7 @@ dotnet_diagnostic.CA5397.severity = suggestion
[*.{js,html,js,hbs,less,css}] [*.{js,html,hbs,less,css,ts,tsx}]
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
+2 -2
View File
@@ -76,7 +76,7 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided. label: Trace Logs have been provided as applicable. Reports may be closed if the required logs are not provided.
description: Trace logs are generally required for all bug reports description: Trace logs are generally required for all bug reports and contain `trace`. Info logs are invalid for bug reports and do not contain `debug` nor `trace`
options: options:
- label: I have followed the steps in the wiki link above and provided the required trace logs that are relevant and show this issue. - label: I have read and followed the steps in the wiki link above and provided the required trace logs - the logs contain `trace` - that are relevant and show this issue.
required: true required: true
+3 -3
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.1.6' majorVersion: '0.3.1'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)' readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
@@ -382,7 +382,7 @@ stages:
- bash: | - bash: |
echo "Uploading source maps to sentry" echo "Uploading source maps to sentry"
curl -sL https://sentry.io/get-cli/ | bash curl -sL https://sentry.io/get-cli/ | bash
RELEASENAME="${READARRVERSION}-${BUILD_SOURCEBRANCHNAME}" RELEASENAME="Readarr@${READARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
sentry-cli releases new --finalize -p readarr -p readarr-ui -p readarr-update "${RELEASENAME}" sentry-cli releases new --finalize -p readarr -p readarr-ui -p readarr-update "${RELEASENAME}"
sentry-cli releases -p readarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite sentry-cli releases -p readarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
sentry-cli releases set-commits --auto "${RELEASENAME}" sentry-cli releases set-commits --auto "${RELEASENAME}"
@@ -984,7 +984,7 @@ stages:
git status git status
if git status | grep modified if git status | grep modified
then then
git commit -am 'Automated API Docs update' git commit -am 'Automated API Docs update [skip ci]'
git push -f --set-upstream origin api-docs git push -f --set-upstream origin api-docs
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/readarr/readarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}' curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/readarr/readarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
else else
+8 -7
View File
@@ -67,23 +67,23 @@ module.exports = (env) => {
output: { output: {
path: distFolder, path: distFolder,
publicPath: '/', publicPath: '/',
filename: '[name].js', filename: '[name]-[contenthash].js',
sourceMapFilename: '[file].map' sourceMapFilename: '[file].map'
}, },
optimization: { optimization: {
moduleIds: 'deterministic', moduleIds: 'deterministic',
chunkIds: 'named', chunkIds: isProduction ? 'deterministic' : 'named'
splitChunks: {
chunks: 'initial',
name: 'vendors'
}
}, },
performance: { performance: {
hints: false hints: false
}, },
experiments: {
topLevelAwait: true
},
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
__DEV__: !isProduction, __DEV__: !isProduction,
@@ -91,7 +91,8 @@ module.exports = (env) => {
}), }),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: 'Content/styles.css' filename: 'Content/styles.css',
chunkFilename: 'Content/[id]-[chunkhash].css'
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
+6 -5
View File
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
@@ -161,16 +162,16 @@ class Blocklist extends Component {
{ {
!isAnyFetching && !!error && !isAnyFetching && !!error &&
<div> <Alert kind={kinds.DANGER}>
{translate('UnableToLoadBlocklist')} {translate('UnableToLoadBlocklist')}
</div> </Alert>
} }
{ {
isAllPopulated && !error && !items.length && isAllPopulated && !error && !items.length &&
<div> <Alert kind={kinds.INFO}>
{translate('NoHistoryBlocklist')} {translate('NoHistoryBlocklist')}
</div> </Alert>
} }
{ {
@@ -214,7 +215,7 @@ class Blocklist extends Component {
isOpen={isConfirmRemoveModalOpen} isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('RemoveSelected')} title={translate('RemoveSelected')}
message={translate('RemoveSelectedMessageText')} message={translate('RemoveSelectedItemBlocklistMessageText')}
confirmLabel={translate('RemoveSelected')} confirmLabel={translate('RemoveSelected')}
onConfirm={this.onRemoveSelectedConfirmed} onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose} onCancel={this.onConfirmRemoveModalClose}
@@ -9,7 +9,7 @@ import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge'; import formatAge from 'Utilities/Number/formatAge';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css'; import styles from './HistoryDetails.css';
@@ -108,7 +108,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ? customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem <DescriptionListItem
title={translate('CustomFormatScore')} title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)} data={formatCustomFormatScore(customFormatScore)}
/> : /> :
null null
} }
@@ -225,7 +225,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ? customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem <DescriptionListItem
title={translate('CustomFormatScore')} title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)} data={formatCustomFormatScore(customFormatScore)}
/> : /> :
null null
} }
@@ -271,7 +271,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ? customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem <DescriptionListItem
title={translate('CustomFormatScore')} title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)} data={formatCustomFormatScore(customFormatScore)}
/> : /> :
null null
} }
+7 -6
View File
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu'; import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
@@ -11,7 +12,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager'; import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props'; import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import HistoryRowConnector from './HistoryRowConnector'; import HistoryRowConnector from './HistoryRowConnector';
@@ -85,9 +86,9 @@ class History extends Component {
{ {
!isFetchingAny && hasError && !isFetchingAny && hasError &&
<div> <Alert kind={kinds.DANGER}>
{translate('UnableToLoadHistory')} {translate('UnableToLoadHistory')}
</div> </Alert>
} }
{ {
@@ -95,9 +96,9 @@ class History extends Component {
// wait for the books to populate because they are never coming. // wait for the books to populate because they are never coming.
isPopulated && !hasError && !items.length && isPopulated && !hasError && !items.length &&
<div> <Alert kind={kinds.INFO}>
No history found {translate('NoHistory')}
</div> </Alert>
} }
{ {
+17 -3
View File
@@ -8,8 +8,9 @@ import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props'; import Tooltip from 'Components/Tooltip/Tooltip';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import { icons, tooltipPositions } from 'Helpers/Props';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal'; import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell'; import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css'; import styles from './HistoryRow.css';
@@ -57,6 +58,7 @@ class HistoryRow extends Component {
book, book,
quality, quality,
customFormats, customFormats,
customFormatScore,
qualityCutoffNotMet, qualityCutoffNotMet,
eventType, eventType,
sourceTitle, sourceTitle,
@@ -177,7 +179,14 @@ class HistoryRow extends Component {
key={name} key={name}
className={styles.customFormatScore} className={styles.customFormatScore}
> >
{formatPreferredWordScore(data.customFormatScore)} <Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell> </TableRowCell>
); );
} }
@@ -244,6 +253,7 @@ HistoryRow.propTypes = {
book: PropTypes.object, book: PropTypes.object,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired, eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
@@ -257,4 +267,8 @@ HistoryRow.propTypes = {
onMarkAsFailedPress: PropTypes.func.isRequired onMarkAsFailedPress: PropTypes.func.isRequired
}; };
HistoryRow.defaultProps = {
customFormats: []
};
export default HistoryRow; export default HistoryRow;
+6 -5
View File
@@ -1,6 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
@@ -12,7 +13,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager'; import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props'; import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems'; import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
@@ -233,17 +234,17 @@ class Queue extends Component {
{ {
!isRefreshing && hasError ? !isRefreshing && hasError ?
<div> <Alert kind={kinds.DANGER}>
{translate('FailedToLoadQueue')} {translate('FailedToLoadQueue')}
</div> : </Alert> :
null null
} }
{ {
isAllPopulated && !hasError && !items.length ? isAllPopulated && !hasError && !items.length ?
<div> <Alert kind={kinds.INFO}>
{translate('QueueIsEmpty')} {translate('QueueIsEmpty')}
</div> : </Alert> :
null null
} }
+6
View File
@@ -16,6 +16,12 @@
width: 150px; width: 150px;
} }
.customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
}
.actions { .actions {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'actions': string; 'actions': string;
'customFormatScore': string;
'progress': string; 'progress': string;
'protocol': string; 'protocol': string;
'quality': string; 'quality': string;
+25 -2
View File
@@ -14,9 +14,11 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell'; import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal'; import RemoveQueueItemModal from './RemoveQueueItemModal';
@@ -44,14 +46,14 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true }); this.setState({ isRemoveQueueItemModalOpen: true });
}; };
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => { onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
const { const {
onRemoveQueueItemPress, onRemoveQueueItemPress,
onQueueRowModalOpenOrClose onQueueRowModalOpenOrClose
} = this.props; } = this.props;
onQueueRowModalOpenOrClose(false); onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist, skipredownload); onRemoveQueueItemPress(blocklist, skipRedownload);
this.setState({ isRemoveQueueItemModalOpen: false }); this.setState({ isRemoveQueueItemModalOpen: false });
}; };
@@ -91,6 +93,7 @@ class QueueRow extends Component {
book, book,
quality, quality,
customFormats, customFormats,
customFormatScore,
protocol, protocol,
indexer, indexer,
outputPath, outputPath,
@@ -222,6 +225,24 @@ class QueueRow extends Component {
); );
} }
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'protocol') { if (name === 'protocol') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
@@ -392,6 +413,7 @@ QueueRow.propTypes = {
book: PropTypes.object, book: PropTypes.object,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object), customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
indexer: PropTypes.string, indexer: PropTypes.string,
outputPath: PropTypes.string, outputPath: PropTypes.string,
@@ -416,6 +438,7 @@ QueueRow.propTypes = {
}; };
QueueRow.defaultProps = { QueueRow.defaultProps = {
customFormats: [],
isGrabbing: false, isGrabbing: false,
isRemoving: false isRemoving: false
}; };
@@ -23,7 +23,7 @@ class RemoveQueueItemModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false, blocklist: false,
skipredownload: false skipRedownload: false
}; };
} }
@@ -34,7 +34,7 @@ class RemoveQueueItemModal extends Component {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false, blocklist: false,
skipredownload: false skipRedownload: false
}); });
}; };
@@ -49,8 +49,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ blocklist: value }); this.setState({ blocklist: value });
}; };
onSkipReDownloadChange = ({ value }) => { onSkipRedownloadChange = ({ value }) => {
this.setState({ skipredownload: value }); this.setState({ skipRedownload: value });
}; };
onRemoveConfirmed = () => { onRemoveConfirmed = () => {
@@ -76,7 +76,7 @@ class RemoveQueueItemModal extends Component {
isPending isPending
} = this.props; } = this.props;
const { remove, blocklist, skipredownload } = this.state; const { remove, blocklist, skipRedownload } = this.state;
return ( return (
<Modal <Modal
@@ -124,7 +124,7 @@ class RemoveQueueItemModal extends Component {
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="blocklist" name="blocklist"
value={blocklist} value={blocklist}
helpText={translate('BlocklistHelpText')} helpText={translate('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange} onChange={this.onBlocklistChange}
/> />
</FormGroup> </FormGroup>
@@ -137,10 +137,10 @@ class RemoveQueueItemModal extends Component {
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="skipredownload" name="skipRedownload"
value={skipredownload} value={skipRedownload}
helpText={translate('SkipredownloadHelpText')} helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipReDownloadChange} onChange={this.onSkipRedownloadChange}
/> />
</FormGroup> </FormGroup>
} }
@@ -24,7 +24,7 @@ class RemoveQueueItemsModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false, blocklist: false,
skipredownload: false skipRedownload: false
}; };
} }
@@ -35,7 +35,7 @@ class RemoveQueueItemsModal extends Component {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false, blocklist: false,
skipredownload: false skipRedownload: false
}); });
}; };
@@ -50,8 +50,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ blocklist: value }); this.setState({ blocklist: value });
}; };
onSkipReDownloadChange = ({ value }) => { onSkipRedownloadChange = ({ value }) => {
this.setState({ skipredownload: value }); this.setState({ skipRedownload: value });
}; };
onRemoveConfirmed = () => { onRemoveConfirmed = () => {
@@ -77,7 +77,7 @@ class RemoveQueueItemsModal extends Component {
allPending allPending
} = this.props; } = this.props;
const { remove, blocklist, skipredownload } = this.state; const { remove, blocklist, skipRedownload } = this.state;
return ( return (
<Modal <Modal
@@ -89,12 +89,12 @@ class RemoveQueueItemsModal extends Component {
onModalClose={this.onModalClose} onModalClose={this.onModalClose}
> >
<ModalHeader> <ModalHeader>
Remove Selected Item{selectedCount > 1 ? 's' : ''} {selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div className={styles.message}> <div className={styles.message}>
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue? {selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', selectedCount) : translate('RemoveSelectedItemQueueMessageText')}
</div> </div>
{ {
@@ -118,14 +118,14 @@ class RemoveQueueItemsModal extends Component {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist {selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="blocklist" name="blocklist"
value={blocklist} value={blocklist}
helpText={translate('BlocklistHelpText')} helpText={translate('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange} onChange={this.onBlocklistChange}
/> />
</FormGroup> </FormGroup>
@@ -138,10 +138,10 @@ class RemoveQueueItemsModal extends Component {
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="skipredownload" name="skipRedownload"
value={skipredownload} value={skipRedownload}
helpText={translate('SkipredownloadHelpText')} helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipReDownloadChange} onChange={this.onSkipRedownloadChange}
/> />
</FormGroup> </FormGroup>
} }
@@ -150,14 +150,14 @@ class RemoveQueueItemsModal extends Component {
<ModalFooter> <ModalFooter>
<Button onPress={this.onModalClose}> <Button onPress={this.onModalClose}>
Close {translate('Close')}
</Button> </Button>
<Button <Button
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={this.onRemoveConfirmed} onPress={this.onRemoveConfirmed}
> >
Remove {translate('Remove')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
+4 -3
View File
@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme'; import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes'; import AppRoutes from './AppRoutes';
function App({ store, history }) { function App({ store, history, hasTranslationsError }) {
return ( return (
<DocumentTitle title={window.Readarr.instanceName}> <DocumentTitle title={window.Readarr.instanceName}>
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<ApplyTheme> <ApplyTheme>
<PageConnector> <PageConnector hasTranslationsError={hasTranslationsError}>
<AppRoutes app={App} /> <AppRoutes app={App} />
</PageConnector> </PageConnector>
</ApplyTheme> </ApplyTheme>
@@ -25,7 +25,8 @@ function App({ store, history }) {
App.propTypes = { App.propTypes = {
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired history: PropTypes.object.isRequired,
hasTranslationsError: PropTypes.bool.isRequired
}; };
export default App; export default App;
+5
View File
@@ -0,0 +1,5 @@
interface ModelBase {
id: number;
}
export default ModelBase;
+48
View File
@@ -0,0 +1,48 @@
import SortDirection from 'Helpers/Props/SortDirection';
export interface Error {
responseJSON: {
message: string;
};
}
export interface AppSectionDeleteState {
isDeleting: boolean;
deleteError: Error;
}
export interface AppSectionSaveState {
isSaving: boolean;
saveError: Error;
}
export interface PagedAppSectionState {
pageSize: number;
}
export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: {
items: T[];
};
}
export interface AppSectionItemState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
item: T;
}
interface AppSectionState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: T[];
sortKey: string;
sortDirection: SortDirection;
}
export default AppSectionState;
+41
View File
@@ -0,0 +1,41 @@
import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState';
interface FilterBuilderPropOption {
id: string;
name: string;
}
export interface FilterBuilderProp<T> {
name: string;
label: string;
type: string;
valueType?: string;
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
}
export interface PropertyFilter {
key: string;
value: boolean | string | number | string[] | number[];
type: string;
}
export interface Filter {
key: string;
label: string;
filers: PropertyFilter[];
}
export interface CustomFilter {
id: number;
type: string;
label: string;
filers: PropertyFilter[];
}
interface AppState {
settings: SettingsAppState;
tags: TagsAppState;
}
export default AppState;
@@ -0,0 +1,40 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer';
import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface NotificationAppState
extends AppSectionState<Notification>,
AppSectionDeleteState {}
export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState {
downloadClients: DownloadClientAppState;
importLists: ImportListAppState;
indexers: IndexerAppState;
notifications: NotificationAppState;
uiSettings: UiSettingsAppState;
}
export default SettingsAppState;
+12
View File
@@ -0,0 +1,12 @@
import ModelBase from 'App/ModelBase';
import AppSectionState, {
AppSectionDeleteState,
} from 'App/State/AppSectionState';
export interface Tag extends ModelBase {
label: string;
}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
export default TagsAppState;
+1 -4
View File
@@ -392,10 +392,7 @@ class AuthorDetails extends Component {
name={icons.ARROW_UP} name={icons.ARROW_UP}
size={30} size={30}
title={translate('GoToAuthorListing')} title={translate('GoToAuthorListing')}
to={{ to={'/'}
pathname: '/',
state: { restoreScrollPosition: true }
}}
/> />
<IconButton <IconButton
@@ -92,6 +92,7 @@ class AuthorDetailsHeader extends Component {
titleWidth titleWidth
} = this.state; } = this.state;
const fanartUrl = getFanartUrl(images);
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160); const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
const continuing = status === 'continuing'; const continuing = status === 'continuing';
@@ -108,9 +109,11 @@ class AuthorDetailsHeader extends Component {
<div className={styles.header} style={{ width }} > <div className={styles.header} style={{ width }} >
<div <div
className={styles.backdrop} className={styles.backdrop}
style={{ style={
backgroundImage: `url(${getFanartUrl(images)})` fanartUrl ?
}} { backgroundImage: `url(${fanartUrl})` } :
null
}
> >
<div className={styles.backdropOverlay} /> <div className={styles.backdropOverlay} />
</div> </div>
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal'; import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput'; import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
@@ -9,6 +10,7 @@ import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/Settings/rootFolders';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel'; import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
import DeleteAuthorModal from './Delete/DeleteAuthorModal'; import DeleteAuthorModal from './Delete/DeleteAuthorModal';
@@ -17,6 +19,10 @@ import styles from './AuthorEditorFooter.css';
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const mapDispatchToProps = {
dispatchFetchRootFolders: fetchRootFolders
};
class AuthorEditorFooter extends Component { class AuthorEditorFooter extends Component {
// //
@@ -39,6 +45,13 @@ class AuthorEditorFooter extends Component {
}; };
} }
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchRootFolders();
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
isSaving, isSaving,
@@ -160,9 +173,9 @@ class AuthorEditorFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'monitored', value: 'Monitored' }, { key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: 'Unmonitored' } { key: 'unmonitored', value: translate('Unmonitored') }
]; ];
return ( return (
@@ -341,7 +354,8 @@ AuthorEditorFooter.propTypes = {
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
onSaveSelected: PropTypes.func.isRequired, onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired, onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired onRetagAuthorPress: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired
}; };
export default AuthorEditorFooter; export default connect(undefined, mapDispatchToProps)(AuthorEditorFooter);
@@ -98,10 +98,10 @@ class TagsModalContent extends Component {
value={applyTags} value={applyTags}
values={applyTagsOptions} values={applyTagsOptions}
helpTexts={[ helpTexts={[
translate('ApplyTagsHelpTexts1'), translate('ApplyTagsHelpTextHowToApplyAuthors'),
translate('ApplyTagsHelpTexts2'), translate('ApplyTagsHelpTextAdd'),
translate('ApplyTagsHelpTexts3'), translate('ApplyTagsHelpTextRemove'),
translate('ApplyTagsHelpTexts4') translate('ApplyTagsHelpTextReplace')
]} ]}
onChange={this.onInputChange} onChange={this.onInputChange}
/> />
@@ -1,8 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector'; import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
@@ -70,9 +72,9 @@ class AuthorHistoryTableContent extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<div> <Alert kind={kinds.DANGER}>
{translate('UnableToLoadHistory')} {translate('UnableToLoadHistory')}
</div> </Alert>
} }
{ {
@@ -17,8 +17,8 @@ function AuthorIndexProgressBar(props) {
detailedProgressBar detailedProgressBar
} = props; } = props;
const progress = bookCount ? bookFileCount / bookCount * 100 : 100; const progress = bookCount ? bookCount / totalBookCount * 100 : 100;
const text = `${bookFileCount} / ${bookCount}`; const text = `${bookCount} / ${totalBookCount}`;
return ( return (
<ProgressBar <ProgressBar
@@ -297,7 +297,7 @@ class AuthorIndexRow extends Component {
progress={progress} progress={progress}
kind={getProgressBarKind(status, monitored, progress)} kind={getProgressBarKind(status, monitored, progress)}
showText={true} showText={true}
text={`${bookFileCount} / ${bookCount}`} text={`${bookCount} / ${totalBookCount}`}
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])} title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
width={125} width={125}
/> />
@@ -33,7 +33,7 @@ class MonitoringOptionsModalContent extends Component {
const { const {
isSaving, isSaving,
saveError saveError
} = prevProps; } = this.props;
if (prevProps.isSaving && !isSaving && !saveError) { if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({ this.setState({
@@ -83,15 +83,18 @@ class BookDetailsHeader extends Component {
titleWidth titleWidth
} = this.state; } = this.state;
const fanartUrl = getFanartUrl(author.images);
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160); const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
return ( return (
<div className={styles.header} style={{ width }}> <div className={styles.header} style={{ width }}>
<div <div
className={styles.backdrop} className={styles.backdrop}
style={{ style={
backgroundImage: `url(${getFanartUrl(author.images)})` fanartUrl ?
}} { backgroundImage: `url(${fanartUrl})` } :
null
}
> >
<div className={styles.backdropOverlay} /> <div className={styles.backdropOverlay} />
</div> </div>
+3 -3
View File
@@ -89,9 +89,9 @@ class BookEditorFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'monitored', value: 'Monitored' }, { key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: 'Unmonitored' } { key: 'unmonitored', value: translate('Unmonitored') }
]; ];
return ( return (
+4 -4
View File
@@ -30,7 +30,7 @@ class BookshelfFooter extends Component {
const { const {
isSaving, isSaving,
saveError saveError
} = prevProps; } = this.props;
if (prevProps.isSaving && !isSaving && !saveError) { if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({ this.setState({
@@ -88,9 +88,9 @@ class BookshelfFooter extends Component {
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true }, { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'monitored', value: 'Monitored' }, { key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: 'Unmonitored' } { key: 'unmonitored', value: translate('Unmonitored') }
]; ];
const noChanges = monitored === NO_CHANGE && const noChanges = monitored === NO_CHANGE &&
+4 -2
View File
@@ -1,6 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AgendaConnector from './Agenda/AgendaConnector'; import AgendaConnector from './Agenda/AgendaConnector';
import * as calendarViews from './calendarViews'; import * as calendarViews from './calendarViews';
@@ -31,9 +33,9 @@ class Calendar extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<div> <Alert kind={kinds.DANGER}>
{translate('UnableToLoadTheCalendar')} {translate('UnableToLoadTheCalendar')}
</div> </Alert>
} }
{ {
+5 -3
View File
@@ -4,7 +4,9 @@ import React from 'react';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import styles from './Alert.css'; import styles from './Alert.css';
function Alert({ className, kind, children, ...otherProps }) { function Alert(props) {
const { className, kind, children, ...otherProps } = props;
return ( return (
<div <div
className={classNames( className={classNames(
@@ -19,8 +21,8 @@ function Alert({ className, kind, children, ...otherProps }) {
} }
Alert.propTypes = { Alert.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string,
kind: PropTypes.oneOf(kinds.all).isRequired, kind: PropTypes.oneOf(kinds.all),
children: PropTypes.node.isRequired children: PropTypes.node.isRequired
}; };
+5
View File
@@ -16,4 +16,9 @@
color: var(--textColor); color: var(--textColor);
font-size: 21px; font-size: 21px;
line-height: inherit; line-height: inherit;
&.small {
color: #909293;
font-size: 18px;
}
} }
+1
View File
@@ -3,6 +3,7 @@
interface CssExports { interface CssExports {
'fieldSet': string; 'fieldSet': string;
'legend': string; 'legend': string;
'small': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
+9 -1
View File
@@ -1,5 +1,7 @@
import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { sizes } from 'Helpers/Props';
import styles from './FieldSet.css'; import styles from './FieldSet.css';
class FieldSet extends Component { class FieldSet extends Component {
@@ -9,13 +11,14 @@ class FieldSet extends Component {
render() { render() {
const { const {
size,
legend, legend,
children children
} = this.props; } = this.props;
return ( return (
<fieldset className={styles.fieldSet}> <fieldset className={styles.fieldSet}>
<legend className={styles.legend}> <legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
{legend} {legend}
</legend> </legend>
{children} {children}
@@ -26,8 +29,13 @@ class FieldSet extends Component {
} }
FieldSet.propTypes = { FieldSet.propTypes = {
size: PropTypes.oneOf(sizes.all).isRequired,
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
children: PropTypes.node children: PropTypes.node
}; };
FieldSet.defaultProps = {
size: sizes.MEDIUM
};
export default FieldSet; export default FieldSet;
@@ -210,7 +210,7 @@ class FilterBuilderRow extends Component {
key: availablePropFilter.name, key: availablePropFilter.name,
value: availablePropFilter.label value: availablePropFilter.label
}; };
}); }).sort((a, b) => a.value.localeCompare(b.value));
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp); const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
@@ -1,4 +1,6 @@
.tag { .tag {
display: flex;
&.isLastTag { &.isLastTag {
.or { .or {
display: none; display: none;
@@ -6,7 +6,7 @@ import styles from './FilterBuilderRowValueTag.css';
function FilterBuilderRowValueTag(props) { function FilterBuilderRowValueTag(props) {
return ( return (
<span <div
className={styles.tag} className={styles.tag}
> >
<TagInputTag <TagInputTag
@@ -15,12 +15,13 @@ function FilterBuilderRowValueTag(props) {
/> />
{ {
!props.isLastTag && props.isLastTag ?
<span className={styles.or}> null :
<div className={styles.or}>
or or
</span> </div>
} }
</span> </div>
); );
} }
@@ -59,6 +59,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
max-width: 90%; max-width: 90%;
max-height: 100%;
width: 350px !important; width: 350px !important;
height: auto !important; height: auto !important;
} }
@@ -578,7 +578,7 @@ EnhancedSelectInput.propTypes = {
className: PropTypes.string, className: PropTypes.string,
disabledClassName: PropTypes.string, disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired, isDisabled: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
+17 -2
View File
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { inputTypes } from 'Helpers/Props'; import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
// import translate from 'Utilities/String/translate'; // import translate from 'Utilities/String/translate';
import AutoCompleteInput from './AutoCompleteInput'; import AutoCompleteInput from './AutoCompleteInput';
@@ -26,6 +26,7 @@ import PathInputConnector from './PathInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector'; import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector'; import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import TagInputConnector from './TagInputConnector'; import TagInputConnector from './TagInputConnector';
import TagSelectInputConnector from './TagSelectInputConnector';
import TextArea from './TextArea'; import TextArea from './TextArea';
import TextInput from './TextInput'; import TextInput from './TextInput';
import TextTagInputConnector from './TextTagInputConnector'; import TextTagInputConnector from './TextTagInputConnector';
@@ -103,6 +104,9 @@ function getComponent(type) {
case inputTypes.TEXT_TAG: case inputTypes.TEXT_TAG:
return TextTagInputConnector; return TextTagInputConnector;
case inputTypes.TAG_SELECT:
return TagSelectInputConnector;
case inputTypes.UMASK: case inputTypes.UMASK:
return UMaskInput; return UMaskInput;
@@ -266,16 +270,27 @@ FormInputGroup.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
containerClassName: PropTypes.string.isRequired, containerClassName: PropTypes.string.isRequired,
inputClassName: PropTypes.string, inputClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.any,
values: PropTypes.arrayOf(PropTypes.any),
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all),
min: PropTypes.number,
max: PropTypes.number,
unit: PropTypes.string, unit: PropTypes.string,
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
helpText: PropTypes.string, helpText: PropTypes.string,
helpTexts: PropTypes.arrayOf(PropTypes.string), helpTexts: PropTypes.arrayOf(PropTypes.string),
helpTextWarning: PropTypes.string, helpTextWarning: PropTypes.string,
helpLink: PropTypes.string, helpLink: PropTypes.string,
autoFocus: PropTypes.bool,
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object,
pending: PropTypes.bool, pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object), errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object) warnings: PropTypes.arrayOf(PropTypes.object),
onChange: PropTypes.func.isRequired
}; };
FormInputGroup.defaultProps = { FormInputGroup.defaultProps = {
+14 -12
View File
@@ -4,16 +4,18 @@ import React from 'react';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import styles from './FormLabel.css'; import styles from './FormLabel.css';
function FormLabel({ function FormLabel(props) {
children, const {
className, children,
errorClassName, className,
size, errorClassName,
name, size,
hasError, name,
isAdvanced, hasError,
...otherProps isAdvanced,
}) { ...otherProps
} = props;
return ( return (
<label <label
{...otherProps} {...otherProps}
@@ -31,13 +33,13 @@ function FormLabel({
} }
FormLabel.propTypes = { FormLabel.propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]).isRequired,
className: PropTypes.string, className: PropTypes.string,
errorClassName: PropTypes.string, errorClassName: PropTypes.string,
size: PropTypes.oneOf(sizes.all), size: PropTypes.oneOf(sizes.all),
name: PropTypes.string, name: PropTypes.string,
hasError: PropTypes.bool, hasError: PropTypes.bool,
isAdvanced: PropTypes.bool.isRequired isAdvanced: PropTypes.bool
}; };
FormLabel.defaultProps = { FormLabel.defaultProps = {
@@ -6,15 +6,17 @@ import { createSelector } from 'reselect';
import { metadataProfileNames } from 'Helpers/Props'; import { metadataProfileNames } from 'Helpers/Props';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByName from 'Utilities/Array/sortByName';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput'; import SelectInput from './SelectInput';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.metadataProfiles', sortByName), createSortedSectionSelector('settings.metadataProfiles', sortByName),
(state, { includeNoChange }) => includeNoChange, (state, { includeNoChange }) => includeNoChange,
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
(state, { includeMixed }) => includeMixed, (state, { includeMixed }) => includeMixed,
(state, { includeNone }) => includeNone, (state, { includeNone }) => includeNone,
(metadataProfiles, includeNoChange, includeMixed, includeNone) => { (metadataProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed, includeNone) => {
const profiles = metadataProfiles.items.filter((item) => item.name !== metadataProfileNames.NONE); const profiles = metadataProfiles.items.filter((item) => item.name !== metadataProfileNames.NONE);
const noneProfile = metadataProfiles.items.find((item) => item.name === metadataProfileNames.NONE); const noneProfile = metadataProfiles.items.find((item) => item.name === metadataProfileNames.NONE);
@@ -36,8 +38,8 @@ function createMapStateToProps() {
if (includeNoChange) { if (includeNoChange) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: 'No Change', value: translate('NoChange'),
disabled: true disabled: includeNoChangeDisabled
}); });
} }
@@ -68,8 +70,8 @@ class MetadataProfileSelectInputConnector extends Component {
values values
} = this.props; } = this.props;
if (!value || !_.some(values, (option) => parseInt(option.key) === value)) { if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key))); const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
if (firstValue) { if (firstValue) {
this.onChange({ name, value: firstValue.key }); this.onChange({ name, value: firstValue.key });
@@ -81,7 +83,7 @@ class MetadataProfileSelectInputConnector extends Component {
// Listeners // Listeners
onChange = ({ name, value }) => { onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) }); this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
}; };
// //
@@ -107,7 +109,8 @@ MetadataProfileSelectInputConnector.propTypes = {
}; };
MetadataProfileSelectInputConnector.defaultProps = { MetadataProfileSelectInputConnector.defaultProps = {
includeNoChange: false includeNoChange: false,
includeNone: true
}; };
export default connect(createMapStateToProps)(MetadataProfileSelectInputConnector); export default connect(createMapStateToProps)(MetadataProfileSelectInputConnector);
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import monitorOptions from 'Utilities/Author/monitorOptions'; import monitorOptions from 'Utilities/Author/monitorOptions';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput'; import SelectInput from './SelectInput';
function MonitorBooksSelectInput(props) { function MonitorBooksSelectInput(props) {
@@ -16,7 +17,7 @@ function MonitorBooksSelectInput(props) {
if (includeNoChange) { if (includeNoChange) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: 'No Change', value: translate('NoChange'),
disabled: true disabled: true
}); });
} }
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import monitorNewItemsOptions from 'Utilities/Author/monitorNewItemsOptions'; import monitorNewItemsOptions from 'Utilities/Author/monitorNewItemsOptions';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput'; import SelectInput from './SelectInput';
function MonitorNewItemsSelectInput(props) { function MonitorNewItemsSelectInput(props) {
@@ -15,7 +16,7 @@ function MonitorNewItemsSelectInput(props) {
if (includeNoChange) { if (includeNoChange) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: 'No Change', value: translate('NoChange'),
disabled: true disabled: true
}); });
} }
+1 -1
View File
@@ -10,7 +10,7 @@ function parseValue(props, value) {
} = props; } = props;
if (value == null || value === '') { if (value == null || value === '') {
return min; return null;
} }
let newValue = isFloat ? parseFloat(value) : parseInt(value); let newValue = isFloat ? parseFloat(value) : parseInt(value);
@@ -31,6 +31,8 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.SELECT; return inputTypes.SELECT;
case 'tag': case 'tag':
return inputTypes.TEXT_TAG; return inputTypes.TEXT_TAG;
case 'tagSelect':
return inputTypes.TAG_SELECT;
case 'textbox': case 'textbox':
return inputTypes.TEXT; return inputTypes.TEXT;
case 'oAuth': case 'oAuth':
@@ -62,6 +64,7 @@ function ProviderFieldFormGroup(props) {
name, name,
label, label,
helpText, helpText,
helpTextWarning,
helpLink, helpLink,
placeholder, placeholder,
value, value,
@@ -95,6 +98,7 @@ function ProviderFieldFormGroup(props) {
name={name} name={name}
label={label} label={label}
helpText={helpText} helpText={helpText}
helpTextWarning={helpTextWarning}
helpLink={helpLink} helpLink={helpLink}
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}
@@ -121,6 +125,7 @@ ProviderFieldFormGroup.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
helpText: PropTypes.string, helpText: PropTypes.string,
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string, helpLink: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
value: PropTypes.any, value: PropTypes.any,
@@ -5,14 +5,16 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByName from 'Utilities/Array/sortByName';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput'; import SelectInput from './SelectInput';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.qualityProfiles', sortByName), createSortedSectionSelector('settings.qualityProfiles', sortByName),
(state, { includeNoChange }) => includeNoChange, (state, { includeNoChange }) => includeNoChange,
(state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
(state, { includeMixed }) => includeMixed, (state, { includeMixed }) => includeMixed,
(qualityProfiles, includeNoChange, includeMixed) => { (qualityProfiles, includeNoChange, includeNoChangeDisabled = true, includeMixed) => {
const values = _.map(qualityProfiles.items, (qualityProfile) => { const values = _.map(qualityProfiles.items, (qualityProfile) => {
return { return {
key: qualityProfile.id, key: qualityProfile.id,
@@ -23,8 +25,8 @@ function createMapStateToProps() {
if (includeNoChange) { if (includeNoChange) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: 'No Change', value: translate('NoChange'),
disabled: true disabled: includeNoChangeDisabled
}); });
} }
@@ -55,8 +57,8 @@ class QualityProfileSelectInputConnector extends Component {
values values
} = this.props; } = this.props;
if (!value || !_.some(values, (option) => parseInt(option.key) === value)) { if (!value || !values.some((option) => option.key === value || parseInt(option.key) === value)) {
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key))); const firstValue = values.find((option) => !isNaN(parseInt(option.key)));
if (firstValue) { if (firstValue) {
this.onChange({ name, value: firstValue.key }); this.onChange({ name, value: firstValue.key });
@@ -68,7 +70,7 @@ class QualityProfileSelectInputConnector extends Component {
// Listeners // Listeners
onChange = ({ name, value }) => { onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) }); this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
}; };
// //
@@ -63,7 +63,7 @@ class RootFolderSelectInput extends Component {
render() { render() {
const { const {
value, includeNoChange,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -71,7 +71,6 @@ class RootFolderSelectInput extends Component {
<div> <div>
<EnhancedSelectInput <EnhancedSelectInput
{...otherProps} {...otherProps}
value={value || ''}
selectedValueComponent={RootFolderSelectInputSelectedValue} selectedValueComponent={RootFolderSelectInputSelectedValue}
optionComponent={RootFolderSelectInputOption} optionComponent={RootFolderSelectInputOption}
onChange={this.onChange} onChange={this.onChange}
@@ -93,7 +92,12 @@ RootFolderSelectInput.propTypes = {
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
includeNoChange: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired
}; };
RootFolderSelectInput.defaultProps = {
includeNoChange: false
};
export default RootFolderSelectInput; export default RootFolderSelectInput;
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import translate from 'Utilities/String/translate';
import RootFolderSelectInput from './RootFolderSelectInput'; import RootFolderSelectInput from './RootFolderSelectInput';
const ADD_NEW_KEY = 'addNew'; const ADD_NEW_KEY = 'addNew';
@@ -12,7 +13,8 @@ function createMapStateToProps() {
(state, { value }) => value, (state, { value }) => value,
(state, { includeMissingValue }) => includeMissingValue, (state, { includeMissingValue }) => includeMissingValue,
(state, { includeNoChange }) => includeNoChange, (state, { includeNoChange }) => includeNoChange,
(rootFolders, value, includeMissingValue, includeNoChange) => { (state, { includeNoChangeDisabled }) => includeNoChangeDisabled,
(rootFolders, value, includeMissingValue, includeNoChange, includeNoChangeDisabled = true) => {
const values = rootFolders.items.map((rootFolder) => { const values = rootFolders.items.map((rootFolder) => {
return { return {
key: rootFolder.path, key: rootFolder.path,
@@ -27,8 +29,8 @@ function createMapStateToProps() {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: '', value: '',
name: 'No Change', name: translate('NoChange'),
isDisabled: true, isDisabled: includeNoChangeDisabled,
isMissing: false isMissing: false
}); });
} }
+1 -1
View File
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string, className: PropTypes.string,
disabledClassName: PropTypes.string, disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
hasError: PropTypes.bool, hasError: PropTypes.bool,
+20 -2
View File
@@ -75,6 +75,18 @@ class TagInput extends Component {
// //
// Listeners // Listeners
onTagEdit = ({ value, ...otherProps }) => {
const currentValue = this.state.value;
if (currentValue && this.props.onTagReplace) {
this.props.onTagReplace(otherProps, { name: currentValue });
} else {
this.props.onTagDelete(otherProps);
}
this.setState({ value });
};
onInputContainerPress = () => { onInputContainerPress = () => {
this._autosuggestRef.input.focus(); this._autosuggestRef.input.focus();
}; };
@@ -188,6 +200,7 @@ class TagInput extends Component {
const { const {
tags, tags,
kind, kind,
canEdit,
tagComponent, tagComponent,
onTagDelete onTagDelete
} = this.props; } = this.props;
@@ -199,8 +212,10 @@ class TagInput extends Component {
kind={kind} kind={kind}
inputProps={inputProps} inputProps={inputProps}
isFocused={this.state.isFocused} isFocused={this.state.isFocused}
canEdit={canEdit}
tagComponent={tagComponent} tagComponent={tagComponent}
onTagDelete={onTagDelete} onTagDelete={onTagDelete}
onTagEdit={this.onTagEdit}
onInputContainerPress={this.onInputContainerPress} onInputContainerPress={this.onInputContainerPress}
/> />
); );
@@ -225,7 +240,7 @@ class TagInput extends Component {
<AutoSuggestInput <AutoSuggestInput
{...otherProps} {...otherProps}
forwardedRef={this._setAutosuggestRef} forwardedRef={this._setAutosuggestRef}
className={styles.internalInput} className={className}
inputContainerClassName={classNames( inputContainerClassName={classNames(
inputContainerClassName, inputContainerClassName,
isFocused && styles.isFocused, isFocused && styles.isFocused,
@@ -262,11 +277,13 @@ TagInput.propTypes = {
placeholder: PropTypes.string.isRequired, placeholder: PropTypes.string.isRequired,
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired, delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
minQueryLength: PropTypes.number.isRequired, minQueryLength: PropTypes.number.isRequired,
canEdit: PropTypes.bool,
hasError: PropTypes.bool, hasError: PropTypes.bool,
hasWarning: PropTypes.bool, hasWarning: PropTypes.bool,
tagComponent: PropTypes.elementType.isRequired, tagComponent: PropTypes.elementType.isRequired,
onTagAdd: PropTypes.func.isRequired, onTagAdd: PropTypes.func.isRequired,
onTagDelete: PropTypes.func.isRequired onTagDelete: PropTypes.func.isRequired,
onTagReplace: PropTypes.func
}; };
TagInput.defaultProps = { TagInput.defaultProps = {
@@ -277,6 +294,7 @@ TagInput.defaultProps = {
placeholder: '', placeholder: '',
delimiters: ['Tab', 'Enter', ' ', ','], delimiters: ['Tab', 'Enter', ' ', ','],
minQueryLength: 1, minQueryLength: 1,
canEdit: false,
tagComponent: TagInputTag tagComponent: TagInputTag
}; };
@@ -138,6 +138,7 @@ class TagInputConnector extends Component {
<TagInput <TagInput
onTagAdd={this.onTagAdd} onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete} onTagDelete={this.onTagDelete}
onTagReplace={this.onTagReplace}
{...this.props} {...this.props}
/> />
); );
@@ -28,8 +28,10 @@ class TagInputInput extends Component {
tags, tags,
inputProps, inputProps,
kind, kind,
canEdit,
tagComponent: TagComponent, tagComponent: TagComponent,
onTagDelete onTagDelete,
onTagEdit
} = this.props; } = this.props;
return ( return (
@@ -46,8 +48,10 @@ class TagInputInput extends Component {
index={index} index={index}
tag={tag} tag={tag}
kind={kind} kind={kind}
canEdit={canEdit}
isLastTag={index === tags.length - 1} isLastTag={index === tags.length - 1}
onDelete={onTagDelete} onDelete={onTagDelete}
onEdit={onTagEdit}
/> />
); );
}) })
@@ -66,8 +70,10 @@ TagInputInput.propTypes = {
inputProps: PropTypes.object.isRequired, inputProps: PropTypes.object.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired, kind: PropTypes.oneOf(kinds.all).isRequired,
isFocused: PropTypes.bool.isRequired, isFocused: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
tagComponent: PropTypes.elementType.isRequired, tagComponent: PropTypes.elementType.isRequired,
onTagDelete: PropTypes.func.isRequired, onTagDelete: PropTypes.func.isRequired,
onTagEdit: PropTypes.func.isRequired,
onInputContainerPress: PropTypes.func.isRequired onInputContainerPress: PropTypes.func.isRequired
}; };
+31 -2
View File
@@ -1,5 +1,34 @@
.tag { .tag {
composes: link from '~Components/Link/Link.css'; display: flex;
justify-content: center;
flex-direction: column;
max-width: 100%;
height: 31px; height: 31px;
} }
.link {
max-width: 100%;
}
.linkWithEdit {
max-width: calc(100% - 9px - 4px - 2px);
}
.editContainer {
display: inline-block;
margin-left: 4px;
padding-left: 2px;
border-left: 1px solid #eee;
}
.editButton {
composes: button from '~Components/Link/IconButton.css';
width: 9px;
}
.label {
composes: label from '~Components/Label.css';
max-width: 100%;
}
+5
View File
@@ -1,6 +1,11 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'editButton': string;
'editContainer': string;
'label': string;
'link': string;
'linkWithEdit': string;
'tag': string; 'tag': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
+48 -8
View File
@@ -1,8 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import tagShape from 'Helpers/Props/Shapes/tagShape'; import tagShape from 'Helpers/Props/Shapes/tagShape';
import styles from './TagInputTag.css'; import styles from './TagInputTag.css';
@@ -24,24 +25,61 @@ class TagInputTag extends Component {
}); });
}; };
onEdit = () => {
const {
index,
tag,
onEdit
} = this.props;
onEdit({
index,
id: tag.id,
value: tag.name
});
};
// //
// Render // Render
render() { render() {
const { const {
tag, tag,
kind kind,
canEdit
} = this.props; } = this.props;
return ( return (
<Link <div
className={styles.tag} className={styles.tag}
tabIndex={-1} tabIndex={-1}
onPress={this.onDelete}
> >
<Label kind={kind}> <Label
{tag.name} className={styles.label}
kind={kind}
>
<Link
className={canEdit ? styles.linkWithEdit : styles.link}
tabIndex={-1}
onPress={this.onDelete}
>
{tag.name}
</Link>
{
canEdit ?
<div className={styles.editContainer}>
<IconButton
className={styles.editButton}
name={icons.EDIT}
size={9}
onPress={this.onEdit}
/>
</div> :
null
}
</Label> </Label>
</Link> </div>
); );
} }
} }
@@ -50,7 +88,9 @@ TagInputTag.propTypes = {
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
tag: PropTypes.shape(tagShape), tag: PropTypes.shape(tagShape),
kind: PropTypes.oneOf(kinds.all).isRequired, kind: PropTypes.oneOf(kinds.all).isRequired,
onDelete: PropTypes.func.isRequired canEdit: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired
}; };
export default TagInputTag; export default TagInputTag;
@@ -0,0 +1,102 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import TagInput from './TagInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state, { values }) => values,
(tags, tagList) => {
const sortedTags = _.sortBy(tagList, 'value');
return {
tags: tags.reduce((acc, tag) => {
const matchingTag = _.find(tagList, { key: tag });
if (matchingTag) {
acc.push({
id: tag,
name: matchingTag.value
});
}
return acc;
}, []),
tagList: sortedTags.map(({ key: id, value: name }) => {
return {
id,
name
};
}),
allTags: sortedTags
};
}
);
}
class TagSelectInputConnector extends Component {
//
// Listeners
onTagAdd = (tag) => {
const {
name,
value,
allTags
} = this.props;
const existingTag =_.some(allTags, { key: tag.id });
const newValue = value.slice();
if (existingTag) {
newValue.push(tag.id);
}
this.props.onChange({ name, value: newValue });
};
onTagDelete = ({ index }) => {
const {
name,
value
} = this.props;
const newValue = value.slice();
newValue.splice(index, 1);
this.props.onChange({
name,
value: newValue
});
};
//
// Render
render() {
return (
<TagInput
onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete}
{...this.props}
/>
);
}
}
TagSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.number).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
allTags: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps)(TagSelectInputConnector);
@@ -71,6 +71,20 @@ class TextTagInputConnector extends Component {
}); });
}; };
onTagReplace = (tagToReplace, newTag) => {
const {
name,
valueArray,
onChange
} = this.props;
const newValue = [...valueArray];
newValue.splice(tagToReplace.index, 1);
newValue.push(newTag.name.trim());
onChange({ name, value: newValue });
};
// //
// Render // Render
@@ -80,6 +94,7 @@ class TextTagInputConnector extends Component {
tagList={[]} tagList={[]}
onTagAdd={this.onTagAdd} onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete} onTagDelete={this.onTagDelete}
onTagReplace={this.onTagReplace}
{...this.props} {...this.props}
/> />
); );
+1
View File
@@ -31,6 +31,7 @@ function Label(props) {
Label.propTypes = { Label.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
title: PropTypes.string,
kind: PropTypes.oneOf(kinds.all).isRequired, kind: PropTypes.oneOf(kinds.all).isRequired,
size: PropTypes.oneOf(sizes.all).isRequired, size: PropTypes.oneOf(sizes.all).isRequired,
outline: PropTypes.bool.isRequired, outline: PropTypes.bool.isRequired,
@@ -39,11 +39,13 @@ function IconButton(props) {
} }
IconButton.propTypes = { IconButton.propTypes = {
...Link.propTypes,
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
iconClassName: PropTypes.string, iconClassName: PropTypes.string,
kind: PropTypes.string, kind: PropTypes.string,
name: PropTypes.object.isRequired, name: PropTypes.object.isRequired,
size: PropTypes.number, size: PropTypes.number,
title: PropTypes.string,
isSpinning: PropTypes.bool, isSpinning: PropTypes.bool,
isDisabled: PropTypes.bool isDisabled: PropTypes.bool
}; };
-110
View File
@@ -1,110 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styles from './Link.css';
class Link extends Component {
//
// Listeners
onClick = (event) => {
const {
isDisabled,
onPress
} = this.props;
if (!isDisabled && onPress) {
onPress(event);
}
};
//
// Render
render() {
const {
className,
component,
to,
target,
isDisabled,
noRouter,
onPress,
...otherProps
} = this.props;
const linkProps = { target };
let el = component;
if (to && typeof to === 'string') {
if ((/\w+?:\/\//).test(to)) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_blank';
linkProps.rel = 'noreferrer';
} else if (noRouter) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_self';
} else {
el = RouterLink;
linkProps.to = `${window.Readarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.target = target;
}
} else if (to && typeof to === 'object') {
el = RouterLink;
linkProps.target = target;
if (to.pathname.startsWith(`${window.Readarr.urlBase}/`)) {
linkProps.to = to;
} else {
const pathname = `${window.Readarr.urlBase}/${to.pathname.replace(/^\//, '')}`;
linkProps.to = {
...to,
pathname
};
}
}
if (el === 'button' || el === 'input') {
linkProps.type = otherProps.type || 'button';
linkProps.disabled = isDisabled;
}
linkProps.className = classNames(
className,
styles.link,
to && styles.to,
isDisabled && 'isDisabled'
);
const props = {
...otherProps,
...linkProps
};
props.onClick = this.onClick;
return (
React.createElement(el, props)
);
}
}
Link.propTypes = {
className: PropTypes.string,
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
target: PropTypes.string,
isDisabled: PropTypes.bool,
noRouter: PropTypes.bool,
onPress: PropTypes.func
};
Link.defaultProps = {
component: 'button',
noRouter: false
};
export default Link;
+96
View File
@@ -0,0 +1,96 @@
import classNames from 'classnames';
import React, {
ComponentClass,
FunctionComponent,
SyntheticEvent,
useCallback,
} from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styles from './Link.css';
interface ReactRouterLinkProps {
to?: string;
}
export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
className?: string;
component?:
| string
| FunctionComponent<LinkProps>
| ComponentClass<LinkProps, unknown>;
to?: string;
target?: string;
isDisabled?: boolean;
noRouter?: boolean;
onPress?(event: SyntheticEvent): void;
}
function Link(props: LinkProps) {
const {
className,
component = 'button',
to,
target,
type,
isDisabled,
noRouter = false,
onPress,
...otherProps
} = props;
const onClick = useCallback(
(event: SyntheticEvent) => {
if (!isDisabled && onPress) {
onPress(event);
}
},
[isDisabled, onPress]
);
const linkProps: React.HTMLProps<HTMLAnchorElement> & ReactRouterLinkProps = {
target,
};
let el = component;
if (to) {
if (/\w+?:\/\//.test(to)) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_blank';
linkProps.rel = 'noreferrer';
} else if (noRouter) {
el = 'a';
linkProps.href = to;
linkProps.target = target || '_self';
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
el = RouterLink;
linkProps.to = `${window.Readarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.target = target;
}
}
if (el === 'button' || el === 'input') {
linkProps.type = type || 'button';
linkProps.disabled = isDisabled;
}
linkProps.className = classNames(
className,
styles.link,
to && styles.to,
isDisabled && 'isDisabled'
);
const elementProps = {
...otherProps,
type,
...linkProps,
};
elementProps.onClick = onClick;
return React.createElement(el, elementProps);
}
export default Link;
@@ -42,6 +42,7 @@ function SpinnerButton(props) {
} }
SpinnerButton.propTypes = { SpinnerButton.propTypes = {
...Button.Props,
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
@@ -13,24 +13,51 @@ class InlineMarkdown extends Component {
data data
} = this.props; } = this.props;
// For now only replace links // For now only replace links or code blocks (not both)
const markdownBlocks = []; const markdownBlocks = [];
if (data) { if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g); const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0; let endIndex = 0;
let match = null; let match = null;
while ((match = regex.exec(data)) !== null) {
while ((match = linkRegex.exec(data)) !== null) {
if (match.index > endIndex) { if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
} }
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>); markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
endIndex = match.index + match[0].length; endIndex = match.index + match[0].length;
} }
if (endIndex !== data.length) { if (endIndex !== data.length && markdownBlocks.length > 0) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
} }
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
endIndex = 0;
match = null;
let matchedCode = false;
while ((match = codeRegex.exec(data)) !== null) {
matchedCode = true;
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<code key={`code-${match.index}`}>{match[0].substring(1, match[0].length - 1)}</code>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
if (markdownBlocks.length === 0) {
markdownBlocks.push(data);
}
} }
return <span className={className}>{markdownBlocks}</span>; return <span className={className}>{markdownBlocks}</span>;
@@ -7,6 +7,7 @@ function ErrorPage(props) {
const { const {
version, version,
isLocalStorageSupported, isLocalStorageSupported,
hasTranslationsError,
authorError, authorError,
customFiltersError, customFiltersError,
tagsError, tagsError,
@@ -20,6 +21,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) { if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (hasTranslationsError) {
errorMessage = 'Failed to load translations from API';
} else if (authorError) { } else if (authorError) {
errorMessage = getErrorMessage(authorError, 'Failed to load author from API'); errorMessage = getErrorMessage(authorError, 'Failed to load author from API');
} else if (customFiltersError) { } else if (customFiltersError) {
@@ -52,6 +55,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = { ErrorPage.propTypes = {
version: PropTypes.string.isRequired, version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired, isLocalStorageSupported: PropTypes.bool.isRequired,
hasTranslationsError: PropTypes.bool.isRequired,
authorError: PropTypes.object, authorError: PropTypes.object,
customFiltersError: PropTypes.object, customFiltersError: PropTypes.object,
tagsError: PropTypes.object, tagsError: PropTypes.object,
@@ -26,6 +26,7 @@ function createCleanAuthorSelector() {
sortName, sortName,
titleSlug, titleSlug,
images, images,
firstCharacter: authorName.charAt(0).toLowerCase(),
tags: tags.reduce((acc, id) => { tags: tags.reduce((acc, id) => {
const matchingTag = allTags.find((tag) => tag.id === id); const matchingTag = allTags.find((tag) => tag.id === id);
@@ -58,6 +59,7 @@ function createCleanBookSelector() {
sortName: title, sortName: title,
titleSlug, titleSlug,
images, images,
firstCharacter: title.charAt(0).toLowerCase(),
tags: [] tags: []
}; };
}); });
@@ -53,10 +53,7 @@ class PageHeader extends Component {
<div className={styles.logoContainer}> <div className={styles.logoContainer}>
<Link <Link
className={styles.logoLink} className={styles.logoLink}
to={{ to={'/'}
pathname: '/',
state: { restoreScrollPosition: true }
}}
> >
<img <img
className={styles.logo} className={styles.logo}
@@ -15,9 +15,36 @@ const fuseOptions = {
function getSuggestions(items, value) { function getSuggestions(items, value) {
const limit = 10; const limit = 10;
let suggestions = [];
const fuse = new Fuse(items, fuseOptions); if (value.length === 1) {
return fuse.search(value, { limit }); for (let i = 0; i < items.length; i++) {
const s = items[i];
if (s.firstCharacter === value.toLowerCase()) {
suggestions.push({
item: items[i],
indices: [
[0, 0]
],
matches: [
{
value: s.title,
key: 'title'
}
],
arrayIndex: 0
});
if (suggestions.length > limit) {
break;
}
}
}
} else {
const fuse = new Fuse(items, fuseOptions);
suggestions = fuse.search(value, { limit });
}
return suggestions;
} }
onmessage = function(e) { onmessage = function(e) {
@@ -225,6 +225,7 @@ class PageConnector extends Component {
render() { render() {
const { const {
hasTranslationsError,
isPopulated, isPopulated,
hasError, hasError,
dispatchFetchAuthor, dispatchFetchAuthor,
@@ -239,11 +240,12 @@ class PageConnector extends Component {
...otherProps ...otherProps
} = this.props; } = this.props;
if (hasError || !this.state.isLocalStorageSupported) { if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) {
return ( return (
<ErrorPage <ErrorPage
{...this.state} {...this.state}
{...otherProps} {...otherProps}
hasTranslationsError={hasTranslationsError}
/> />
); );
} }
@@ -264,6 +266,7 @@ class PageConnector extends Component {
} }
PageConnector.propTypes = { PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
@@ -1,6 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
function PageSectionContent(props) { function PageSectionContent(props) {
const { const {
@@ -17,7 +19,7 @@ function PageSectionContent(props) {
); );
} else if (!isFetching && !!error) { } else if (!isFetching && !!error) {
return ( return (
<div>{errorMessage}</div> <Alert kind={kinds.DANGER}>{errorMessage}</Alert>
); );
} else if (isPopulated && !error) { } else if (isPopulated && !error) {
return ( return (
+32 -32
View File
@@ -16,6 +16,38 @@
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
color: var(--white); color: var(--white);
transition: width 0.6s ease; transition: width 0.6s ease;
&.primary {
background-color: var(--primaryColor);
}
&.danger {
background-color: var(--dangerColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(#f05050 shade(5%)), color(#f05050 shade(5%)) 5px, color(#f05050 shade(15%)) 5px, color(#f05050 shade(15%)) 10px);
}
}
&.success {
background-color: var(--successColor);
}
&.purple {
background-color: var(--purple);
}
&.warning {
background-color: var(--warningColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, #ffa500, #ffa500 5px, color(#ffa500 tint(15%)) 5px, color(#ffa500 tint(15%)) 10px);
}
}
&.info {
background-color: var(--infoColor);
}
} }
.frontTextContainer { .frontTextContainer {
@@ -45,38 +77,6 @@
cursor: default; cursor: default;
} }
.primary {
background-color: var(--primaryColor);
}
.danger {
background-color: var(--dangerColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(#f05050 shade(5%)), color(#f05050 shade(5%)) 5px, color(#f05050 shade(15%)) 5px, color(#f05050 shade(15%)) 10px);
}
}
.success {
background-color: var(--successColor);
}
.purple {
background-color: var(--purple);
}
.warning {
background-color: var(--warningColor);
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, #ffa500, #ffa500 5px, color(#ffa500 tint(15%)) 5px, color(#ffa500 tint(15%)) 10px);
}
}
.info {
background-color: var(--infoColor);
}
.small { .small {
height: $progressBarSmallHeight; height: $progressBarSmallHeight;
+2 -2
View File
@@ -38,7 +38,7 @@ function ProgressBar(props) {
{ {
showText && width ? showText && width ?
<div <div
className={styles.backTextContainer} className={classNames(styles.backTextContainer, styles[kind])}
style={{ width: actualWidth }} style={{ width: actualWidth }}
> >
<div className={styles.backText}> <div className={styles.backText}>
@@ -67,7 +67,7 @@ function ProgressBar(props) {
{ {
showText ? showText ?
<div <div
className={styles.frontTextContainer} className={classNames(styles.frontTextContainer, styles[kind])}
style={{ width: progressPercent }} style={{ width: progressPercent }}
> >
<div <div
+12
View File
@@ -0,0 +1,12 @@
import React from 'react';
interface Column {
name: string;
label: string | React.ReactNode;
columnLabel?: string;
isSortable?: boolean;
isVisible: boolean;
isModifiable?: boolean;
}
export default Column;
+2
View File
@@ -52,6 +52,7 @@ function Table(props) {
scrollDirections.HORIZONTAL : scrollDirections.HORIZONTAL :
scrollDirections.NONE scrollDirections.NONE
} }
autoFocus={false}
> >
<table className={className}> <table className={className}>
<TableHeader> <TableHeader>
@@ -120,6 +121,7 @@ function Table(props) {
} }
Table.propTypes = { Table.propTypes = {
...TableHeaderCell.props,
className: PropTypes.string, className: PropTypes.string,
horizontalScroll: PropTypes.bool.isRequired, horizontalScroll: PropTypes.bool.isRequired,
selectAll: PropTypes.bool.isRequired, selectAll: PropTypes.bool.isRequired,
@@ -1,18 +1,19 @@
{ {
"name": "", "name": "Readarr",
"icons": [ "icons": [
{ {
"src": "/Content/Images/Icons/android-chrome-192x192.png", "src": "android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/Content/Images/Icons/android-chrome-512x512.png", "src": "android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
], ],
"start_url": "../../../../",
"theme_color": "#3a3f51", "theme_color": "#3a3f51",
"background_color": "#3a3f51", "background_color": "#3a3f51",
"display": "standalone" "display": "standalone"
} }
@@ -0,0 +1,11 @@
import { useEffect, useRef } from 'react';
export default function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
@@ -0,0 +1,113 @@
import { cloneDeep } from 'lodash';
import { useReducer } from 'react';
import ModelBase from 'App/ModelBase';
import areAllSelected from 'Utilities/Table/areAllSelected';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
export type SelectedState = Record<number, boolean>;
export interface SelectState {
selectedState: SelectedState;
lastToggled: number | null;
allSelected: boolean;
allUnselected: boolean;
}
export type SelectAction =
| { type: 'reset' }
| { type: 'selectAll'; items: ModelBase[] }
| { type: 'unselectAll'; items: ModelBase[] }
| {
type: 'toggleSelected';
id: number;
isSelected: boolean;
shiftKey: boolean;
items: ModelBase[];
}
| {
type: 'removeItem';
id: number;
}
| {
type: 'updateItems';
items: ModelBase[];
};
export type Dispatch = (action: SelectAction) => void;
const initialState = {
selectedState: {},
lastToggled: null,
allSelected: false,
allUnselected: true,
items: [],
};
function getSelectedState(items: ModelBase[], existingState: SelectedState) {
return items.reduce((acc: SelectedState, item) => {
const id = item.id;
acc[id] = existingState[id] ?? false;
return acc;
}, {});
}
function selectReducer(state: SelectState, action: SelectAction): SelectState {
const { selectedState } = state;
switch (action.type) {
case 'reset': {
return cloneDeep(initialState);
}
case 'selectAll': {
return {
...selectAll(selectedState, true),
};
}
case 'unselectAll': {
return {
...selectAll(selectedState, false),
};
}
case 'toggleSelected': {
const result = {
...toggleSelected(
state,
action.items,
action.id,
action.isSelected,
action.shiftKey
),
};
return result;
}
case 'updateItems': {
const nextSelectedState = getSelectedState(action.items, selectedState);
return {
...state,
...areAllSelected(nextSelectedState),
selectedState: nextSelectedState,
};
}
default: {
throw new Error(`Unhandled action type: ${action.type}`);
}
}
}
export default function useSelectState(): [SelectState, Dispatch] {
const selectedState = getSelectedState([], {});
const [state, dispatch] = useReducer(selectReducer, {
selectedState,
lastToggled: null,
allSelected: false,
allUnselected: true,
});
return [state, dispatch];
}
@@ -0,0 +1,6 @@
enum SortDirection {
Ascending = 'ascending',
Descending = 'descending',
}
export default SortDirection;
+2
View File
@@ -68,6 +68,7 @@ import {
faInfoCircle as fasInfoCircle, faInfoCircle as fasInfoCircle,
faLaptop as fasLaptop, faLaptop as fasLaptop,
faLevelUpAlt as fasLevelUpAlt, faLevelUpAlt as fasLevelUpAlt,
faListCheck as fasListCheck,
faLongArrowAltRight as fasLongArrowAltRight, faLongArrowAltRight as fasLongArrowAltRight,
faMedkit as fasMedkit, faMedkit as fasMedkit,
faMinus as fasMinus, faMinus as fasMinus,
@@ -166,6 +167,7 @@ export const INFO = fasInfoCircle;
export const INTERACTIVE = fasUser; export const INTERACTIVE = fasUser;
export const KEYBOARD = farKeyboard; export const KEYBOARD = farKeyboard;
export const LOGOUT = fasSignOutAlt; export const LOGOUT = fasSignOutAlt;
export const MANAGE = fasListCheck;
export const MEDIA_INFO = farFileInvoice; export const MEDIA_INFO = farFileInvoice;
export const MISSING = fasExclamationTriangle; export const MISSING = fasExclamationTriangle;
export const MONITORED = fasBookmark; export const MONITORED = fasBookmark;
+2
View File
@@ -22,6 +22,7 @@ export const TAG = 'tag';
export const TEXT = 'text'; export const TEXT = 'text';
export const TEXT_AREA = 'textArea'; export const TEXT_AREA = 'textArea';
export const TEXT_TAG = 'textTag'; export const TEXT_TAG = 'textTag';
export const TAG_SELECT = 'tagSelect';
export const UMASK = 'umask'; export const UMASK = 'umask';
export const all = [ export const all = [
@@ -49,5 +50,6 @@ export const all = [
TEXT, TEXT,
TEXT_AREA, TEXT_AREA,
TEXT_TAG, TEXT_TAG,
TAG_SELECT,
UMASK UMASK
]; ];
@@ -258,7 +258,9 @@ class InteractiveImportRow extends Component {
> >
{ {
showReleaseGroupPlaceholder ? showReleaseGroupPlaceholder ?
<InteractiveImportRowCellPlaceholder /> : <InteractiveImportRowCellPlaceholder
isOptional={true}
/> :
releaseGroup releaseGroup
} }
</TableRowCellButton> </TableRowCellButton>
@@ -5,3 +5,7 @@
height: 25px; height: 25px;
border: 2px dashed var(--dangerColor); border: 2px dashed var(--dangerColor);
} }
.optional {
border: 2px dashed var(--gray);
}
@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'optional': string;
'placeholder': string; 'placeholder': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
@@ -1,10 +0,0 @@
import React from 'react';
import styles from './InteractiveImportRowCellPlaceholder.css';
function InteractiveImportRowCellPlaceholder() {
return (
<span className={styles.placeholder} />
);
}
export default InteractiveImportRowCellPlaceholder;
@@ -0,0 +1,22 @@
import classNames from 'classnames';
import React from 'react';
import styles from './InteractiveImportRowCellPlaceholder.css';
interface InteractiveImportRowCellPlaceholderProps {
isOptional?: boolean;
}
function InteractiveImportRowCellPlaceholder(
props: InteractiveImportRowCellPlaceholderProps
) {
return (
<span
className={classNames(
styles.placeholder,
props.isOptional && styles.optional
)}
/>
);
}
export default InteractiveImportRowCellPlaceholder;
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -92,9 +93,9 @@ class SelectQualityModalContent extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<div> <Alert kind={kinds.DANGER}>
{translate('UnableToLoadQualities')} {translate('UnableToLoadQualities')}
</div> </Alert>
} }
{ {
@@ -7,3 +7,9 @@
.filteredMessage { .filteredMessage {
margin-top: 10px; margin-top: 10px;
} }
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}
@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'blankpad': string;
'filterMenuContainer': string; 'filterMenuContainer': string;
'filteredMessage': string; 'filteredMessage': string;
} }
@@ -104,7 +104,7 @@ function InteractiveSearch(props) {
{ {
!isFetching && error ? !isFetching && error ?
<div> <div className={styles.blankpad}>
Unable to load results for this book search. Try again later Unable to load results for this book search. Try again later
</div> : </div> :
null null
@@ -112,7 +112,7 @@ function InteractiveSearch(props) {
{ {
!isFetching && isPopulated && !totalReleasesCount ? !isFetching && isPopulated && !totalReleasesCount ?
<div> <div className={styles.blankpad}>
No results found No results found
</div> : </div> :
null null
@@ -120,7 +120,7 @@ function InteractiveSearch(props) {
{ {
!!totalReleasesCount && isPopulated && !items.length ? !!totalReleasesCount && isPopulated && !items.length ?
<div> <div className={styles.blankpad}>
All results are hidden by the applied filter All results are hidden by the applied filter
</div> : </div> :
null null
@@ -15,7 +15,7 @@ import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge'; import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import Peers from './Peers'; import Peers from './Peers';
import styles from './InteractiveSearchRow.css'; import styles from './InteractiveSearchRow.css';
@@ -172,7 +172,7 @@ class InteractiveSearchRow extends Component {
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
<Tooltip <Tooltip
anchor={ anchor={
formatPreferredWordScore(customFormatScore, customFormats.length) formatCustomFormatScore(customFormatScore, customFormats.length)
} }
tooltip={<BookFormats formats={customFormats} />} tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM} position={tooltipPositions.BOTTOM}
@@ -78,7 +78,7 @@ class Specification extends Component {
<IconButton <IconButton
className={styles.cloneButton} className={styles.cloneButton}
title={translate('Clone')} title={translate('CloneCondition')}
name={icons.CLONE} name={icons.CLONE}
onPress={this.onCloneSpecificationPress} onPress={this.onCloneSpecificationPress}
/> />
@@ -92,14 +92,14 @@ class Specification extends Component {
{ {
negate && negate &&
<Label kind={kinds.DANGER}> <Label kind={kinds.DANGER}>
Negated {translate('Negated')}
</Label> </Label>
} }
{ {
required && required &&
<Label kind={kinds.SUCCESS}> <Label kind={kinds.SUCCESS}>
Required {translate('Required')}
</Label> </Label>
} }
</div> </div>
@@ -114,8 +114,8 @@ class Specification extends Component {
<ConfirmModal <ConfirmModal
isOpen={this.state.isDeleteSpecificationModalOpen} isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteFormat')} title={translate('DeleteCondition')}
message={translate('DeleteFormatMessageText', [name])} message={translate('DeleteConditionMessageText', [name])}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification} onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose} onCancel={this.onDeleteSpecificationModalClose}
@@ -8,6 +8,7 @@ import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector'; import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector'; import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector';
import RemotePathMappingsConnector from './RemotePathMappings/RemotePathMappingsConnector'; import RemotePathMappingsConnector from './RemotePathMappings/RemotePathMappingsConnector';
@@ -23,7 +24,8 @@ class DownloadClientSettings extends Component {
this.state = { this.state = {
isSaving: false, isSaving: false,
hasPendingChanges: false hasPendingChanges: false,
isManageDownloadClientsOpen: false
}; };
} }
@@ -38,6 +40,14 @@ class DownloadClientSettings extends Component {
this.setState(payload); this.setState(payload);
}; };
onManageDownloadClientsPress = () => {
this.setState({ isManageDownloadClientsOpen: true });
};
onManageDownloadClientsModalClose = () => {
this.setState({ isManageDownloadClientsOpen: false });
};
onSavePress = () => { onSavePress = () => {
if (this._saveCallback) { if (this._saveCallback) {
this._saveCallback(); this._saveCallback();
@@ -55,7 +65,8 @@ class DownloadClientSettings extends Component {
const { const {
isSaving, isSaving,
hasPendingChanges hasPendingChanges,
isManageDownloadClientsOpen
} = this.state; } = this.state;
return ( return (
@@ -73,6 +84,12 @@ class DownloadClientSettings extends Component {
isSpinning={isTestingAll} isSpinning={isTestingAll}
onPress={dispatchTestAllDownloadClients} onPress={dispatchTestAllDownloadClients}
/> />
<PageToolbarButton
label={translate('ManageClients')}
iconName={icons.MANAGE}
onPress={this.onManageDownloadClientsPress}
/>
</Fragment> </Fragment>
} }
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
@@ -87,6 +104,11 @@ class DownloadClientSettings extends Component {
/> />
<RemotePathMappingsConnector /> <RemotePathMappingsConnector />
<ManageDownloadClientsModal
isOpen={isManageDownloadClientsOpen}
onModalClose={this.onManageDownloadClientsModalClose}
/>
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import Card from 'Components/Card'; import Card from 'Components/Card';
import Label from 'Components/Label'; import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import EditDownloadClientModalConnector from './EditDownloadClientModalConnector'; import EditDownloadClientModalConnector from './EditDownloadClientModalConnector';
@@ -56,7 +57,9 @@ class DownloadClient extends Component {
id, id,
name, name,
enable, enable,
priority priority,
tags,
tagList
} = this.props; } = this.props;
return ( return (
@@ -94,6 +97,11 @@ class DownloadClient extends Component {
} }
</div> </div>
<TagList
tags={tags}
tagList={tagList}
/>
<EditDownloadClientModalConnector <EditDownloadClientModalConnector
id={id} id={id}
isOpen={this.state.isEditDownloadClientModalOpen} isOpen={this.state.isEditDownloadClientModalOpen}
@@ -120,6 +128,8 @@ DownloadClient.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
enable: PropTypes.bool.isRequired, enable: PropTypes.bool.isRequired,
priority: PropTypes.number.isRequired, priority: PropTypes.number.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteDownloadClient: PropTypes.func.isRequired onConfirmDeleteDownloadClient: PropTypes.func.isRequired
}; };
@@ -50,6 +50,7 @@ class DownloadClients extends Component {
const { const {
items, items,
onConfirmDeleteDownloadClient, onConfirmDeleteDownloadClient,
tagList,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -71,6 +72,7 @@ class DownloadClients extends Component {
<DownloadClient <DownloadClient
key={item.id} key={item.id}
{...item} {...item}
tagList={tagList}
onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient} onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient}
/> />
); );
@@ -109,6 +111,7 @@ DownloadClients.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteDownloadClient: PropTypes.func.isRequired onConfirmDeleteDownloadClient: PropTypes.func.isRequired
}; };
@@ -4,13 +4,20 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions'; import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByName from 'Utilities/Array/sortByName';
import DownloadClients from './DownloadClients'; import DownloadClients from './DownloadClients';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.downloadClients', sortByName), createSortedSectionSelector('settings.downloadClients', sortByName),
(downloadClients) => downloadClients createTagsSelector(),
(downloadClients, tagList) => {
return {
...downloadClients,
tagList
};
}
); );
} }
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -13,7 +14,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditDownloadClientModalContent.css'; import styles from './EditDownloadClientModalContent.css';
@@ -45,8 +46,12 @@ class EditDownloadClientModalContent extends Component {
implementationName, implementationName,
name, name,
enable, enable,
protocol,
priority, priority,
removeCompletedDownloads,
removeFailedDownloads,
fields, fields,
tags,
message message
} = item; } = item;
@@ -142,6 +147,49 @@ class EditDownloadClientModalContent extends Component {
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('DownloadClientTagHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
<FieldSet
size={sizes.SMALL}
legend={translate('CompletedDownloadHandling')}
>
<FormGroup>
<FormLabel>{translate('RemoveCompleted')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeCompletedDownloads"
helpText={translate('RemoveCompletedDownloadsHelpText')}
{...removeCompletedDownloads}
onChange={onInputChange}
/>
</FormGroup>
{
protocol.value !== 'torrent' &&
<FormGroup>
<FormLabel>{translate('RemoveFailed')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeFailedDownloads"
helpText={translate('RemoveFailedDownloadsHelpText')}
{...removeFailedDownloads}
onChange={onInputChange}
/>
</FormGroup>
}
</FieldSet>
</Form> </Form>
} }
</ModalBody> </ModalBody>
@@ -0,0 +1,28 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ManageDownloadClientsEditModalContent from './ManageDownloadClientsEditModalContent';
interface ManageDownloadClientsEditModalProps {
isOpen: boolean;
downloadClientIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
function ManageDownloadClientsEditModal(
props: ManageDownloadClientsEditModalProps
) {
const { isOpen, downloadClientIds, onSavePress, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageDownloadClientsEditModalContent
downloadClientIds={downloadClientIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default ManageDownloadClientsEditModal;
@@ -0,0 +1,16 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.selected {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}

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