Compare commits

...

149 Commits

Author SHA1 Message Date
Bogdan d27b062d6a Bump version to 2.4.0 2024-05-13 17:01:53 +03:00
Bogdan febb3ef485 Bump Npgsql to 7.0.7 2024-05-13 15:19:22 +03:00
sillock1 30d9891bf0 New: Optionally use Environment Variables for settings in config.xml 2024-05-12 15:42:47 +03:00
Bogdan 2621acdae5 Minor translations to album interactive search 2024-05-12 01:01:41 +03:00
Bogdan 18772553f2 Fallback value for statistics 2024-05-10 19:42:50 +03:00
Weblate 0300bf2dd2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/uk/
Translation: Servarr/Lidarr
2024-05-10 14:04:49 +03:00
Bogdan 77861e4303 Refactor PasswordInput to use type password
(cherry picked from commit c7c1e3ac9e5bffd4d92298fed70916e3808613fd)
2024-05-10 14:04:24 +03:00
Bogdan 3545a7451e Fixed: Text color for inputs on login page 2024-05-10 01:32:27 +03:00
Mark McDowall 9f8c4530ca New: Dark theme for login screen
(cherry picked from commit cae134ec7b331d1c906343716472f3d043614b2c)

Closes #4798
2024-05-10 01:29:30 +03:00
Mickaël Thomas 9da690f807 New: Support stoppedUP and stoppedDL states from qBittorrent
(cherry picked from commit 73a4bdea5247ee87e6bbae95f5325e1f03c88a7f)

Closes #4795
2024-05-10 00:56:42 +03:00
Bogdan 31f342b8ad Use number input for seed ratio
(cherry picked from commit 1eddf3a152fae04142263c02a3e3b317ff2feeb2)

Plus translations

Closes #4802
2024-05-10 00:53:56 +03:00
Bogdan 596a36d45f Fixed: Notifications with only On Rename enabled 2024-05-10 00:50:52 +03:00
Bogdan 94bb8a436b Fixed: Parsing long downloading/seeding values from Transmission
(cherry picked from commit 8360dd7a7bab1dfb49a40aae382b47e9253d9fd1)
2024-05-09 05:31:58 +03:00
Weblate 94d2a20b6a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/uk/
Translation: Servarr/Lidarr
2024-05-05 12:33:43 +03:00
Bogdan a25e5aae10 Fixed: Indexer flags for torrent release pushes
(cherry picked from commit 47ba002806fe2c2004a649aa193ae318343a84e4)
2024-05-05 12:32:44 +03:00
Mark McDowall f4a02ffc83 Forward X-Forwarded-Host header
(cherry picked from commit 3fbe4361386e9fb8dafdf82ad9f00f02bec746cc)
2024-05-05 12:32:32 +03:00
Bogdan 1bdcf91014 Bump version to 2.3.3 2024-05-05 12:32:12 +03:00
Bogdan 4d28d3f25a Fixed: Initialize databases after app folder migrations
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-05-05 01:03:15 +03:00
Bogdan 9660ec37cd Use newer Node.js task for in pipelines 2024-04-29 14:41:31 +03:00
Bogdan 66c7521f4b Fixed: Limit titles in task name to 10 artists
(cherry picked from commit c81ae6546118e954e481894d0b3fa6e9a20359c7)

Closes #4777
2024-04-28 13:51:01 +03:00
Stevie Robinson 8b57b33c99 New: Don't initially select 0 byte files in Interactive Import
(cherry picked from commit 04bd535cfca5e25c6a2d5417c6f18d5bf5180f67)

Closes #4776
2024-04-28 13:49:04 +03:00
Mark McDowall 580e4becbe Fixed: Improve paths longer than 256 on Windows failing to hardlink
(cherry picked from commit a97fbcc40a6247bf59678425cf460588fd4dbecd)
2024-04-28 13:42:37 +03:00
Christopher 5f248aa25e New: Remove qBitorrent torrents that reach inactive seeding time
(cherry picked from commit d738035fed859eb475051f3df494b9c975a42e82)
2024-04-28 13:42:22 +03:00
Bogdan a735eccb65 Bump version to 2.3.2 2024-04-28 12:57:55 +03:00
Weblate d11ed42830 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Altair <villagermd@outlook.com>
Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Fara <faraindahhh@gmail.com>
Co-authored-by: Fonkio <maxime.fabre10@gmail.com>
Co-authored-by: Gandrushka <andrew.pyndyk@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Jacopo Luca Maria Latrofa <jacopo.latrofa@gmail.com>
Co-authored-by: Mailme Dashite <mailmedashite@protonmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: YSLG <1451164040@qq.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: maodun96 <435795439@qq.com>
Co-authored-by: toeiazarothis <patrickdealmeida89000@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/zh_CN/
Translation: Servarr/Lidarr
2024-04-27 21:14:04 +03:00
Bogdan b0038dd143 Fixed: Retrying download on not suppressed HTTP errors 2024-04-27 21:09:14 +03:00
Bogdan 2e242aeb7b Database corruption message linking to wiki 2024-04-25 11:30:05 +03:00
Bogdan 416d505316 Bump dotnet to 6.0.29 2024-04-22 07:58:44 +03:00
Bogdan 4816f35256 Bump typescript eslint plugin and parser 2024-04-21 12:45:16 +03:00
Josh McKinney e42e0a72eb Add dev container workspace
Allows the linting and style settings for the frontend to be applied even when you load the main repo as a workspace

(cherry picked from commit d6278fced49b26be975c3a6039b38a94f700864b)

Closes #4756
2024-04-21 12:05:07 +03:00
Bogdan db9e62f79d Convert store selectors to Typescript
Closes #3937
2024-04-21 12:04:13 +03:00
Bogdan bc69fa4842 Bump frontend dependencies 2024-04-21 11:11:55 +03:00
Bogdan 86dad72c49 Bump version to 2.3.1 2024-04-21 09:15:31 +03:00
Bogdan 4a8d6c367d Bump skipping spotify tests 2024-04-20 18:37:21 +03:00
Bogdan c1926f8758 Fixed: Skip move when source and destination are the same
Co-authored-by: Qstick <qstick@gmail.com>
2024-04-20 17:56:04 +03:00
Bogdan 7820bcf91f Bump SixLabors.ImageSharp to 3.1.4 2024-04-19 08:00:59 +03:00
Servarr 431ad0a028 Automated API Docs update 2024-04-18 15:33:13 +03:00
Bogdan 59cf7a95c3 Fixed: Re-testing edited providers will forcibly test them
(cherry picked from commit e9662544621b2d1fb133ff9d96d0eb20b8198725)
2024-04-16 10:54:12 +03:00
Bogdan e17e3633f8 Don't block task queue for queued update task for Rescan Folders
Towards #4551
2024-04-14 15:45:35 +03:00
Bogdan 46da2b49c6 Bump version to 2.3.0 2024-04-13 07:10:44 +03:00
Josh McKinney 3071977284 Add DevContainer, VSCode config and extensions.json
(cherry picked from commit 5061dc4b5e5ea9925740496a5939a1762788b793)

Closes #4740
2024-04-11 23:16:31 +03:00
Mark McDowall b14e2bb618 New: Auto tag artists based on tags present/absent on artists
(cherry picked from commit f4c19a384bd9bb4e35c9fa0ca5d9a448c04e409e)

Closes #4742
2024-04-11 23:02:04 +03:00
Mark McDowall 8c09c0cb5c New: Option to prefix app name on Telegram notification titles
(cherry picked from commit 37863a8deb339ef730b2dd5be61e1da1311fdd23)

Closes #4739
2024-04-11 22:43:54 +03:00
Weblate 8cebb21c2d 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: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: myrad2267 <myrad2267@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/zh_CN/
Translation: Servarr/Lidarr
2024-04-10 02:21:56 +03:00
Bogdan 74ac263b74 New: Detect shfs mounts in disk space
(cherry picked from commit 1aef91041e404f76f278f430e4e53140fb125792)
2024-04-10 02:21:35 +03:00
Bogdan adcec90ef8 Fixed: Sorting by Artist Name in Missing/Cutoff Unmet under Postgres
Fixes #4736
Fixes #3392
Closes #4374
2024-04-08 18:14:14 +03:00
Qstick daf8b94c8e Added table identifier to OrderBy to avoid column ambiguity on joins
Co-Authored-By: Richard <1252123+kharenis@users.noreply.github.com>
(cherry picked from commit c57ceac4debf7419be84096f997ba7b75c906586)

Closes #3993
2024-04-08 17:25:31 +03:00
Bogdan 7c4f0c597e Bump version to 2.2.5 2024-04-07 07:57:46 +03:00
Bogdan 1d2af2aab4 Fix translation for indexer priority help text 2024-04-07 02:48:33 +03:00
Stevie Robinson 5d537689fb New: Informational text on Custom Formats modal
(cherry picked from commit 238ba85f0a2639608d9890292dfe0b96c0212f10)

Closes #4729
2024-04-06 16:39:40 +03:00
Cuki ca6beea62b Fixed: Use widely supported display mode for PWA
(cherry picked from commit 1562d3bae3002947f9e428321d2b162ad69c3309)
2024-04-06 16:36:04 +03:00
Mark McDowall a82c919093 Fixed: Cleanse BHD RSS key in log files
(cherry picked from commit 60ee7cc716d344fc904fa6fb28f7be0386ae710d)
2024-04-06 16:35:39 +03:00
Mark McDowall 2941e0c4b7 Fixed: Sending ntfy.sh notifications with unicode characters
(cherry picked from commit a169ebff2adda5c8585c6aae6249b1c1f7c12264)
2024-04-06 16:35:21 +03:00
Bogdan ca0b900d92 Fixed: (Gazelle) Ignore ineligible releases with Use Freeleech Token 2024-04-06 06:33:35 +03:00
Weblate 72f1b2075b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Michael5564445 <michaelvelosk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/zh_CN/
Translation: Servarr/Lidarr
2024-04-06 06:21:20 +03:00
Bogdan e847828191 Fixed: Album release selection in manual import 2024-03-31 21:08:44 +03:00
Bogdan 2a10505dff Bump skipping spotify tests 2024-03-28 13:02:04 +02:00
Mark McDowall 28f2eb974d Fixed: Task with removed artists causing error
(cherry picked from commit fc6494c569324c839debdb1d08dde23b8f1b8d76)

Closes #4696
2024-03-28 13:02:01 +02:00
Servarr 13ce040e4d Automated API Docs update 2024-03-28 13:01:34 +02:00
Weblate f477f9b287 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Altair <villagermd@outlook.com>
Co-authored-by: Casselluu <jack10193@163.com>
Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Stanislav <prekop3@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: shimmyx <shimmygodx@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/zh_CN/
Translation: Servarr/Lidarr
2024-03-28 10:32:27 +02:00
Bogdan 0e84008669 New: Advanced settings toggle in import list, notification and download client modals
(cherry picked from commit 13c925b3418d1d48ec041e3d97ab51aaf2b8977a)
2024-03-28 10:31:57 +02:00
Louis R 52b5ff6fdd Fixed: Exceptions when checking for routable IPv4 addresses
(cherry picked from commit 060b789bc6f10f667795697eb536d4bd3851da49)
2024-03-28 10:31:32 +02:00
Carlos Gustavo Sarmiento 1d0de51917 Fixed: qBittorrent not correctly handling retention during testing
(cherry picked from commit 588372fd950fc85f5e9a4275fbcb423b247ed0ee)
2024-03-28 10:31:19 +02:00
Stevie Robinson a8648fdb71 Fixed: Handling torrents with relative path in rTorrent
(cherry picked from commit 35d0e6a6f806c68756450a7d199600d7fb49d6c5)
2024-03-28 10:31:06 +02:00
Bogdan f890a8c18f New: Allow HEAD requests to ping endpoint
(cherry picked from commit 7353fe479dbb8d0dab76993ebed92d48e1b05524)
2024-03-28 10:30:54 +02:00
Mark McDowall e730cf6307 Fixed: Task progress messages in the UI
(cherry picked from commit c6417337812f3578a27f9dc1e44fdad80f557271)

Closes #4689
2024-03-22 11:23:16 +02:00
Bogdan 9f4d821a2d Bump version to 2.2.4 2024-03-17 13:48:47 +02:00
Weblate ce6e4555ec Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dennis Langthjem <dennis@langthjem.dk>
Co-authored-by: Gianmarco Novelli <rinogaetano94@live.it>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ihor Mudryi <mudryy33@gmail.com>
Co-authored-by: MadaxDeLuXe <madaxdeluxe@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: infoaitek24 <info@aitekph.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: vfaergestad <vgf@hotmail.no>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/uk/
Translation: Servarr/Lidarr
2024-03-16 18:15:32 +02:00
Mark McDowall 55eaecb3c8 Fixed: Disabled select option still selectable
(cherry picked from commit 063dba22a803295adee4fdcbe42718af3e85ca78)

Closes #4679
2024-03-14 15:46:28 +02:00
Bogdan 63e36f71d2 Ensure not allowed cursor is shown for disabled select inputs 2024-03-14 15:46:28 +02:00
Bogdan 89e184e768 Ensure artists are populated in PageConnector 2024-03-14 15:46:28 +02:00
Mark McDowall 873a225f0c New: Show artist names after task name when applicable
(cherry picked from commit 6d552f2a60f44052079b5e8944f5e1bbabac56e0)

Closes #4678
2024-03-14 15:46:19 +02:00
Stevie Robinson b81170d911 Fixed: Wrapping of naming tokens with alternate separators
(cherry picked from commit 80630bf97f5bb3b49d4824dc039d2edfc74e4797)

Closes #4561
Closes #4677
2024-03-14 15:16:13 +02:00
Servarr 5ffde40320 Automated API Docs update 2024-03-14 07:58:46 +02:00
Mark McDowall ebfa68087d Fixed: Release push with only Magnet URL
(cherry picked from commit 9f705e4161af3f4dd55b399d56b0b9c5a36e181b)
2024-03-14 07:12:50 +02:00
Bogdan 1db0eb1029 New: Indexer flags
(cherry picked from commit 7a768b5d0faf9aa57e78aee19cefee8fb19a42d5)
2024-03-10 17:30:09 +02:00
Bogdan 967b58017a Bump ImageSharp, Polly 2024-03-10 13:11:15 +02:00
Bogdan 3754d611c7 Bump version to 2.2.3 2024-03-10 09:05:47 +02:00
Weblate 8035d4202f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/
Translation: Servarr/Lidarr
2024-03-09 11:10:55 +02:00
Bogdan 468f3acf85 Translations for InteractiveSearchRow
Closes #4077
2024-03-09 11:04:45 +02:00
Bogdan 29c77ec3a1 Fix version in namespace for AutoTagging 2024-03-09 10:57:24 +02:00
bpoxy d04bb5333a Fixed: Matching of custom formats during track files import
(cherry picked from commit 7fedfe7423a525f05008c2c7c15e3cb0c9c38fe5)

Closes #3484
2024-03-08 15:51:48 +02:00
Bogdan 0d76fbcf0d New: XXL modal size 2024-03-08 11:42:53 +02:00
Weblate 3df140b1f0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Jason54 <jason54700.jg@gmail.com>
Co-authored-by: Mark Martines <mark-martines@hotmail.com>
Co-authored-by: Maxence Winandy <maxence.winandy@gmail.com>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: linkin931 <931linkin@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/tr/
Translation: Servarr/Lidarr
2024-03-08 11:08:43 +02:00
Bogdan 340ae78f46 Prevent NullRef in naming when truncating a null Release Group
(cherry picked from commit 13e29bd257ccfccb09e66c940ffabeb6503c05b5)
2024-03-08 08:29:21 +02:00
Helvio Pedreschi 881fabad93 Fixed: WebApp functionality on Apple devices
(cherry picked from commit c7dd7abf892eead7796fcc482aa2f2aabaf88712)
2024-03-08 08:27:45 +02:00
Mark McDowall be8f7e5618 Fixed: Overly aggressive exception release group parsing
(cherry picked from commit 0183812cc58dad0e555125ddd8b33a85cbdecbf2)
2024-03-08 08:27:15 +02:00
Bogdan c9743448fd Configurable URL Base setting for Kodi connections 2024-03-04 03:10:45 +02:00
Bogdan 47e647ddb1 Fixed: URL Base setting for Kodi connections 2024-03-03 13:47:49 +02:00
Mark McDowall f6529d5ad3 New: URL Base setting for Media Server connections
Plus some translations

(cherry picked from commit 9fd193d2a82d5c2cdc0f36c1f984e4b6b68aaa8d)
2024-03-03 13:36:39 +02:00
Mark McDowall fb1b7274d0 Queue Manual Import commands at high priority
(cherry picked from commit 64c6a8879beb1b17122c8f6f74bf7b3cf4dd1570)
2024-03-03 13:20:24 +02:00
Louis R 33b12a532c Fixed: Don't disable IPv6 in IPv6-only Environment
(cherry picked from commit 13af6f57796e54c3949cf340e03f020e6f8575c4)
2024-03-03 13:19:46 +02:00
nopoz cea5ee503f New: Add download directory & move completed for Deluge
(cherry picked from commit 07bd159436935a7adb87ae1b6924a4d42d719b0f)
2024-03-03 13:19:29 +02:00
Bogdan 475590a21b Bump version to 2.2.2 2024-03-03 13:11:33 +02:00
Weblate 0ca0f68af1 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Nicolò Castagnola <nipica@outlook.it>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/tr/
Translation: Servarr/Lidarr
2024-03-03 02:32:43 +02:00
Bogdan 2c19b5aa61 Ignore spotify mapping test more temporarily 2024-03-02 09:19:32 +02:00
Weblate 7e0c5e0da5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Alexander <Funk2256@gmail.com>
Co-authored-by: EDUYO <eduardoestabiel@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Sadi A. Nogueira <contato@sadi.eti.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: wgwqd <wgwqd@163.com>
Co-authored-by: 闫锦彪 <yanjinbiaohere@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/zh_CN/
Translation: Servarr/Lidarr
2024-03-02 03:59:19 +02:00
Mark McDowall adecb7f73c Increase migration timeout to 5 minutes
(cherry picked from commit 086d3b5afaa7680d22835ca66da2afcb6dd5865e)
2024-03-02 03:58:14 +02:00
Mark McDowall 98a90e2f8f New: Bypass archived history for failed downloads in SABnzbd
(cherry picked from commit c99d81e79ba5e6ecec01ddd942440d8a48a1c23b)
2024-03-02 03:57:42 +02:00
Bogdan ce2bb5be1f Update caniuse-lite
(cherry picked from commit 64f4365fe98b569efdf436710d5f56684f2aab66)
2024-03-02 03:56:51 +02:00
Bogdan e446c25a01 New: Options button for Missing/Cutoff Unmet
(cherry picked from commit 2773f77e1c4e3a8c8d01bcbea67333801c7840df)
2024-03-02 03:55:43 +02:00
servarr[bot] d38c101acd
Fixed: Multi-word genres in Auto Tags (#4601)
(cherry picked from commit 5c4f82999368edfedd038a0a27d323e04b81a400)

Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-03-02 03:33:37 +02:00
Bogdan 022fbf864c Fixed: Selection of last added custom filter
(cherry picked from commit 1f97679868012b70beecc553557e96e6c8bc80e3)

Closes #4627
2024-02-29 06:50:59 +02:00
Bogdan 3ff9b8bd85 Bump version to 2.2.1 2024-02-29 06:43:28 +02:00
Bogdan 57926a61d2 Bump node to v20.x on builder 2024-02-23 20:18:49 +02:00
bakerboy448 87f88af7ee
Update name for errors with metadata API 2024-02-22 01:01:40 +02:00
Weblate 30fc3fc70a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Chaoshuai Lü <lcs@meta.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/zh_CN/
Translation: Servarr/Lidarr
2024-02-16 16:55:10 +02:00
Bogdan 4abca0c896 Fixed: Don't die on album deleted notifications with the artist already removed 2024-02-16 16:52:23 +02:00
Bogdan b2f595436b Improve messaging on indexer specified download client is not available
(cherry picked from commit 84e657482d37eed35f09c6dab3c2b8b5ebd5bac4)
2024-02-14 03:10:26 +02:00
Bogdan e7ae0b9e22 Fixed: Refresh tags state to clear removed tags by housekeeping
(cherry picked from commit 2510f44c25bee6fede27d9fa2b9614176d12cb55)

(cherry picked from commit ed27bcf213bdbc5cede650f89eb65593dc9631b4)
2024-02-14 03:10:01 +02:00
Bogdan 0431b257e1 Show download client ID as hint in select options
(cherry picked from commit c0b17d9345367ab6500b7cca6bb70c1e3b930284)
2024-02-14 03:09:47 +02:00
abcasada 479e8cce20 Hints for week column and short dates in UI settings
(cherry picked from commit 4558f552820b52bb1f9cd97fdabe03654ce9924a)

(cherry picked from commit f1d343218cdbd5a63abeb2eb97bba1105dc8035d)
2024-02-14 03:09:33 +02:00
Bogdan 27723eb3ea Revert "New: Preserve replaygain tags"
This reverts commit 2a8c67badc.
2024-02-12 22:11:25 +02:00
Qstick 616b529c9a Fix CalendarPageConnector import sort 2024-02-11 21:31:38 -06:00
Qstick 8b85d4c941 Translate Frontend Utilities
Closes #4096

Co-Authored-By: Stevie Robinson <stevie.robinson@gmail.com>
2024-02-11 21:30:31 -06:00
Servarr f13b095040 Automated API Docs update 2024-02-11 21:05:15 -06:00
Qstick a4af75b60c New: Calendar filtering by tags
Closes #3658
Closes #4211

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2024-02-11 20:51:28 -06:00
Qstick c7faf7cc25
Bump version to 2.2.0 2024-02-11 19:29:42 -06:00
Weblate 7f0fab0cf6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Lucas <sixagag973@fkcod.com>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/hu/
Translation: Servarr/Lidarr
2024-02-12 00:58:17 +02:00
Bogdan d68f207e9b Ignore spotify mapping test temporarily 2024-02-11 06:20:59 +02:00
Bogdan f1efd05207 Fixed: Spotify Playlist selection 2024-02-11 06:18:36 +02:00
Weblate 59efffd40f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hicabi Erdem <bilgi@hicabierdem.com>
Co-authored-by: Magyar <kochnorbert@icloud.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aghus <aghus.m@outlook.com>
Co-authored-by: bai0012 <baicongrui@gmail.com>
Co-authored-by: bogdan-rgb <b.hmelniczky@yandex.ru>
Co-authored-by: savin-msk <ns@a77.io>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/zh_CN/
Translation: Servarr/Lidarr
2024-02-11 05:01:03 +02:00
Mark McDowall 6c90ac74e9 Fixed: Don't use sub folder to check for free disk space for update
(cherry picked from commit f722d49b3a9efefa65bef1b24d90be9332ca62ea)

Closes #4566
2024-02-07 08:57:49 +02:00
Mark McDowall f5eee52194 New: Log database engine version on startup
(cherry picked from commit 6ab1d8e16b29e98b4d2ebb68e0356f6f2d3a2c10)
2024-02-07 08:56:49 +02:00
Mark McDowall 0871949b74 Fixed: Redirecting after login
(cherry picked from commit 745b92daf4bf4b9562ffe52dad84a12a5561add5)
2024-02-07 08:56:27 +02:00
Bogdan 1536e90053 New: Artist info in Album Delete event for Webhooks
Fixes #4552
2024-02-07 00:39:11 +02:00
Bogdan c744231141 Translations for settings index 2024-02-07 00:23:44 +02:00
Mark McDowall efe0a3d283 Typings cleanup and improvements
Appease linter

(cherry picked from commit b2c43fb2a67965d68d3d35b72302b0cddb5aca23)
(cherry picked from commit 3b5e83670b844cf7c20bf7d744d9fbc96fde6902)

Closes #3516
Closes #3510
Closes #2778
2024-02-06 22:05:23 +02:00
Qstick 8e5942d5c5 Parse Exception Release Groups
Closes #4541
Closes #4327
Closes #4250
Closes #3221
Closes #2658

Co-Authored-By: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2024-02-05 21:18:50 -06:00
Weblate 6471353bcd Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translation: Servarr/Lidarr
2024-02-05 19:59:35 -06:00
Qstick c3c50498bd Debian Install Script
Closes #4382
Closes #4383
Closes #4401
Closes #4407
Closes #4423
Closes #4474

Co-Authored-By: Stevie Robinson <stevie.robinson@gmail.com>
2024-02-05 19:54:47 -06:00
Bogdan 6ae99acea7 Fix tests for storing last search time for albums 2024-02-05 22:36:53 +02:00
Bogdan d8066ec172 New: Size column for albums
Closes #3292
2024-02-05 22:26:54 +02:00
Mark McDowall a9b16d298f Improve CF calculation for files without scene name
Fixed: Use original filename instead of complete path when calculating CF for existing file without scene name

(cherry picked from commit 997aabbc3cfc3c9c5c220786d1d08cfceec5e2f2)

Closes #3359
2024-02-05 21:31:32 +02:00
Mark McDowall 0bdd5f3278 Fixed: A potential issue when extra files for multiple artists have the same relative path
(cherry picked from commit a6a68b4cae7688506c45ff6cf10989fe6596c274)

Closes #2760
2024-02-05 21:24:24 +02:00
Mark McDowall 2f0d02b3bc Allow Discography to be grabbed automatically if all albums will be released within 24 hours
(cherry picked from commit 87795708b5caf63e63570cb62c6043a781ee89ae)

Closes #3181
2024-02-05 20:53:46 +02:00
Bogdan abefdca0fc Fix Missing/CutoffUnmet search all warnings 2024-02-05 20:43:13 +02:00
Bogdan 2fc966af0c New: Missing/Cutoff Unmet searches will search for albums that haven't been searched recently first
Closes #3250

Simplify filter expression for cutoff unmet album search
2024-02-05 20:43:11 +02:00
Mark McDowall 5d8f9c9e27 New: Store last search time for AlbumSearch
(cherry picked from commit 9af57c6786)
2024-02-05 19:11:14 +02:00
Mark McDowall 0bcbf9df81 Fixed: Don't convert artist/album selection filter to lower case in state
(cherry picked from commit ca52eb76ca2e286479f1803f399d5f5b563cfb41)

Closes #1857
2024-02-05 14:20:28 +02:00
Bogdan 49883d0e30 Add some translations for artist table index 2024-02-05 14:15:10 +02:00
Servarr 09e9162aa6 Automated API Docs update 2024-02-04 14:43:01 -06:00
Qstick d38c44d25e
New: Option to disable cover art embed in files (#4547)
* New: Option to disable cover art embed in files

Fixes #2488

* Update src/NzbDrone.Core/MediaFiles/AudioTagService.cs

Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-02-04 14:26:15 -06:00
Qstick 3702fa773c New: Filter by Monitor New Items
Closes #3707
2024-02-04 14:01:30 -06:00
Qstick aecf5bba49 Fixed: Correctly map artist logos to clearlogo type
Closes #2627
2024-02-04 13:09:11 -06:00
Qstick 6e43d8a4fe New: Ability to import aiff files
Closes #4102
2024-02-04 12:50:55 -06:00
Qstick 2a8c67badc New: Preserve replaygain tags 2024-02-04 12:01:06 -06:00
Stevie Robinson 0121095b3e New: Add additional CleanNameThe/CleanTitleThe naming tokens
(cherry picked from commit 81aaf00a4cd2b3a2f8ddf67226c13bb51ea39dda)

Add some translations and fix the validation for track naming

Closes #4197
2024-02-04 18:26:02 +02:00
Bogdan 2f80957f11 Bump version to 2.1.7 2024-02-04 12:54:53 +02:00
387 changed files with 8981 additions and 3971 deletions

View File

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

View File

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

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

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

1
.gitignore vendored
View File

@ -126,6 +126,7 @@ coverage*.xml
coverage*.json
setup/Output/
*.~is
.mono
# VS outout folders
bin

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

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

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

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

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

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

View File

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

View File

@ -0,0 +1,182 @@
#!/bin/bash
### Description: Lidarr .NET Debian install
### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0
### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN
### Version v1.0.0 2023-12-29 - StevieTV - adapted from servarr script for Lidarr installs
### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
### Boilerplate Warning
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
scriptversion="1.0.3"
scriptdate="2024-01-06"
set -euo pipefail
echo "Running Lidarr Install Script - Version [$scriptversion] as of [$scriptdate]"
# Am I root?, need root!
if [ "$EUID" -ne 0 ]; then
echo "Please run as root."
exit
fi
app="lidarr"
app_port="8686"
app_prereq="curl sqlite3 wget"
app_umask="0002"
branch="main"
# Constants
### Update these variables as required for your specific instance
installdir="/opt" # {Update me if needed} Install Location
bindir="${installdir}/${app^}" # Full Path to Install Location
datadir="/var/lib/$app/" # {Update me if needed} AppData directory to use
app_bin=${app^} # Binary Name of the app
# This script should not be ran from installdir, otherwise later in the script the extracted files will be removed before they can be moved to installdir.
if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ]; then
echo "You should not run this script from the intended install directory. The script will exit. Please re-run it from another directory"
exit
fi
# Prompt User
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
app_uid=$(echo "$app_uid" | tr -d ' ')
app_uid=${app_uid:-$app}
# Prompt Group
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
app_guid=$(echo "$app_guid" | tr -d ' ')
app_guid=${app_guid:-media}
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
# Create User / Group as needed
if [ "$app_guid" != "$app_uid" ]; then
if ! getent group "$app_guid" >/dev/null; then
groupadd "$app_guid"
fi
fi
if ! getent passwd "$app_uid" >/dev/null; then
adduser --system --no-create-home --ingroup "$app_guid" "$app_uid"
echo "Created and added User [$app_uid] to Group [$app_guid]"
fi
if ! getent group "$app_guid" | grep -qw "$app_uid"; then
echo "User [$app_uid] did not exist in Group [$app_guid]"
usermod -a -G "$app_guid" "$app_uid"
echo "Added User [$app_uid] to Group [$app_guid]"
fi
# Stop the App if running
if service --status-all | grep -Fq "$app"; then
systemctl stop "$app"
systemctl disable "$app".service
echo "Stopped existing $app"
fi
# Create Appdata Directory
# AppData
mkdir -p "$datadir"
chown -R "$app_uid":"$app_guid" "$datadir"
chmod 775 "$datadir"
echo "Directories created"
# Download and install the App
# prerequisite packages
echo ""
echo "Installing pre-requisite Packages"
# shellcheck disable=SC2086
apt update && apt install -y $app_prereq
echo ""
ARCH=$(dpkg --print-architecture)
# get arch
dlbase="https://lidarr.servarr.com/v1/update/$branch/updatefile?os=linux&runtime=netcore"
case "$ARCH" in
"amd64") DLURL="${dlbase}&arch=x64" ;;
"armhf") DLURL="${dlbase}&arch=arm" ;;
"arm64") DLURL="${dlbase}&arch=arm64" ;;
*)
echo "Arch not supported"
exit 1
;;
esac
echo ""
echo "Removing previous tarballs"
# -f to Force so we fail if it doesnt exist
rm -f "${app^}".*.tar.gz
echo ""
echo "Downloading..."
wget --content-disposition "$DLURL"
tar -xvzf "${app^}".*.tar.gz
echo ""
echo "Installation files downloaded and extracted"
# remove existing installs
echo "Removing existing installation"
rm -rf "$bindir"
echo "Installing..."
mv "${app^}" $installdir
chown "$app_uid":"$app_guid" -R "$bindir"
chmod 775 "$bindir"
rm -rf "${app^}.*.tar.gz"
# Ensure we check for an update in case user installs older version or different branch
touch "$datadir"/update_required
chown "$app_uid":"$app_guid" "$datadir"/update_required
echo "App Installed"
# Configure Autostart
# Remove any previous app .service
echo "Removing old service file"
rm -rf /etc/systemd/system/"$app".service
# Create app .service with correct user startup
echo "Creating service file"
cat <<EOF | tee /etc/systemd/system/"$app".service >/dev/null
[Unit]
Description=${app^} Daemon
After=syslog.target network.target
[Service]
User=$app_uid
Group=$app_guid
UMask=$app_umask
Type=simple
ExecStart=$bindir/$app_bin -nobrowser -data=$datadir
TimeoutStopSec=20
KillMode=process
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
# Start the App
echo "Service file created. Attempting to start the app"
systemctl -q daemon-reload
systemctl enable --now -q "$app"
# Finish Update/Installation
host=$(hostname -I)
ip_local=$(grep -oP '^\S*' <<<"$host")
echo ""
echo "Install complete"
sleep 10
STATUS="$(systemctl is-active "$app")"
if [ "${STATUS}" = "active" ]; then
echo "Browse to http://$ip_local:$app_port for the ${app^} GUI"
else
echo "${app^} failed to start"
fi
# Exit
exit 0

View File

@ -0,0 +1,20 @@
# This file is owned by the lidarr package, DO NOT MODIFY MANUALLY
# Instead use 'dpkg-reconfigure -plow lidarr' to modify User/Group/UMask/-data
# Or use systemd built-in override functionality using 'systemctl edit lidarr'
[Unit]
Description=Lidarr Daemon
After=network.target
[Service]
User=lidarr
Group=lidarr
UMask=002
Type=simple
ExecStart=/opt/Lidarr/Lidarr -nobrowser -data=/var/lib/lidarr
TimeoutStopSec=20
KillMode=process
Restart=on-failure
[Install]
WantedBy=multi-user.target

View File

@ -28,7 +28,8 @@ module.exports = {
globals: {
expect: false,
chai: false,
sinon: false
sinon: false,
JSX: true
},
parserOptions: {

View File

@ -53,7 +53,7 @@ class DeleteAlbumModalContent extends Component {
render() {
const {
title,
statistics,
statistics = {},
onModalClose
} = this.props;

View File

@ -175,7 +175,7 @@ class AlbumDetailsMedium extends Component {
</Table> :
<div className={styles.noTracks}>
No tracks in this medium
{translate('NoTracksInThisMedium')}
</div>
}
<div className={styles.collapseButtonContainer}>

View File

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

View File

@ -4,6 +4,7 @@ interface CssExports {
'audio': string;
'customFormatScore': string;
'duration': string;
'indexerFlags': string;
'monitored': string;
'size': string;
'status': string;

View File

@ -2,15 +2,19 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AlbumFormats from 'Album/AlbumFormats';
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
import IndexerFlags from 'Album/IndexerFlags';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { tooltipPositions } from 'Helpers/Props';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import TrackActionsCell from './TrackActionsCell';
import styles from './TrackRow.css';
@ -32,6 +36,7 @@ class TrackRow extends Component {
trackFileSize,
customFormats,
customFormatScore,
indexerFlags,
columns,
deleteTrackFile
} = this.props;
@ -141,12 +146,30 @@ class TrackRow extends Component {
customFormats.length
)}
tooltip={<AlbumFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
position={tooltipPositions.LEFT}
/>
</TableRowCell>
);
}
if (name === 'indexerFlags') {
return (
<TableRowCell
key={name}
className={styles.indexerFlags}
>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
);
}
if (name === 'size') {
return (
<TableRowCell
@ -208,12 +231,14 @@ TrackRow.propTypes = {
trackFileSize: PropTypes.number,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
indexerFlags: PropTypes.number.isRequired,
mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired
};
TrackRow.defaultProps = {
customFormats: []
customFormats: [],
indexerFlags: 0
};
export default TrackRow;

View File

@ -13,7 +13,8 @@ function createMapStateToProps() {
trackFilePath: trackFile ? trackFile.path : null,
trackFileSize: trackFile ? trackFile.size : null,
customFormats: trackFile ? trackFile.customFormats : [],
customFormatScore: trackFile ? trackFile.customFormatScore : 0
customFormatScore: trackFile ? trackFile.customFormatScore : 0,
indexerFlags: trackFile ? trackFile.indexerFlags : 0
};
}
);

View File

@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component {
title,
artistName,
albumType,
statistics,
statistics = {},
item,
isSaving,
onInputChange,

View File

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

View File

@ -15,7 +15,7 @@ function AlbumInteractiveSearchModal(props) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
size={sizes.EXTRA_EXTRA_LARGE}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>

View File

@ -7,6 +7,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { scrollDirections } from 'Helpers/Props';
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
import translate from 'Utilities/String/translate';
function AlbumInteractiveSearchModalContent(props) {
const {
@ -18,7 +19,10 @@ function AlbumInteractiveSearchModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Interactive Search {albumId != null && `- ${albumTitle}`}
{albumTitle === undefined ?
translate('InteractiveSearchModalHeader') :
translate('InteractiveSearchModalHeaderTitle', { title: albumTitle })
}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
@ -32,7 +36,7 @@ function AlbumInteractiveSearchModalContent(props) {
<ModalFooter>
<Button onPress={onModalClose}>
Close
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>

View File

@ -12,11 +12,10 @@ function App({ store, history }) {
<DocumentTitle title={window.Lidarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
<ApplyTheme />
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ConnectedRouter>
</Provider>
</DocumentTitle>

View File

@ -1,50 +0,0 @@
import PropTypes from 'prop-types';
import React, { Fragment, useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.theme || window.Lidarr.theme,
(
theme
) => {
return {
theme
};
}
);
}
function ApplyTheme({ theme, children }) {
// Update the CSS Variables
const updateCSSVariables = useCallback(() => {
const arrayOfVariableKeys = Object.keys(themes[theme]);
const arrayOfVariableValues = Object.values(themes[theme]);
// Loop through each array key and set the CSS Variables
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
// Based on our snippet from MDN
document.documentElement.style.setProperty(
`--${cssVariableKey}`,
arrayOfVariableValues[index]
);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables(theme);
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
ApplyTheme.propTypes = {
theme: PropTypes.string.isRequired,
children: PropTypes.object.isRequired
};
export default connect(createMapStateToProps)(ApplyTheme);

View File

@ -0,0 +1,37 @@
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
import AppState from './State/AppState';
interface ApplyThemeProps {
children: ReactNode;
}
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Lidarr.theme,
(theme) => {
return theme;
}
);
}
function ApplyTheme({ children }: ApplyThemeProps) {
const theme = useSelector(createThemeSelector());
const updateCSSVariables = useCallback(() => {
Object.entries(themes[theme]).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables();
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
export default ApplyTheme;

View File

@ -1,8 +1,11 @@
import AlbumAppState from './AlbumAppState';
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import QueueAppState from './QueueAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState';
import TrackFilesAppState from './TrackFilesAppState';
import TracksAppState from './TracksAppState';
@ -52,12 +55,15 @@ interface AppState {
app: AppSectionState;
artist: ArtistAppState;
artistIndex: ArtistIndexAppState;
calendar: CalendarAppState;
commands: CommandAppState;
history: HistoryAppState;
queue: QueueAppState;
settings: SettingsAppState;
tags: TagsAppState;
trackFiles: TrackFilesAppState;
tracksSelection: TracksAppState;
system: SystemAppState;
}
export default AppState;

View File

@ -0,0 +1,10 @@
import Album from 'Album/Album';
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
interface CalendarAppState
extends AppSectionState<Album>,
AppSectionFilterState<Album> {}
export default CalendarAppState;

View File

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

View File

@ -1,11 +1,13 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemState,
AppSectionSaveState,
AppSectionSchemaState,
} from 'App/State/AppSectionState';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import MetadataProfile from 'typings/MetadataProfile';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
@ -44,17 +46,19 @@ export interface RootFolderAppState
AppSectionDeleteState,
AppSectionSaveState {}
export type UiSettingsAppState = AppSectionState<UiSettings>;
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
downloadClients: DownloadClientAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;
metadataProfiles: MetadataProfilesAppState;
notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState;
rootFolders: RootFolderAppState;
uiSettings: UiSettingsAppState;
ui: UiSettingsAppState;
}
export default SettingsAppState;

View File

@ -0,0 +1,10 @@
import SystemStatus from 'typings/SystemStatus';
import { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
interface SystemAppState {
status: SystemStatusAppState;
}
export default SystemAppState;

View File

@ -1,12 +1,32 @@
import ModelBase from 'App/ModelBase';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
export interface Tag extends ModelBase {
label: string;
}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
export interface TagDetail extends ModelBase {
label: string;
autoTagIds: number[];
delayProfileIds: number[];
downloadClientIds: [];
importListIds: number[];
indexerIds: number[];
notificationIds: number[];
restrictionIds: number[];
artistIds: number[];
}
export interface TagDetailAppState
extends AppSectionState<TagDetail>,
AppSectionDeleteState,
AppSectionSaveState {}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
details: TagDetailAppState;
}
export default TagsAppState;

View File

@ -36,6 +36,7 @@ interface Artist extends ModelBase {
nextAlbum?: Album;
qualityProfileId: number;
metadataProfileId: number;
monitorNewItems: string;
ratings: Ratings;
rootFolderPath: string;
sortName: string;

View File

@ -135,14 +135,14 @@ class DeleteArtistModalContent extends Component {
<ModalFooter>
<Button onPress={onModalClose}>
Close
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteArtistConfirmed}
>
Delete
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
@ -161,9 +161,7 @@ DeleteArtistModalContent.propTypes = {
};
DeleteArtistModalContent.defaultProps = {
statistics: {
trackFileCount: 0
}
statistics: {}
};
export default DeleteArtistModalContent;

View File

@ -10,6 +10,7 @@
width: 42px;
}
.size,
.status {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'monitored': string;
'size': string;
'status': string;
'title': string;
}

View File

@ -10,6 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { kinds, sizes } from 'Helpers/Props';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './AlbumRow.css';
@ -87,7 +88,8 @@ class AlbumRow extends Component {
const {
trackCount = 0,
trackFileCount = 0,
totalTrackCount = 0
totalTrackCount = 0,
sizeOnDisk = 0
} = statistics;
return (
@ -196,6 +198,17 @@ class AlbumRow extends Component {
);
}
if (name === 'size') {
return (
<TableRowCell
key={name}
className={styles.size}
>
{!!sizeOnDisk && formatBytes(sizeOnDisk)}
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell

View File

@ -192,7 +192,7 @@ class ArtistDetails extends Component {
artistName,
ratings,
path,
statistics,
statistics = {},
qualityProfileId,
monitored,
genres,

View File

@ -196,7 +196,7 @@ class ArtistDetailsSeason extends Component {
trackFileCount,
monitoredAlbumCount,
hasMonitoredAlbums,
sizeOnDisk
sizeOnDisk = 0
} = getAlbumStatistics(items);
const {

View File

@ -7,6 +7,8 @@ import React, {
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
import ArtistAppState, { ArtistIndexAppState } from 'App/State/ArtistAppState';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import NoArtist from 'Artist/NoArtist';
import { RSS_SYNC } from 'Commands/commandNames';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -89,16 +91,19 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
sortKey,
sortDirection,
view,
} = useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
}: ArtistAppState & ArtistIndexAppState & ClientSideCollectionAppState =
useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
const isRssSyncExecuting = useSelector(
createCommandExecutingSelector(RSS_SYNC)
);
const { isSmallScreen } = useSelector(createDimensionsSelector());
const dispatch = useDispatch();
const scrollerRef = useRef<HTMLDivElement>();
const scrollerRef = useRef<HTMLDivElement>(null);
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
undefined
);
const [isSelectMode, setIsSelectMode] = useState(false);
useEffect(() => {
@ -118,14 +123,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
}, [isSelectMode, setIsSelectMode]);
const onTableOptionChange = useCallback(
(payload) => {
(payload: unknown) => {
dispatch(setArtistTableOption(payload));
},
[dispatch]
);
const onViewSelect = useCallback(
(value) => {
(value: string) => {
dispatch(setArtistView({ view: value }));
if (scrollerRef.current) {
@ -136,14 +141,14 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
);
const onSortSelect = useCallback(
(value) => {
(value: string) => {
dispatch(setArtistSort({ sortKey: value }));
},
[dispatch]
);
const onFilterSelect = useCallback(
(value) => {
(value: string) => {
dispatch(setArtistFilter({ selectedFilterKey: value }));
},
[dispatch]
@ -158,15 +163,15 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
}, [setIsOptionsModalOpen]);
const onJumpBarItemPress = useCallback(
(character) => {
(character: string) => {
setJumpToCharacter(character);
},
[setJumpToCharacter]
);
const onScroll = useCallback(
({ scrollTop }) => {
setJumpToCharacter(null);
({ scrollTop }: { scrollTop: number }) => {
setJumpToCharacter(undefined);
scrollPositions.artistIndex = scrollTop;
},
[setJumpToCharacter]
@ -180,10 +185,10 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
};
}
const characters = items.reduce((acc, item) => {
const characters = items.reduce((acc: Record<string, number>, item) => {
let char = item.sortName.charAt(0);
if (!isNaN(char)) {
if (!isNaN(Number(char))) {
char = '#';
}
@ -300,6 +305,8 @@ const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
<PageContentBody
ref={scrollerRef}
className={styles.contentBody}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop}
onScroll={onScroll}

View File

@ -23,7 +23,13 @@ function createFilterBuilderPropsSelector() {
);
}
export default function ArtistIndexFilterModal(props) {
interface ArtistIndexFilterModalProps {
isOpen: boolean;
}
export default function ArtistIndexFilterModal(
props: ArtistIndexFilterModalProps
) {
const sectionItems = useSelector(createArtistSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'artist';
@ -31,7 +37,7 @@ export default function ArtistIndexFilterModal(props) {
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload) => {
(payload: unknown) => {
dispatch(setArtistFilter(payload));
},
[dispatch]
@ -39,6 +45,7 @@ export default function ArtistIndexFilterModal(props) {
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}

View File

@ -206,7 +206,7 @@ function ArtistIndexBanner(props: ArtistIndexBannerProps) {
</div>
) : null}
{showQualityProfile ? (
{showQualityProfile && !!qualityProfile?.name ? (
<div className={styles.title} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>

View File

@ -1,8 +1,9 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner';
import useMeasure from 'Helpers/Hooks/useMeasure';
@ -21,7 +22,7 @@ const columnPaddingSmallScreen = parseInt(
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
const ADDITIONAL_COLUMN_COUNT = {
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
small: 3,
medium: 2,
large: 1,
@ -41,17 +42,17 @@ interface CellItemData {
interface ArtistIndexBannersProps {
items: Artist[];
sortKey?: string;
sortKey: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const artistIndexSelector = createSelector(
(state) => state.artistIndex.bannerOptions,
(state: AppState) => state.artistIndex.bannerOptions,
(bannerOptions) => {
return {
bannerOptions,
@ -108,7 +109,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
} = props;
const { bannerOptions } = useSelector(artistIndexSelector);
const ref: React.MutableRefObject<Grid> = useRef();
const ref = useRef<Grid>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
@ -222,8 +223,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
}, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -232,7 +233,7 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -255,8 +256,8 @@ export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
const scrollTop = rowIndex * rowHeight + padding;
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current.scrollTo(0, scrollTop);
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current?.scrollTo(0, scrollTop);
}
}
}, [

View File

@ -59,7 +59,7 @@ function ArtistIndexBannerOptionsModalContent(
const dispatch = useDispatch();
const onBannerOptionChange = useCallback(
({ name, value }) => {
({ name, value }: { name: string; value: unknown }) => {
dispatch(setArtistBannerOption({ [name]: value }));
},
[dispatch]

View File

@ -1,10 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react';
import { CustomFilter } from 'App/State/AppState';
import ArtistIndexFilterModal from 'Artist/Index/ArtistIndexFilterModal';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
function ArtistIndexFilterMenu(props) {
interface ArtistIndexFilterMenuProps {
selectedFilterKey: string | number;
filters: object[];
customFilters: CustomFilter[];
isDisabled: boolean;
onFilterSelect(filterName: string): unknown;
}
function ArtistIndexFilterMenu(props: ArtistIndexFilterMenuProps) {
const {
selectedFilterKey,
filters,
@ -26,15 +34,6 @@ function ArtistIndexFilterMenu(props) {
);
}
ArtistIndexFilterMenu.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired,
};
ArtistIndexFilterMenu.defaultProps = {
showCustomFilters: false,
};

View File

@ -1,11 +1,19 @@
import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import SortMenu from 'Components/Menu/SortMenu';
import SortMenuItem from 'Components/Menu/SortMenuItem';
import { align, sortDirections } from 'Helpers/Props';
import { align } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import translate from 'Utilities/String/translate';
function ArtistIndexSortMenu(props) {
interface SeriesIndexSortMenuProps {
sortKey?: string;
sortDirection?: SortDirection;
isDisabled: boolean;
onSortSelect(sortKey: string): unknown;
}
function ArtistIndexSortMenu(props: SeriesIndexSortMenuProps) {
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
return (
@ -17,7 +25,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Monitored/Status
{translate('MonitoredStatus')}
</SortMenuItem>
<SortMenuItem
@ -26,7 +34,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Name
{translate('Name')}
</SortMenuItem>
<SortMenuItem
@ -35,7 +43,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Type
{translate('Type')}
</SortMenuItem>
<SortMenuItem
@ -44,7 +52,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Quality Profile
{translate('QualityProfile')}
</SortMenuItem>
<SortMenuItem
@ -53,7 +61,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Metadata Profile
{translate('MetadataProfile')}
</SortMenuItem>
<SortMenuItem
@ -62,7 +70,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Next Album
{translate('NextAlbum')}
</SortMenuItem>
<SortMenuItem
@ -71,7 +79,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Last Album
{translate('Last Album')}
</SortMenuItem>
<SortMenuItem
@ -80,7 +88,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Added
{translate('Added')}
</SortMenuItem>
<SortMenuItem
@ -89,7 +97,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Albums
{translate('Albums')}
</SortMenuItem>
<SortMenuItem
@ -98,7 +106,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Tracks
{translate('Tracks')}
</SortMenuItem>
<SortMenuItem
@ -107,7 +115,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Track Count
{translate('TrackCount')}
</SortMenuItem>
<SortMenuItem
@ -116,7 +124,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Path
{translate('Path')}
</SortMenuItem>
<SortMenuItem
@ -125,7 +133,7 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Size on Disk
{translate('SizeOnDisk')}
</SortMenuItem>
<SortMenuItem
@ -134,18 +142,11 @@ function ArtistIndexSortMenu(props) {
sortDirection={sortDirection}
onPress={onSortSelect}
>
Tags
{translate('Tags')}
</SortMenuItem>
</MenuContent>
</SortMenu>
);
}
ArtistIndexSortMenu.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
isDisabled: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired,
};
export default ArtistIndexSortMenu;

View File

@ -1,4 +1,3 @@
import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import ViewMenu from 'Components/Menu/ViewMenu';
@ -6,7 +5,13 @@ import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function ArtistIndexViewMenu(props) {
interface ArtistIndexViewMenuProps {
view: string;
isDisabled: boolean;
onViewSelect(value: string): unknown;
}
function ArtistIndexViewMenu(props: ArtistIndexViewMenuProps) {
const { view, isDisabled, onViewSelect } = props;
return (
@ -36,10 +41,4 @@ function ArtistIndexViewMenu(props) {
);
}
ArtistIndexViewMenu.propTypes = {
view: PropTypes.string.isRequired,
isDisabled: PropTypes.bool.isRequired,
onViewSelect: PropTypes.func.isRequired,
};
export default ArtistIndexViewMenu;

View File

@ -1,15 +1,51 @@
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import Album from 'Album/Album';
import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import dimensions from 'Styles/Variables/dimensions';
import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow';
import styles from './ArtistIndexOverviewInfo.css';
interface RowProps {
name: string;
showProp: string;
valueProp: string;
}
interface RowInfoProps {
title: string;
iconName: IconDefinition;
label: string;
}
interface ArtistIndexOverviewInfoProps {
height: number;
showMonitored: boolean;
showQualityProfile: boolean;
showLastAlbum: boolean;
showAdded: boolean;
showAlbumCount: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
monitored: boolean;
nextAlbum?: Album;
qualityProfile?: QualityProfile;
lastAlbum?: Album;
added?: string;
albumCount: number;
path: string;
sizeOnDisk?: number;
sortKey: string;
}
const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight);
const rows = [
@ -50,11 +86,17 @@ const rows = [
},
];
function getInfoRowProps(row, props, uiSettings) {
function getInfoRowProps(
row: RowProps,
props: ArtistIndexOverviewInfoProps,
uiSettings: UiSettings
): RowInfoProps | null {
const { name } = row;
if (name === 'monitored') {
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
const monitoredText = props.monitored
? translate('Monitored')
: translate('Unmonitored');
return {
title: monitoredText,
@ -63,9 +105,9 @@ function getInfoRowProps(row, props, uiSettings) {
};
}
if (name === 'qualityProfileId') {
if (name === 'qualityProfileId' && !!props.qualityProfile?.name) {
return {
title: 'Quality Profile',
title: translate('QualityProfile'),
iconName: icons.PROFILE,
label: props.qualityProfile.name,
};
@ -78,15 +120,16 @@ function getInfoRowProps(row, props, uiSettings) {
return {
title: `Last Album: ${lastAlbum.title}`,
iconName: icons.CALENDAR,
label: getRelativeDate(
lastAlbum.releaseDate,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true,
}
),
label:
getRelativeDate(
lastAlbum.releaseDate,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true,
}
) ?? '',
};
}
@ -98,10 +141,11 @@ function getInfoRowProps(row, props, uiSettings) {
return {
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
iconName: icons.ADD,
label: getRelativeDate(added, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: true,
}),
label:
getRelativeDate(added, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: true,
}) ?? '',
};
}
@ -116,7 +160,7 @@ function getInfoRowProps(row, props, uiSettings) {
}
return {
title: 'Album Count',
title: translate('AlbumCount'),
iconName: icons.CIRCLE,
label: albums,
};
@ -124,7 +168,7 @@ function getInfoRowProps(row, props, uiSettings) {
if (name === 'path') {
return {
title: 'Path',
title: translate('Path'),
iconName: icons.FOLDER,
label: props.path,
};
@ -132,31 +176,13 @@ function getInfoRowProps(row, props, uiSettings) {
if (name === 'sizeOnDisk') {
return {
title: 'Size on Disk',
title: translate('SizeOnDisk'),
iconName: icons.DRIVE,
label: formatBytes(props.sizeOnDisk),
};
}
}
interface ArtistIndexOverviewInfoProps {
height: number;
showMonitored: boolean;
showQualityProfile: boolean;
showLastAlbum: boolean;
showAdded: boolean;
showAlbumCount: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
monitored: boolean;
nextAlbum?: Album;
qualityProfile: object;
lastAlbum?: Album;
added?: string;
albumCount: number;
path: string;
sizeOnDisk?: number;
sortKey: string;
return null;
}
function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
@ -175,6 +201,8 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
const { name, showProp, valueProp } = row;
const isVisible =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(7053)
props[valueProp] != null && (props[showProp] || props.sortKey === name);
return {
@ -219,6 +247,10 @@ function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
const infoRowProps = getInfoRowProps(row, props, uiSettings);
if (infoRowProps == null) {
return null;
}
return <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
})}
</div>

View File

@ -1,11 +1,12 @@
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
import React from 'react';
import Icon from 'Components/Icon';
import styles from './ArtistIndexOverviewInfoRow.css';
interface ArtistIndexOverviewInfoRowProps {
title?: string;
iconName: object;
label: string;
iconName?: IconDefinition;
label: string | null;
}
function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) {

View File

@ -1,5 +1,5 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import Artist from 'Artist/Artist';
@ -33,11 +33,11 @@ interface RowItemData {
interface ArtistIndexOverviewsProps {
items: Artist[];
sortKey?: string;
sortKey: string;
sortDirection?: string;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -79,7 +79,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
const { size: posterSize, detailedProgressBar } = useSelector(
selectOverviewOptions
);
const listRef: React.MutableRefObject<List> = useRef();
const listRef = useRef<List>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
@ -136,8 +136,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
}, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -146,7 +146,7 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop);
listRef.current?.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -175,8 +175,8 @@ function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
scrollTop += offset;
}
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
listRef.current?.scrollTo(scrollTop);
scrollerRef.current?.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);

View File

@ -60,7 +60,7 @@ function ArtistIndexOverviewOptionsModalContent(
const dispatch = useDispatch();
const onOverviewOptionChange = useCallback(
({ name, value }) => {
({ name, value }: { name: string; value: unknown }) => {
dispatch(setArtistOverviewOption({ [name]: value }));
},
[dispatch]

View File

@ -206,7 +206,7 @@ function ArtistIndexPoster(props: ArtistIndexPosterProps) {
</div>
) : null}
{showQualityProfile ? (
{showQualityProfile && !!qualityProfile?.name ? (
<div className={styles.title} title={translate('QualityProfile')}>
{qualityProfile.name}
</div>

View File

@ -1,8 +1,9 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster';
import useMeasure from 'Helpers/Hooks/useMeasure';
@ -21,7 +22,7 @@ const columnPaddingSmallScreen = parseInt(
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
const ADDITIONAL_COLUMN_COUNT = {
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
small: 3,
medium: 2,
large: 1,
@ -41,17 +42,17 @@ interface CellItemData {
interface ArtistIndexPostersProps {
items: Artist[];
sortKey?: string;
sortKey: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const artistIndexSelector = createSelector(
(state) => state.artistIndex.posterOptions,
(state: AppState) => state.artistIndex.posterOptions,
(posterOptions) => {
return {
posterOptions,
@ -108,7 +109,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
} = props;
const { posterOptions } = useSelector(artistIndexSelector);
const ref: React.MutableRefObject<Grid> = useRef();
const ref = useRef<Grid>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
@ -231,8 +232,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
}, [isSmallScreen, size, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -241,7 +242,7 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -264,8 +265,8 @@ export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
const scrollTop = rowIndex * rowHeight + padding;
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current.scrollTo(0, scrollTop);
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current?.scrollTo(0, scrollTop);
}
}
}, [

View File

@ -59,7 +59,7 @@ function ArtistIndexPosterOptionsModalContent(
const dispatch = useDispatch();
const onPosterOptionChange = useCallback(
({ name, value }) => {
({ name, value }: { name: string; value: unknown }) => {
dispatch(setArtistPosterOption({ [name]: value }));
},
[dispatch]

View File

@ -1,6 +1,7 @@
import _ from 'lodash';
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Statistics } from 'Album/Album';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
@ -56,8 +57,8 @@ function AlbumDetails(props: AlbumDetailsProps) {
disambiguation,
albumType,
monitored,
statistics,
isSaving,
statistics = {} as Statistics,
isSaving = false,
} = album;
return (

View File

@ -11,7 +11,7 @@ interface AlbumStudioAlbumProps {
artistId: number;
albumId: number;
title: string;
disambiguation: string;
disambiguation?: string;
albumType: string;
monitored: boolean;
statistics: Statistics;

View File

@ -33,7 +33,7 @@ function ChangeMonitoringModalContent(
const [monitor, setMonitor] = useState(NO_CHANGE);
const onInputChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setMonitor(value);
},
[setMonitor]

View File

@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { SyntheticEvent, useCallback } from 'react';
import { useSelect } from 'App/SelectContext';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
@ -15,8 +15,9 @@ function ArtistIndexPosterSelect(props: ArtistIndexPosterSelectProps) {
const isSelected = selectState.selectedState[artistId];
const onSelectPress = useCallback(
(event) => {
const shiftKey = event.nativeEvent.shiftKey;
(event: SyntheticEvent) => {
const nativeEvent = event.nativeEvent as PointerEvent;
const shiftKey = nativeEvent.shiftKey;
selectDispatch({
type: 'toggleSelected',

View File

@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
interface ArtistIndexSelectAllButtonProps {
label: string;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
overflowComponent: React.FunctionComponent<never>;
}
function ArtistIndexSelectAllButton(props: ArtistIndexSelectAllButtonProps) {

View File

@ -24,6 +24,14 @@ import OrganizeArtistModal from './Organize/OrganizeArtistModal';
import TagsModal from './Tags/TagsModal';
import styles from './ArtistIndexSelectFooter.css';
interface SavePayload {
monitored?: boolean;
qualityProfileId?: number;
metadataProfileId?: number;
rootFolderPath?: string;
moveFiles?: boolean;
}
const artistEditorSelector = createSelector(
(state: AppState) => state.artist,
(artist) => {
@ -79,7 +87,7 @@ function ArtistIndexSelectFooter() {
}, [setIsEditModalOpen]);
const onSavePress = useCallback(
(payload) => {
(payload: SavePayload) => {
setIsSavingArtist(true);
setIsEditModalOpen(false);
@ -118,7 +126,7 @@ function ArtistIndexSelectFooter() {
}, [setIsTagsModalOpen]);
const onApplyTagsPress = useCallback(
(tags, applyTags) => {
(tags: number[], applyTags: string) => {
setIsSavingTags(true);
setIsTagsModalOpen(false);

View File

@ -7,7 +7,7 @@ interface ArtistIndexSelectModeButtonProps {
label: string;
iconName: IconDefinition;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
overflowComponent: React.FunctionComponent<never>;
onPress: () => void;
}

View File

@ -28,9 +28,15 @@ function RetagArtistModalContent(props: RetagArtistModalContentProps) {
const dispatch = useDispatch();
const artistNames = useMemo(() => {
const artists = artistIds.map((id) => {
return allArtists.find((a) => a.id === id);
});
const artists = artistIds.reduce((acc: Artist[], id) => {
const a = allArtists.find((a) => a.id === id);
if (a) {
acc.push(a);
}
return acc;
}, []);
const sorted = orderBy(artists, ['sortName']);

View File

@ -15,6 +15,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { bulkDeleteArtist, setDeleteOption } from 'Store/Actions/artistActions';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './DeleteArtistModalContent.css';
@ -37,16 +38,16 @@ function DeleteArtistModalContent(props: DeleteArtistModalContentProps) {
const [deleteFiles, setDeleteFiles] = useState(false);
const artists = useMemo(() => {
const artists = artistIds.map((id) => {
const artists = useMemo((): Artist[] => {
const artistList = artistIds.map((id) => {
return allArtists.find((a) => a.id === id);
});
}) as Artist[];
return orderBy(artists, ['sortName']);
return orderBy(artistList, ['sortName']);
}, [artistIds, allArtists]);
const onDeleteFilesChange = useCallback(
({ value }) => {
({ value }: CheckInputChanged) => {
setDeleteFiles(value);
},
[setDeleteFiles]

View File

@ -35,7 +35,7 @@ const monitoredOptions = [
get value() {
return translate('NoChange');
},
disabled: true,
isDisabled: true,
},
{
key: 'monitored',
@ -66,7 +66,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
const save = useCallback(
(moveFiles) => {
(moveFiles: boolean) => {
let hasChanges = false;
const payload: SavePayload = {};
@ -114,7 +114,7 @@ function EditArtistModalContent(props: EditArtistModalContentProps) {
);
const onInputChange = useCallback(
({ name, value }) => {
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'monitored':
setMonitored(value);

View File

@ -1,6 +1,7 @@
import { uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Tag } from 'App/State/TagsAppState';
import Artist from 'Artist/Artist';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@ -28,7 +29,7 @@ function TagsModalContent(props: TagsModalContentProps) {
const { artistIds, onModalClose, onApplyTagsPress } = props;
const allArtists: Artist[] = useSelector(createAllArtistSelector());
const tagList = useSelector(createTagsSelector());
const tagList: Tag[] = useSelector(createTagsSelector());
const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add');
@ -48,14 +49,14 @@ function TagsModalContent(props: TagsModalContentProps) {
}, [artistIds, allArtists]);
const onTagsChange = useCallback(
({ value }) => {
({ value }: { value: number[] }) => {
setTags(value);
},
[setTags]
);
const onApplyTagsChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setApplyTags(value);
},
[setApplyTags]

View File

@ -67,6 +67,7 @@
flex: 1 0 125px;
}
.monitorNewItems,
.nextAlbum,
.lastAlbum,
.added,

View File

@ -14,6 +14,7 @@ interface CssExports {
'lastAlbum': string;
'link': string;
'metadataProfileId': string;
'monitorNewItems': string;
'nextAlbum': string;
'overlayTitle': string;
'path': string;

View File

@ -23,7 +23,9 @@ import Column from 'Components/Table/Column';
import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { SelectStateInputProps } from 'typings/props';
import formatBytes from 'Utilities/Number/formatBytes';
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate';
import AlbumsCell from './AlbumsCell';
import hasGrowableColumns from './hasGrowableColumns';
@ -56,6 +58,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
monitored,
status,
path,
monitorNewItems,
nextAlbum,
lastAlbum,
added,
@ -126,7 +129,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
}, [setIsDeleteArtistModalOpen]);
const onSelectedChange = useCallback(
({ id, value, shiftKey }) => {
({ id, value, shiftKey }: SelectStateInputProps) => {
selectDispatch({
type: 'toggleSelected',
id,
@ -217,15 +220,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'qualityProfileId') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{qualityProfile.name}
</VirtualTableRowCell>
);
}
if (name === 'qualityProfileId') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{qualityProfile.name}
{qualityProfile?.name ?? ''}
</VirtualTableRowCell>
);
}
@ -233,7 +228,15 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'metadataProfileId') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{metadataProfile.name}
{metadataProfile?.name ?? ''}
</VirtualTableRowCell>
);
}
if (name === 'monitorNewItems') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{translate(firstCharToUpper(monitorNewItems))}
</VirtualTableRowCell>
);
}
@ -252,7 +255,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
}
return (
<VirtualTableRowCell key={name} className={styles[name]}>
None
{translate('None')}
</VirtualTableRowCell>
);
}
@ -271,13 +274,15 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
}
return (
<VirtualTableRowCell key={name} className={styles[name]}>
None
{translate('None')}
</VirtualTableRowCell>
);
}
if (name === 'added') {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
key={name}
className={styles[name]}
@ -328,7 +333,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) {
if (name === 'path') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{path}
<span title={path}>{path}</span>
</VirtualTableRowCell>
);
}

View File

@ -1,8 +1,9 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Artist from 'Artist/Artist';
import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow';
import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
@ -30,17 +31,17 @@ interface RowItemData {
interface ArtistIndexTableProps {
items: Artist[];
sortKey?: string;
sortKey: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const columnsSelector = createSelector(
(state) => state.artistIndex.columns,
(state: AppState) => state.artistIndex.columns,
(columns) => columns
);
@ -93,7 +94,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
const columns = useSelector(columnsSelector);
const { showBanners } = useSelector(selectTableOptions);
const listRef: React.MutableRefObject<List> = useRef();
const listRef = useRef<List<RowItemData>>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
const windowWidth = window.innerWidth;
@ -104,7 +105,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
}, [showBanners]);
useEffect(() => {
const current = scrollerRef.current as HTMLElement;
const current = scrollerRef?.current as HTMLElement;
if (isSmallScreen) {
setSize({
@ -128,8 +129,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -138,7 +139,7 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop);
listRef.current?.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -167,8 +168,8 @@ function ArtistIndexTable(props: ArtistIndexTableProps) {
scrollTop += offset;
}
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
listRef.current?.scrollTo(scrollTop);
scrollerRef?.current?.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);

View File

@ -31,6 +31,7 @@
flex: 1 0 125px;
}
.monitorNewItems,
.nextAlbum,
.lastAlbum,
.added,

View File

@ -11,6 +11,7 @@ interface CssExports {
'lastAlbum': string;
'latestAlbum': string;
'metadataProfileId': string;
'monitorNewItems': string;
'nextAlbum': string;
'path': string;
'qualityProfileId': string;

View File

@ -15,6 +15,7 @@ import {
setArtistSort,
setArtistTableOption,
} from 'Store/Actions/artistIndexActions';
import { CheckInputChanged } from 'typings/inputs';
import hasGrowableColumns from './hasGrowableColumns';
import styles from './ArtistIndexTableHeader.css';
@ -32,21 +33,21 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
const [selectState, selectDispatch] = useSelect();
const onSortPress = useCallback(
(value) => {
(value: string) => {
dispatch(setArtistSort({ sortKey: value }));
},
[dispatch]
);
const onTableOptionChange = useCallback(
(payload) => {
(payload: unknown) => {
dispatch(setArtistTableOption(payload));
},
[dispatch]
);
const onSelectAllChange = useCallback(
({ value }) => {
({ value }: CheckInputChanged) => {
selectDispatch({
type: value ? 'selectAll' : 'unselectAll',
});
@ -94,6 +95,8 @@ function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
<VirtualTableHeaderCell
key={name}
className={classNames(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
styles[name],
name === 'sortName' && showBanners && styles.banner,
name === 'sortName' &&

View File

@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import selectTableOptions from './selectTableOptions';
@ -19,7 +20,7 @@ function ArtistIndexTableOptions(props: ArtistIndexTableOptionsProps) {
const { showBanners, showSearchAction } = tableOptions;
const onTableOptionChangeWrapper = useCallback(
({ name, value }) => {
({ name, value }: CheckInputChanged) => {
onTableOptionChange({
tableOptions: {
...tableOptions,

View File

@ -1,5 +1,6 @@
import { createSelector } from 'reselect';
import Artist from 'Artist/Artist';
import Command from 'Commands/Command';
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
@ -12,25 +13,21 @@ function createArtistIndexItemSelector(artistId: number) {
createArtistQualityProfileSelector(artistId),
createArtistMetadataProfileSelector(artistId),
createExecutingCommandsSelector(),
(artist: Artist, qualityProfile, metadataProfile, executingCommands) => {
// If an artist is deleted this selector may fire before the parent
// selectors, which will result in an undefined artist, if that happens
// we want to return early here and again in the render function to avoid
// trying to show an artist that has no information available.
if (!artist) {
return {};
}
(
artist: Artist,
qualityProfile,
metadataProfile,
executingCommands: Command[]
) => {
const isRefreshingArtist = executingCommands.some((command) => {
return (
command.name === REFRESH_ARTIST && command.body.artistId === artist.id
command.name === REFRESH_ARTIST && command.body.artistId === artistId
);
});
const isSearchingArtist = executingCommands.some((command) => {
return (
command.name === ARTIST_SEARCH && command.body.artistId === artist.id
command.name === ARTIST_SEARCH && command.body.artistId === artistId
);
});

View File

@ -14,7 +14,7 @@ function ArtistInteractiveSearchModal(props) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
size={sizes.EXTRA_EXTRA_LARGE}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>

View File

@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setCalendarFilter } from 'Store/Actions/calendarActions';
function createCalendarSelector() {
return createSelector(
(state: AppState) => state.calendar.items,
(calendar) => {
return calendar;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.calendar.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface CalendarFilterModalProps {
isOpen: boolean;
}
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
const sectionItems = useSelector(createCalendarSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'calendar';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setCalendarFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@ -14,6 +14,7 @@ import { align, icons } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import CalendarConnector from './CalendarConnector';
import CalendarFilterModal from './CalendarFilterModal';
import CalendarLinkModal from './iCal/CalendarLinkModal';
import LegendConnector from './Legend/LegendConnector';
import CalendarOptionsModal from './Options/CalendarOptionsModal';
@ -78,6 +79,7 @@ class CalendarPage extends Component {
const {
selectedFilterKey,
filters,
customFilters,
hasArtist,
artistError,
artistIsFetching,
@ -137,7 +139,8 @@ class CalendarPage extends Component {
isDisabled={!hasArtist}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
customFilters={customFilters}
filterModalConnectorComponent={CalendarFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
@ -204,6 +207,7 @@ class CalendarPage extends Component {
CalendarPage.propTypes = {
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasArtist: PropTypes.bool.isRequired,
artistError: PropTypes.object,
artistIsFetching: PropTypes.bool.isRequired,

View File

@ -6,6 +6,7 @@ import withCurrentPage from 'Components/withCurrentPage';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@ -59,6 +60,7 @@ function createMapStateToProps() {
return createSelector(
(state) => state.calendar.selectedFilterKey,
(state) => state.calendar.filters,
createCustomFiltersSelector('calendar'),
createArtistCountSelector(),
createUISettingsSelector(),
createMissingAlbumIdsSelector(),
@ -67,6 +69,7 @@ function createMapStateToProps() {
(
selectedFilterKey,
filters,
customFilters,
artistCount,
uiSettings,
missingAlbumIds,
@ -76,6 +79,7 @@ function createMapStateToProps() {
return {
selectedFilterKey,
filters,
customFilters,
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasArtist: !!artistCount.count,
artistError: artistCount.error,

View File

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

View File

@ -1,3 +1,4 @@
import { maxBy } from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormInputGroup from 'Components/Form/FormInputGroup';
@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
if (id) {
dispatchSetFilter({ selectedFilterKey: id });
} else {
const last = customFilters[customFilters.length -1];
const last = maxBy(customFilters, 'id');
dispatchSetFilter({ selectedFilterKey: last.id });
}
@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
this.setState({
labelErrors: [
{
message: 'Label is required'
message: translate('LabelIsRequired')
}
]
});
@ -146,7 +147,7 @@ class FilterBuilderModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Custom Filter
{translate('CustomFilter')}
</ModalHeader>
<ModalBody>
@ -166,7 +167,9 @@ class FilterBuilderModalContent extends Component {
</div>
</div>
<div className={styles.label}>{translate('Filters')}</div>
<div className={styles.label}>
{translate('Filters')}
</div>
<div className={styles.rows}>
{

View File

@ -11,6 +11,7 @@ import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
import MonitorNewItemsFilterBuilderRowValue from './MonitorNewItemsFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
@ -68,6 +69,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.METADATA_PROFILE:
return MetadataProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.MONITOR_NEW_ITEMS:
return MonitorNewItemsFilterBuilderRowValue;
case filterBuilderValueTypes.PROTOCOL:
return ProtocolFilterBuilderRowValue;

View File

@ -0,0 +1,33 @@
import React from 'react';
import FilterBuilderRowValueProps from 'Components/Filter/Builder/FilterBuilderRowValueProps';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const options = [
{
id: 'all',
get name() {
return translate('AllAlbums');
},
},
{
id: 'new',
get name() {
return translate('New');
},
},
{
id: 'none',
get name() {
return translate('None');
},
},
];
function MonitorNewItemsFilterBuilderRowValue(
props: FilterBuilderRowValueProps
) {
return <FilterBuilderRowValue tagList={options} {...props} />;
}
export default MonitorNewItemsFilterBuilderRowValue;

View File

@ -37,8 +37,8 @@ class CustomFilter extends Component {
dispatchSetFilter
} = this.props;
// Assume that delete and then unmounting means the delete was successful.
// Moving this check to a ancestor would be more accurate, but would have
// Assume that delete and then unmounting means the deletion was successful.
// Moving this check to an ancestor would be more accurate, but would have
// more boilerplate.
if (this.state.isDeleting && id === selectedFilterKey) {
dispatchSetFilter({ selectedFilterKey: 'all' });

View File

@ -0,0 +1,53 @@
import React, { useCallback } from 'react';
import TagInputConnector from './TagInputConnector';
interface ArtistTagInputProps {
name: string;
value: number | number[];
onChange: ({
name,
value,
}: {
name: string;
value: number | number[];
}) => void;
}
export default function ArtistTagInput(props: ArtistTagInputProps) {
const { value, onChange, ...otherProps } = props;
const isArray = Array.isArray(value);
const handleChange = useCallback(
({ name, value: newValue }: { name: string; value: number[] }) => {
if (isArray) {
onChange({ name, value: newValue });
} else {
onChange({
name,
value: newValue.length ? newValue[newValue.length - 1] : 0,
});
}
},
[isArray, onChange]
);
let finalValue: number[] = [];
if (isArray) {
finalValue = value;
} else if (value === 0) {
finalValue = [];
} else {
finalValue = [value];
}
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
<TagInputConnector
{...otherProps}
value={finalValue}
onChange={handleChange}
/>
);
}

View File

@ -25,7 +25,8 @@ function createMapStateToProps() {
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
return {
key: downloadClient.id,
value: downloadClient.name
value: downloadClient.name,
hint: `(${downloadClient.id})`
};
});

View File

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

View File

@ -4,6 +4,7 @@ import Link from 'Components/Link/Link';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector';
import ArtistTagInput from './ArtistTagInput';
import AutoCompleteInput from './AutoCompleteInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
@ -12,6 +13,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
@ -83,6 +85,9 @@ function getComponent(type) {
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInput;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
@ -95,6 +100,9 @@ function getComponent(type) {
case inputTypes.DYNAMIC_SELECT:
return EnhancedSelectInputConnector;
case inputTypes.ARTIST_TAG:
return ArtistTagInput;
case inputTypes.SERIES_TYPE_SELECT:
return SeriesTypeSelectInput;
@ -292,6 +300,7 @@ FormInputGroup.propTypes = {
includeNoChangeDisabled: PropTypes.bool,
includeNone: PropTypes.bool,
selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number,
pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,6 @@ import TableBody from 'Components/Table/TableBody';
import TableRow from 'Components/Table/TableRow';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import styles from './PlaylistInput.css';
@ -46,7 +45,17 @@ class PlaylistInput extends Component {
onChange
} = this.props;
const oldSelected = getSelectedIds(prevState.selectedState, { parseIds: false }).sort();
const oldSelected = _.reduce(
prevState.selectedState,
(result, value, id) => {
if (value) {
result.push(id);
}
return result;
},
[]
).sort();
const newSelected = this.getSelectedIds().sort();
if (!_.isEqual(oldSelected, newSelected)) {
@ -61,7 +70,17 @@ class PlaylistInput extends Component {
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState, { parseIds: false });
return _.reduce(
this.state.selectedState,
(result, value, id) => {
if (value) {
result.push(id);
}
return result;
},
[]
);
};
//

View File

@ -29,6 +29,8 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.DYNAMIC_SELECT;
}
return inputTypes.SELECT;
case 'artistTag':
return inputTypes.ARTIST_TAG;
case 'tag':
return inputTypes.TEXT_TAG;
case 'tagSelect':

View File

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

View File

@ -22,7 +22,7 @@ function SeriesTypeSelectInput(props) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
disabled: includeNoChangeDisabled
isDisabled: includeNoChangeDisabled
});
}
@ -30,7 +30,7 @@ function SeriesTypeSelectInput(props) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
isDisabled: true
});
}

View File

@ -77,6 +77,7 @@ class TextTagInputConnector extends Component {
render() {
return (
<TagInput
delimiters={['Tab', 'Enter', ',']}
tagList={[]}
onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete}

View File

@ -63,6 +63,13 @@
width: 1280px;
}
.extraExtraLarge {
composes: modal;
width: 1600px;
}
@media only screen and (max-width: $breakpointExtraLarge) {
.modal.extraLarge {
width: 90%;
@ -90,7 +97,8 @@
.modal.small,
.modal.medium,
.modal.large,
.modal.extraLarge {
.modal.extraLarge,
.modal.extraExtraLarge {
max-height: 100%;
width: 100%;
height: 100% !important;

View File

@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'extraExtraLarge': string;
'extraLarge': string;
'large': string;
'medium': string;

View File

@ -6,7 +6,14 @@ import { createSelector } from 'reselect';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchArtist } from 'Store/Actions/artistActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
import {
fetchImportLists,
fetchIndexerFlags,
fetchLanguages,
fetchMetadataProfiles,
fetchQualityProfiles,
fetchUISettings
} from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions';
import { fetchTags } from 'Store/Actions/tagActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@ -44,6 +51,7 @@ const selectAppProps = createSelector(
);
const selectIsPopulated = createSelector(
(state) => state.artist.isPopulated,
(state) => state.customFilters.isPopulated,
(state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated,
@ -51,9 +59,11 @@ const selectIsPopulated = createSelector(
(state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.metadataProfiles.isPopulated,
(state) => state.settings.importLists.isPopulated,
(state) => state.settings.indexerFlags.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
(
artistsIsPopulated,
customFiltersIsPopulated,
tagsIsPopulated,
uiSettingsIsPopulated,
@ -61,10 +71,12 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated,
metadataProfilesIsPopulated,
importListsIsPopulated,
indexerFlagsIsPopulated,
systemStatusIsPopulated,
translationsIsPopulated
) => {
return (
artistsIsPopulated &&
customFiltersIsPopulated &&
tagsIsPopulated &&
uiSettingsIsPopulated &&
@ -72,6 +84,7 @@ const selectIsPopulated = createSelector(
qualityProfilesIsPopulated &&
metadataProfilesIsPopulated &&
importListsIsPopulated &&
indexerFlagsIsPopulated &&
systemStatusIsPopulated &&
translationsIsPopulated
);
@ -79,6 +92,7 @@ const selectIsPopulated = createSelector(
);
const selectErrors = createSelector(
(state) => state.artist.error,
(state) => state.customFilters.error,
(state) => state.tags.error,
(state) => state.settings.ui.error,
@ -86,9 +100,11 @@ const selectErrors = createSelector(
(state) => state.settings.qualityProfiles.error,
(state) => state.settings.metadataProfiles.error,
(state) => state.settings.importLists.error,
(state) => state.settings.indexerFlags.error,
(state) => state.system.status.error,
(state) => state.app.translations.error,
(
artistsError,
customFiltersError,
tagsError,
uiSettingsError,
@ -96,10 +112,12 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
indexerFlagsError,
systemStatusError,
translationsError
) => {
const hasError = !!(
artistsError ||
customFiltersError ||
tagsError ||
uiSettingsError ||
@ -107,6 +125,7 @@ const selectErrors = createSelector(
qualityProfilesError ||
metadataProfilesError ||
importListsError ||
indexerFlagsError ||
systemStatusError ||
translationsError
);
@ -120,6 +139,7 @@ const selectErrors = createSelector(
qualityProfilesError,
metadataProfilesError,
importListsError,
indexerFlagsError,
systemStatusError,
translationsError
};
@ -177,6 +197,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchImportLists() {
dispatch(fetchImportLists());
},
dispatchFetchIndexerFlags() {
dispatch(fetchIndexerFlags());
},
dispatchFetchUISettings() {
dispatch(fetchUISettings());
},
@ -217,6 +240,7 @@ class PageConnector extends Component {
this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchMetadataProfiles();
this.props.dispatchFetchImportLists();
this.props.dispatchFetchIndexerFlags();
this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
@ -243,6 +267,7 @@ class PageConnector extends Component {
dispatchFetchQualityProfiles,
dispatchFetchMetadataProfiles,
dispatchFetchImportLists,
dispatchFetchIndexerFlags,
dispatchFetchUISettings,
dispatchFetchStatus,
dispatchFetchTranslations,
@ -284,6 +309,7 @@ PageConnector.propTypes = {
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,

View File

@ -1,5 +1,5 @@
import React, { forwardRef, ReactNode, useCallback } from 'react';
import Scroller from 'Components/Scroller/Scroller';
import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
import ScrollDirection from 'Helpers/Props/ScrollDirection';
import { isLocked } from 'Utilities/scrollLock';
import styles from './PageContentBody.css';
@ -9,14 +9,11 @@ interface PageContentBodyProps {
innerClassName?: string;
children: ReactNode;
initialScrollTop?: number;
onScroll?: (payload) => void;
onScroll?: (payload: OnScroll) => void;
}
const PageContentBody = forwardRef(
(
props: PageContentBodyProps,
ref: React.MutableRefObject<HTMLDivElement>
) => {
(props: PageContentBodyProps, ref: ForwardedRef<HTMLDivElement>) => {
const {
className = styles.contentBody,
innerClassName = styles.innerContentBody,
@ -26,7 +23,7 @@ const PageContentBody = forwardRef(
} = props;
const onScrollWrapper = useCallback(
(payload) => {
(payload: OnScroll) => {
if (onScroll && !isLocked()) {
onScroll(payload);
}

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