Compare commits

...

117 Commits

Author SHA1 Message Date
Bogdan 75efbd45e1 Fixed: Calculating seed time for qBittorrent
(cherry picked from commit 1b3ff64cc521396f9f1623617052c497649325a8)
2023-09-08 04:17:10 +03:00
Weblate 00cac507ad Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
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-09-07 11:58:51 +03:00
Bogdan c4850505b0 New: Add Plex Media Server notifications 2023-09-07 10:18:24 +03:00
Bogdan 75213c86a1 Bump dotnet to 6.0.21 2023-09-05 16:30:21 +03:00
Bogdan b8c3a42643 Migrate to merged proposals now included in babel/present-env
Closes #2899
2023-09-05 03:03:33 +03:00
Bogdan 8acb034aa6 Use not allowed cursor for disabled select options
(cherry picked from commit 229a4bba05d1f42089aa92b1d938747e152590b2)
2023-09-05 02:58:09 +03:00
Bogdan 889d32552b Update UI dev packages 2023-09-01 14:29:20 +03:00
Bogdan adc5f4db97 Fixed: Increase timeout when downloading updates
(cherry picked from commit 467ce70291c793042ffb3ed8942c42e7bc1424d5)
2023-09-01 04:05:16 +03:00
Weblate 9d08050f96 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: monopolo11 <bernardorn21@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
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-08-31 20:59:43 +03:00
Weblate f8cffbb4cf Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: Renan da Mota Ciciliato <renanciciliato@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: brokje1988 <brokje1988@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-29 18:12:42 +03:00
Weblate 14aeb66142 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: He Zhu <zhuhe202@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-27 19:22:06 +03:00
Bogdan 37e8e11e31 Ensure the correct icons are spinning when refreshing authors and books 2023-08-25 21:51:52 +03:00
Bogdan bdb2f14936 Prevent NullRef in GetChangedAuthors when metadata is down 2023-08-25 21:49:15 +03:00
Mark McDowall a97af657be Improved UI error messages (stack trace and version)
(cherry picked from commit 37c355da51b654cea7309678c32a83a5cbe43d1f)

Closes #2207
2023-08-24 21:15:47 +03:00
Servarr 301127e6dc Automated API Docs update 2023-08-24 00:44:35 +03:00
Bogdan 1f95bcae4e New: Async HttpClient
(cherry picked from commit 0feee191462dd3e5dde66e476e8b4b46a85ec4f0)
2023-08-24 00:38:31 +03:00
Bogdan 29118cda45 New: Use HTTP/2 in HttpClient
(cherry picked from commit 78593f428acc578785f9ecfdd41fbf2443d93d84)
2023-08-24 00:38:31 +03:00
Bogdan 09beaa939d Fixed: (FileList) Prevent double query escaping in search requests 2023-08-24 00:38:31 +03:00
Bogdan 2107624f1c Prevent health checks warnings for disabled notifications
(cherry picked from commit 5a7f42a63e25d6abdb187c37e92a908a6b85fb4d)
2023-08-23 04:58:22 +03:00
Weblate c1c2076e5c 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-08-23 04:57:55 +03:00
Bogdan c31a797bd8 Revert "Switch to Parallel.ForEach for list processing with MaxParallelism"
This reverts commit ebb2b4eca3.
2023-08-22 06:03:14 +03:00
Qstick ebb2b4eca3 Switch to Parallel.ForEach for list processing with MaxParallelism
(cherry picked from commit 0f93e04186f24abdb0cf0b3ba6a3505fda834e06)
2023-08-21 04:44:59 +03:00
Qstick 3ec5d9b9fe Use default MemoryAllocator for ImageSharp resizing
(cherry picked from commit c1a3a8249befde0a1b68e7845d5d2346066457a1)
2023-08-21 04:42:59 +03:00
Qstick 1ad84a7c44 Fixed: Ignore case when comparing torrent infohash
(cherry picked from commit 7986488c6d1687b0810b3bcac2c1dae725e770ac)
2023-08-20 16:10:26 -05:00
Bogdan 9d67c18254 Bump version to 0.3.4 2023-08-20 12:24:38 +03:00
Qstick 2e39c7340c Windows installer improvements
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
(cherry picked from commit 02c95658c4a5d38e1d0997ed5ef9bbd219ffc32c)
2023-08-19 18:41:06 +03:00
Qstick f75add984f Cleanup distribution files
(cherry picked from commit 8b291d932f687297f18491469c44751e37e81173)
2023-08-19 18:41:06 +03:00
Bogdan f0f6c3eb35 Update azure pipelines 2023-08-19 18:41:06 +03:00
Robin Dadswell d94f866aeb Adds Pipeline testing for Postgres15 Databases 2023-08-19 18:41:06 +03:00
Robin Dadswell 618f07d138 Bump Npgsql to 7.0.4 2023-08-19 18:41:06 +03:00
Bogdan 3db33c988a Align logs filename with upstream 2023-08-19 18:33:37 +03:00
Bogdan 28e38b7f17 Prevent new builds on API docs updates 2023-08-19 14:12:03 +03:00
Servarr ca403e6f31 Automated API Docs update [skip ci] 2023-08-19 13:56:48 +03:00
Weblate 51351dee1d 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: Robert A. Viana <robert.abreu@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bn/
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/hu/
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/
Translation: Servarr/Readarr
2023-08-19 13:56:29 +03:00
Mark McDowall 2081f2e321 Fixed: Allow decimals for Custom Format size
(cherry picked from commit 7f5ddff568ce9f87bd45420cbd36690b190bd633)

Closes #2839
2023-08-19 13:31:48 +03:00
Stevie Robinson c10a32534c Add info box to Remote Path Mappings Settings
(cherry picked from commit d8f3d7d3eafeafd8d4372db0076a63f935a29218)

Closes #2835
2023-08-19 13:30:36 +03:00
Mark McDowall 0e415c6ce3 New: Status message when downloading metadata in qBittorrent
(cherry picked from commit 8aa872edf4737798d4836f68fbd0697ee0511c41)
2023-08-19 13:28:19 +03:00
Mark McDowall a8eb674071 Fixed: Ignore IOException deleting download folder after import
(cherry picked from commit d05cb40088a51eef5a2830bf2b55f5d05955578f)
2023-08-19 13:28:08 +03:00
Mark McDowall 7f8a1cf849 New: Success check mark on blue buttons is now white instead of green
(cherry picked from commit 566fae9d5857a10bd69c718368e7847e5a733faa)
2023-08-19 13:27:57 +03:00
Stevie Robinson a3c0d10240 Translate Updated and Connection Lost Modals in frontend
(cherry picked from commit 074aa6f4457bf83173e6ba7209c452a6e0659a35)

Closes #2806
2023-08-19 02:20:04 +03:00
Bogdan 3ddeaaefe2 Use named tokens in frontend translate function 2023-08-19 02:14:53 +03:00
Bogdan 6d99de4fe0 Add default update branches as autocomplete values 2023-08-19 02:14:53 +03:00
Bogdan 8b36a5ce92 Don't block update UI settings under docker 2023-08-19 02:14:53 +03:00
Bogdan 1202a43466 Show warning when using the docker update mechanism
(cherry picked from commit cc538c4b2d33a1734c45c0667776d946596107e9)

Closes #2805
2023-08-19 02:14:49 +03:00
Mark McDowall 82bc2d1aa4 Fixed: Don't block updates under docker unless configured in package_info
(cherry picked from commit 5a7e34e291c2715aa67161e5c455d25e80f498df)

Closes #2772
2023-08-19 02:14:18 +03:00
Bogdan ed9af393b7 Fix flaky automation tests 2023-08-19 01:51:37 +03:00
bakerboy448 0799cfc885 Remove reddit from readme 2023-08-18 20:27:16 +03:00
Mark McDowall 331d0c9a9c New: Ignore inaccessible files with getting files
(cherry picked from commit e5aa8584100d96a2077c57f74ae5b2ceab63de19)
2023-08-18 18:37:26 +03:00
Bogdan 03c93c9c84 Fix test in DiskSpaceServiceFixture 2023-08-18 14:15:33 +03:00
Mark McDowall 60f6ed030b Fix GetBestRootFolderPath tests
(cherry picked from commit 63a911a9a549749b5460c2b9fea48a25e78c52a4)
2023-08-18 14:15:33 +03:00
Mark McDowall cc70d61735 Fixed: UI loading when author or root folder path is for wrong OS
(cherry picked from commit 5f7217844533907d7fc6287a48efb31987736c4c)
2023-08-18 14:15:33 +03:00
Bogdan a7b965100d Fix BookInfoProxySearchFixture test 2023-08-18 13:28:18 +03:00
Bogdan 8901118aef Add default schema values for root folders
(cherry picked from commit 7ae82a982ce17efa46c0a0b23256a26edf90babb)
2023-08-18 12:58:06 +03:00
Bogdan 99c17d7698 Improve messaging for Interactive Search
(cherry picked from commit 7893fdde104959c4f3c32d9e1000e2479f7a5b12)

Closes #2752
2023-08-18 12:49:56 +03:00
Bogdan b84e83b082 Replace docker detection for cgroup v2
(cherry picked from commit 78d4dee4610c5f3f90cc69469004008aa64900b8)
2023-08-18 10:58:16 +03:00
Qstick 4249f5324a Cleanup other provider status code
(cherry picked from commit c281a7818adce8db728d2a104f4444cb9c0baf2c)
2023-08-16 12:18:20 +03:00
Qstick 9e1630e9a4 New: Notifications (Connect) Status
(cherry picked from commit e3545801721e00d4e5cac3fa534e66dcbe9d2d05)
(cherry picked from commit cb27b05a6c046ca0a6e4998f7e7ecd6b45add1a2)
2023-08-16 12:18:20 +03:00
Weblate 68b2773913 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: David Molero <contact@dolvem.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: deepserket <deepserket@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translation: Servarr/Readarr
2023-08-15 20:08:44 +03:00
Bogdan ad446b358e Fix combined search tests 2023-08-13 14:17:32 +03:00
Bogdan 423a8ecbe1 Bump version to 0.3.3 2023-08-13 13:40:33 +03:00
Bogdan 29a12aa3b0 Add one minute back-off level for all providers
(cherry picked from commit d8f314ff0ef64e8d90b21b7865e46be74db5e570)

Closes #2792
2023-08-12 10:52:44 +03:00
Weblate 695781dde5 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-08-11 21:02:05 +03:00
Servarr 4e8ddd3018 Automated API Docs update [skip ci] 2023-08-11 20:04:43 +03:00
Bogdan eb67231a45 New: Show successful grabs in Interactive Search with green icon
(cherry picked from commit 366b2b8b52d8375f1f41719a09893136009a5b48)

Closes #2780
2023-08-11 19:51:21 +03:00
Mark McDowall 3d3a458828 New: Add additional logging when renaming extra files
(cherry picked from commit 1ae0dc81f73ef74078f07fd5536a7d9058df649d)

Closes #2782
2023-08-11 19:48:46 +03:00
Bogdan a11930a03f add @types/lodash 2023-08-11 19:40:21 +03:00
Bogdan abaf39d67e Add simplified translations 2023-08-11 19:40:01 +03:00
Bogdan 894a5943e4 Simplify column translations
(cherry picked from commit 551ea18caf50353c4c8dbeba5e42d266dbbfb54d)

Closes #2759
2023-08-11 19:14:44 +03:00
Bogdan be26647afb New: More translations for columns
(cherry picked from commit aee8579d1823b7dfb94c0055fe33b5fb5a7fbf17)

Towards #2733
2023-08-11 18:56:33 +03:00
Mark McDowall b319a4bce7 Fixed: Translations for columns
(cherry picked from commit 6d53d2a153a98070c42d0619c15902b6bd5dfab4)

Closes #2702
2023-08-11 18:53:50 +03:00
Mark McDowall f03fd7e95e Fixed: Improve translation loading
(cherry picked from commit 73c5ec1da4dd00301e1b0dddbcea37590a99b045)

Closes #2699
2023-08-11 18:52:34 +03:00
Mark McDowall 7f25a3c4b1 UI loading improvements
Fixed: Caching for dynamically loaded JS files
Fixed: Incorrect caching of initialize.js
(cherry picked from commit f0cb5b81f140c67fa84162e094cc4e0f3476f5da)

Closes #2690
Closes #2696
2023-08-11 18:21:47 +03:00
Weblate e3247dc505 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ivan Mazzoli <dreadtank27@gmail.com>
Co-authored-by: Nir Israel Hen <nirisraelh@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: byakurau <byakurau1@gmail.com>
Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/
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/it/
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/
Translation: Servarr/Readarr
2023-08-10 19:59:14 +03:00
Weblate 3677fd6d34 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ivan Mazzoli <dreadtank27@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: byakurau <byakurau1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translation: Servarr/Readarr
2023-08-10 19:58:44 +03:00
Bogdan 4f6901b1ff Fixed: Ensure failing providers are marked as failed when testing all
(cherry picked from commit f6c05d4456a5667398319e249614e2eed115621e)
2023-08-10 19:58:25 +03:00
Bogdan ce820f6f73 Fixed: Detect Docker when using control group v2 2023-08-09 10:48:37 +03:00
Bogdan 53e6cb24b7 Bump version to 0.3.2 2023-08-06 19:27:59 +03:00
Bogdan 7c1ca8acc1 New: Health check for indexers with invalid download client
(cherry picked from commit 377fce6fe15c0875c4bd33f1371a31af79c9310c)

Closes #2760
2023-08-06 19:25:00 +03:00
Bogdan 5e9e578101 Ensure path is valid before watching it
(cherry picked from commit 1245b2c58b5a1b5fb4aee9a4f974ecfb131de2bd)
2023-08-06 19:24:58 +03:00
Bogdan 156407c541 Add @types/redux-actions 2023-08-06 19:12:55 +03:00
Mark McDowall 1ef6c60318 Sync Popover with upstream
(cherry picked from commit bdcfef80d627e777d7932c54cda04cbe7c656ffc)
2023-08-06 19:12:04 +03:00
servarr[bot] 73b3b1848b Filter user issues from Sentry
(cherry picked from commit 03d361f5537bfc0caba1b86085f974570942fdbc)

Co-authored-by: Qstick <qstick@gmail.com>
2023-08-05 21:56:25 +03:00
Weblate 33fbd95707 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albert <zuozl1992@foxmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magnus <magnus.fladvad@gmail.com>
Co-authored-by: Stjepan <stjepstjepanovic@gmail.com>
Co-authored-by: Thirrian <matthiaslantermann@gmail.com>
Co-authored-by: stormaac <yxc.frank@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/hr/
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/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-08-03 22:55:45 +03:00
Mark McDowall 704635f758 New: Book interactive search modal size
(cherry picked from commit 1f619e27f1e8905bc96ce54c483171469d204650)

Closes #2243
2023-08-01 18:32:14 +03:00
Bogdan 263e807de2 Ensure yarn packages are installed when running only LintUI 2023-07-31 08:33:52 +03:00
Mark McDowall 9ac9bd25c1 Re-order frontend build steps
(cherry picked from commit 97ad6682f7d54af8886144bc5a179fa7242f1f1f)
2023-07-31 08:01:50 +03: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
320 changed files with 6242 additions and 2696 deletions
+1 -2
View File
@@ -3,8 +3,7 @@
# Explicitly set bash scripts to have unix endings
*.sh text eol=lf
distribution/debian/* text eol=lf
macOS/Readarr text eol=lf
distribution/osx/Readarr text eol=lf
# Custom for Visual Studio
*.cs diff=csharp
-1
View File
@@ -30,7 +30,6 @@ Note that only one type of a given book is supported. If you want both an audiob
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/readarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://readarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/readarr)
Note: GitHub Issues are for Bugs and Feature Requests Only
+278 -125
View File
@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.2.4'
majorVersion: '0.3.4'
minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.408'
dotnetVersion: '6.0.413'
nodeVersion: '16.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
@@ -27,6 +27,10 @@ trigger:
include:
- develop
- master
paths:
exclude:
- .github
- src/Readarr.Api.*/openapi.json
pr:
branches:
@@ -34,82 +38,37 @@ pr:
- develop
paths:
exclude:
- .github
- src/NzbDrone.Core/Localization/Core
- src/Readarr.Api.*/openapi.json
stages:
- stage: Build_Backend_Windows
displayName: Build Backend
dependsOn: []
- stage: Setup
displayName: Setup
jobs:
- job: Backend
strategy:
matrix:
Windows:
osName: 'Windows'
imageName: ${{ variables.windowsImage }}
enableAnalysis: 'false'
- job:
displayName: Build Variables
pool:
vmImage: $(imageName)
variables:
# Disable stylecop here - linting errors get caught by the analyze task
EnableAnalyzers: $(enableAnalysis)
vmImage: ${{ variables.linuxImage }}
steps:
# Set the build name properly. The 'name' property won't recursively expand so hack here:
- bash: echo "##vso[build.updatebuildnumber]$READARRVERSION"
displayName: Set Build Name
- checkout: self
submodules: true
fetchDepth: 1
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
if [[ $BUILD_REASON == "PullRequest" ]]; then
git diff origin/develop...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)"
echo $? > not_backend_update
else
echo 0 > not_backend_update
fi
displayName: Extra Platform Support
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder)
displayName: Cache NuGet packages
- bash: ./build.sh --backend --enable-bsd
displayName: Build Readarr Backend
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
displayName: Clean up intermediate output
- publish: $(outputFolder)
artifact: '$(osName)Backend'
displayName: Publish Backend
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
- stage: Build_Backend_Other
displayName: Build Backend (Other OS)
dependsOn: []
cat not_backend_update
displayName: Check for Backend File Changes
- publish: not_backend_update
artifact: not_backend_update
displayName: Publish update type
- stage: Build_Backend
displayName: Build Backend
dependsOn: Setup
jobs:
- job: Backend
strategy:
@@ -122,6 +81,10 @@ stages:
osName: 'Mac'
imageName: ${{ variables.macImage }}
enableAnalysis: 'false'
Windows:
osName: 'Windows'
imageName: ${{ variables.windowsImage }}
enableAnalysis: 'false'
pool:
vmImage: $(imageName)
@@ -137,22 +100,17 @@ stages:
inputs:
version: $(dotnetVersion)
- bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
displayName: Extra Platform Support
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder)
displayName: Cache NuGet packages
displayName: Enable Extra Platform Support
- bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Readarr Backend
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
@@ -160,10 +118,38 @@ stages:
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
displayName: Clean up intermediate output
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- publish: $(outputFolder)
artifact: '$(osName)Backend'
displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Build_Frontend
displayName: Frontend
dependsOn: []
dependsOn: Setup
jobs:
- job: Build
strategy:
@@ -192,7 +178,6 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: |
yarn | "$(osName)"
yarn
path: $(yarnCacheFolder)
displayName: Cache Yarn packages
- bash: ./build.sh --frontend
@@ -207,7 +192,7 @@ stages:
- stage: Installer
dependsOn:
- Build_Backend_Windows
- Build_Backend
- Build_Frontend
jobs:
- job: Windows_Installer
@@ -231,8 +216,8 @@ stages:
displayName: Fetch Frontend
- bash: |
./build.sh --packages --installer
cp setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
cp setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
cp distribution/windows/setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
cp distribution/windows/setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create Installers
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller'
@@ -240,7 +225,7 @@ stages:
- stage: Packages
dependsOn:
- Build_Backend_Windows
- Build_Backend
- Build_Frontend
jobs:
- job: Other_Packages
@@ -409,11 +394,26 @@ stages:
- stage: Unit_Test
displayName: Unit Tests
dependsOn: Build_Backend_Windows
condition: succeeded()
dependsOn: Build_Backend
jobs:
- job: Prepare
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'not_backend_update'
targetPath: '.'
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
name: setVar
- job: Unit
displayName: Unit Native
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace:
clean: all
@@ -479,6 +479,8 @@ stages:
- job: Unit_Docker
displayName: Unit Docker
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy:
matrix:
alpine:
@@ -531,11 +533,13 @@ stages:
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
- job: Unit_LinuxCore_Postgres14
displayName: Unit Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests
artifactName: linux-x64-tests
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
@@ -556,7 +560,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-Tests'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
@@ -579,15 +583,84 @@ stages:
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres15
displayName: Unit Native LinuxCore with Postgres15 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
failTaskOnFailedTests: true
- stage: Integration
displayName: Integration
dependsOn: Packages
jobs:
- job: Prepare
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'not_backend_update'
targetPath: '.'
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
name: setVar
- job: Integration_Native
displayName: Integration Native
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy:
matrix:
MacCore:
@@ -649,8 +722,10 @@ stages:
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
- job: Integration_LinuxCore_Postgres14
displayName: Integration Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
Readarr__Postgres__Host: 'localhost'
@@ -705,12 +780,77 @@ stages:
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres15
displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Readarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_FreeBSD
displayName: Integration Native FreeBSD
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace:
clean: all
variables:
@@ -755,6 +895,8 @@ stages:
- job: Integration_Docker
displayName: Integration Docker
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy:
matrix:
alpine:
@@ -833,16 +975,19 @@ stages:
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz'
failBuild: true
Mac:
osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz'
failBuild: true
Windows:
osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip'
failBuild: true
pool:
vmImage: $(imageName)
@@ -888,20 +1033,35 @@ stages:
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
- publish: $(Build.ArtifactStagingDirectory)/screenshots
artifact: '$(osName)AutomationScreenshots'
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
displayName: Publish Screenshot Bundle
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(osName) Automation Tests'
failTaskOnFailedTests: true
failTaskOnFailedTests: $(failBuild)
displayName: Publish Test Results
- stage: Analyze
dependsOn: []
dependsOn:
- Setup
displayName: Analyze
jobs:
- job: Prepare
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'not_backend_update'
targetPath: '.'
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
name: setVar
- job: Lint_Frontend
displayName: Lint Frontend
strategy:
@@ -927,7 +1087,6 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: |
yarn | "$(osName)"
yarn
path: $(yarnCacheFolder)
displayName: Cache Yarn packages
- bash: ./build.sh --lint
@@ -959,8 +1118,13 @@ stages:
- job: Api_Docs
displayName: API Docs
dependsOn: Prepare
condition: |
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
and
(
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
)
pool:
vmImage: ${{ variables.windowsImage }}
@@ -981,10 +1145,9 @@ stages:
git config --global user.name "Servarr"
git checkout -b api-docs
git add .
git status
if git status | grep modified
if git status | grep -q modified
then
git commit -am 'Automated API Docs update [skip ci]'
git commit -am 'Automated API Docs update'
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"}'
else
@@ -1008,33 +1171,25 @@ stages:
- job: Analyze_Backend
displayName: Backend
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
disable.coverage.autogenerate: 'true'
EnableAnalyzers: 'false'
pool:
vmImage: ${{ variables.linuxImage }}
vmImage: ${{ variables.windowsImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core 2.1'
inputs:
version: 2.1.815
- task: UseDotNet@2
displayName: 'Install .net core 3.1'
inputs:
version: 3.1.413
- task: UseDotNet@2
displayName: 'Install .net core 5.0'
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: self # Need history for Sonar analysis
submodules: true
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder)
displayName: Cache NuGet packages
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
- task: SonarCloudPrepare@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs:
@@ -1045,16 +1200,14 @@ stages:
projectName: 'Readarr'
projectVersion: '$(readarrVersion)'
extraProperties: |
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,./src/Libraries/**
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
sonar.coverage.exclusions=**/Readarr.Api.V1/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: |
./build.sh --backend -f net6.0 -r linux-x64
TEST_DIR=_tests/net6.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
@@ -1077,7 +1230,6 @@ stages:
- Unit_Test
- Integration
- Automation
- Build_Backend_Other
condition: eq(variables['system.pullrequest.isfork'], false)
displayName: Build Status Report
jobs:
@@ -1101,3 +1253,4 @@ stages:
DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)
+9 -10
View File
@@ -23,7 +23,7 @@ UpdateVersionNumber()
echo "Updating Version Info"
sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$READARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props
sed -i'' -e "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props
sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$READARRVERSION<\/string>/g" macOS/Readarr.app/Contents/Info.plist
sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$READARRVERSION<\/string>/g" distribution/osx/Readarr.app/Contents/Info.plist
fi
}
@@ -183,7 +183,7 @@ PackageMacOSApp()
rm -rf $folder
mkdir -p $folder
cp -r macOS/Readarr.app $folder
cp -r distribution/osx/Readarr.app $folder
mkdir -p $folder/Readarr.app/Contents/MacOS
echo "Copying Binaries"
@@ -245,7 +245,7 @@ BuildInstaller()
local framework="$1"
local runtime="$2"
./_inno/ISCC.exe setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
./_inno/ISCC.exe distribution/windows/setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
}
InstallInno()
@@ -391,22 +391,21 @@ then
fi
fi
if [ "$FRONTEND" = "YES" ];
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
then
YarnInstall
RunWebpack
fi
if [ "$LINT" = "YES" ];
then
if [ -z "$FRONTEND" ];
then
YarnInstall
fi
LintUI
fi
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ];
then
UpdateVersionNumber
@@ -44,16 +44,16 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}"
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
[Dirs]
Name: "{app}"; Permissions: users-modify
[Files]
Source: "..\_artifacts\{#Runtime}\{#Framework}\Readarr\Readarr.exe"; DestDir: "{app}\bin"; Flags: ignoreversion
Source: "..\_artifacts\{#Runtime}\{#Framework}\Readarr\*"; Excludes: "Readarr.Update"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Readarr\Readarr.exe"; DestDir: "{app}\bin"; Flags: ignoreversion
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Readarr\*"; Excludes: "Readarr.Update"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
@@ -72,12 +72,13 @@ Filename: "{app}\bin\Readarr.exe"; Description: "Open Readarr Web UI"; Flags: po
Filename: "{app}\bin\Readarr.exe"; Description: "Start Readarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
[UninstallRun]
Filename: "{app}\bin\Readarr.Console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
Filename: "{app}\bin\readarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
[Code]
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ResultCode: Integer;
begin
Exec(ExpandConstant('{commonappdata}\Readarr\bin\Readarr.Console.exe'), '/u', '', 0, ewWaitUntilTerminated, ResultCode)
Exec('net', 'stop readarr', '', 0, ewWaitUntilTerminated, ResultCode)
Exec('sc', 'delete readarr', '', 0, ewWaitUntilTerminated, ResultCode)
end;
+4 -4
View File
@@ -4,14 +4,14 @@ module.exports = {
plugins: [
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
['@babel/plugin-transform-optional-chaining', { loose }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
// Stage 2
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-transform-export-namespace-from',
// Stage 3
['@babel/plugin-proposal-class-properties', { loose }],
['@babel/plugin-transform-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import'
],
env: {
+5 -3
View File
@@ -36,7 +36,7 @@ module.exports = (env) => {
},
entry: {
index: 'index.js'
index: 'index.ts'
},
resolve: {
@@ -91,13 +91,15 @@ module.exports = (env) => {
}),
new MiniCssExtractPlugin({
filename: 'Content/styles.css'
filename: 'Content/styles.css',
chunkFilename: 'Content/[id]-[chunkhash].css'
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
filename: 'index.html',
publicPath: '/'
publicPath: '/',
inject: false
}),
new FileManagerPlugin({
@@ -9,7 +9,7 @@ import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime';
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 styles from './HistoryDetails.css';
@@ -108,7 +108,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
@@ -225,7 +225,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
@@ -271,7 +271,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)}
data={formatCustomFormatScore(customFormatScore)}
/> :
null
}
+2 -2
View File
@@ -10,7 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, tooltipPositions } from 'Helpers/Props';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css';
@@ -180,7 +180,7 @@ class HistoryRow extends Component {
className={styles.customFormatScore}
>
<Tooltip
anchor={formatPreferredWordScore(
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
+4 -4
View File
@@ -18,7 +18,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
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 QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
@@ -46,14 +46,14 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true });
};
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => {
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist, skipredownload);
onRemoveQueueItemPress(blocklist, skipRedownload);
this.setState({ isRemoveQueueItemModalOpen: false });
};
@@ -232,7 +232,7 @@ class QueueRow extends Component {
className={styles.customFormatScore}
>
<Tooltip
anchor={formatPreferredWordScore(
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
@@ -23,7 +23,7 @@ class RemoveQueueItemModal extends Component {
this.state = {
remove: true,
blocklist: false,
skipredownload: false
skipRedownload: false
};
}
@@ -34,7 +34,7 @@ class RemoveQueueItemModal extends Component {
this.setState({
remove: true,
blocklist: false,
skipredownload: false
skipRedownload: false
});
};
@@ -49,8 +49,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ blocklist: value });
};
onSkipReDownloadChange = ({ value }) => {
this.setState({ skipredownload: value });
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
@@ -76,7 +76,7 @@ class RemoveQueueItemModal extends Component {
isPending
} = this.props;
const { remove, blocklist, skipredownload } = this.state;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
@@ -137,10 +137,10 @@ class RemoveQueueItemModal extends Component {
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipredownload"
value={skipredownload}
helpText={translate('SkipredownloadHelpText')}
onChange={this.onSkipReDownloadChange}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup>
}
@@ -24,7 +24,7 @@ class RemoveQueueItemsModal extends Component {
this.state = {
remove: true,
blocklist: false,
skipredownload: false
skipRedownload: false
};
}
@@ -35,7 +35,7 @@ class RemoveQueueItemsModal extends Component {
this.setState({
remove: true,
blocklist: false,
skipredownload: false
skipRedownload: false
});
};
@@ -50,8 +50,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ blocklist: value });
};
onSkipReDownloadChange = ({ value }) => {
this.setState({ skipredownload: value });
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
@@ -77,7 +77,7 @@ class RemoveQueueItemsModal extends Component {
allPending
} = this.props;
const { remove, blocklist, skipredownload } = this.state;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
@@ -138,10 +138,10 @@ class RemoveQueueItemsModal extends Component {
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipredownload"
value={skipredownload}
helpText={translate('SkipredownloadHelpText')}
onChange={this.onSkipReDownloadChange}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup>
}
+3 -4
View File
@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes';
function App({ store, history, hasTranslationsError }) {
function App({ store, history }) {
return (
<DocumentTitle title={window.Readarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector hasTranslationsError={hasTranslationsError}>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
@@ -25,8 +25,7 @@ function App({ store, history, hasTranslationsError }) {
App.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
hasTranslationsError: PropTypes.bool.isRequired
history: PropTypes.object.isRequired
};
export default App;
@@ -1,6 +1,7 @@
.version {
margin: 0 3px;
font-weight: bold;
font-family: var(--defaultFontFamily);
}
.maintenance {
+7 -8
View File
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
@@ -64,12 +65,12 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Readarr Updated
{translate('AppUpdated', { appName: 'Readarr' })}
</ModalHeader>
<ModalBody>
<div>
Version <span className={styles.version}>{version}</span> of Readarr has been installed, in order to get the latest changes you'll need to reload Readarr.
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Readarr', version })} blockClassName={styles.version} />
</div>
{
@@ -77,16 +78,14 @@ function AppUpdatedModalContent(props) {
<div>
{
!update.changes &&
<div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
}
{
!!update.changes &&
<div>
<div className={styles.changes}>
What's new?
{translate('WhatsNew')}
</div>
<UpdateChanges
@@ -113,14 +112,14 @@ function AppUpdatedModalContent(props) {
<Button
onPress={onSeeChangesPress}
>
Recent Changes
{translate('RecentChanges')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={onModalClose}
>
Reload
{translate('Reload')}
</Button>
</ModalFooter>
</ModalContent>
+5 -4
View File
@@ -7,6 +7,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ConnectionLostModal.css';
function ConnectionLostModal(props) {
@@ -22,16 +23,16 @@ function ConnectionLostModal(props) {
>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Connection Lost
{translate('ConnectionLost')}
</ModalHeader>
<ModalBody>
<div>
Readarr has lost its connection to the backend and will need to be reloaded to restore functionality.
{translate('ConnectionLostToBackend', { appName: 'Readarr' })}
</div>
<div className={styles.automatic}>
Readarr will try to connect automatically, or you can click reload below.
{translate('ConnectionLostReconnect', { appName: 'Readarr' })}
</div>
</ModalBody>
<ModalFooter>
@@ -39,7 +40,7 @@ function ConnectionLostModal(props) {
kind={kinds.PRIMARY}
onPress={onModalClose}
>
Reload
{translate('Reload')}
</Button>
</ModalFooter>
</ModalContent>
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
@@ -9,6 +10,7 @@ import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/Settings/rootFolders';
import translate from 'Utilities/String/translate';
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
import DeleteAuthorModal from './Delete/DeleteAuthorModal';
@@ -17,6 +19,10 @@ import styles from './AuthorEditorFooter.css';
const NO_CHANGE = 'noChange';
const mapDispatchToProps = {
dispatchFetchRootFolders: fetchRootFolders
};
class AuthorEditorFooter extends Component {
//
@@ -39,6 +45,13 @@ class AuthorEditorFooter extends Component {
};
}
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchRootFolders();
}
componentDidUpdate(prevProps) {
const {
isSaving,
@@ -341,7 +354,8 @@ AuthorEditorFooter.propTypes = {
showMetadataProfile: PropTypes.bool.isRequired,
onSaveSelected: 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);
@@ -16,7 +16,7 @@ import AuthorIndex from './AuthorIndex';
function createMapStateToProps() {
return createSelector(
createAuthorClientSideCollectionItemsSelector('authorIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
@@ -24,17 +24,17 @@ function createMapStateToProps() {
(
author,
isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting,
dimensionsState
) => {
return {
...author,
isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen
};
}
@@ -14,14 +14,39 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const nameOptions = [
{ key: 'firstLast', value: translate('NameFirstLast') },
{ key: 'lastFirst', value: translate('NameLastFirst') }
{
key: 'firstLast',
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
];
const posterSizeOptions = [
{ key: 'small', value: 'Small' },
{ key: 'medium', value: 'Medium' },
{ key: 'large', value: 'Large' }
{
key: 'small',
get value() {
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
];
class AuthorIndexOverviewOptionsModalContent extends Component {
@@ -14,15 +14,45 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const posterSizeOptions = [
{ key: 'small', value: 'Small' },
{ key: 'medium', value: 'Medium' },
{ key: 'large', value: 'Large' }
{
key: 'small',
get value() {
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
];
const nameOptions = [
{ key: 'no', value: translate('NoName') },
{ key: 'firstLast', value: translate('NameFirstLast') },
{ key: 'lastFirst', value: translate('NameLastFirst') }
{
key: 'no',
get value() {
return translate('NoName');
}
},
{
key: 'firstLast',
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
];
class AuthorIndexPosterOptionsModalContent extends Component {
@@ -17,8 +17,8 @@ function AuthorIndexProgressBar(props) {
detailedProgressBar
} = props;
const progress = bookCount ? bookFileCount / bookCount * 100 : 100;
const text = `${bookFileCount} / ${bookCount}`;
const progress = bookCount ? bookCount / totalBookCount * 100 : 100;
const text = `${bookCount} / ${totalBookCount}`;
return (
<ProgressBar
@@ -297,7 +297,7 @@ class AuthorIndexRow extends Component {
progress={progress}
kind={getProgressBarKind(status, monitored, progress)}
showText={true}
text={`${bookFileCount} / ${bookCount}`}
text={`${bookCount} / ${totalBookCount}`}
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
width={125}
/>
@@ -7,8 +7,18 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const nameOptions = [
{ key: 'firstLast', value: translate('NameFirstLast') },
{ key: 'lastFirst', value: translate('NameLastFirst') }
{
key: 'firstLast',
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
];
class AuthorIndexTableOptions extends Component {
@@ -6,4 +6,5 @@
.statusIcon {
width: 20px !important;
text-align: center;
}
@@ -33,7 +33,7 @@ class MonitoringOptionsModalContent extends Component {
const {
isSaving,
saveError
} = prevProps;
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
@@ -16,8 +16,8 @@ import BookIndex from './BookIndex';
function createMapStateToProps() {
return createSelector(
createBookClientSideCollectionItemsSelector('bookIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.REFRESH_BOOK),
createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.BULK_REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent';
function BookInteractiveSearchModal(props) {
@@ -14,6 +15,7 @@ function BookInteractiveSearchModal(props) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
+2 -2
View File
@@ -30,7 +30,7 @@ class BookshelfFooter extends Component {
const {
isSaving,
saveError
} = prevProps;
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
@@ -143,7 +143,7 @@ class BookshelfFooter extends Component {
<div>
<div className={styles.label}>
{selectedCount} Author(s) Selected
{translate('CountAuthorsSelected', { selectedCount })}
</div>
<SpinnerButton
@@ -25,6 +25,10 @@
white-space: pre-wrap;
}
.version {
margin-top: 20px;
}
@media only screen and (max-width: $breakpointMedium) {
.image {
height: 250px;
@@ -6,6 +6,7 @@ interface CssExports {
'image': string;
'imageContainer': string;
'message': string;
'version': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -1,60 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './ErrorBoundaryError.css';
function ErrorBoundaryError(props) {
const {
className,
messageClassName,
detailsClassName,
message,
error,
info
} = props;
return (
<div className={className}>
<div className={messageClassName}>
{message}
</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{
error &&
<div>
{error.toString()}
</div>
}
<div className={styles.info}>
{info.componentStack}
</div>
</details>
</div>
);
}
ErrorBoundaryError.propTypes = {
className: PropTypes.string.isRequired,
messageClassName: PropTypes.string.isRequired,
detailsClassName: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
error: PropTypes.object.isRequired,
info: PropTypes.object.isRequired
};
ErrorBoundaryError.defaultProps = {
className: styles.container,
messageClassName: styles.message,
detailsClassName: styles.details,
message: 'There was an error loading this content'
};
export default ErrorBoundaryError;
@@ -0,0 +1,77 @@
import React, { useEffect, useState } from 'react';
import StackTrace from 'stacktrace-js';
import translate from 'Utilities/String/translate';
import styles from './ErrorBoundaryError.css';
interface ErrorBoundaryErrorProps {
className: string;
messageClassName: string;
detailsClassName: string;
message: string;
error: Error;
info: {
componentStack: string;
};
}
function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
const {
className = styles.container,
messageClassName = styles.message,
detailsClassName = styles.details,
message = translate('ErrorLoadingContent'),
error,
info,
} = props;
const [detailedError, setDetailedError] = useState<
StackTrace.StackFrame[] | null
>(null);
useEffect(() => {
if (error) {
StackTrace.fromError(error).then((de) => {
setDetailedError(de);
});
} else {
setDetailedError(null);
}
}, [error, setDetailedError]);
return (
<div className={className}>
<div className={messageClassName}>{message}</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Readarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{error ? <div>{error.message}</div> : null}
{detailedError ? (
detailedError.map((d, index) => {
return (
<div key={index}>
{` at ${d.functionName} (${d.fileName}:${d.lineNumber}:${d.columnNumber})`}
</div>
);
})
) : (
<div>{info.componentStack}</div>
)}
{
<div className={styles.version}>
Version: {window.Readarr.version}
</div>
}
</details>
</div>
);
}
export default ErrorBoundaryError;
@@ -206,9 +206,11 @@ class FilterBuilderRow extends Component {
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
const { name, label } = availablePropFilter;
return {
key: availablePropFilter.name,
value: availablePropFilter.label
key: name,
value: typeof label === 'function' ? label() : label
};
}).sort((a, b) => a.value.localeCompare(b.value));
@@ -9,6 +9,10 @@
&:hover {
background-color: var(--inputHoverBackgroundColor);
}
&.isDisabled {
cursor: not-allowed;
}
}
.optionCheck {
+1 -1
View File
@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) {
const { value } = this.props;
if (value !== prevProps.value && !this.state.isFocused) {
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
this.setState({
value: value == null ? '' : value.toString()
});
+2 -2
View File
@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key}
{...otherOptionProps}
>
{optionValue}
{typeof optionValue === 'function' ? optionValue() : optionValue}
</option>
);
})
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string,
disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,
+2 -2
View File
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return (
<span
className={containerClassName}
title={title}
title={typeof title === 'function' ? title() : title}
>
{icon}
</span>
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};
@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
render() {
const {
kind,
isSpinning,
error,
children,
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK;
let iconKind = kinds.SUCCESS;
let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
if (hasWarning) {
iconName = icons.WARNING;
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
return (
<SpinnerButton
kind={kind}
isSpinning={isSpinning}
{...otherProps}
>
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
}
SpinnerErrorButton.propTypes = {
kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object,
children: PropTypes.node.isRequired
@@ -10,27 +10,55 @@ class InlineMarkdown extends Component {
render() {
const {
className,
data
data,
blockClassName
} = this.props;
// For now only replace links
// For now only replace links or code blocks (not both)
const markdownBlocks = [];
if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0;
let match = null;
while ((match = regex.exec(data)) !== null) {
while ((match = linkRegex.exec(data)) !== null) {
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
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));
}
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}`} className={blockClassName ?? null}>{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>;
@@ -39,7 +67,8 @@ class InlineMarkdown extends Component {
InlineMarkdown.propTypes = {
className: PropTypes.string,
data: PropTypes.string
data: PropTypes.string,
blockClassName: PropTypes.string
};
export default InlineMarkdown;
@@ -32,7 +32,7 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
{typeof filter.label === 'function' ? filter.label() : filter.label}
</FilterMenuItem>
);
})
+4 -4
View File
@@ -7,7 +7,7 @@ function ErrorPage(props) {
const {
version,
isLocalStorageSupported,
hasTranslationsError,
translationsError,
authorError,
customFiltersError,
tagsError,
@@ -21,8 +21,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) {
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 (translationsError) {
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
} else if (authorError) {
errorMessage = getErrorMessage(authorError, 'Failed to load author from API');
} else if (customFiltersError) {
@@ -55,7 +55,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = {
version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired,
hasTranslationsError: PropTypes.bool.isRequired,
translationsError: PropTypes.object,
authorError: PropTypes.object,
customFiltersError: PropTypes.object,
tagsError: PropTypes.object,
+20 -10
View File
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
@@ -52,6 +52,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
(
customFiltersIsPopulated,
tagsIsPopulated,
@@ -60,7 +61,8 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated,
metadataProfilesIsPopulated,
importListsIsPopulated,
systemStatusIsPopulated
systemStatusIsPopulated,
translationsIsPopulated
) => {
return (
customFiltersIsPopulated &&
@@ -70,7 +72,8 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated &&
metadataProfilesIsPopulated &&
importListsIsPopulated &&
systemStatusIsPopulated
systemStatusIsPopulated &&
translationsIsPopulated
);
}
);
@@ -84,6 +87,7 @@ const selectErrors = createSelector(
(state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error,
(state) => state.system.status.error,
(state) => state.app.translations.error,
(
customFiltersError,
tagsError,
@@ -92,7 +96,8 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
systemStatusError
systemStatusError,
translationsError
) => {
const hasError = !!(
customFiltersError ||
@@ -102,7 +107,8 @@ const selectErrors = createSelector(
qualityProfilesError ||
metadataProfilesError ||
importListsError ||
systemStatusError
systemStatusError ||
translationsError
);
return {
@@ -114,7 +120,8 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
systemStatusError
systemStatusError,
translationsError
};
}
);
@@ -176,6 +183,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() {
dispatch(fetchStatus());
},
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) {
dispatch(saveDimensions(dimensions));
},
@@ -210,6 +220,7 @@ class PageConnector extends Component {
this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
}
}
@@ -225,7 +236,6 @@ class PageConnector extends Component {
render() {
const {
hasTranslationsError,
isPopulated,
hasError,
dispatchFetchAuthor,
@@ -237,15 +247,15 @@ class PageConnector extends Component {
dispatchFetchImportLists,
dispatchFetchUISettings,
dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps
} = this.props;
if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) {
if (hasError || !this.state.isLocalStorageSupported) {
return (
<ErrorPage
{...this.state}
{...otherProps}
hasTranslationsError={hasTranslationsError}
/>
);
}
@@ -266,7 +276,6 @@ class PageConnector extends Component {
}
PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired,
@@ -280,6 +289,7 @@ PageConnector.propTypes = {
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired
};
@@ -21,28 +21,28 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.AUTHOR_CONTINUING,
title: 'Library',
title: () => translate('Library'),
to: '/',
alias: '/authors',
children: [
{
title: 'Authors',
title: () => translate('Authors'),
to: '/authors'
},
{
title: 'Books',
title: () => translate('Books'),
to: '/books'
},
{
title: 'Add New',
title: () => translate('AddNew'),
to: '/add/search'
},
{
title: 'Bookshelf',
title: () => translate('Bookshelf'),
to: '/shelf'
},
{
title: 'Unmapped Files',
title: () => translate('UnmappedFiles'),
to: '/unmapped'
}
]
@@ -50,26 +50,26 @@ const links = [
{
iconName: icons.CALENDAR,
title: 'Calendar',
title: () => translate('Calendar'),
to: '/calendar'
},
{
iconName: icons.ACTIVITY,
title: 'Activity',
title: () => translate('Activity'),
to: '/activity/queue',
children: [
{
title: 'Queue',
title: () => translate('Queue'),
to: '/activity/queue',
statusComponent: QueueStatusConnector
},
{
title: 'History',
title: () => translate('History'),
to: '/activity/history'
},
{
title: 'Blocklist',
title: () => translate('Blocklist'),
to: '/activity/blocklist'
}
]
@@ -77,15 +77,15 @@ const links = [
{
iconName: icons.WARNING,
title: 'Wanted',
title: () => translate('Wanted'),
to: '/wanted/missing',
children: [
{
title: 'Missing',
title: () => translate('Missing'),
to: '/wanted/missing'
},
{
title: 'Cutoff Unmet',
title: () => translate('CutoffUnmet'),
to: '/wanted/cutoffunmet'
}
]
@@ -93,55 +93,55 @@ const links = [
{
iconName: icons.SETTINGS,
title: 'Settings',
title: () => translate('Settings'),
to: '/settings',
children: [
{
title: 'Media Management',
title: () => translate('MediaManagement'),
to: '/settings/mediamanagement'
},
{
title: 'Profiles',
title: () => translate('Profiles'),
to: '/settings/profiles'
},
{
title: 'Quality',
title: () => translate('Quality'),
to: '/settings/quality'
},
{
title: translate('CustomFormats'),
title: () => translate('CustomFormats'),
to: '/settings/customformats'
},
{
title: translate('Indexers'),
title: () => translate('Indexers'),
to: '/settings/indexers'
},
{
title: 'Download Clients',
title: () => translate('DownloadClients'),
to: '/settings/downloadclients'
},
{
title: 'Import Lists',
title: () => translate('ImportLists'),
to: '/settings/importlists'
},
{
title: 'Connect',
title: () => translate('Connect'),
to: '/settings/connect'
},
{
title: 'Metadata',
title: () => translate('Metadata'),
to: '/settings/metadata'
},
{
title: 'Tags',
title: () => translate('Tags'),
to: '/settings/tags'
},
{
title: 'General',
title: () => translate('General'),
to: '/settings/general'
},
{
title: 'UI',
title: () => translate('Ui'),
to: '/settings/ui'
}
]
@@ -149,32 +149,32 @@ const links = [
{
iconName: icons.SYSTEM,
title: 'System',
title: () => translate('System'),
to: '/system/status',
children: [
{
title: 'Status',
title: () => translate('Status'),
to: '/system/status',
statusComponent: HealthStatusConnector
},
{
title: 'Tasks',
title: () => translate('Tasks'),
to: '/system/tasks'
},
{
title: 'Backup',
title: () => translate('Backup'),
to: '/system/backup'
},
{
title: 'Updates',
title: () => translate('Updates'),
to: '/system/updates'
},
{
title: 'Events',
title: () => translate('Events'),
to: '/system/events'
},
{
title: 'Log Files',
title: () => translate('LogFiles'),
to: '/system/logs/files'
}
]
@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
}
<span className={isChildItem ? styles.noIcon : null}>
{title}
{typeof title === 'function' ? title() : title}
</span>
{
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = {
iconName: PropTypes.object,
title: PropTypes.string.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
to: PropTypes.string.isRequired,
isActive: PropTypes.bool,
isActiveParent: PropTypes.bool,
+3 -1
View File
@@ -1,8 +1,10 @@
import React from 'react';
type PropertyFunction<T> = () => T;
interface Column {
name: string;
label: string | React.ReactNode;
label: string | PropertyFunction<string> | React.ReactNode;
columnLabel?: string;
isSortable?: boolean;
isVisible: boolean;
+1 -1
View File
@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)}
{...column}
>
{column.label}
{typeof column.label === 'function' ? column.label() : column.label}
</TableHeaderCell>
);
})
@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
const {
className,
name,
label,
columnLabel,
isSortable,
isVisible,
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
{...otherProps}
component="th"
className={className}
title={columnLabel}
label={typeof label === 'function' ? label() : label}
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress}
>
{children}
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = {
className: PropTypes.string,
name: PropTypes.string.isRequired,
columnLabel: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSortable: PropTypes.bool,
isVisible: PropTypes.bool,
isModifiable: PropTypes.bool,
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false}
onChange={onVisibleChange}
/>
{label}
{typeof label === 'function' ? label() : label}
</label>
{
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
<TableOptionsColumn
name={name}
label={label}
label={typeof label === 'function' ? label() : label}
isVisible={isVisible}
isModifiable={isModifiable}
index={index}
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,
+7 -1
View File
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { tooltipPositions } from 'Helpers/Props';
import Tooltip from './Tooltip';
import styles from './Popover.css';
@@ -30,8 +31,13 @@ function Popover(props) {
}
Popover.propTypes = {
className: PropTypes.string,
bodyClassName: PropTypes.string,
anchor: PropTypes.node.isRequired,
title: PropTypes.string.isRequired,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
position: PropTypes.oneOf(tooltipPositions.all),
canFlip: PropTypes.bool
};
export default Popover;
@@ -0,0 +1,8 @@
enum TooltipPosition {
Top = 'top',
Right = 'right',
Bottom = 'bottom',
Left = 'left',
}
export default TooltipPosition;
+2
View File
@@ -6,6 +6,7 @@ export const BOOKSHELF = 'bookshelf';
export const KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect';
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
export const FLOAT = 'float';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
@@ -34,6 +35,7 @@ export const all = [
KEY_VALUE_LIST,
MONITOR_BOOKS_SELECT,
MONITOR_NEW_ITEMS_SELECT,
FLOAT,
NUMBER,
OAUTH,
PASSWORD,
@@ -69,7 +69,7 @@ const columns = [
name: 'customFormats',
label: React.createElement(Icon, {
name: icons.INTERACTIVE,
title: translate('CustomFormat')
title: () => translate('CustomFormat')
}),
isSortable: true,
isVisible: true
@@ -91,9 +91,9 @@ const filterExistingFilesOptions = {
};
const importModeOptions = [
{ key: 'chooseImportMode', value: translate('ChooseImportMethod'), disabled: true },
{ key: 'move', value: translate('MoveFiles') },
{ key: 'copy', value: translate('HardlinkCopyFiles') }
{ key: 'chooseImportMode', value: () => translate('ChooseImportMethod'), disabled: true },
{ key: 'move', value: () => translate('MoveFiles') },
{ key: 'copy', value: () => translate('HardlinkCopyFiles') }
];
const SELECT = 'select';
@@ -1,10 +1,11 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, sortDirections } from 'Helpers/Props';
import { icons, kinds, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveSearch.css';
@@ -56,7 +57,7 @@ const columns = [
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: translate('CustomFormatScore')
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
@@ -112,17 +113,17 @@ function InteractiveSearch(props) {
{
!isFetching && isPopulated && !totalReleasesCount ?
<div className={styles.blankpad}>
No results found
</div> :
<Alert kind={kinds.INFO}>
{translate('NoResultsFound')}
</Alert> :
null
}
{
!!totalReleasesCount && isPopulated && !items.length ?
<div className={styles.blankpad}>
All results are hidden by the applied filter
</div> :
<Alert kind={kinds.WARNING}>
{translate('AllResultsAreHiddenByTheAppliedFilter')}
</Alert> :
null
}
@@ -157,7 +158,7 @@ function InteractiveSearch(props) {
{
totalReleasesCount !== items.length && !!items.length ?
<div className={styles.filteredMessage}>
Some results are hidden by the applied filter
{translate('SomeResultsAreHiddenByTheAppliedFilter')}
</div> :
null
}
@@ -15,7 +15,7 @@ import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
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 Peers from './Peers';
import styles from './InteractiveSearchRow.css';
@@ -32,6 +32,18 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed, grabError, downloadAllowed) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError || !downloadAllowed) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
@@ -172,7 +184,7 @@ class InteractiveSearchRow extends Component {
<TableRowCell className={styles.customFormatScore}>
<Tooltip
anchor={
formatPreferredWordScore(customFormatScore, customFormats.length)
formatCustomFormatScore(customFormatScore, customFormats.length)
}
tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
@@ -212,7 +224,7 @@ class InteractiveSearchRow extends Component {
{
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError || !downloadAllowed ? kinds.DANGER : kinds.DEFAULT}
kind={getDownloadKind(isGrabbed, grabError, downloadAllowed)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
@@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER}
title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', [name])}
message={translate('DeleteCustomFormatMessageText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat}
@@ -115,7 +115,7 @@ class Specification extends Component {
isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER}
title={translate('DeleteCondition')}
message={translate('DeleteConditionMessageText', [name])}
message={translate('DeleteConditionMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose}
@@ -113,7 +113,7 @@ class DownloadClient extends Component {
isOpen={this.state.isDeleteDownloadClientModalOpen}
kind={kinds.DANGER}
title={translate('DeleteDownloadClient')}
message={translate('DeleteDownloadClientMessageText', [name])}
message={translate('DeleteDownloadClientMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteDownloadClient}
onCancel={this.onDeleteDownloadClientModalClose}
@@ -27,9 +27,25 @@ interface ManageDownloadClientsEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'enabled', value: translate('Enabled') },
{ key: 'disabled', value: translate('Disabled') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
];
function ManageDownloadClientsEditModalContent(
@@ -164,7 +180,7 @@ function ManageDownloadClientsEditModalContent(
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('CountDownloadClientsSelected', [selectedCount])}
{translate('CountDownloadClientsSelected', { selectedCount })}
</div>
<div>
@@ -36,37 +36,37 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: translate('Implementation'),
label: () => translate('Implementation'),
isSortable: true,
isVisible: true,
},
{
name: 'enable',
label: translate('Enabled'),
label: () => translate('Enabled'),
isSortable: true,
isVisible: true,
},
{
name: 'priority',
label: translate('Priority'),
label: () => translate('Priority'),
isSortable: true,
isVisible: true,
},
{
name: 'removeCompletedDownloads',
label: translate('RemoveCompleted'),
label: () => translate('RemoveCompleted'),
isSortable: true,
isVisible: true,
},
{
name: 'removeFailedDownloads',
label: translate('RemoveFailed'),
label: () => translate('RemoveFailed'),
isSortable: true,
isVisible: true,
},
@@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelectedDownloadClients')}
message={translate('DeleteSelectedDownloadClientsMessageText', [
selectedIds.length,
])}
message={translate('DeleteSelectedDownloadClientsMessageText', {
count: selectedIds.length,
})}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
@@ -72,9 +72,24 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') },
{
key: 'add',
get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
];
return (
@@ -1,10 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
import RemotePathMapping from './RemotePathMapping';
@@ -50,6 +52,11 @@ class RemotePathMappings extends Component {
errorMessage={translate('UnableToLoadRemotePathMappings')}
{...otherProps}
>
<Alert kind={kinds.INFO}>
<InlineMarkdown data={translate('RemotePathMappingsInfo', { app: 'Readarr', wikiLink: 'https://wiki.servarr.com/readarr/settings#remote-path-mappings' })} />
</Alert>
<div className={styles.remotePathMappingsHeader}>
<div className={styles.host}>
{translate('Host')}
@@ -103,7 +103,6 @@ class GeneralSettings extends Component {
isResettingApiKey,
isWindows,
isWindowsService,
isDocker,
mode,
packageUpdateMechanism,
onInputChange,
@@ -171,7 +170,6 @@ class GeneralSettings extends Component {
settings={settings}
isWindows={isWindows}
packageUpdateMechanism={packageUpdateMechanism}
isDocker={isDocker}
onInputChange={onInputChange}
/>
@@ -214,7 +212,6 @@ GeneralSettings.propTypes = {
hasSettings: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired,
isWindowsService: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
mode: PropTypes.string.isRequired,
packageUpdateMechanism: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired,
@@ -26,7 +26,6 @@ function createMapStateToProps() {
isResettingApiKey,
isWindows: systemStatus.isWindows,
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service',
isDocker: systemStatus.isDocker,
mode: systemStatus.mode,
packageUpdateMechanism: systemStatus.packageUpdateMechanism,
...sectionSettings
+13 -25
View File
@@ -8,12 +8,17 @@ import { inputTypes, sizes } from 'Helpers/Props';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
const branchValues = [
'master',
'develop',
'nightly'
];
function UpdateSettings(props) {
const {
advancedSettings,
settings,
isWindows,
isDocker,
packageUpdateMechanism,
onInputChange
} = props;
@@ -44,32 +49,21 @@ function UpdateSettings(props) {
updateOptions.push({ key: 'script', value: 'Script' });
if (isDocker) {
return (
<FieldSet legend={translate('Updates')}>
<div>
{translate('UpdatingIsDisabledInsideADockerContainerUpdateTheContainerImageInstead')}
</div>
</FieldSet>
);
}
return (
<FieldSet legend={translate('Updates')}>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>
{translate('Branch')}
</FormLabel>
<FormLabel>{translate('Branch')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
type={inputTypes.AUTO_COMPLETE}
name="branch"
helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')}
helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr"
{...branch}
values={branchValues}
onChange={onInputChange}
readOnly={usingExternalUpdateMechanism}
/>
@@ -83,14 +77,13 @@ function UpdateSettings(props) {
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>
{translate('Automatic')}
</FormLabel>
<FormLabel>{translate('Automatic')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Readarr' }) : undefined}
onChange={onInputChange}
{...updateAutomatically}
/>
@@ -100,9 +93,7 @@ function UpdateSettings(props) {
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>
{translate('Mechanism')}
</FormLabel>
<FormLabel>{translate('Mechanism')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
@@ -121,9 +112,7 @@ function UpdateSettings(props) {
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>
{translate('ScriptPath')}
</FormLabel>
<FormLabel>{translate('ScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
@@ -144,7 +133,6 @@ UpdateSettings.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
settings: PropTypes.object.isRequired,
isWindows: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
packageUpdateMechanism: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired
};
@@ -107,7 +107,7 @@ class ImportList extends Component {
isOpen={this.state.isDeleteImportListModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', [name])}
message={translate('DeleteImportListMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteImportList}
onCancel={this.onDeleteImportListModalClose}
@@ -27,9 +27,25 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange';
const autoAddOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'enabled', value: translate('Enabled') },
{ key: 'disabled', value: translate('Disabled') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
];
function ManageImportListsEditModalContent(
@@ -168,7 +184,7 @@ function ManageImportListsEditModalContent(
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('CountImportListsSelected', [selectedCount])}
{translate('CountImportListsSelected', { selectedCount })}
</div>
<div>
@@ -36,43 +36,43 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: translate('Implementation'),
label: () => translate('Implementation'),
isSortable: true,
isVisible: true,
},
{
name: 'qualityProfileId',
label: translate('QualityProfile'),
label: () => translate('QualityProfile'),
isSortable: true,
isVisible: true,
},
{
name: 'metadataProfileId',
label: translate('MetadataProfile'),
label: () => translate('MetadataProfile'),
isSortable: true,
isVisible: true,
},
{
name: 'rootFolderPath',
label: translate('RootFolder'),
label: () => translate('RootFolder'),
isSortable: true,
isVisible: true,
},
{
name: 'enableAutomaticAdd',
label: translate('AutoAdd'),
label: () => translate('AutoAdd'),
isSortable: true,
isVisible: true,
},
{
name: 'tags',
label: translate('Tags'),
label: () => translate('Tags'),
isSortable: true,
isVisible: true,
},
@@ -283,9 +283,9 @@ function ManageImportListsModalContent(
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelectedImportLists')}
message={translate('DeleteSelectedImportListsMessageText', [
selectedIds.length,
])}
message={translate('DeleteSelectedImportListsMessageText', {
count: selectedIds.length,
})}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
@@ -70,9 +70,24 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') },
{
key: 'add',
get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
];
return (
@@ -152,7 +152,7 @@ class Indexer extends Component {
isOpen={this.state.isDeleteIndexerModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', [name])}
message={translate('DeleteIndexerMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexer}
onCancel={this.onDeleteIndexerModalClose}
@@ -27,9 +27,25 @@ interface ManageIndexersEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'enabled', value: translate('Enabled') },
{ key: 'disabled', value: translate('Disabled') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
];
function ManageIndexersEditModalContent(
@@ -162,7 +178,7 @@ function ManageIndexersEditModalContent(
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('CountIndexersSelected', [selectedCount])}
{translate('CountIndexersSelected', { selectedCount })}
</div>
<div>
@@ -36,43 +36,43 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: translate('Implementation'),
label: () => translate('Implementation'),
isSortable: true,
isVisible: true,
},
{
name: 'enableRss',
label: translate('EnableRSS'),
label: () => translate('EnableRSS'),
isSortable: true,
isVisible: true,
},
{
name: 'enableAutomaticSearch',
label: translate('EnableAutomaticSearch'),
label: () => translate('EnableAutomaticSearch'),
isSortable: true,
isVisible: true,
},
{
name: 'enableInteractiveSearch',
label: translate('EnableInteractiveSearch'),
label: () => translate('EnableInteractiveSearch'),
isSortable: true,
isVisible: true,
},
{
name: 'priority',
label: translate('Priority'),
label: () => translate('Priority'),
isSortable: true,
isVisible: true,
},
{
name: 'tags',
label: translate('Tags'),
label: () => translate('Tags'),
isSortable: true,
isVisible: true,
},
@@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelectedIndexers')}
message={translate('DeleteSelectedIndexersMessageText', [
selectedIds.length,
])}
message={translate('DeleteSelectedIndexersMessageText', {
count: selectedIds.length,
})}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
@@ -70,9 +70,24 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') },
{
key: 'add',
get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
];
return (
@@ -95,7 +95,7 @@ class RootFolder extends Component {
isOpen={this.state.isDeleteRootFolderModalOpen}
kind={kinds.DANGER}
title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', [name])}
message={translate('DeleteRootFolderMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteRootFolder}
onCancel={this.onDeleteRootFolderModalClose}
@@ -11,16 +11,51 @@ import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
const writeAudioTagOptions = [
{ key: 'no', value: translate('WriteTagsNo') },
{ key: 'sync', value: translate('WriteTagsSync') },
{ key: 'allFiles', value: translate('WriteTagsAll') },
{ key: 'newFiles', value: translate('WriteTagsNew') }
{
key: 'no',
get value() {
return translate('WriteTagsNo');
}
},
{
key: 'sync',
get value() {
return translate('WriteTagsSync');
}
},
{
key: 'allFiles',
get value() {
return translate('WriteTagsAll');
}
},
{
key: 'newFiles',
get value() {
return translate('WriteTagsNew');
}
}
];
const writeBookTagOptions = [
{ key: 'sync', value: translate('WriteTagsSync') },
{ key: 'allFiles', value: translate('WriteTagsAll') },
{ key: 'newFiles', value: translate('WriteTagsNew') }
{
key: 'sync',
get value() {
return translate('WriteTagsSync');
}
},
{
key: 'allFiles',
get value() {
return translate('WriteTagsAll');
}
},
{
key: 'newFiles',
get value() {
return translate('WriteTagsNew');
}
}
];
function MetadataProvider(props) {
@@ -227,7 +227,7 @@ class Notification extends Component {
isOpen={this.state.isDeleteNotificationModalOpen}
kind={kinds.DANGER}
title={translate('DeleteNotification')}
message={translate('DeleteNotificationMessageText', [name])}
message={translate('DeleteNotificationMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteNotification}
onCancel={this.onDeleteNotificationModalClose}
@@ -144,7 +144,7 @@ class MetadataProfile extends Component {
isOpen={this.state.isDeleteMetadataProfileModalOpen}
kind={kinds.DANGER}
title={translate('DeleteMetadataProfile')}
message={translate('DeleteMetadataProfileMessageText', [name])}
message={translate('DeleteMetadataProfileMessageText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteMetadataProfile}
@@ -162,7 +162,7 @@ class QualityProfile extends Component {
isOpen={this.state.isDeleteQualityProfileModalOpen}
kind={kinds.DANGER}
title={translate('DeleteQualityProfile')}
message={translate('DeleteQualityProfileMessageText', [name])}
message={translate('DeleteQualityProfileMessageText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteQualityProfile}
+1 -1
View File
@@ -139,7 +139,7 @@ function Settings() {
className={styles.link}
to="/settings/ui"
>
{translate('UI')}
{translate('Ui')}
</Link>
<div className={styles.summary}>
@@ -4,6 +4,8 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import monitorNewItemsOptions from 'Utilities/Author/monitorNewItemsOptions';
import monitorOptions from 'Utilities/Author/monitorOptions';
//
// Variables
@@ -51,6 +53,10 @@ export default {
port: 8080,
useSsl: false,
outputProfile: 'default',
defaultQualityProfileId: 0,
defaultMetadataProfileId: 0,
defaultMonitorOption: monitorOptions[0].key,
defaultNewItemMonitorOption: monitorNewItemsOptions[0].key,
defaultTags: []
},
isSaving: false,
+20 -1
View File
@@ -4,6 +4,7 @@ import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import { fetchTranslations as fetchAppTranslations } from 'Utilities/String/translate';
import createHandleActions from './Creators/createHandleActions';
function getDimensions(width, height) {
@@ -41,7 +42,12 @@ export const defaultState = {
isReconnecting: false,
isDisconnected: false,
isRestarting: false,
isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen
isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen,
translations: {
isFetching: true,
isPopulated: false,
error: null
}
};
//
@@ -53,6 +59,7 @@ export const SAVE_DIMENSIONS = 'app/saveDimensions';
export const SET_VERSION = 'app/setVersion';
export const SET_APP_VALUE = 'app/setAppValue';
export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible';
export const FETCH_TRANSLATIONS = 'app/fetchTranslations';
export const PING_SERVER = 'app/pingServer';
@@ -66,6 +73,7 @@ export const setAppValue = createAction(SET_APP_VALUE);
export const showMessage = createAction(SHOW_MESSAGE);
export const hideMessage = createAction(HIDE_MESSAGE);
export const pingServer = createThunk(PING_SERVER);
export const fetchTranslations = createThunk(FETCH_TRANSLATIONS);
//
// Helpers
@@ -127,6 +135,17 @@ function pingServerAfterTimeout(getState, dispatch) {
export const actionHandlers = handleThunks({
[PING_SERVER]: function(getState, payload, dispatch) {
pingServerAfterTimeout(getState, dispatch);
},
[FETCH_TRANSLATIONS]: async function(getState, payload, dispatch) {
const isFetchingComplete = await fetchAppTranslations();
dispatch(setAppValue({
translations: {
isFetching: false,
isPopulated: isFetchingComplete,
error: isFetchingComplete ? null : 'Failed to load translations from API'
}
}));
}
});
+5 -5
View File
@@ -24,12 +24,12 @@ export const section = 'books';
export const filters = [
{
key: 'all',
label: translate('All'),
label: () => translate('All'),
filters: []
},
{
key: 'monitored',
label: translate('Monitored'),
label: () => translate('Monitored'),
filters: [
{
key: 'monitored',
@@ -40,7 +40,7 @@ export const filters = [
},
{
key: 'unmonitored',
label: translate('Unmonitored'),
label: () => translate('Unmonitored'),
filters: [
{
key: 'monitored',
@@ -51,7 +51,7 @@ export const filters = [
},
{
key: 'missing',
label: translate('Missing'),
label: () => translate('Missing'),
filters: [
{
key: 'monitored',
@@ -67,7 +67,7 @@ export const filters = [
},
{
key: 'wanted',
label: translate('Wanted'),
label: () => translate('Wanted'),
filters: [
{
key: 'monitored',
+20 -20
View File
@@ -60,32 +60,32 @@ export const defaultState = {
columns: [
{
name: 'status',
columnLabel: translate('Status'),
columnLabel: () => translate('Status'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'authorMetadata.sortName',
label: translate('Author'),
label: () => translate('Author'),
isSortable: true,
isVisible: true
},
{
name: 'books.title',
label: translate('BookTitle'),
label: () => translate('BookTitle'),
isSortable: true,
isVisible: true
},
{
name: 'books.releaseDate',
label: translate('ReleaseDate'),
label: () => translate('ReleaseDate'),
isSortable: true,
isVisible: false
},
{
name: 'quality',
label: translate('Quality'),
label: () => translate('Quality'),
isSortable: true,
isVisible: true
},
@@ -97,64 +97,64 @@ export const defaultState = {
},
{
name: 'customFormatScore',
columnLabel: translate('CustomFormatScore'),
columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, {
name: icons.SCORE,
title: translate('CustomFormatScore')
title: () => translate('CustomFormatScore')
}),
isVisible: false
},
{
name: 'protocol',
label: translate('Protocol'),
label: () => translate('Protocol'),
isSortable: true,
isVisible: false
},
{
name: 'indexer',
label: translate('Indexer'),
label: () => translate('Indexer'),
isSortable: true,
isVisible: false
},
{
name: 'downloadClient',
label: translate('DownloadClient'),
label: () => translate('DownloadClient'),
isSortable: true,
isVisible: false
},
{
name: 'title',
label: translate('ReleaseTitle'),
label: () => translate('ReleaseTitle'),
isSortable: true,
isVisible: false
},
{
name: 'size',
label: translate('Size'),
label: () => translate('Size'),
isSortable: true,
isVisible: false
},
{
name: 'outputPath',
label: translate('OutputPath'),
label: () => translate('OutputPath'),
isSortable: false,
isVisible: false
},
{
name: 'estimatedCompletionTime',
label: translate('TimeLeft'),
label: () => translate('TimeLeft'),
isSortable: true,
isVisible: true
},
{
name: 'progress',
label: translate('Progress'),
label: () => translate('Progress'),
isSortable: true,
isVisible: true
},
{
name: 'actions',
columnLabel: translate('Actions'),
columnLabel: () => translate('Actions'),
isVisible: true,
isModifiable: false
}
@@ -371,13 +371,13 @@ export const actionHandlers = handleThunks({
id,
remove,
blocklist,
skipredownload
skipRedownload
} = payload;
dispatch(updateItem({ section: paged, id, isRemoving: true }));
const promise = createAjaxRequest({
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipredownload=${skipredownload}`,
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
method: 'DELETE'
}).request;
@@ -395,7 +395,7 @@ export const actionHandlers = handleThunks({
ids,
remove,
blocklist,
skipredownload
skipRedownload
} = payload;
dispatch(batchActions([
@@ -411,7 +411,7 @@ export const actionHandlers = handleThunks({
]));
const promise = createAjaxRequest({
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipredownload=${skipredownload}`,
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
method: 'DELETE',
dataType: 'json',
data: JSON.stringify({ ids })
+1 -1
View File
@@ -199,7 +199,7 @@ export const defaultState = {
},
{
name: 'customFormatScore',
label: translate('CustomFormatScore'),
label: () => translate('CustomFormatScore'),
type: filterBuilderTypes.NUMBER
},
{
+5 -5
View File
@@ -82,34 +82,34 @@ export const defaultState = {
columns: [
{
name: 'level',
columnLabel: translate('Level'),
columnLabel: () => translate('Level'),
isSortable: false,
isVisible: true,
isModifiable: false
},
{
name: 'time',
label: translate('Time'),
label: () => translate('Time'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'logger',
label: translate('Component'),
label: () => translate('Component'),
isSortable: false,
isVisible: true,
isModifiable: false
},
{
name: 'message',
label: translate('Message'),
label: () => translate('Message'),
isVisible: true,
isModifiable: false
},
{
name: 'actions',
columnLabel: translate('Actions'),
columnLabel: () => translate('Actions'),
isSortable: true,
isVisible: true,
isModifiable: false
@@ -36,10 +36,17 @@ function mergeColumns(path, initialState, persistedState, computedState) {
const column = initialColumns.find((i) => i.name === persistedColumn.name);
if (column) {
columns.push({
...column,
isVisible: persistedColumn.isVisible
});
const newColumn = {};
// We can't use a spread operator or Object.assign to clone the column
// or any accessors are lost and can break translations.
for (const prop of Object.keys(column)) {
Object.defineProperty(newColumn, prop, Object.getOwnPropertyDescriptor(column, prop));
}
newColumn.isVisible = persistedColumn.isVisible;
columns.push(newColumn);
}
});
+1 -1
View File
@@ -138,7 +138,7 @@ class BackupRow extends Component {
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', [name])}
message={translate('DeleteBackupMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose}
+3 -3
View File
@@ -21,17 +21,17 @@ const columns = [
},
{
name: 'name',
label: 'Name',
label: () => translate('Name'),
isVisible: true
},
{
name: 'size',
label: 'Size',
label: () => translate('Size'),
isVisible: true
},
{
name: 'time',
label: 'Time',
label: () => translate('Time'),
isVisible: true
},
{
@@ -146,7 +146,7 @@ class RestoreBackupModalContent extends Component {
<ModalBody>
{
!!id && `Would you like to restore the backup '${name}'?`
!!id && translate('WouldYouLikeToRestoreBackup', { name })
}
{
+2 -2
View File
@@ -19,12 +19,12 @@ import LogFilesTableRow from './LogFilesTableRow';
const columns = [
{
name: 'filename',
label: 'Filename',
label: () => translate('Filename'),
isVisible: true
},
{
name: 'lastWriteTime',
label: 'Last Write Time',
label: () => translate('LastWriteTime'),
isVisible: true
},
{
@@ -15,17 +15,17 @@ import styles from './DiskSpace.css';
const columns = [
{
name: 'path',
label: 'Location',
label: () => translate('Location'),
isVisible: true
},
{
name: 'freeSpace',
label: 'Free Space',
label: () => translate('FreeSpace'),
isVisible: true
},
{
name: 'totalSpace',
label: 'Total Space',
label: () => translate('TotalSpace'),
isVisible: true
},
{
+10 -2
View File
@@ -39,6 +39,14 @@ function getInternalLink(source) {
to="/settings/downloadclients"
/>
);
case 'NotificationStatusCheck':
return (
<IconButton
name={icons.SETTINGS}
title={translate('Settings')}
to="/settings/connect"
/>
);
case 'RootFolderCheck':
return (
<IconButton
@@ -95,12 +103,12 @@ const columns = [
},
{
name: 'message',
label: 'Message',
label: () => translate('Message'),
isVisible: true
},
{
name: 'actions',
label: 'Actions',
label: () => translate('Actions'),
isVisible: true
}
];
@@ -15,27 +15,27 @@ const columns = [
},
{
name: 'commandName',
label: translate('Name'),
label: () => translate('Name'),
isVisible: true
},
{
name: 'queued',
label: translate('Queued'),
label: () => translate('Queued'),
isVisible: true
},
{
name: 'started',
label: translate('Started'),
label: () => translate('Started'),
isVisible: true
},
{
name: 'ended',
label: translate('Ended'),
label: () => translate('Ended'),
isVisible: true
},
{
name: 'duration',
label: translate('Duration'),
label: () => translate('Duration'),
isVisible: true
},
{
@@ -10,27 +10,27 @@ import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
const columns = [
{
name: 'name',
label: 'Name',
label: () => translate('Name'),
isVisible: true
},
{
name: 'interval',
label: 'Interval',
label: () => translate('Interval'),
isVisible: true
},
{
name: 'lastExecution',
label: 'Last Execution',
label: () => translate('LastExecution'),
isVisible: true
},
{
name: 'lastDuration',
label: 'Last Duration',
label: () => translate('LastDuration'),
isVisible: true
},
{
name: 'nextExecution',
label: 'Next Execution',
label: () => translate('NextExecution'),
isVisible: true
},
{
@@ -1,5 +1,7 @@
function formatPreferredWordScore(input, customFormatsLength = 0) {
function formatCustomFormatScore(
input?: number,
customFormatsLength = 0
): string {
const score = Number(input);
if (score > 0) {
@@ -7,10 +9,10 @@ function formatPreferredWordScore(input, customFormatsLength = 0) {
}
if (score < 0) {
return score;
return `${score}`;
}
return customFormatsLength > 0 ? '+0' : '';
}
export default formatPreferredWordScore;
export default formatCustomFormatScore;
@@ -1,36 +0,0 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
function getTranslations() {
return createAjaxRequest({
global: false,
dataType: 'json',
url: '/localization'
}).request;
}
let translations = {};
export function fetchTranslations() {
return new Promise(async(resolve) => {
try {
const data = await getTranslations();
translations = data.Strings;
resolve(true);
} catch (error) {
resolve(false);
}
});
}
export default function translate(key, args = []) {
const translation = translations[key] || key;
if (args) {
return translation.replace(/\{(\d+)\}/g, (match, index) => {
return args[index];
});
}
return translation;
}
@@ -0,0 +1,44 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
function getTranslations() {
return createAjaxRequest({
global: false,
dataType: 'json',
url: '/localization',
}).request;
}
let translations: Record<string, string> = {};
export async function fetchTranslations(): Promise<boolean> {
return new Promise(async (resolve) => {
try {
const data = await getTranslations();
translations = data.Strings;
resolve(true);
} catch (error) {
resolve(false);
}
});
}
export default function translate(
key: string,
tokens?: Record<string, string | number | boolean>
) {
const translation = translations[key] || key;
if (tokens) {
// Fallback to the old behaviour for translations not yet updated to use named tokens
Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
});
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
}
return translation;
}
+15
View File
@@ -0,0 +1,15 @@
import { createBrowserHistory } from 'history';
import React from 'react';
import { render } from 'react-dom';
import createAppStore from 'Store/createAppStore';
import App from './App/App';
export async function bootstrap() {
const history = createBrowserHistory();
const store = createAppStore(history);
render(
<App store={store} history={history} />,
document.getElementById('root')
);
}

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