Compare commits

...

160 Commits

Author SHA1 Message Date
Weblate fc402743aa Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: DavidHenryThoreau <sorau@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translation: Servarr/Readarr
2023-10-13 12:37:22 +03:00
Bogdan b9d53ed732 Add PostgreSQL specific query for cleaning multiple monitored editions
Fixes #2995
2023-10-12 02:20:28 +03:00
Bogdan d248747635 Fixed: Avoid logging evaluations when not using any Remote Path Mappings
(cherry picked from commit 44eb729ccc13237f4439006159bd616e8bdb5750)
2023-10-10 07:12:09 +03:00
Bogdan d70224c811 Add status test all button for IndexerLongTermStatusCheck
(cherry picked from commit 4ffa1816bd2305550abee20cea27e1296a99ddf6)
2023-10-10 07:11:58 +03:00
Bogdan acdf8c8aa8 Bump version to 0.3.8 2023-10-08 07:09:11 +03:00
Bogdan 3ed41554ce Log Notifiarr errors as warnings 2023-10-07 22:58:40 +03:00
Bogdan ce808c6d7b Prevent mapping null metadata responses
Fixes #2971
2023-10-07 01:40:49 +03:00
Weblate 63b1b56a4f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Garkus98 <ivan12061998@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: blankhang <blankhang@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/zh_CN/
Translation: Servarr/Readarr
2023-10-07 01:04:37 +03:00
Bogdan a5647bedc8 Remove reddit from support issues 2023-10-07 00:25:19 +03:00
Servarr fe659bb79d Automated API Docs update 2023-10-04 06:59:02 +03:00
MxMarx 9918535509 New: Author Added notification
(cherry picked from commit https://github.com/Radarr/Radarr/commit/f890aadffa5ae579bcf65abdcf3e3948837084a9)
2023-10-04 06:47:23 +03:00
William Brockhus f9a6db40b8 Fixed: Ignore timezone when comparing tag dates 2023-10-04 06:26:08 +03:00
Bogdan 6273d69ed6 Fix tests 2023-10-04 05:50:57 +03:00
dependabot[bot] 7012380e95 Bump postcss from 8.4.23 to 8.4.31
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.23 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.23...8.4.31)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-04 04:05:39 +03:00
Bogdan b001ecd698 Preserve the protocol for fanart images
Closes #2944
2023-10-01 17:29:46 +03:00
Bogdan e28becdda4 Preserve the protocol in Author Image
Closes #2942
2023-10-01 17:27:38 +03:00
Mark McDowall eae06695e8 Fixed: Completed downloads in Qbit missing import path
(cherry picked from commit 35365665cfd436ac276dd9591e23333bd26cf789)

Closes #2959
2023-10-01 17:23:21 +03:00
Weblate 54a9af2ced Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mr cmuc <github@nextcos.de>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/de/
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/zh_CN/
Translation: Servarr/Readarr
2023-10-01 17:20:42 +03:00
Stevie Robinson c9b55266fc Fixed: qBittorent history retention to allow at least 14 days seeding
(cherry picked from commit 33b87acabf2b4c71ee24cda1a466dec6f4f76996)
2023-10-01 17:20:16 +03:00
bakerboy448 05b64406a4 Fixed: Only apply remote path mappings for completed items in Qbit
(cherry picked from commit 583eb52ddc01b608ab6cb17e863a8830c17b7b75)
2023-10-01 17:20:06 +03:00
Bogdan 1f37c5387b Revert "Avoid returning null in static resource mapper Task"
This reverts commit d7305b9753.
2023-10-01 03:26:00 +03:00
Stevie Robinson 4a6c7042fe Fixed: SABnzbd history retention to allow at least 14 days
(cherry picked from commit a3938d8e0264b48b35f4715cbc15329fb489218a)
2023-09-27 19:47:13 +03:00
Bogdan d7305b9753 Avoid returning null in static resource mapper Task
(cherry picked from commit a1ea7accb32bc72f61ed4531d109f76fad843939)
2023-09-27 18:54:41 +03:00
Bogdan bd56643eaa Bump version to 0.3.7 2023-09-24 16:24:12 +03:00
Stevie Robinson 44e6de2e23 Add health check for dl clients removing completed downloads + enable for sab and qbit
(cherry picked from commit 7f2cd8a0e99b537a1c616998514bacdd8468a016)

Closes #2939
2023-09-19 21:44:45 +03:00
Mark McDowall b209d047fa Fixed: Don't try to create metadata images if source files doesn't exist
(cherry picked from commit 9a1022386a031c928fc0495d6ab990ebce605ec1)

Closes #2933
2023-09-19 21:37:09 +03:00
Mark McDowall fd5ab27df6 New: Don't treat 400 responses from Notifiarr as errors
(cherry picked from commit 5eb420bbe12f59d0a5392abf3d351be28ca210e6)

Closes #2938
2023-09-19 21:35:26 +03:00
Bogdan 4a89befd79 Log request failures in Notifiarr 2023-09-19 21:34:12 +03:00
Bogdan 1a30293c33 Check for empty description as well in ParseQuality 2023-09-19 21:33:26 +03:00
Bogdan f5c2a6bf51 Fix use of empty Author SortName in filename 2023-09-19 18:55:24 +03:00
Weblate f3d90fdaf1 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Akashi2020 <dieux02400@gmail.com>
Co-authored-by: Anthony Veaudry <anthonyveaudry@gmail.com>
Co-authored-by: Gyuyeop Kim <rlarbduq777@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Herve Lauwerier <hervelauwerier@gmail.com>
Co-authored-by: Richard de Souza Leite <rs9010482@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mati300m <mateusz.smolec@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/fr/
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/
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-19 14:32:26 +03:00
MxMarx 04c5671a0a Fixed: Release Push api broken when no indexer id is specified 2023-09-19 14:31:44 +03:00
Qstick 22cc88c5e7 Fixed: Show correct error on unauthorized caps call
(cherry picked from commit f2b0fc946e1fb1b4649f1b46a003bd2add09a461)
2023-09-19 14:28:41 +03:00
Bogdan ca0c95a2d2 Fixed: Skip parsing releases without title
(cherry picked from commit c7824bb593291634bf14a5f7aa689666969b03bf)
2023-09-19 14:28:14 +03:00
Mark McDowall 419f790d66 Fixed: Don't allow quality profile to be created without all qualities
(cherry picked from commit 32e1ae2f64827272d351991838200884876e52b4)
2023-09-19 14:28:01 +03:00
Bogdan 9fe08429bc Use await on reading the response content
(cherry picked from commit 82d586e7015d7ea06356ca436024a8af5a4fb677)
2023-09-18 03:24:30 +03:00
Bogdan 71f4a88ab3 Bump version to 0.3.6 2023-09-17 12:02:41 +03:00
Bogdan 30b283eda3 Fixed: Ignore inaccessible mount points
(cherry picked from commit 60f18249b05daa20523542beef54bc126d963d1e)
2023-09-14 06:40:05 +03:00
Bogdan e23d0bbfa1 Add housekeeping task to unmonitor multiple monitored editions 2023-09-10 20:30:55 +03:00
Weblate 765a2aa01b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Qstick <qstick@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/readarr/da/
Translation: Servarr/Readarr
2023-09-10 20:30:40 +03:00
Bogdan 64895c3210 Bump version to 0.3.5 2023-09-10 09:04:03 +03:00
Weblate 03ab84a814 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: He Zhu <zhuhe202@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
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-10 00:33:32 -05:00
Bogdan b77e5b14e1 Fixed: macOS version detection
(cherry picked from commit 060be6177a5477c94823e6a423c42064dedc1afb)

Closes #2908
2023-09-08 05:08:09 +03:00
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
377 changed files with 7430 additions and 3042 deletions
+1 -2
View File
@@ -3,8 +3,7 @@
# Explicitly set bash scripts to have unix endings # Explicitly set bash scripts to have unix endings
*.sh text eol=lf *.sh text eol=lf
distribution/debian/* text eol=lf distribution/osx/Readarr text eol=lf
macOS/Readarr text eol=lf
# Custom for Visual Studio # Custom for Visual Studio
*.cs diff=csharp *.cs diff=csharp
+1 -1
View File
@@ -1,5 +1,5 @@
name: Bug Report name: Bug Report
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first' description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
labels: ['Type: Bug', 'Status: Needs Triage'] labels: ['Type: Bug', 'Status: Needs Triage']
body: body:
- type: checkboxes - type: checkboxes
-3
View File
@@ -3,6 +3,3 @@ contact_links:
- name: Support via Discord - name: Support via Discord
url: https://readarr.com/discord url: https://readarr.com/discord
about: Chat with users and devs on support and setup related topics. about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/Readarr
about: Discuss and search thru support topics.
+1 -2
View File
@@ -15,8 +15,7 @@ jobs:
issue-comment: > issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively :wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord) to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
or [Subreddit](https://reddit.com/r/readarr)
close-issue: true close-issue: true
lock-issue: false lock-issue: false
- uses: dessant/support-requests@v3 - uses: dessant/support-requests@v3
-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) [![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) [![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 Note: GitHub Issues are for Bugs and Feature Requests Only
+294 -141
View File
@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.2.4' majorVersion: '0.3.8'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)' readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.408' dotnetVersion: '6.0.413'
nodeVersion: '16.X' nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
@@ -27,6 +27,10 @@ trigger:
include: include:
- develop - develop
- master - master
paths:
exclude:
- .github
- src/Readarr.Api.*/openapi.json
pr: pr:
branches: branches:
@@ -34,82 +38,37 @@ pr:
- develop - develop
paths: paths:
exclude: exclude:
- .github
- src/NzbDrone.Core/Localization/Core - src/NzbDrone.Core/Localization/Core
- src/Readarr.Api.*/openapi.json
stages: stages:
- stage: Setup
- stage: Build_Backend_Windows displayName: Setup
displayName: Build Backend
dependsOn: []
jobs: jobs:
- job: Backend - job:
strategy: displayName: Build Variables
matrix:
Windows:
osName: 'Windows'
imageName: ${{ variables.windowsImage }}
enableAnalysis: 'false'
pool: pool:
vmImage: $(imageName) vmImage: ${{ variables.linuxImage }}
variables:
# Disable stylecop here - linting errors get caught by the analyze task
EnableAnalyzers: $(enableAnalysis)
steps: steps:
# Set the build name properly. The 'name' property won't recursively expand so hack here: # Set the build name properly. The 'name' property won't recursively expand so hack here:
- bash: echo "##vso[build.updatebuildnumber]$READARRVERSION" - bash: echo "##vso[build.updatebuildnumber]$READARRVERSION"
displayName: Set Build Name displayName: Set Build Name
- checkout: self
submodules: true
fetchDepth: 1
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- bash: | - bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}" if [[ $BUILD_REASON == "PullRequest" ]]; then
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" git diff origin/develop...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)"
echo $? > not_backend_update
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then else
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS echo 0 > not_backend_update
fi fi
displayName: Extra Platform Support cat not_backend_update
- task: Cache@2 displayName: Check for Backend File Changes
inputs: - publish: not_backend_update
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props' artifact: not_backend_update
path: $(nugetCacheFolder) displayName: Publish update type
displayName: Cache NuGet packages - stage: Build_Backend
- bash: ./build.sh --backend --enable-bsd displayName: Build Backend
displayName: Build Readarr Backend dependsOn: Setup
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: []
jobs: jobs:
- job: Backend - job: Backend
strategy: strategy:
@@ -122,6 +81,10 @@ stages:
osName: 'Mac' osName: 'Mac'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
enableAnalysis: 'false' enableAnalysis: 'false'
Windows:
osName: 'Windows'
imageName: ${{ variables.windowsImage }}
enableAnalysis: 'false'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
@@ -137,22 +100,17 @@ stages:
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}" BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" echo $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
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 sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi fi
displayName: Extra Platform Support displayName: Enable 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-extra-platforms - bash: ./build.sh --backend --enable-extra-platforms
displayName: Build Readarr Backend displayName: Build Readarr Backend
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- bash: | - bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \; find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \; find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
@@ -160,10 +118,38 @@ stages:
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \; find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
displayName: Clean up intermediate output displayName: Clean up intermediate output
condition: and(succeeded(), ne(variables['osName'], 'Windows')) 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 - stage: Build_Frontend
displayName: Frontend displayName: Frontend
dependsOn: [] dependsOn: Setup
jobs: jobs:
- job: Build - job: Build
strategy: strategy:
@@ -192,7 +178,6 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --frontend - bash: ./build.sh --frontend
@@ -204,10 +189,10 @@ stages:
artifact: '$(osName)Frontend' artifact: '$(osName)Frontend'
displayName: Publish Frontend displayName: Publish Frontend
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Installer - stage: Installer
dependsOn: dependsOn:
- Build_Backend_Windows - Build_Backend
- Build_Frontend - Build_Frontend
jobs: jobs:
- job: Windows_Installer - job: Windows_Installer
@@ -231,8 +216,8 @@ stages:
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: | - bash: |
./build.sh --packages --installer ./build.sh --packages --installer
cp setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe cp distribution/windows/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-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create Installers displayName: Create Installers
- publish: $(Build.ArtifactStagingDirectory) - publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller' artifact: 'WindowsInstaller'
@@ -240,7 +225,7 @@ stages:
- stage: Packages - stage: Packages
dependsOn: dependsOn:
- Build_Backend_Windows - Build_Backend
- Build_Frontend - Build_Frontend
jobs: jobs:
- job: Other_Packages - job: Other_Packages
@@ -406,14 +391,29 @@ stages:
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr) SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
SENTRY_ORG: $(sentryOrg) SENTRY_ORG: $(sentryOrg)
SENTRY_URL: $(sentryUrl) SENTRY_URL: $(sentryUrl)
- stage: Unit_Test - stage: Unit_Test
displayName: Unit Tests displayName: Unit Tests
dependsOn: Build_Backend_Windows dependsOn: Build_Backend
condition: succeeded()
jobs: 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 - job: Unit
displayName: Unit Native displayName: Unit Native
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace: workspace:
clean: all clean: all
@@ -479,6 +479,8 @@ stages:
- job: Unit_Docker - job: Unit_Docker
displayName: Unit Docker displayName: Unit Docker
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
alpine: alpine:
@@ -492,11 +494,11 @@ stages:
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
container: $[ variables['containerImage'] ] container: $[ variables['containerImage'] ]
timeoutInMinutes: 10 timeoutInMinutes: 10
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .NET'
@@ -530,12 +532,14 @@ stages:
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres - job: Unit_LinuxCore_Postgres14
displayName: Unit Native LinuxCore with Postgres Database displayName: Unit Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests artifactName: linux-x64-tests
Readarr__Postgres__Host: 'localhost' Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432' Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr' Readarr__Postgres__User: 'readarr'
@@ -545,7 +549,7 @@ stages:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10 timeoutInMinutes: 10
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
@@ -556,7 +560,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'linux-x64-Tests' artifactName: $(artifactName)
targetPath: $(testsFolder) targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \; - bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable displayName: Make Test Dummy Executable
@@ -579,15 +583,84 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' 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 failTaskOnFailedTests: true
- stage: Integration - stage: Integration
displayName: Integration displayName: Integration
dependsOn: Packages dependsOn: Packages
jobs: 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 - job: Integration_Native
displayName: Integration Native displayName: Integration Native
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
MacCore: MacCore:
@@ -608,7 +681,7 @@ stages:
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
@@ -630,7 +703,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -649,8 +722,10 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres - job: Integration_LinuxCore_Postgres14
displayName: Integration Native LinuxCore with Postgres Database displayName: Integration Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
Readarr__Postgres__Host: 'localhost' Readarr__Postgres__Host: 'localhost'
@@ -682,7 +757,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -705,12 +780,77 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' 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 failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_FreeBSD - job: Integration_FreeBSD
displayName: Integration Native FreeBSD displayName: Integration Native FreeBSD
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
workspace: workspace:
clean: all clean: all
variables: variables:
@@ -755,6 +895,8 @@ stages:
- job: Integration_Docker - job: Integration_Docker
displayName: Integration Docker displayName: Integration Docker
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy: strategy:
matrix: matrix:
alpine: alpine:
@@ -773,7 +915,7 @@ stages:
container: $[ variables['containerImage'] ] container: $[ variables['containerImage'] ]
timeoutInMinutes: 15 timeoutInMinutes: 15
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .NET'
@@ -801,7 +943,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -823,7 +965,7 @@ stages:
- stage: Automation - stage: Automation
displayName: Automation displayName: Automation
dependsOn: Packages dependsOn: Packages
jobs: jobs:
- job: Automation - job: Automation
strategy: strategy:
@@ -833,20 +975,23 @@ stages:
artifactName: 'linux-x64' artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
failBuild: true
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64' artifactName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz' pattern: 'Readarr.*.osx-core-x64.tar.gz'
failBuild: true
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64' artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip' pattern: 'Readarr.*.windows-core-x64.zip'
failBuild: true
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core' displayName: 'Install .net core'
@@ -868,7 +1013,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1 - task: ExtractFiles@1
inputs: inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package displayName: Extract Package
- bash: | - bash: |
@@ -888,20 +1033,35 @@ stages:
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots' TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
- publish: $(Build.ArtifactStagingDirectory)/screenshots - publish: $(Build.ArtifactStagingDirectory)/screenshots
artifact: '$(osName)AutomationScreenshots' artifact: '$(osName)AutomationScreenshots'
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
displayName: Publish Screenshot Bundle displayName: Publish Screenshot Bundle
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
- task: PublishTestResults@2 - task: PublishTestResults@2
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(osName) Automation Tests' testRunTitle: '$(osName) Automation Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: $(failBuild)
displayName: Publish Test Results displayName: Publish Test Results
- stage: Analyze - stage: Analyze
dependsOn: [] dependsOn:
- Setup
displayName: Analyze displayName: Analyze
jobs: 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 - job: Lint_Frontend
displayName: Lint Frontend displayName: Lint Frontend
strategy: strategy:
@@ -927,7 +1087,6 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --lint - bash: ./build.sh --lint
@@ -956,11 +1115,16 @@ stages:
cliProjectVersion: '$(readarrVersion)' cliProjectVersion: '$(readarrVersion)'
cliSources: './frontend' cliSources: './frontend'
- task: SonarCloudAnalyze@1 - task: SonarCloudAnalyze@1
- job: Api_Docs - job: Api_Docs
displayName: API Docs displayName: API Docs
dependsOn: Prepare
condition: | 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: pool:
vmImage: ${{ variables.windowsImage }} vmImage: ${{ variables.windowsImage }}
@@ -973,7 +1137,7 @@ stages:
- checkout: self - checkout: self
submodules: true submodules: true
persistCredentials: true persistCredentials: true
fetchDepth: 1 fetchDepth: 1
- bash: ./docs.sh Windows - bash: ./docs.sh Windows
displayName: Create openapi.json displayName: Create openapi.json
- bash: | - bash: |
@@ -981,10 +1145,9 @@ stages:
git config --global user.name "Servarr" git config --global user.name "Servarr"
git checkout -b api-docs git checkout -b api-docs
git add . git add .
git status if git status | grep -q modified
if git status | grep modified
then 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 git push -f --set-upstream origin api-docs
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/readarr/readarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}' curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/readarr/readarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
else else
@@ -1008,33 +1171,25 @@ stages:
- job: Analyze_Backend - job: Analyze_Backend
displayName: Backend displayName: Backend
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
disable.coverage.autogenerate: 'true' disable.coverage.autogenerate: 'true'
EnableAnalyzers: 'false'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.windowsImage }}
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .net core 2.1' displayName: 'Install .net core'
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'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- checkout: self # Need history for Sonar analysis - checkout: self # Need history for Sonar analysis
submodules: true submodules: true
- task: Cache@2 - powershell: Set-Service SCardSvr -StartupType Manual
inputs: displayName: Enable Windows Test Service
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder)
displayName: Cache NuGet packages
- task: SonarCloudPrepare@1 - task: SonarCloudPrepare@1
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs: inputs:
@@ -1045,16 +1200,14 @@ stages:
projectName: 'Readarr' projectName: 'Readarr'
projectVersion: '$(readarrVersion)' projectVersion: '$(readarrVersion)'
extraProperties: | 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.coverage.exclusions=**/Readarr.Api.V1/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: | - bash: |
./build.sh --backend -f net6.0 -r linux-x64 ./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/linux-x64/publish/ ./test.sh Linux Unit Coverage TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests displayName: Coverage Unit Tests
env:
NUGET_PACKAGES: $(nugetCacheFolder)
- task: SonarCloudAnalyze@1 - task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results displayName: Publish SonarCloud Results
@@ -1077,7 +1230,6 @@ stages:
- Unit_Test - Unit_Test
- Integration - Integration
- Automation - Automation
- Build_Backend_Other
condition: eq(variables['system.pullrequest.isfork'], false) condition: eq(variables['system.pullrequest.isfork'], false)
displayName: Build Status Report displayName: Build Status Report
jobs: jobs:
@@ -1101,3 +1253,4 @@ stages:
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId) DISCORDTHREADID: $(discordThreadId)
+9 -10
View File
@@ -23,7 +23,7 @@ UpdateVersionNumber()
echo "Updating Version Info" echo "Updating Version Info"
sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$READARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props 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/<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 fi
} }
@@ -183,7 +183,7 @@ PackageMacOSApp()
rm -rf $folder rm -rf $folder
mkdir -p $folder mkdir -p $folder
cp -r macOS/Readarr.app $folder cp -r distribution/osx/Readarr.app $folder
mkdir -p $folder/Readarr.app/Contents/MacOS mkdir -p $folder/Readarr.app/Contents/MacOS
echo "Copying Binaries" echo "Copying Binaries"
@@ -245,7 +245,7 @@ BuildInstaller()
local framework="$1" local framework="$1"
local runtime="$2" 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() InstallInno()
@@ -391,22 +391,21 @@ then
fi fi
fi fi
if [ "$FRONTEND" = "YES" ]; if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
then then
YarnInstall YarnInstall
RunWebpack
fi fi
if [ "$LINT" = "YES" ]; if [ "$LINT" = "YES" ];
then then
if [ -z "$FRONTEND" ];
then
YarnInstall
fi
LintUI LintUI
fi fi
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ]; if [ "$PACKAGES" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
@@ -44,16 +44,16 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks] [Tasks]
Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}" 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: "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 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 Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
[Dirs] [Dirs]
Name: "{app}"; Permissions: users-modify Name: "{app}"; Permissions: users-modify
[Files] [Files]
Source: "..\_artifacts\{#Runtime}\{#Framework}\Readarr\Readarr.exe"; DestDir: "{app}\bin"; Flags: ignoreversion 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\*"; Excludes: "Readarr.Update"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons] [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; Filename: "{app}\bin\Readarr.exe"; Description: "Start Readarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
[UninstallRun] [UninstallRun]
Filename: "{app}\bin\Readarr.Console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist Filename: "{app}\bin\readarr.console.exe"; Parameters: "/u"; Flags: waituntilterminated skipifdoesntexist
[Code] [Code]
function PrepareToInstall(var NeedsRestart: Boolean): String; function PrepareToInstall(var NeedsRestart: Boolean): String;
var var
ResultCode: Integer; ResultCode: Integer;
begin 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; end;
+4 -4
View File
@@ -4,14 +4,14 @@ module.exports = {
plugins: [ plugins: [
// Stage 1 // Stage 1
'@babel/plugin-proposal-export-default-from', '@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }], ['@babel/plugin-transform-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }], ['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
// Stage 2 // Stage 2
'@babel/plugin-proposal-export-namespace-from', '@babel/plugin-transform-export-namespace-from',
// Stage 3 // Stage 3
['@babel/plugin-proposal-class-properties', { loose }], ['@babel/plugin-transform-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import' '@babel/plugin-syntax-dynamic-import'
], ],
env: { env: {
+5 -3
View File
@@ -36,7 +36,7 @@ module.exports = (env) => {
}, },
entry: { entry: {
index: 'index.js' index: 'index.ts'
}, },
resolve: { resolve: {
@@ -91,13 +91,15 @@ module.exports = (env) => {
}), }),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: 'Content/styles.css' filename: 'Content/styles.css',
chunkFilename: 'Content/[id]-[chunkhash].css'
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs', template: 'frontend/src/index.ejs',
filename: 'index.html', filename: 'index.html',
publicPath: '/' publicPath: '/',
inject: false
}), }),
new FileManagerPlugin({ new FileManagerPlugin({
@@ -9,7 +9,7 @@ import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge'; import formatAge from 'Utilities/Number/formatAge';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css'; import styles from './HistoryDetails.css';
@@ -108,7 +108,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ? customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem <DescriptionListItem
title={translate('CustomFormatScore')} title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)} data={formatCustomFormatScore(customFormatScore)}
/> : /> :
null null
} }
@@ -225,7 +225,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ? customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem <DescriptionListItem
title={translate('CustomFormatScore')} title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)} data={formatCustomFormatScore(customFormatScore)}
/> : /> :
null null
} }
@@ -271,7 +271,7 @@ function HistoryDetails(props) {
customFormatScore && customFormatScore !== '0' ? customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem <DescriptionListItem
title={translate('CustomFormatScore')} title={translate('CustomFormatScore')}
data={formatPreferredWordScore(customFormatScore)} data={formatCustomFormatScore(customFormatScore)}
/> : /> :
null null
} }
+2 -2
View File
@@ -10,7 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, tooltipPositions } from 'Helpers/Props'; import { icons, tooltipPositions } from 'Helpers/Props';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal'; import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell'; import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css'; import styles from './HistoryRow.css';
@@ -180,7 +180,7 @@ class HistoryRow extends Component {
className={styles.customFormatScore} className={styles.customFormatScore}
> >
<Tooltip <Tooltip
anchor={formatPreferredWordScore( anchor={formatCustomFormatScore(
customFormatScore, customFormatScore,
customFormats.length customFormats.length
)} )}
+4 -4
View File
@@ -18,7 +18,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell'; import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal'; import RemoveQueueItemModal from './RemoveQueueItemModal';
@@ -46,14 +46,14 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true }); this.setState({ isRemoveQueueItemModalOpen: true });
}; };
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => { onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
const { const {
onRemoveQueueItemPress, onRemoveQueueItemPress,
onQueueRowModalOpenOrClose onQueueRowModalOpenOrClose
} = this.props; } = this.props;
onQueueRowModalOpenOrClose(false); onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist, skipredownload); onRemoveQueueItemPress(blocklist, skipRedownload);
this.setState({ isRemoveQueueItemModalOpen: false }); this.setState({ isRemoveQueueItemModalOpen: false });
}; };
@@ -232,7 +232,7 @@ class QueueRow extends Component {
className={styles.customFormatScore} className={styles.customFormatScore}
> >
<Tooltip <Tooltip
anchor={formatPreferredWordScore( anchor={formatCustomFormatScore(
customFormatScore, customFormatScore,
customFormats.length customFormats.length
)} )}
@@ -23,7 +23,7 @@ class RemoveQueueItemModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false, blocklist: false,
skipredownload: false skipRedownload: false
}; };
} }
@@ -34,7 +34,7 @@ class RemoveQueueItemModal extends Component {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false, blocklist: false,
skipredownload: false skipRedownload: false
}); });
}; };
@@ -49,8 +49,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ blocklist: value }); this.setState({ blocklist: value });
}; };
onSkipReDownloadChange = ({ value }) => { onSkipRedownloadChange = ({ value }) => {
this.setState({ skipredownload: value }); this.setState({ skipRedownload: value });
}; };
onRemoveConfirmed = () => { onRemoveConfirmed = () => {
@@ -76,7 +76,7 @@ class RemoveQueueItemModal extends Component {
isPending isPending
} = this.props; } = this.props;
const { remove, blocklist, skipredownload } = this.state; const { remove, blocklist, skipRedownload } = this.state;
return ( return (
<Modal <Modal
@@ -137,10 +137,10 @@ class RemoveQueueItemModal extends Component {
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="skipredownload" name="skipRedownload"
value={skipredownload} value={skipRedownload}
helpText={translate('SkipredownloadHelpText')} helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipReDownloadChange} onChange={this.onSkipRedownloadChange}
/> />
</FormGroup> </FormGroup>
} }
@@ -24,7 +24,7 @@ class RemoveQueueItemsModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false, blocklist: false,
skipredownload: false skipRedownload: false
}; };
} }
@@ -35,7 +35,7 @@ class RemoveQueueItemsModal extends Component {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false, blocklist: false,
skipredownload: false skipRedownload: false
}); });
}; };
@@ -50,8 +50,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ blocklist: value }); this.setState({ blocklist: value });
}; };
onSkipReDownloadChange = ({ value }) => { onSkipRedownloadChange = ({ value }) => {
this.setState({ skipredownload: value }); this.setState({ skipRedownload: value });
}; };
onRemoveConfirmed = () => { onRemoveConfirmed = () => {
@@ -77,7 +77,7 @@ class RemoveQueueItemsModal extends Component {
allPending allPending
} = this.props; } = this.props;
const { remove, blocklist, skipredownload } = this.state; const { remove, blocklist, skipRedownload } = this.state;
return ( return (
<Modal <Modal
@@ -138,10 +138,10 @@ class RemoveQueueItemsModal extends Component {
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="skipredownload" name="skipRedownload"
value={skipredownload} value={skipRedownload}
helpText={translate('SkipredownloadHelpText')} helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipReDownloadChange} onChange={this.onSkipRedownloadChange}
/> />
</FormGroup> </FormGroup>
} }
+3 -4
View File
@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme'; import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes'; import AppRoutes from './AppRoutes';
function App({ store, history, hasTranslationsError }) { function App({ store, history }) {
return ( return (
<DocumentTitle title={window.Readarr.instanceName}> <DocumentTitle title={window.Readarr.instanceName}>
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<ApplyTheme> <ApplyTheme>
<PageConnector hasTranslationsError={hasTranslationsError}> <PageConnector>
<AppRoutes app={App} /> <AppRoutes app={App} />
</PageConnector> </PageConnector>
</ApplyTheme> </ApplyTheme>
@@ -25,8 +25,7 @@ function App({ store, history, hasTranslationsError }) {
App.propTypes = { App.propTypes = {
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired
hasTranslationsError: PropTypes.bool.isRequired
}; };
export default App; export default App;
@@ -1,6 +1,7 @@
.version { .version {
margin: 0 3px; margin: 0 3px;
font-weight: bold; font-weight: bold;
font-family: var(--defaultFontFamily);
} }
.maintenance { .maintenance {
+7 -8
View File
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
@@ -64,12 +65,12 @@ function AppUpdatedModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Readarr Updated {translate('AppUpdated', { appName: 'Readarr' })}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <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> </div>
{ {
@@ -77,16 +78,14 @@ function AppUpdatedModalContent(props) {
<div> <div>
{ {
!update.changes && !update.changes &&
<div className={styles.maintenance}> <div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
{translate('MaintenanceRelease')}
</div>
} }
{ {
!!update.changes && !!update.changes &&
<div> <div>
<div className={styles.changes}> <div className={styles.changes}>
What's new? {translate('WhatsNew')}
</div> </div>
<UpdateChanges <UpdateChanges
@@ -113,14 +112,14 @@ function AppUpdatedModalContent(props) {
<Button <Button
onPress={onSeeChangesPress} onPress={onSeeChangesPress}
> >
Recent Changes {translate('RecentChanges')}
</Button> </Button>
<Button <Button
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
onPress={onModalClose} onPress={onModalClose}
> >
Reload {translate('Reload')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
+5 -4
View File
@@ -7,6 +7,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ConnectionLostModal.css'; import styles from './ConnectionLostModal.css';
function ConnectionLostModal(props) { function ConnectionLostModal(props) {
@@ -22,16 +23,16 @@ function ConnectionLostModal(props) {
> >
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Connection Lost {translate('ConnectionLost')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
Readarr has lost its connection to the backend and will need to be reloaded to restore functionality. {translate('ConnectionLostToBackend', { appName: 'Readarr' })}
</div> </div>
<div className={styles.automatic}> <div className={styles.automatic}>
Readarr will try to connect automatically, or you can click reload below. {translate('ConnectionLostReconnect', { appName: 'Readarr' })}
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
@@ -39,7 +40,7 @@ function ConnectionLostModal(props) {
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
onPress={onModalClose} onPress={onModalClose}
> >
Reload {translate('Reload')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
+3 -6
View File
@@ -7,13 +7,10 @@ function findImage(images, coverType) {
} }
function getUrl(image, coverType, size) { function getUrl(image, coverType, size) {
if (image) { const imageUrl = image?.url;
// Remove protocol
let url = image.url;
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`); if (imageUrl) {
return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
return url;
} }
} }
@@ -25,12 +25,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight); const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) { function getFanartUrl(images) {
const fanartImage = images.find((x) => x.coverType === 'fanart'); return images.find((x) => x.coverType === 'fanart')?.url;
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
} }
class AuthorDetailsHeader extends Component { class AuthorDetailsHeader extends Component {
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal'; import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput'; import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
@@ -9,6 +10,7 @@ import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/Settings/rootFolders';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel'; import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
import DeleteAuthorModal from './Delete/DeleteAuthorModal'; import DeleteAuthorModal from './Delete/DeleteAuthorModal';
@@ -17,6 +19,10 @@ import styles from './AuthorEditorFooter.css';
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const mapDispatchToProps = {
dispatchFetchRootFolders: fetchRootFolders
};
class AuthorEditorFooter extends Component { class AuthorEditorFooter extends Component {
// //
@@ -39,6 +45,13 @@ class AuthorEditorFooter extends Component {
}; };
} }
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchRootFolders();
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
isSaving, isSaving,
@@ -341,7 +354,8 @@ AuthorEditorFooter.propTypes = {
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
onSaveSelected: PropTypes.func.isRequired, onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired, onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired onRetagAuthorPress: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired
}; };
export default AuthorEditorFooter; export default connect(undefined, mapDispatchToProps)(AuthorEditorFooter);
@@ -16,7 +16,7 @@ import AuthorIndex from './AuthorIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createAuthorClientSideCollectionItemsSelector('authorIndex'), createAuthorClientSideCollectionItemsSelector('authorIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR), createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR), createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
@@ -24,17 +24,17 @@ function createMapStateToProps() {
( (
author, author,
isRefreshingAuthor, isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
isRssSyncExecuting,
dimensionsState dimensionsState
) => { ) => {
return { return {
...author, ...author,
isRefreshingAuthor, isRefreshingAuthor,
isRssSyncExecuting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }
@@ -14,14 +14,39 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const nameOptions = [ 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 = [ const posterSizeOptions = [
{ key: 'small', value: 'Small' }, {
{ key: 'medium', value: 'Medium' }, key: 'small',
{ key: 'large', value: 'Large' } get value() {
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
]; ];
class AuthorIndexOverviewOptionsModalContent extends Component { class AuthorIndexOverviewOptionsModalContent extends Component {
@@ -14,15 +14,45 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const posterSizeOptions = [ const posterSizeOptions = [
{ key: 'small', value: 'Small' }, {
{ key: 'medium', value: 'Medium' }, key: 'small',
{ key: 'large', value: 'Large' } get value() {
return translate('Small');
}
},
{
key: 'medium',
get value() {
return translate('Medium');
}
},
{
key: 'large',
get value() {
return translate('Large');
}
}
]; ];
const nameOptions = [ const nameOptions = [
{ key: 'no', value: translate('NoName') }, {
{ key: 'firstLast', value: translate('NameFirstLast') }, key: 'no',
{ key: 'lastFirst', value: translate('NameLastFirst') } get value() {
return translate('NoName');
}
},
{
key: 'firstLast',
get value() {
return translate('NameFirstLast');
}
},
{
key: 'lastFirst',
get value() {
return translate('NameLastFirst');
}
}
]; ];
class AuthorIndexPosterOptionsModalContent extends Component { class AuthorIndexPosterOptionsModalContent extends Component {
@@ -17,8 +17,8 @@ function AuthorIndexProgressBar(props) {
detailedProgressBar detailedProgressBar
} = props; } = props;
const progress = bookCount ? bookFileCount / bookCount * 100 : 100; const progress = bookCount ? bookCount / totalBookCount * 100 : 100;
const text = `${bookFileCount} / ${bookCount}`; const text = `${bookCount} / ${totalBookCount}`;
return ( return (
<ProgressBar <ProgressBar
@@ -297,7 +297,7 @@ class AuthorIndexRow extends Component {
progress={progress} progress={progress}
kind={getProgressBarKind(status, monitored, progress)} kind={getProgressBarKind(status, monitored, progress)}
showText={true} showText={true}
text={`${bookFileCount} / ${bookCount}`} text={`${bookCount} / ${totalBookCount}`}
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])} title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
width={125} width={125}
/> />
@@ -7,8 +7,18 @@ import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const nameOptions = [ 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 { class AuthorIndexTableOptions extends Component {
@@ -6,4 +6,5 @@
.statusIcon { .statusIcon {
width: 20px !important; width: 20px !important;
text-align: center;
} }
@@ -33,7 +33,7 @@ class MonitoringOptionsModalContent extends Component {
const { const {
isSaving, isSaving,
saveError saveError
} = prevProps; } = this.props;
if (prevProps.isSaving && !isSaving && !saveError) { if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({ this.setState({
@@ -21,12 +21,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight); const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) { function getFanartUrl(images) {
const fanartImage = images.find((x) => x.coverType === 'fanart'); return images.find((x) => x.coverType === 'fanart')?.url;
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
} }
class BookDetailsHeader extends Component { class BookDetailsHeader extends Component {
@@ -16,8 +16,8 @@ import BookIndex from './BookIndex';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookClientSideCollectionItemsSelector('bookIndex'), createBookClientSideCollectionItemsSelector('bookIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.BULK_REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.REFRESH_BOOK), createCommandExecutingSelector(commandNames.BULK_REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH), createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH), createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent'; import BookInteractiveSearchModalContent from './BookInteractiveSearchModalContent';
function BookInteractiveSearchModal(props) { function BookInteractiveSearchModal(props) {
@@ -14,6 +15,7 @@ function BookInteractiveSearchModal(props) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={false} closeOnBackgroundClick={false}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
+2 -2
View File
@@ -30,7 +30,7 @@ class BookshelfFooter extends Component {
const { const {
isSaving, isSaving,
saveError saveError
} = prevProps; } = this.props;
if (prevProps.isSaving && !isSaving && !saveError) { if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({ this.setState({
@@ -143,7 +143,7 @@ class BookshelfFooter extends Component {
<div> <div>
<div className={styles.label}> <div className={styles.label}>
{selectedCount} Author(s) Selected {translate('CountAuthorsSelected', { selectedCount })}
</div> </div>
<SpinnerButton <SpinnerButton
@@ -25,6 +25,10 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
.version {
margin-top: 20px;
}
@media only screen and (max-width: $breakpointMedium) { @media only screen and (max-width: $breakpointMedium) {
.image { .image {
height: 250px; height: 250px;
@@ -6,6 +6,7 @@ interface CssExports {
'image': string; 'image': string;
'imageContainer': string; 'imageContainer': string;
'message': string; 'message': string;
'version': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default 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 selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => { const keyOptions = filterBuilderProps.map((availablePropFilter) => {
const { name, label } = availablePropFilter;
return { return {
key: availablePropFilter.name, key: name,
value: availablePropFilter.label value: typeof label === 'function' ? label() : label
}; };
}).sort((a, b) => a.value.localeCompare(b.value)); }).sort((a, b) => a.value.localeCompare(b.value));
@@ -9,6 +9,10 @@
&:hover { &:hover {
background-color: var(--inputHoverBackgroundColor); background-color: var(--inputHoverBackgroundColor);
} }
&.isDisabled {
cursor: not-allowed;
}
} }
.optionCheck { .optionCheck {
+1 -1
View File
@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { value } = this.props; const { value } = this.props;
if (value !== prevProps.value && !this.state.isFocused) { if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
this.setState({ this.setState({
value: value == null ? '' : value.toString() value: value == null ? '' : value.toString()
}); });
+2 -2
View File
@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key} value={key}
{...otherOptionProps} {...otherOptionProps}
> >
{optionValue} {typeof optionValue === 'function' ? optionValue() : optionValue}
</option> </option>
); );
}) })
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string, className: PropTypes.string,
disabledClassName: PropTypes.string, disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
hasError: PropTypes.bool, hasError: PropTypes.bool,
+2 -2
View File
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return ( return (
<span <span
className={containerClassName} className={containerClassName}
title={title} title={typeof title === 'function' ? title() : title}
> >
{icon} {icon}
</span> </span>
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired, name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired, kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
title: PropTypes.string, title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired fixedWidth: PropTypes.bool.isRequired
}; };
@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
render() { render() {
const { const {
kind,
isSpinning, isSpinning,
error, error,
children, children,
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError; const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK; let iconName = icons.CHECK;
let iconKind = kinds.SUCCESS; let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
if (hasWarning) { if (hasWarning) {
iconName = icons.WARNING; iconName = icons.WARNING;
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
return ( return (
<SpinnerButton <SpinnerButton
kind={kind}
isSpinning={isSpinning} isSpinning={isSpinning}
{...otherProps} {...otherProps}
> >
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
} }
SpinnerErrorButton.propTypes = { SpinnerErrorButton.propTypes = {
kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
children: PropTypes.node.isRequired children: PropTypes.node.isRequired
@@ -10,27 +10,55 @@ class InlineMarkdown extends Component {
render() { render() {
const { const {
className, className,
data data,
blockClassName
} = this.props; } = this.props;
// For now only replace links // For now only replace links or code blocks (not both)
const markdownBlocks = []; const markdownBlocks = [];
if (data) { if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g); const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0; let endIndex = 0;
let match = null; let match = null;
while ((match = regex.exec(data)) !== null) {
while ((match = linkRegex.exec(data)) !== null) {
if (match.index > endIndex) { if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
} }
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>); markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
endIndex = match.index + match[0].length; endIndex = match.index + match[0].length;
} }
if (endIndex !== data.length) { if (endIndex !== data.length && markdownBlocks.length > 0) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
} }
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
endIndex = 0;
match = null;
let matchedCode = false;
while ((match = codeRegex.exec(data)) !== null) {
matchedCode = true;
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<code key={`code-${match.index}`} 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>; return <span className={className}>{markdownBlocks}</span>;
@@ -39,7 +67,8 @@ class InlineMarkdown extends Component {
InlineMarkdown.propTypes = { InlineMarkdown.propTypes = {
className: PropTypes.string, className: PropTypes.string,
data: PropTypes.string data: PropTypes.string,
blockClassName: PropTypes.string
}; };
export default InlineMarkdown; export default InlineMarkdown;
@@ -32,7 +32,7 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect} onPress={onFilterSelect}
> >
{filter.label} {typeof filter.label === 'function' ? filter.label() : filter.label}
</FilterMenuItem> </FilterMenuItem>
); );
}) })
+4 -4
View File
@@ -7,7 +7,7 @@ function ErrorPage(props) {
const { const {
version, version,
isLocalStorageSupported, isLocalStorageSupported,
hasTranslationsError, translationsError,
authorError, authorError,
customFiltersError, customFiltersError,
tagsError, tagsError,
@@ -21,8 +21,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) { if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (hasTranslationsError) { } else if (translationsError) {
errorMessage = 'Failed to load translations from API'; errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
} else if (authorError) { } else if (authorError) {
errorMessage = getErrorMessage(authorError, 'Failed to load author from API'); errorMessage = getErrorMessage(authorError, 'Failed to load author from API');
} else if (customFiltersError) { } else if (customFiltersError) {
@@ -55,7 +55,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = { ErrorPage.propTypes = {
version: PropTypes.string.isRequired, version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired, isLocalStorageSupported: PropTypes.bool.isRequired,
hasTranslationsError: PropTypes.bool.isRequired, translationsError: PropTypes.object,
authorError: PropTypes.object, authorError: PropTypes.object,
customFiltersError: PropTypes.object, customFiltersError: PropTypes.object,
tagsError: PropTypes.object, tagsError: PropTypes.object,
+20 -10
View File
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect'; 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 { fetchAuthor } from 'Store/Actions/authorActions';
import { fetchBooks } from 'Store/Actions/bookActions'; import { fetchBooks } from 'Store/Actions/bookActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
@@ -52,6 +52,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.metadataProfiles.isPopulated, (state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated, (state) => state.settings.importLists.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
( (
customFiltersIsPopulated, customFiltersIsPopulated,
tagsIsPopulated, tagsIsPopulated,
@@ -60,7 +61,8 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated, qualityProfilesIsPopulated,
metadataProfilesIsPopulated, metadataProfilesIsPopulated,
importListsIsPopulated, importListsIsPopulated,
systemStatusIsPopulated systemStatusIsPopulated,
translationsIsPopulated
) => { ) => {
return ( return (
customFiltersIsPopulated && customFiltersIsPopulated &&
@@ -70,7 +72,8 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated && qualityProfilesIsPopulated &&
metadataProfilesIsPopulated && metadataProfilesIsPopulated &&
importListsIsPopulated && importListsIsPopulated &&
systemStatusIsPopulated systemStatusIsPopulated &&
translationsIsPopulated
); );
} }
); );
@@ -84,6 +87,7 @@ const selectErrors = createSelector(
(state) => state.settings.metadataProfiles.error, (state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error, (state) => state.settings.importLists.error,
(state) => state.system.status.error, (state) => state.system.status.error,
(state) => state.app.translations.error,
( (
customFiltersError, customFiltersError,
tagsError, tagsError,
@@ -92,7 +96,8 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
systemStatusError systemStatusError,
translationsError
) => { ) => {
const hasError = !!( const hasError = !!(
customFiltersError || customFiltersError ||
@@ -102,7 +107,8 @@ const selectErrors = createSelector(
qualityProfilesError || qualityProfilesError ||
metadataProfilesError || metadataProfilesError ||
importListsError || importListsError ||
systemStatusError systemStatusError ||
translationsError
); );
return { return {
@@ -114,7 +120,8 @@ const selectErrors = createSelector(
qualityProfilesError, qualityProfilesError,
metadataProfilesError, metadataProfilesError,
importListsError, importListsError,
systemStatusError systemStatusError,
translationsError
}; };
} }
); );
@@ -176,6 +183,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() { dispatchFetchStatus() {
dispatch(fetchStatus()); dispatch(fetchStatus());
}, },
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) { onResize(dimensions) {
dispatch(saveDimensions(dimensions)); dispatch(saveDimensions(dimensions));
}, },
@@ -210,6 +220,7 @@ class PageConnector extends Component {
this.props.dispatchFetchImportLists(); this.props.dispatchFetchImportLists();
this.props.dispatchFetchUISettings(); this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus(); this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
} }
} }
@@ -225,7 +236,6 @@ class PageConnector extends Component {
render() { render() {
const { const {
hasTranslationsError,
isPopulated, isPopulated,
hasError, hasError,
dispatchFetchAuthor, dispatchFetchAuthor,
@@ -237,15 +247,15 @@ class PageConnector extends Component {
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps ...otherProps
} = this.props; } = this.props;
if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) { if (hasError || !this.state.isLocalStorageSupported) {
return ( return (
<ErrorPage <ErrorPage
{...this.state} {...this.state}
{...otherProps} {...otherProps}
hasTranslationsError={hasTranslationsError}
/> />
); );
} }
@@ -266,7 +276,6 @@ class PageConnector extends Component {
} }
PageConnector.propTypes = { PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
@@ -280,6 +289,7 @@ PageConnector.propTypes = {
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired onSidebarVisibleChange: PropTypes.func.isRequired
}; };
@@ -21,28 +21,28 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [ const links = [
{ {
iconName: icons.AUTHOR_CONTINUING, iconName: icons.AUTHOR_CONTINUING,
title: 'Library', title: () => translate('Library'),
to: '/', to: '/',
alias: '/authors', alias: '/authors',
children: [ children: [
{ {
title: 'Authors', title: () => translate('Authors'),
to: '/authors' to: '/authors'
}, },
{ {
title: 'Books', title: () => translate('Books'),
to: '/books' to: '/books'
}, },
{ {
title: 'Add New', title: () => translate('AddNew'),
to: '/add/search' to: '/add/search'
}, },
{ {
title: 'Bookshelf', title: () => translate('Bookshelf'),
to: '/shelf' to: '/shelf'
}, },
{ {
title: 'Unmapped Files', title: () => translate('UnmappedFiles'),
to: '/unmapped' to: '/unmapped'
} }
] ]
@@ -50,26 +50,26 @@ const links = [
{ {
iconName: icons.CALENDAR, iconName: icons.CALENDAR,
title: 'Calendar', title: () => translate('Calendar'),
to: '/calendar' to: '/calendar'
}, },
{ {
iconName: icons.ACTIVITY, iconName: icons.ACTIVITY,
title: 'Activity', title: () => translate('Activity'),
to: '/activity/queue', to: '/activity/queue',
children: [ children: [
{ {
title: 'Queue', title: () => translate('Queue'),
to: '/activity/queue', to: '/activity/queue',
statusComponent: QueueStatusConnector statusComponent: QueueStatusConnector
}, },
{ {
title: 'History', title: () => translate('History'),
to: '/activity/history' to: '/activity/history'
}, },
{ {
title: 'Blocklist', title: () => translate('Blocklist'),
to: '/activity/blocklist' to: '/activity/blocklist'
} }
] ]
@@ -77,15 +77,15 @@ const links = [
{ {
iconName: icons.WARNING, iconName: icons.WARNING,
title: 'Wanted', title: () => translate('Wanted'),
to: '/wanted/missing', to: '/wanted/missing',
children: [ children: [
{ {
title: 'Missing', title: () => translate('Missing'),
to: '/wanted/missing' to: '/wanted/missing'
}, },
{ {
title: 'Cutoff Unmet', title: () => translate('CutoffUnmet'),
to: '/wanted/cutoffunmet' to: '/wanted/cutoffunmet'
} }
] ]
@@ -93,55 +93,55 @@ const links = [
{ {
iconName: icons.SETTINGS, iconName: icons.SETTINGS,
title: 'Settings', title: () => translate('Settings'),
to: '/settings', to: '/settings',
children: [ children: [
{ {
title: 'Media Management', title: () => translate('MediaManagement'),
to: '/settings/mediamanagement' to: '/settings/mediamanagement'
}, },
{ {
title: 'Profiles', title: () => translate('Profiles'),
to: '/settings/profiles' to: '/settings/profiles'
}, },
{ {
title: 'Quality', title: () => translate('Quality'),
to: '/settings/quality' to: '/settings/quality'
}, },
{ {
title: translate('CustomFormats'), title: () => translate('CustomFormats'),
to: '/settings/customformats' to: '/settings/customformats'
}, },
{ {
title: translate('Indexers'), title: () => translate('Indexers'),
to: '/settings/indexers' to: '/settings/indexers'
}, },
{ {
title: 'Download Clients', title: () => translate('DownloadClients'),
to: '/settings/downloadclients' to: '/settings/downloadclients'
}, },
{ {
title: 'Import Lists', title: () => translate('ImportLists'),
to: '/settings/importlists' to: '/settings/importlists'
}, },
{ {
title: 'Connect', title: () => translate('Connect'),
to: '/settings/connect' to: '/settings/connect'
}, },
{ {
title: 'Metadata', title: () => translate('Metadata'),
to: '/settings/metadata' to: '/settings/metadata'
}, },
{ {
title: 'Tags', title: () => translate('Tags'),
to: '/settings/tags' to: '/settings/tags'
}, },
{ {
title: 'General', title: () => translate('General'),
to: '/settings/general' to: '/settings/general'
}, },
{ {
title: 'UI', title: () => translate('Ui'),
to: '/settings/ui' to: '/settings/ui'
} }
] ]
@@ -149,32 +149,32 @@ const links = [
{ {
iconName: icons.SYSTEM, iconName: icons.SYSTEM,
title: 'System', title: () => translate('System'),
to: '/system/status', to: '/system/status',
children: [ children: [
{ {
title: 'Status', title: () => translate('Status'),
to: '/system/status', to: '/system/status',
statusComponent: HealthStatusConnector statusComponent: HealthStatusConnector
}, },
{ {
title: 'Tasks', title: () => translate('Tasks'),
to: '/system/tasks' to: '/system/tasks'
}, },
{ {
title: 'Backup', title: () => translate('Backup'),
to: '/system/backup' to: '/system/backup'
}, },
{ {
title: 'Updates', title: () => translate('Updates'),
to: '/system/updates' to: '/system/updates'
}, },
{ {
title: 'Events', title: () => translate('Events'),
to: '/system/events' to: '/system/events'
}, },
{ {
title: 'Log Files', title: () => translate('LogFiles'),
to: '/system/logs/files' to: '/system/logs/files'
} }
] ]
@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
} }
<span className={isChildItem ? styles.noIcon : null}> <span className={isChildItem ? styles.noIcon : null}>
{title} {typeof title === 'function' ? title() : title}
</span> </span>
{ {
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = { PageSidebarItem.propTypes = {
iconName: PropTypes.object, iconName: PropTypes.object,
title: PropTypes.string.isRequired, title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
to: PropTypes.string.isRequired, to: PropTypes.string.isRequired,
isActive: PropTypes.bool, isActive: PropTypes.bool,
isActiveParent: PropTypes.bool, isActiveParent: PropTypes.bool,
+3 -1
View File
@@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
type PropertyFunction<T> = () => T;
interface Column { interface Column {
name: string; name: string;
label: string | React.ReactNode; label: string | PropertyFunction<string> | React.ReactNode;
columnLabel?: string; columnLabel?: string;
isSortable?: boolean; isSortable?: boolean;
isVisible: boolean; isVisible: boolean;
+1 -1
View File
@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)} {...getTableHeaderCellProps(otherProps)}
{...column} {...column}
> >
{column.label} {typeof column.label === 'function' ? column.label() : column.label}
</TableHeaderCell> </TableHeaderCell>
); );
}) })
@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
const { const {
className, className,
name, name,
label,
columnLabel, columnLabel,
isSortable, isSortable,
isVisible, isVisible,
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
{...otherProps} {...otherProps}
component="th" component="th"
className={className} className={className}
title={columnLabel} label={typeof label === 'function' ? label() : label}
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress} onPress={this.onPress}
> >
{children} {children}
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = { TableHeaderCell.propTypes = {
className: PropTypes.string, className: PropTypes.string,
name: PropTypes.string.isRequired, 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, isSortable: PropTypes.bool,
isVisible: PropTypes.bool, isVisible: PropTypes.bool,
isModifiable: PropTypes.bool, isModifiable: PropTypes.bool,
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false} isDisabled={isModifiable === false}
onChange={onVisibleChange} onChange={onVisibleChange}
/> />
{label} {typeof label === 'function' ? label() : label}
</label> </label>
{ {
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = { TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired, isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
<TableOptionsColumn <TableOptionsColumn
name={name} name={name}
label={label} label={typeof label === 'function' ? label() : label}
isVisible={isVisible} isVisible={isVisible}
isModifiable={isModifiable} isModifiable={isModifiable}
index={index} index={index}
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
TableOptionsColumnDragSource.propTypes = { TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired, isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
+7 -1
View File
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { tooltipPositions } from 'Helpers/Props';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
import styles from './Popover.css'; import styles from './Popover.css';
@@ -30,8 +31,13 @@ function Popover(props) {
} }
Popover.propTypes = { Popover.propTypes = {
className: PropTypes.string,
bodyClassName: PropTypes.string,
anchor: PropTypes.node.isRequired,
title: PropTypes.string.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; 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 KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect'; export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect';
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect'; export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
export const FLOAT = 'float';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
@@ -34,6 +35,7 @@ export const all = [
KEY_VALUE_LIST, KEY_VALUE_LIST,
MONITOR_BOOKS_SELECT, MONITOR_BOOKS_SELECT,
MONITOR_NEW_ITEMS_SELECT, MONITOR_NEW_ITEMS_SELECT,
FLOAT,
NUMBER, NUMBER,
OAUTH, OAUTH,
PASSWORD, PASSWORD,
@@ -69,7 +69,7 @@ const columns = [
name: 'customFormats', name: 'customFormats',
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.INTERACTIVE, name: icons.INTERACTIVE,
title: translate('CustomFormat') title: () => translate('CustomFormat')
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@@ -91,9 +91,9 @@ const filterExistingFilesOptions = {
}; };
const importModeOptions = [ const importModeOptions = [
{ key: 'chooseImportMode', value: translate('ChooseImportMethod'), disabled: true }, { key: 'chooseImportMode', value: () => translate('ChooseImportMethod'), disabled: true },
{ key: 'move', value: translate('MoveFiles') }, { key: 'move', value: () => translate('MoveFiles') },
{ key: 'copy', value: translate('HardlinkCopyFiles') } { key: 'copy', value: () => translate('HardlinkCopyFiles') }
]; ];
const SELECT = 'select'; const SELECT = 'select';
@@ -1,10 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, sortDirections } from 'Helpers/Props'; import { icons, kinds, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import InteractiveSearchRow from './InteractiveSearchRow'; import InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveSearch.css'; import styles from './InteractiveSearch.css';
@@ -56,7 +57,7 @@ const columns = [
name: 'customFormatScore', name: 'customFormatScore',
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.SCORE, name: icons.SCORE,
title: translate('CustomFormatScore') title: () => translate('CustomFormatScore')
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
@@ -112,17 +113,17 @@ function InteractiveSearch(props) {
{ {
!isFetching && isPopulated && !totalReleasesCount ? !isFetching && isPopulated && !totalReleasesCount ?
<div className={styles.blankpad}> <Alert kind={kinds.INFO}>
No results found {translate('NoResultsFound')}
</div> : </Alert> :
null null
} }
{ {
!!totalReleasesCount && isPopulated && !items.length ? !!totalReleasesCount && isPopulated && !items.length ?
<div className={styles.blankpad}> <Alert kind={kinds.WARNING}>
All results are hidden by the applied filter {translate('AllResultsAreHiddenByTheAppliedFilter')}
</div> : </Alert> :
null null
} }
@@ -157,7 +158,7 @@ function InteractiveSearch(props) {
{ {
totalReleasesCount !== items.length && !!items.length ? totalReleasesCount !== items.length && !!items.length ?
<div className={styles.filteredMessage}> <div className={styles.filteredMessage}>
Some results are hidden by the applied filter {translate('SomeResultsAreHiddenByTheAppliedFilter')}
</div> : </div> :
null null
} }
@@ -15,7 +15,7 @@ import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge'; import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore'; import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import Peers from './Peers'; import Peers from './Peers';
import styles from './InteractiveSearchRow.css'; import styles from './InteractiveSearchRow.css';
@@ -32,6 +32,18 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
return icons.DOWNLOAD; 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) { function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) { if (isGrabbing) {
return ''; return '';
@@ -172,7 +184,7 @@ class InteractiveSearchRow extends Component {
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
<Tooltip <Tooltip
anchor={ anchor={
formatPreferredWordScore(customFormatScore, customFormats.length) formatCustomFormatScore(customFormatScore, customFormats.length)
} }
tooltip={<BookFormats formats={customFormats} />} tooltip={<BookFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM} position={tooltipPositions.BOTTOM}
@@ -212,7 +224,7 @@ class InteractiveSearchRow extends Component {
{ {
<SpinnerIconButton <SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)} name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError || !downloadAllowed ? kinds.DANGER : kinds.DEFAULT} kind={getDownloadKind(isGrabbed, grabError, downloadAllowed)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)} title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing} isSpinning={isGrabbing}
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress} onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
@@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen} isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCustomFormat')} title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', [name])} message={translate('DeleteCustomFormatMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat} onConfirm={this.onConfirmDeleteCustomFormat}
@@ -115,7 +115,7 @@ class Specification extends Component {
isOpen={this.state.isDeleteSpecificationModalOpen} isOpen={this.state.isDeleteSpecificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCondition')} title={translate('DeleteCondition')}
message={translate('DeleteConditionMessageText', [name])} message={translate('DeleteConditionMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteSpecification} onConfirm={this.onConfirmDeleteSpecification}
onCancel={this.onDeleteSpecificationModalClose} onCancel={this.onDeleteSpecificationModalClose}
@@ -113,7 +113,7 @@ class DownloadClient extends Component {
isOpen={this.state.isDeleteDownloadClientModalOpen} isOpen={this.state.isDeleteDownloadClientModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteDownloadClient')} title={translate('DeleteDownloadClient')}
message={translate('DeleteDownloadClientMessageText', [name])} message={translate('DeleteDownloadClientMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteDownloadClient} onConfirm={this.onConfirmDeleteDownloadClient}
onCancel={this.onDeleteDownloadClientModalClose} onCancel={this.onDeleteDownloadClientModalClose}
@@ -27,9 +27,25 @@ interface ManageDownloadClientsEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const enableOptions = [ const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, {
{ key: 'enabled', value: translate('Enabled') }, key: NO_CHANGE,
{ key: 'disabled', value: translate('Disabled') }, get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
]; ];
function ManageDownloadClientsEditModalContent( function ManageDownloadClientsEditModalContent(
@@ -164,7 +180,7 @@ function ManageDownloadClientsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountDownloadClientsSelected', [selectedCount])} {translate('CountDownloadClientsSelected', { selectedCount })}
</div> </div>
<div> <div>
@@ -36,37 +36,37 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: translate('Name'), label: () => translate('Name'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'implementation', name: 'implementation',
label: translate('Implementation'), label: () => translate('Implementation'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enable', name: 'enable',
label: translate('Enabled'), label: () => translate('Enabled'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'priority', name: 'priority',
label: translate('Priority'), label: () => translate('Priority'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'removeCompletedDownloads', name: 'removeCompletedDownloads',
label: translate('RemoveCompleted'), label: () => translate('RemoveCompleted'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'removeFailedDownloads', name: 'removeFailedDownloads',
label: translate('RemoveFailed'), label: () => translate('RemoveFailed'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@@ -286,9 +286,9 @@ function ManageDownloadClientsModalContent(
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedDownloadClients')} title={translate('DeleteSelectedDownloadClients')}
message={translate('DeleteSelectedDownloadClientsMessageText', [ message={translate('DeleteSelectedDownloadClientsMessageText', {
selectedIds.length, count: selectedIds.length,
])} })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -72,9 +72,24 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]); }, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [ const applyTagsOptions = [
{ key: 'add', value: translate('Add') }, {
{ key: 'remove', value: translate('Remove') }, key: 'add',
{ key: 'replace', value: translate('Replace') }, get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
]; ];
return ( return (
@@ -1,10 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageSectionContent from 'Components/Page/PageSectionContent'; import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector'; import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
import RemotePathMapping from './RemotePathMapping'; import RemotePathMapping from './RemotePathMapping';
@@ -50,6 +52,11 @@ class RemotePathMappings extends Component {
errorMessage={translate('UnableToLoadRemotePathMappings')} errorMessage={translate('UnableToLoadRemotePathMappings')}
{...otherProps} {...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.remotePathMappingsHeader}>
<div className={styles.host}> <div className={styles.host}>
{translate('Host')} {translate('Host')}
@@ -103,7 +103,6 @@ class GeneralSettings extends Component {
isResettingApiKey, isResettingApiKey,
isWindows, isWindows,
isWindowsService, isWindowsService,
isDocker,
mode, mode,
packageUpdateMechanism, packageUpdateMechanism,
onInputChange, onInputChange,
@@ -171,7 +170,6 @@ class GeneralSettings extends Component {
settings={settings} settings={settings}
isWindows={isWindows} isWindows={isWindows}
packageUpdateMechanism={packageUpdateMechanism} packageUpdateMechanism={packageUpdateMechanism}
isDocker={isDocker}
onInputChange={onInputChange} onInputChange={onInputChange}
/> />
@@ -214,7 +212,6 @@ GeneralSettings.propTypes = {
hasSettings: PropTypes.bool.isRequired, hasSettings: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired, isWindows: PropTypes.bool.isRequired,
isWindowsService: PropTypes.bool.isRequired, isWindowsService: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
mode: PropTypes.string.isRequired, mode: PropTypes.string.isRequired,
packageUpdateMechanism: PropTypes.string.isRequired, packageUpdateMechanism: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
@@ -26,7 +26,6 @@ function createMapStateToProps() {
isResettingApiKey, isResettingApiKey,
isWindows: systemStatus.isWindows, isWindows: systemStatus.isWindows,
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service', isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service',
isDocker: systemStatus.isDocker,
mode: systemStatus.mode, mode: systemStatus.mode,
packageUpdateMechanism: systemStatus.packageUpdateMechanism, packageUpdateMechanism: systemStatus.packageUpdateMechanism,
...sectionSettings ...sectionSettings
+13 -25
View File
@@ -8,12 +8,17 @@ import { inputTypes, sizes } from 'Helpers/Props';
import titleCase from 'Utilities/String/titleCase'; import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const branchValues = [
'master',
'develop',
'nightly'
];
function UpdateSettings(props) { function UpdateSettings(props) {
const { const {
advancedSettings, advancedSettings,
settings, settings,
isWindows, isWindows,
isDocker,
packageUpdateMechanism, packageUpdateMechanism,
onInputChange onInputChange
} = props; } = props;
@@ -44,32 +49,21 @@ function UpdateSettings(props) {
updateOptions.push({ key: 'script', value: 'Script' }); updateOptions.push({ key: 'script', value: 'Script' });
if (isDocker) {
return (
<FieldSet legend={translate('Updates')}>
<div>
{translate('UpdatingIsDisabledInsideADockerContainerUpdateTheContainerImageInstead')}
</div>
</FieldSet>
);
}
return ( return (
<FieldSet legend={translate('Updates')}> <FieldSet legend={translate('Updates')}>
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel> <FormLabel>{translate('Branch')}</FormLabel>
{translate('Branch')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.AUTO_COMPLETE}
name="branch" name="branch"
helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')} helpText={usingExternalUpdateMechanism ? translate('UsingExternalUpdateMechanismBranchUsedByExternalUpdateMechanism') : translate('UsingExternalUpdateMechanismBranchToUseToUpdateReadarr')}
helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr" helpLink="https://wiki.servarr.com/readarr/faq#how-do-I-update-my-readarr"
{...branch} {...branch}
values={branchValues}
onChange={onInputChange} onChange={onInputChange}
readOnly={usingExternalUpdateMechanism} readOnly={usingExternalUpdateMechanism}
/> />
@@ -83,14 +77,13 @@ function UpdateSettings(props) {
isAdvanced={true} isAdvanced={true}
size={sizes.MEDIUM} size={sizes.MEDIUM}
> >
<FormLabel> <FormLabel>{translate('Automatic')}</FormLabel>
{translate('Automatic')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="updateAutomatically" name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')} helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Readarr' }) : undefined}
onChange={onInputChange} onChange={onInputChange}
{...updateAutomatically} {...updateAutomatically}
/> />
@@ -100,9 +93,7 @@ function UpdateSettings(props) {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel> <FormLabel>{translate('Mechanism')}</FormLabel>
{translate('Mechanism')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
@@ -121,9 +112,7 @@ function UpdateSettings(props) {
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
isAdvanced={true} isAdvanced={true}
> >
<FormLabel> <FormLabel>{translate('ScriptPath')}</FormLabel>
{translate('ScriptPath')}
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
@@ -144,7 +133,6 @@ UpdateSettings.propTypes = {
advancedSettings: PropTypes.bool.isRequired, advancedSettings: PropTypes.bool.isRequired,
settings: PropTypes.object.isRequired, settings: PropTypes.object.isRequired,
isWindows: PropTypes.bool.isRequired, isWindows: PropTypes.bool.isRequired,
isDocker: PropTypes.bool.isRequired,
packageUpdateMechanism: PropTypes.string.isRequired, packageUpdateMechanism: PropTypes.string.isRequired,
onInputChange: PropTypes.func.isRequired onInputChange: PropTypes.func.isRequired
}; };
@@ -107,7 +107,7 @@ class ImportList extends Component {
isOpen={this.state.isDeleteImportListModalOpen} isOpen={this.state.isDeleteImportListModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteImportList')} title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', [name])} message={translate('DeleteImportListMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteImportList} onConfirm={this.onConfirmDeleteImportList}
onCancel={this.onDeleteImportListModalClose} onCancel={this.onDeleteImportListModalClose}
@@ -27,9 +27,25 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const autoAddOptions = [ const autoAddOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, {
{ key: 'enabled', value: translate('Enabled') }, key: NO_CHANGE,
{ key: 'disabled', value: translate('Disabled') }, get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
]; ];
function ManageImportListsEditModalContent( function ManageImportListsEditModalContent(
@@ -168,7 +184,7 @@ function ManageImportListsEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountImportListsSelected', [selectedCount])} {translate('CountImportListsSelected', { selectedCount })}
</div> </div>
<div> <div>
@@ -36,43 +36,43 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: translate('Name'), label: () => translate('Name'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'implementation', name: 'implementation',
label: translate('Implementation'), label: () => translate('Implementation'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'qualityProfileId', name: 'qualityProfileId',
label: translate('QualityProfile'), label: () => translate('QualityProfile'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'metadataProfileId', name: 'metadataProfileId',
label: translate('MetadataProfile'), label: () => translate('MetadataProfile'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'rootFolderPath', name: 'rootFolderPath',
label: translate('RootFolder'), label: () => translate('RootFolder'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableAutomaticAdd', name: 'enableAutomaticAdd',
label: translate('AutoAdd'), label: () => translate('AutoAdd'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'tags', name: 'tags',
label: translate('Tags'), label: () => translate('Tags'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@@ -283,9 +283,9 @@ function ManageImportListsModalContent(
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedImportLists')} title={translate('DeleteSelectedImportLists')}
message={translate('DeleteSelectedImportListsMessageText', [ message={translate('DeleteSelectedImportListsMessageText', {
selectedIds.length, count: selectedIds.length,
])} })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -70,9 +70,24 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]); }, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [ const applyTagsOptions = [
{ key: 'add', value: translate('Add') }, {
{ key: 'remove', value: translate('Remove') }, key: 'add',
{ key: 'replace', value: translate('Replace') }, get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
]; ];
return ( return (
@@ -152,7 +152,7 @@ class Indexer extends Component {
isOpen={this.state.isDeleteIndexerModalOpen} isOpen={this.state.isDeleteIndexerModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteIndexer')} title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', [name])} message={translate('DeleteIndexerMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexer} onConfirm={this.onConfirmDeleteIndexer}
onCancel={this.onDeleteIndexerModalClose} onCancel={this.onDeleteIndexerModalClose}
@@ -27,9 +27,25 @@ interface ManageIndexersEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const enableOptions = [ const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, {
{ key: 'enabled', value: translate('Enabled') }, key: NO_CHANGE,
{ key: 'disabled', value: translate('Disabled') }, get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
]; ];
function ManageIndexersEditModalContent( function ManageIndexersEditModalContent(
@@ -162,7 +178,7 @@ function ManageIndexersEditModalContent(
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountIndexersSelected', [selectedCount])} {translate('CountIndexersSelected', { selectedCount })}
</div> </div>
<div> <div>
@@ -36,43 +36,43 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [ const COLUMNS = [
{ {
name: 'name', name: 'name',
label: translate('Name'), label: () => translate('Name'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'implementation', name: 'implementation',
label: translate('Implementation'), label: () => translate('Implementation'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableRss', name: 'enableRss',
label: translate('EnableRSS'), label: () => translate('EnableRSS'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableAutomaticSearch', name: 'enableAutomaticSearch',
label: translate('EnableAutomaticSearch'), label: () => translate('EnableAutomaticSearch'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'enableInteractiveSearch', name: 'enableInteractiveSearch',
label: translate('EnableInteractiveSearch'), label: () => translate('EnableInteractiveSearch'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'priority', name: 'priority',
label: translate('Priority'), label: () => translate('Priority'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{ {
name: 'tags', name: 'tags',
label: translate('Tags'), label: () => translate('Tags'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
@@ -281,9 +281,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isOpen={isDeleteModalOpen} isOpen={isDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteSelectedIndexers')} title={translate('DeleteSelectedIndexers')}
message={translate('DeleteSelectedIndexersMessageText', [ message={translate('DeleteSelectedIndexersMessageText', {
selectedIds.length, count: selectedIds.length,
])} })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete} onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose} onCancel={onDeleteModalClose}
@@ -70,9 +70,24 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [tags, applyTags, onApplyTagsPress]); }, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions = [ const applyTagsOptions = [
{ key: 'add', value: translate('Add') }, {
{ key: 'remove', value: translate('Remove') }, key: 'add',
{ key: 'replace', value: translate('Replace') }, get value() {
return translate('Add');
},
},
{
key: 'remove',
get value() {
return translate('Remove');
},
},
{
key: 'replace',
get value() {
return translate('Replace');
},
},
]; ];
return ( return (
@@ -95,7 +95,7 @@ class RootFolder extends Component {
isOpen={this.state.isDeleteRootFolderModalOpen} isOpen={this.state.isDeleteRootFolderModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteRootFolder')} title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', [name])} message={translate('DeleteRootFolderMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteRootFolder} onConfirm={this.onConfirmDeleteRootFolder}
onCancel={this.onDeleteRootFolderModalClose} onCancel={this.onDeleteRootFolderModalClose}
@@ -11,16 +11,51 @@ import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const writeAudioTagOptions = [ const writeAudioTagOptions = [
{ key: 'no', value: translate('WriteTagsNo') }, {
{ key: 'sync', value: translate('WriteTagsSync') }, key: 'no',
{ key: 'allFiles', value: translate('WriteTagsAll') }, get value() {
{ key: 'newFiles', value: translate('WriteTagsNew') } 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 = [ const writeBookTagOptions = [
{ key: 'sync', value: translate('WriteTagsSync') }, {
{ key: 'allFiles', value: translate('WriteTagsAll') }, key: 'sync',
{ key: 'newFiles', value: translate('WriteTagsNew') } get value() {
return translate('WriteTagsSync');
}
},
{
key: 'allFiles',
get value() {
return translate('WriteTagsAll');
}
},
{
key: 'newFiles',
get value() {
return translate('WriteTagsNew');
}
}
]; ];
function MetadataProvider(props) { function MetadataProvider(props) {
@@ -60,6 +60,7 @@ class Notification extends Component {
onReleaseImport, onReleaseImport,
onUpgrade, onUpgrade,
onRename, onRename,
onAuthorAdded,
onAuthorDelete, onAuthorDelete,
onBookDelete, onBookDelete,
onBookFileDelete, onBookFileDelete,
@@ -73,6 +74,7 @@ class Notification extends Component {
supportsOnReleaseImport, supportsOnReleaseImport,
supportsOnUpgrade, supportsOnUpgrade,
supportsOnRename, supportsOnRename,
supportsOnAuthorAdded,
supportsOnAuthorDelete, supportsOnAuthorDelete,
supportsOnBookDelete, supportsOnBookDelete,
supportsOnBookFileDelete, supportsOnBookFileDelete,
@@ -136,6 +138,14 @@ class Notification extends Component {
null null
} }
{
supportsOnAuthorAdded && onAuthorAdded ?
<Label kind={kinds.SUCCESS}>
{translate('OnAuthorAdded')}
</Label> :
null
}
{ {
supportsOnAuthorDelete && onAuthorDelete ? supportsOnAuthorDelete && onAuthorDelete ?
<Label kind={kinds.SUCCESS}> <Label kind={kinds.SUCCESS}>
@@ -227,7 +237,7 @@ class Notification extends Component {
isOpen={this.state.isDeleteNotificationModalOpen} isOpen={this.state.isDeleteNotificationModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteNotification')} title={translate('DeleteNotification')}
message={translate('DeleteNotificationMessageText', [name])} message={translate('DeleteNotificationMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteNotification} onConfirm={this.onConfirmDeleteNotification}
onCancel={this.onDeleteNotificationModalClose} onCancel={this.onDeleteNotificationModalClose}
@@ -244,6 +254,7 @@ Notification.propTypes = {
onReleaseImport: PropTypes.bool.isRequired, onReleaseImport: PropTypes.bool.isRequired,
onUpgrade: PropTypes.bool.isRequired, onUpgrade: PropTypes.bool.isRequired,
onRename: PropTypes.bool.isRequired, onRename: PropTypes.bool.isRequired,
onAuthorAdded: PropTypes.bool.isRequired,
onAuthorDelete: PropTypes.bool.isRequired, onAuthorDelete: PropTypes.bool.isRequired,
onBookDelete: PropTypes.bool.isRequired, onBookDelete: PropTypes.bool.isRequired,
onBookFileDelete: PropTypes.bool.isRequired, onBookFileDelete: PropTypes.bool.isRequired,
@@ -257,6 +268,7 @@ Notification.propTypes = {
supportsOnReleaseImport: PropTypes.bool.isRequired, supportsOnReleaseImport: PropTypes.bool.isRequired,
supportsOnUpgrade: PropTypes.bool.isRequired, supportsOnUpgrade: PropTypes.bool.isRequired,
supportsOnRename: PropTypes.bool.isRequired, supportsOnRename: PropTypes.bool.isRequired,
supportsOnAuthorAdded: PropTypes.bool.isRequired,
supportsOnAuthorDelete: PropTypes.bool.isRequired, supportsOnAuthorDelete: PropTypes.bool.isRequired,
supportsOnBookDelete: PropTypes.bool.isRequired, supportsOnBookDelete: PropTypes.bool.isRequired,
supportsOnBookFileDelete: PropTypes.bool.isRequired, supportsOnBookFileDelete: PropTypes.bool.isRequired,
@@ -19,6 +19,7 @@ function NotificationEventItems(props) {
onReleaseImport, onReleaseImport,
onUpgrade, onUpgrade,
onRename, onRename,
onAuthorAdded,
onAuthorDelete, onAuthorDelete,
onBookDelete, onBookDelete,
onBookFileDelete, onBookFileDelete,
@@ -32,6 +33,7 @@ function NotificationEventItems(props) {
supportsOnReleaseImport, supportsOnReleaseImport,
supportsOnUpgrade, supportsOnUpgrade,
supportsOnRename, supportsOnRename,
supportsOnAuthorAdded,
supportsOnAuthorDelete, supportsOnAuthorDelete,
supportsOnBookDelete, supportsOnBookDelete,
supportsOnBookFileDelete, supportsOnBookFileDelete,
@@ -123,6 +125,17 @@ function NotificationEventItems(props) {
/> />
</div> </div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onAuthorAdded"
helpText={translate('OnAuthorAddedHelpText')}
isDisabled={!supportsOnAuthorAdded.value}
{...onAuthorAdded}
onChange={onInputChange}
/>
</div>
<div> <div>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
@@ -144,7 +144,7 @@ class MetadataProfile extends Component {
isOpen={this.state.isDeleteMetadataProfileModalOpen} isOpen={this.state.isDeleteMetadataProfileModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteMetadataProfile')} title={translate('DeleteMetadataProfile')}
message={translate('DeleteMetadataProfileMessageText', [name])} message={translate('DeleteMetadataProfileMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteMetadataProfile} onConfirm={this.onConfirmDeleteMetadataProfile}
@@ -162,7 +162,7 @@ class QualityProfile extends Component {
isOpen={this.state.isDeleteQualityProfileModalOpen} isOpen={this.state.isDeleteQualityProfileModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteQualityProfile')} title={translate('DeleteQualityProfile')}
message={translate('DeleteQualityProfileMessageText', [name])} message={translate('DeleteQualityProfileMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteQualityProfile} onConfirm={this.onConfirmDeleteQualityProfile}
+1 -1
View File
@@ -139,7 +139,7 @@ function Settings() {
className={styles.link} className={styles.link}
to="/settings/ui" to="/settings/ui"
> >
{translate('UI')} {translate('Ui')}
</Link> </Link>
<div className={styles.summary}> <div className={styles.summary}>
@@ -106,6 +106,7 @@ export default {
selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport; selectedSchema.onReleaseImport = selectedSchema.supportsOnReleaseImport;
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade; selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
selectedSchema.onRename = selectedSchema.supportsOnRename; selectedSchema.onRename = selectedSchema.supportsOnRename;
selectedSchema.onAuthorAdded = selectedSchema.supportsOnAuthorAdded;
selectedSchema.onAuthorDelete = selectedSchema.supportsOnAuthorDelete; selectedSchema.onAuthorDelete = selectedSchema.supportsOnAuthorDelete;
selectedSchema.onBookDelete = selectedSchema.supportsOnBookDelete; selectedSchema.onBookDelete = selectedSchema.supportsOnBookDelete;
selectedSchema.onBookFileDelete = selectedSchema.supportsOnBookFileDelete; selectedSchema.onBookFileDelete = selectedSchema.supportsOnBookFileDelete;
@@ -4,6 +4,8 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
import monitorNewItemsOptions from 'Utilities/Author/monitorNewItemsOptions';
import monitorOptions from 'Utilities/Author/monitorOptions';
// //
// Variables // Variables
@@ -51,6 +53,10 @@ export default {
port: 8080, port: 8080,
useSsl: false, useSsl: false,
outputProfile: 'default', outputProfile: 'default',
defaultQualityProfileId: 0,
defaultMetadataProfileId: 0,
defaultMonitorOption: monitorOptions[0].key,
defaultNewItemMonitorOption: monitorNewItemsOptions[0].key,
defaultTags: [] defaultTags: []
}, },
isSaving: false, isSaving: false,
+20 -1
View File
@@ -4,6 +4,7 @@ import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import getSectionState from 'Utilities/State/getSectionState'; import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState'; import updateSectionState from 'Utilities/State/updateSectionState';
import { fetchTranslations as fetchAppTranslations } from 'Utilities/String/translate';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
function getDimensions(width, height) { function getDimensions(width, height) {
@@ -41,7 +42,12 @@ export const defaultState = {
isReconnecting: false, isReconnecting: false,
isDisconnected: false, isDisconnected: false,
isRestarting: 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_VERSION = 'app/setVersion';
export const SET_APP_VALUE = 'app/setAppValue'; export const SET_APP_VALUE = 'app/setAppValue';
export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible'; export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible';
export const FETCH_TRANSLATIONS = 'app/fetchTranslations';
export const PING_SERVER = 'app/pingServer'; 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 showMessage = createAction(SHOW_MESSAGE);
export const hideMessage = createAction(HIDE_MESSAGE); export const hideMessage = createAction(HIDE_MESSAGE);
export const pingServer = createThunk(PING_SERVER); export const pingServer = createThunk(PING_SERVER);
export const fetchTranslations = createThunk(FETCH_TRANSLATIONS);
// //
// Helpers // Helpers
@@ -127,6 +135,17 @@ function pingServerAfterTimeout(getState, dispatch) {
export const actionHandlers = handleThunks({ export const actionHandlers = handleThunks({
[PING_SERVER]: function(getState, payload, dispatch) { [PING_SERVER]: function(getState, payload, dispatch) {
pingServerAfterTimeout(getState, 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 = [ export const filters = [
{ {
key: 'all', key: 'all',
label: translate('All'), label: () => translate('All'),
filters: [] filters: []
}, },
{ {
key: 'monitored', key: 'monitored',
label: translate('Monitored'), label: () => translate('Monitored'),
filters: [ filters: [
{ {
key: 'monitored', key: 'monitored',
@@ -40,7 +40,7 @@ export const filters = [
}, },
{ {
key: 'unmonitored', key: 'unmonitored',
label: translate('Unmonitored'), label: () => translate('Unmonitored'),
filters: [ filters: [
{ {
key: 'monitored', key: 'monitored',
@@ -51,7 +51,7 @@ export const filters = [
}, },
{ {
key: 'missing', key: 'missing',
label: translate('Missing'), label: () => translate('Missing'),
filters: [ filters: [
{ {
key: 'monitored', key: 'monitored',
@@ -67,7 +67,7 @@ export const filters = [
}, },
{ {
key: 'wanted', key: 'wanted',
label: translate('Wanted'), label: () => translate('Wanted'),
filters: [ filters: [
{ {
key: 'monitored', key: 'monitored',
+20 -20
View File
@@ -60,32 +60,32 @@ export const defaultState = {
columns: [ columns: [
{ {
name: 'status', name: 'status',
columnLabel: translate('Status'), columnLabel: () => translate('Status'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'authorMetadata.sortName', name: 'authorMetadata.sortName',
label: translate('Author'), label: () => translate('Author'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'books.title', name: 'books.title',
label: translate('BookTitle'), label: () => translate('BookTitle'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'books.releaseDate', name: 'books.releaseDate',
label: translate('ReleaseDate'), label: () => translate('ReleaseDate'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'quality', name: 'quality',
label: translate('Quality'), label: () => translate('Quality'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
@@ -97,64 +97,64 @@ export const defaultState = {
}, },
{ {
name: 'customFormatScore', name: 'customFormatScore',
columnLabel: translate('CustomFormatScore'), columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.SCORE, name: icons.SCORE,
title: translate('CustomFormatScore') title: () => translate('CustomFormatScore')
}), }),
isVisible: false isVisible: false
}, },
{ {
name: 'protocol', name: 'protocol',
label: translate('Protocol'), label: () => translate('Protocol'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'indexer', name: 'indexer',
label: translate('Indexer'), label: () => translate('Indexer'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'downloadClient', name: 'downloadClient',
label: translate('DownloadClient'), label: () => translate('DownloadClient'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'title', name: 'title',
label: translate('ReleaseTitle'), label: () => translate('ReleaseTitle'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'size', name: 'size',
label: translate('Size'), label: () => translate('Size'),
isSortable: true, isSortable: true,
isVisible: false isVisible: false
}, },
{ {
name: 'outputPath', name: 'outputPath',
label: translate('OutputPath'), label: () => translate('OutputPath'),
isSortable: false, isSortable: false,
isVisible: false isVisible: false
}, },
{ {
name: 'estimatedCompletionTime', name: 'estimatedCompletionTime',
label: translate('TimeLeft'), label: () => translate('TimeLeft'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'progress', name: 'progress',
label: translate('Progress'), label: () => translate('Progress'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'actions', name: 'actions',
columnLabel: translate('Actions'), columnLabel: () => translate('Actions'),
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
} }
@@ -371,13 +371,13 @@ export const actionHandlers = handleThunks({
id, id,
remove, remove,
blocklist, blocklist,
skipredownload skipRedownload
} = payload; } = payload;
dispatch(updateItem({ section: paged, id, isRemoving: true })); dispatch(updateItem({ section: paged, id, isRemoving: true }));
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipredownload=${skipredownload}`, url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
method: 'DELETE' method: 'DELETE'
}).request; }).request;
@@ -395,7 +395,7 @@ export const actionHandlers = handleThunks({
ids, ids,
remove, remove,
blocklist, blocklist,
skipredownload skipRedownload
} = payload; } = payload;
dispatch(batchActions([ dispatch(batchActions([
@@ -411,7 +411,7 @@ export const actionHandlers = handleThunks({
])); ]));
const promise = createAjaxRequest({ const promise = createAjaxRequest({
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipredownload=${skipredownload}`, url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
method: 'DELETE', method: 'DELETE',
dataType: 'json', dataType: 'json',
data: JSON.stringify({ ids }) data: JSON.stringify({ ids })
+1 -1
View File
@@ -199,7 +199,7 @@ export const defaultState = {
}, },
{ {
name: 'customFormatScore', name: 'customFormatScore',
label: translate('CustomFormatScore'), label: () => translate('CustomFormatScore'),
type: filterBuilderTypes.NUMBER type: filterBuilderTypes.NUMBER
}, },
{ {
+5 -5
View File
@@ -82,34 +82,34 @@ export const defaultState = {
columns: [ columns: [
{ {
name: 'level', name: 'level',
columnLabel: translate('Level'), columnLabel: () => translate('Level'),
isSortable: false, isSortable: false,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'time', name: 'time',
label: translate('Time'), label: () => translate('Time'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'logger', name: 'logger',
label: translate('Component'), label: () => translate('Component'),
isSortable: false, isSortable: false,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'message', name: 'message',
label: translate('Message'), label: () => translate('Message'),
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
}, },
{ {
name: 'actions', name: 'actions',
columnLabel: translate('Actions'), columnLabel: () => translate('Actions'),
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
isModifiable: false isModifiable: false
@@ -36,10 +36,17 @@ function mergeColumns(path, initialState, persistedState, computedState) {
const column = initialColumns.find((i) => i.name === persistedColumn.name); const column = initialColumns.find((i) => i.name === persistedColumn.name);
if (column) { if (column) {
columns.push({ const newColumn = {};
...column,
isVisible: persistedColumn.isVisible // 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} isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteBackup')} title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', [name])} message={translate('DeleteBackupMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress} onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose} onCancel={this.onConfirmDeleteModalClose}
+3 -3
View File
@@ -21,17 +21,17 @@ const columns = [
}, },
{ {
name: 'name', name: 'name',
label: 'Name', label: () => translate('Name'),
isVisible: true isVisible: true
}, },
{ {
name: 'size', name: 'size',
label: 'Size', label: () => translate('Size'),
isVisible: true isVisible: true
}, },
{ {
name: 'time', name: 'time',
label: 'Time', label: () => translate('Time'),
isVisible: true isVisible: true
}, },
{ {
@@ -146,7 +146,7 @@ class RestoreBackupModalContent extends Component {
<ModalBody> <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 = [ const columns = [
{ {
name: 'filename', name: 'filename',
label: 'Filename', label: () => translate('Filename'),
isVisible: true isVisible: true
}, },
{ {
name: 'lastWriteTime', name: 'lastWriteTime',
label: 'Last Write Time', label: () => translate('LastWriteTime'),
isVisible: true isVisible: true
}, },
{ {

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