1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-06 13:31:28 -05:00

Compare commits

...

78 Commits

Author SHA1 Message Date
bogdan
4c1b3a7b82 Bump MailKit and Microsoft.Data.SqlClient 2025-09-07 10:45:27 -05:00
Mark McDowall
6396d83fa7 Change authentication to Forms if set to Basic
(cherry picked from commit dfb6fdfbeb7ce85b287b41fed80f2511727353e5)
2025-09-07 10:44:48 -05:00
Bogdan
bd203d841a Fixed: Validation for tags label 2025-09-07 10:44:48 -05:00
Bogdan
e96d7580f4 Fixed: Removed support for movie file tokens in Movie Folder Format 2025-09-07 10:44:48 -05:00
Bogdan
eb0f7c62b6 New: Validation for movie file tokens in Movie Folder Format 2025-09-07 10:44:48 -05:00
Mark McDowall
41fa0de230 New: Remove Basic Auth
(cherry picked from commit 0f9e063e2146812f6e963363eee70a524612f354)
2025-09-07 10:44:48 -05:00
bakerboy448
cdc6a6dd27 New: Default wanted language for quality profiles changed to Original 2025-09-07 10:44:48 -05:00
Bogdan
1891ac1536 Bump Swashbuckle to 8.1.4 2025-09-07 10:44:48 -05:00
Bogdan
2539d46f7c Bump version to 6.0.0 2025-09-07 10:44:43 -05:00
Bogdan
32fe345144 New: Support removed for linux-x86 2025-09-07 10:44:20 -05:00
Bogdan
9d2193636e New: Migrate appdata folder for .NET 8 on OSX 2025-09-07 10:44:20 -05:00
Bogdan
1e898c2647 New: Bump to .NET 8
Co-authored-by: Qstick <qstick@gmail.com>
2025-09-07 10:44:20 -05:00
bakerboy448
a00ee08750 Bump to 5.28.1 2025-09-07 00:31:28 -05:00
Michon van Dooren
54cbbe05d9 New: (NFO Metadata) Include the TMDB Collection ID (#11164) 2025-09-06 05:58:07 -05:00
Mark McDowall
57f602eb02 New: Changing icon during import to blue 2025-09-03 17:02:22 -05:00
bakerboy448
e841c9b764 Bump to 5.28.0 2025-09-03 07:07:57 -05:00
bakerboy448
81bbaf8946 Fixed: Add missing translation keys 2025-09-01 11:29:59 -05:00
bakerboy448
8b4288fa18 New: UI Note that Filters are for movie properties only
Co-authored-by: PearsonFlyer <john@theediguy.com>

Closes #11200
2025-09-01 11:29:59 -05:00
Weblate
9aa3061e8e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Xoores <servarr-35466@xoores.cz>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-08-30 12:09:23 -05:00
Weblate
308c58f729 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ReDFiRe <wwsoft@abv.bg>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: rtme <pps.kmg@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2025-08-21 21:39:34 +01:00
grapexy
d38492188a New: Georgian language support (#11209) 2025-08-17 22:11:49 -05:00
bakerboy448
50e75e1362 Bump to 5.27.5 2025-08-17 14:44:46 -04:00
bakerboy448
f36845c251 Fixed: Parse UHDBDRip as BluRay quality 2025-08-16 10:35:25 -05:00
bakerboy448
110a338fb6 Fixed: TMDb List Paging (#11201) 2025-08-16 09:20:34 -05:00
Mark McDowall
3fcbaf9259 New: Move auth success logging to debug
Closes #7978

(cherry picked from commit 9ebe043bd94d036fe2ab45f3bb3f882cda48e211)
2025-08-12 12:20:36 -05:00
Luigi
576eff1890 New: Select with poster click in movie selection (#11187) 2025-08-12 11:49:58 -05:00
jkhsjdhjs
b0284bda07 Fixed: Parse HDDVDRip as BluRay 2025-08-11 18:53:36 -05:00
Bwaffles
c78666009d New: Add Year sorting to Discover page 2025-08-11 18:52:56 -05:00
Mark McDowall
b51d1beaaa Don't log debug messages for API key validation
(cherry picked from commit 78ca30d1f81361a2dabaddd0036b764859b858af)
2025-08-11 18:32:42 -05:00
Weblate
4d22bf1ceb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ArLab1 <arnaud.laberge@hotmail.com>
Co-authored-by: Ethan Francois <ethanfrancois0@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: adri1m64 <adrien.melle@laposte.net>
Co-authored-by: deamok <deamok@gmail.com>
Co-authored-by: dtorner <dtorner@gmail.com>
Co-authored-by: scdani <csonkadancsi@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2025-08-11 18:31:56 -05:00
bakerboy448
f9562b9b76 Bump version to 5.27.4 2025-08-03 01:10:22 -05:00
bakerboy448
6851c26328 Bump SixLabors.ImageSharp to 3.1.11 2025-07-31 21:52:59 -05:00
bakerboy448
e29be26fc9 Fixed: Prevent using Original names with other movie file tokens (#11175)
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2025-07-25 08:16:18 -05:00
bakerboy448
f6bd2f52d5 Bump version to 5.27.3 2025-07-23 08:51:28 -05:00
Denis Gheorghescu
8bef9b4da7 New:(Pushcut) Improved Notification Details (#10897) 2025-07-19 10:43:18 -04:00
Mark McDowall
787c387036 Return error if Manual Import called without items
(cherry picked from commit 4bdb0408f1bafa38b777a41babb1a775f99a94c1)
2025-07-09 16:19:57 -05:00
bakerboy448
0525256115 Bump version to 5.27.2 2025-07-08 18:37:58 -05:00
bakerboy448
5767e181b7 New: Improve Reject for Unknown Movie Messaging (#11063) 2025-07-08 18:25:51 -05:00
Mark McDowall
1cf3ef5dff New: Improve stored UI settings for multiple instances under the same host
Closes #10671
Fixes #11146

(cherry picked from commit 6677fd11168de6dbf78d03bfedf67b89dfe1df53)
2025-07-08 18:07:52 -05:00
Weblate
b6bad2398c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ACHN SYPS <achn.syps@gmail.com>
Co-authored-by: EdiTurn <yyxstter@gmail.com>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: josef <josef.holzapfel@proton.me>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2025-07-08 18:06:46 -05:00
nuxen
16308e4b1c Fixed: xvid not always detected correctly (#11138) 2025-07-07 20:00:13 -05:00
bakerboy448
bd7465fae4 Fixed: Allow Discover Exclusions of Movies without Year (Year 0)
Fixes #11135
2025-06-28 09:07:26 -05:00
Weblate
c0d70485c3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-06-19 18:20:22 -05:00
Bogdan
c743383912 Fixed: Deleting tags from UI
Fixes #11131
2025-06-16 20:34:00 +03:00
Servarr
d93c1d7808 Automated API Docs update 2025-06-16 20:22:31 +03:00
nuxen
0e2e7e4259 New: Support for multiple movieIds in Rename API endpoint 2025-06-16 19:09:19 +03:00
Bogdan
e6b27512c9 Bump version to 5.27.1 2025-06-15 09:20:29 +03:00
Bogdan
dae5e86b2c Fixed: Skip title searches for Newznab/Torznab indexers when movie year is missing
Prevents useless text searches of `Movie Title 0` when year is missing.

Fixes #10569
2025-06-14 13:20:11 +03:00
Bogdan
71f032d175 Bump Polly to 8.6.0 2025-06-11 23:13:54 +03:00
Bogdan
5a6db29dbd Bump version to 5.27.0 2025-06-11 23:11:20 +03:00
Weblate
2dac2dd35b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Qqqqqquexx <946921515@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translation: Servarr/Radarr
2025-06-11 15:01:35 +03:00
Stevie Robinson
b829638a77 Fixed: Include network drive types in Disk Space
(cherry picked from commit 9ffcd141a515e99604881a4ef383dadafef31eeb)
2025-06-10 15:31:34 +03:00
Mark McDowall
b6b7f13839 Prevent should refresh movie metadata from failing
Fixed: Prevent error checking if movie metadata should be refreshed from failing refresh movies task
(cherry picked from commit 3eed84c67938fed308e562e69cf7bcd727063803)
2025-06-10 15:31:34 +03:00
Mark McDowall
a9ad197b75 New: Update wording when removing a root folder
(cherry picked from commit 51c17fd3122f7b96a4155593d465ba32870d0c91)
2025-06-10 15:31:34 +03:00
Mark McDowall
1b28116a7e Fixed: Escape backticks in discord notifications
(cherry picked from commit 70c74fc1769f2094a14faaa103c49d744534be9f)
2025-06-10 15:31:34 +03:00
Bogdan
5870c88e1c Fix fullscreen automation screenshots 2025-06-09 22:05:09 +03:00
Servarr
0629832bd0 Automated API Docs update 2025-06-09 15:22:52 +03:00
Bogdan
430897c710 Fixed: Hide separators when page toolbar shows all buttons on small screens
Fixes #11124
2025-06-09 15:11:45 +03:00
Bogdan
9c42246eef Bump SixLabors.ImageSharp to 3.1.9 2025-06-08 11:32:36 +03:00
Weblate
489a86b253 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2025-06-08 10:42:02 +03:00
Robert Dailey
9c8d3b679d Add 'qualitydefinition/limits' endpoint to get size limitations
(cherry picked from commit 24f03fc1e96eba215f96312c791cf167f10499c7)
2025-06-08 10:41:37 +03:00
Bogdan
b2e51d1613 Bump version to 5.26.2 2025-06-08 10:30:06 +03:00
Michael Peleshenko
a95b1f2992 Fixed: Handling movies with empty IMDB IDs in lists clean library 2025-06-07 11:35:44 +03:00
Mark McDowall
ac33b15048 Convert Tags to TypeScript
(cherry picked from commit 60529f0bacf2398838ef8d7843490a35046a1093)
2025-06-04 22:16:24 +03:00
Bogdan
d28f03af28 Fixed: Allow more prefixes and suffixes for Release Year naming token 2025-06-04 19:50:08 +03:00
Bogdan
73b99d0be2 Add translation for missing movies count from collection 2025-06-04 18:54:09 +03:00
Stevie Robinson
15c34a61de New: Ability to clone Import Lists
(cherry picked from commit 2314d0b506e30d3a965497a052bc5e54fa0beb81)

Closes #10948
2025-06-04 18:34:13 +03:00
Mark McDowall
b99c536306 Convert ImportLists to TypeScript
(cherry picked from commit 10e3a237ef972540abcf4348bb56973d7ee19bd7)
2025-06-04 18:28:50 +03:00
Mark McDowall
2ebf391f85 Convert Media Management settings to TypeScript
(cherry picked from commit 27f81117ed188712600d8daf3ccb5121f9808458)
2025-06-04 17:50:00 +03:00
Mark McDowall
3945a2eeb8 Convert Indexer settings to TypeScript
(cherry picked from commit 6e008a8e855e67bb14b0e04bdb9042eebcacb59f)
2025-06-04 15:57:46 +03:00
Mark McDowall
e6980df590 Convert SettingsToolbar to TypeScript
(cherry picked from commit fd09ca6e719a96f760006ed0f08756faa20b6f75)
2025-06-04 14:43:30 +03:00
nuxen
187dd79b9c Fixed: Allow opening curly bracket as prefix in naming format 2025-06-03 16:27:31 +03:00
Bogdan
22ef334de6 Fix translation token for root folders load error 2025-06-03 15:22:38 +03:00
Weblate
c9eb9b8b98 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Tur3Q <andrejturan@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: cyrille <oscarboehringer@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-06-03 14:31:18 +03:00
Bogdan
9c74c40fc6 Fixed: Quality sliders on some browsers
Fixes #11109
2025-06-01 18:07:25 +03:00
Mark McDowall
8911cbe872 Sync react-slider props for Quality sliders with upstream
(cherry picked from commit 9dab2ba6e4316879e4db8db47363476a5c4f13b2)
2025-06-01 17:54:31 +03:00
Ghworg
7e541d4653 Fixed: Display media info bitrates in bits (#11087) 2025-06-01 14:50:53 +03:00
Bogdan
1cc2237ac0 Bump version to 5.26.1 2025-06-01 10:39:43 +03:00
312 changed files with 6397 additions and 6995 deletions

View File

@@ -2,7 +2,7 @@
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "Radarr",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"nodeGypDependencies": true,

9
.gitignore vendored
View File

@@ -165,15 +165,12 @@ Thumbs.db
/tools/Addins/*
packages.config.md5sum
# Common IntelliJ Platform excludes
# Ignore Rider projects completely for now
.idea/
# ignore node_modules symlink
node_modules
node_modules.nosync
# API doc generation
.config/
# Ignore Jetbrains IntelliJ Workspace Directories
.idea/

2
.vscode/launch.json vendored
View File

@@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build dotnet",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/_output/net6.0/Radarr",
"program": "${workspaceFolder}/_output/net8.0/Radarr",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console

View File

@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.26.0'
majorVersion: '6.0.0'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.427'
dotnetVersion: '8.0.405'
nodeVersion: '20.X'
innoVersion: '6.2.2'
windowsImage: 'windows-2022'
@@ -106,7 +106,7 @@ stages:
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
fi
displayName: Enable Extra Platform Support
- bash: ./build.sh --backend --enable-extra-platforms
@@ -122,27 +122,23 @@ stages:
artifact: '$(osName)Backend'
displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish'
- publish: '$(testsFolder)/net8.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
- publish: '$(testsFolder)/net8.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
- publish: '$(testsFolder)/net8.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
- publish: '$(testsFolder)/net8.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
- publish: '$(testsFolder)/net8.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
@@ -189,7 +185,7 @@ stages:
artifact: '$(osName)Frontend'
displayName: Publish Frontend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Installer
dependsOn:
- Build_Backend
@@ -260,21 +256,21 @@ stages:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/win-x64/net8.0
- task: ArchiveFiles@2
displayName: Create win-x86 zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
rootFolderOrFile: $(artifactsFolder)/win-x86/net8.0
- task: ArchiveFiles@2
displayName: Create osx-x64 app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net8.0
- task: ArchiveFiles@2
displayName: Create osx-x64 tar
inputs:
@@ -282,14 +278,14 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/osx-x64/net8.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net8.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 tar
inputs:
@@ -297,7 +293,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net8.0
- task: ArchiveFiles@2
displayName: Create linux-x64 tar
inputs:
@@ -305,7 +301,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-x64/net8.0
- task: ArchiveFiles@2
displayName: Create linux-musl-x64 tar
inputs:
@@ -313,15 +309,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-x86 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net8.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs:
@@ -329,7 +317,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-arm/net8.0
- task: ArchiveFiles@2
displayName: Create linux-musl-arm tar
inputs:
@@ -337,7 +325,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net8.0
- task: ArchiveFiles@2
displayName: Create linux-arm64 tar
inputs:
@@ -345,7 +333,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net8.0
- task: ArchiveFiles@2
displayName: Create linux-musl-arm64 tar
inputs:
@@ -353,7 +341,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net8.0
- task: ArchiveFiles@2
displayName: Create freebsd-x64 tar
inputs:
@@ -361,7 +349,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net8.0
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'Packages'
displayName: Publish Packages
@@ -392,7 +380,7 @@ stages:
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
SENTRY_ORG: $(sentryOrg)
SENTRY_URL: $(sentryUrl)
- stage: Unit_Test
displayName: Unit Tests
dependsOn: Build_Backend
@@ -493,29 +481,19 @@ stages:
testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool:
vmImage: ${{ variables.linuxImage }}
container: $[ variables['containerImage'] ]
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .NET'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -559,7 +537,7 @@ stages:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
@@ -611,12 +589,12 @@ stages:
Radarr__Postgres__Port: '5432'
Radarr__Postgres__User: 'radarr'
Radarr__Postgres__Password: 'radarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
@@ -699,7 +677,7 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
@@ -721,7 +699,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -776,7 +754,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -840,7 +818,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -926,29 +904,18 @@ stages:
artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Radarr.*.linux-core-x86.tar.gz'
pool:
vmImage: ${{ variables.linuxImage }}
container: $[ variables['containerImage'] ]
timeoutInMinutes: 15
steps:
- task: UseDotNet@2
displayName: 'Install .NET'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -965,7 +932,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -988,7 +955,7 @@ stages:
- stage: Automation
displayName: Automation
dependsOn: Packages
jobs:
- job: Automation
strategy:
@@ -1014,7 +981,7 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
@@ -1036,7 +1003,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -1161,7 +1128,7 @@ stages:
- checkout: self
submodules: true
persistCredentials: true
fetchDepth: 1
fetchDepth: 1
- bash: ./docs.sh Windows
displayName: Create openapi.json
- bash: |
@@ -1230,13 +1197,13 @@ stages:
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: |
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
./build.sh --backend -f net8.0 -r win-x64
TEST_DIR=_tests/net8.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@5.3.11
- task: reportgenerator@5
displayName: Generate Coverage Report
inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
@@ -1274,4 +1241,3 @@ stages:
DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)

View File

@@ -33,14 +33,14 @@ EnableExtraPlatformsInSDK()
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
fi
}
@@ -79,9 +79,9 @@ Build()
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
dotnet msbuild -restore $slnFile -p:SelfContained=True -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
else
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
dotnet msbuild -restore $slnFile -p:SelfContained=True -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
fi
ProgressEnd 'Build'
@@ -137,7 +137,7 @@ PackageLinux()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "net8.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -165,7 +165,7 @@ PackageMacOS()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "net8.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -377,15 +377,14 @@ then
Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
PackageTests "net6.0" "win-x64"
PackageTests "net6.0" "win-x86"
PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64"
PackageTests "net8.0" "win-x64"
PackageTests "net8.0" "win-x86"
PackageTests "net8.0" "linux-x64"
PackageTests "net8.0" "linux-musl-x64"
PackageTests "net8.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
PackageTests "net8.0" "freebsd-x64"
fi
else
PackageTests "$FRAMEWORK" "$RID"
@@ -413,20 +412,19 @@ then
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
Package "net6.0" "win-x64"
Package "net6.0" "win-x86"
Package "net6.0" "linux-x64"
Package "net6.0" "linux-musl-x64"
Package "net6.0" "linux-arm64"
Package "net6.0" "linux-musl-arm64"
Package "net6.0" "linux-arm"
Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64"
Package "net8.0" "win-x64"
Package "net8.0" "win-x86"
Package "net8.0" "linux-x64"
Package "net8.0" "linux-musl-x64"
Package "net8.0" "linux-arm64"
Package "net8.0" "linux-musl-arm64"
Package "net8.0" "linux-arm"
Package "net8.0" "linux-musl-arm"
Package "net8.0" "osx-x64"
Package "net8.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
Package "net8.0" "freebsd-x64"
fi
else
Package "$FRAMEWORK" "$RID"
@@ -436,7 +434,7 @@ fi
if [ "$INSTALLER" = "YES" ];
then
InstallInno
BuildInstaller "net6.0" "win-x64"
BuildInstaller "net6.0" "win-x86"
BuildInstaller "net8.0" "win-x64"
BuildInstaller "net8.0" "win-x86"
RemoveInno
fi

View File

@@ -1,7 +1,7 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
FRAMEWORK="net8.0"
PLATFORM=$1
ARCHITECTURE="${2:-x64}"
@@ -38,7 +38,7 @@ dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 8.1.4 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &

View File

@@ -26,7 +26,7 @@ function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Details</ModalHeader>
<ModalHeader>{translate('Details')}</ModalHeader>
<ModalBody>
<DescriptionList>

View File

@@ -304,7 +304,7 @@ function Queue() {
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Refresh"
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isRefreshing}
onPress={handleRefreshPress}

View File

@@ -90,7 +90,7 @@ function QueueStatus(props: QueueStatusProps) {
if (trackedDownloadState === 'importing') {
title += ` - ${translate('Importing')}`;
iconKind = kinds.PURPLE;
iconKind = kinds.PRIMARY;
}
if (trackedDownloadState === 'failedPending') {

View File

@@ -108,7 +108,7 @@ class ImportMovie extends Component {
{
!rootFoldersFetching && !!rootFoldersError ?
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
{translate('RootFoldersLoadError')}
</Alert> :
null
}

View File

@@ -93,7 +93,7 @@ class ImportMovieSelectFolder extends Component {
{
!isFetching && error ?
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
{translate('RootFoldersLoadError')}
</Alert> :
null
}

View File

@@ -15,9 +15,9 @@ import MovieIndex from 'Movie/Index/MovieIndex';
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import ImportListSettings from 'Settings/ImportLists/ImportListSettings';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import MediaManagement from 'Settings/MediaManagement/MediaManagement';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Profiles from 'Settings/Profiles/Profiles';
@@ -99,10 +99,7 @@ function AppRoutes() {
<Route exact={true} path="/settings" component={Settings} />
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route path="/settings/mediamanagement" component={MediaManagement} />
<Route path="/settings/profiles" component={Profiles} />
@@ -113,17 +110,14 @@ function AppRoutes() {
component={CustomFormatSettingsPage}
/>
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
<Route path="/settings/indexers" component={IndexerSettings} />
<Route
path="/settings/downloadclients"
component={DownloadClientSettingsConnector}
/>
<Route
path="/settings/importlists"
component={ImportListSettingsConnector}
/>
<Route path="/settings/importlists" component={ImportListSettings} />
<Route path="/settings/connect" component={NotificationSettings} />

View File

@@ -43,9 +43,15 @@ export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: {
items: T[];
};
schema: T[];
selectedSchema?: T;
}
export interface AppSectionItemSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: T;
}
export interface AppSectionItemState<T> {
@@ -61,9 +67,10 @@ export interface AppSectionProviderState<T>
AppSectionSaveState {
isFetching: boolean;
isPopulated: boolean;
isTesting?: boolean;
error: Error;
items: T[];
pendingChanges: Partial<T>;
pendingChanges?: Partial<T>;
}
interface AppSectionState<T> {

View File

@@ -1,12 +1,15 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemSchemaState,
AppSectionItemState,
AppSectionSaveState,
AppSectionSchemaState,
PagedAppSectionState,
} from 'App/State/AppSectionState';
import Language from 'Language/Language';
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
import CustomFormat from 'typings/CustomFormat';
import DelayProfile from 'typings/DelayProfile';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import ImportListExclusion from 'typings/ImportListExclusion';
@@ -16,12 +19,34 @@ import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General';
import IndexerOptions from 'typings/Settings/IndexerOptions';
import MediaManagement from 'typings/Settings/MediaManagement';
import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import UiSettings from 'typings/Settings/UiSettings';
import MetadataAppState from './MetadataAppState';
type Presets<T> = T & {
presets: T[];
};
export interface AutoTaggingAppState
extends AppSectionState<AutoTagging>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface AutoTaggingSpecificationAppState
extends AppSectionState<AutoTaggingSpecification>,
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState<AutoTaggingSpecification> {}
export interface DelayProfileAppState
extends AppSectionState<DelayProfile>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
@@ -33,6 +58,10 @@ export interface GeneralAppState
extends AppSectionItemState<General>,
AppSectionSaveState {}
export interface MediaManagementAppState
extends AppSectionItemState<MediaManagement>,
AppSectionSaveState {}
export interface NamingAppState
extends AppSectionItemState<NamingConfig>,
AppSectionSaveState {}
@@ -42,12 +71,20 @@ export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState<Presets<ImportList>> {
isTestingAll: boolean;
}
export interface IndexerOptionsAppState
extends AppSectionItemState<IndexerOptions>,
AppSectionSaveState {}
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {
AppSectionSaveState,
AppSectionSchemaState<Presets<Indexer>> {
isTestingAll: boolean;
}
@@ -57,7 +94,7 @@ export interface NotificationAppState
export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
AppSectionItemSchemaState<QualityProfile> {}
export interface ReleaseProfilesAppState
extends AppSectionState<ReleaseProfile>,
@@ -88,15 +125,20 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
advancedSettings: boolean;
autoTaggings: AutoTaggingAppState;
autoTaggingSpecifications: AutoTaggingSpecificationAppState;
customFormats: CustomFormatAppState;
delayProfiles: DelayProfileAppState;
downloadClients: DownloadClientAppState;
general: GeneralAppState;
importListExclusions: ImportListExclusionsSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexerOptions: IndexerOptionsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
mediaManagement: MediaManagementAppState;
metadata: MetadataAppState;
naming: NamingAppState;
namingExamples: NamingExamplesAppState;

View File

@@ -17,7 +17,7 @@ export interface TagDetail extends ModelBase {
indexerIds: number[];
movieIds: number[];
notificationIds: number[];
restrictionIds: number[];
releaseProfileIds: number[];
}
export interface TagDetailAppState

View File

@@ -196,7 +196,7 @@ class CollectionOverview extends Component {
size={13}
/>
<span className={styles.status}>
{`${missingMovies} missing movie(s)`}
{translate('CountMissingMoviesFromLibrary', { count: missingMovies })}
</span>
</Label>

View File

@@ -56,6 +56,8 @@ function CustomFiltersModalContent(props) {
{translate('AddCustomFilter')}
</Button>
</div>
<br />
{translate('FilterMoviePropertiesOnlyNotFileWarning')}
</ModalBody>
<ModalFooter>

View File

@@ -5,18 +5,20 @@ import { ValidationError, ValidationWarning } from 'typings/pending';
import styles from './Form.css';
export interface FormProps {
id?: string;
children: ReactNode;
validationErrors?: ValidationError[];
validationWarnings?: ValidationWarning[];
}
function Form({
id,
children,
validationErrors = [],
validationWarnings = [],
}: FormProps) {
return (
<div>
<div id={id}>
{validationErrors.length || validationWarnings.length ? (
<div className={styles.validationFailures}>
{validationErrors.map((error, index) => {

View File

@@ -18,7 +18,7 @@ function createQualityProfilesSelector(
includeMixed: boolean
) {
return createSelector(
createSortedSectionSelector(
createSortedSectionSelector<QualityProfile, QualityProfilesAppState>(
'settings.qualityProfiles',
sortByProp<QualityProfile, 'name'>('name')
),

View File

@@ -26,6 +26,10 @@
color: var(--warningColor);
}
.primary {
color: var(--primaryColor);
}
.purple {
color: var(--purple);
}

View File

@@ -6,6 +6,7 @@ interface CssExports {
'disabled': string;
'info': string;
'pink': string;
'primary': string;
'purple': string;
'success': string;
'warning': string;

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { Error } from 'App/State/AppSectionState';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
@@ -6,7 +7,7 @@ import { kinds } from 'Helpers/Props';
interface PageSectionContentProps {
isFetching: boolean;
isPopulated: boolean;
error?: object;
error?: Error;
errorMessage: string;
children: React.ReactNode;
}
@@ -18,7 +19,7 @@ function PageSectionContent({
errorMessage,
children,
}: PageSectionContentProps) {
if (isFetching) {
if (isFetching && !isPopulated) {
return <LoadingIndicator />;
}

View File

@@ -16,10 +16,10 @@ const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin);
const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1;
interface PageToolbarSectionProps {
export interface PageToolbarSectionProps {
children?:
| (ReactElement<PageToolbarButtonProps> | ReactElement<never>)
| (ReactElement<PageToolbarButtonProps> | ReactElement<never>)[];
| (ReactElement<PageToolbarButtonProps> | ReactElement<never> | null)
| (ReactElement<PageToolbarButtonProps> | ReactElement<never> | null)[];
alignContent?: Extract<Align, keyof typeof styles>;
collapseButtons?: boolean;
}
@@ -80,8 +80,12 @@ function PageToolbarSection({
if (buttonCount - 1 === maxButtons) {
const overflowItems: PageToolbarButtonProps[] = [];
const buttonsWithoutSeparators = validChildren.filter(
(child) => Object.keys(child.props).length > 0
);
return {
buttons: validChildren,
buttons: buttonsWithoutSeparators,
buttonCount,
overflowItems,
};

View File

@@ -47,6 +47,15 @@ function DiscoverMovieSortMenu(props) {
{translate('Studio')}
</SortMenuItem>
<SortMenuItem
name="year"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Year')}
</SortMenuItem>
<SortMenuItem
name="inCinemas"
sortKey={sortKey}

View File

@@ -36,7 +36,8 @@
.imdbRating,
.rottenTomatoesRating,
.traktRating,
.runtime {
.runtime,
.year {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 90px;

View File

@@ -22,6 +22,7 @@ interface CssExports {
'studio': string;
'tmdbRating': string;
'traktRating': string;
'year': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -61,7 +61,8 @@
.imdbRating,
.rottenTomatoesRating,
.traktRating,
.runtime {
.runtime,
.year {
composes: cell;
flex: 0 0 90px;

View File

@@ -28,6 +28,7 @@ interface CssExports {
'studio': string;
'tmdbRating': string;
'traktRating': string;
'year': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -167,6 +167,14 @@ class DiscoverMovieRow extends Component {
);
}
if (name === 'year') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{year}
</VirtualTableRowCell>
);
}
if (name === 'collection') {
return (
<VirtualTableRowCell

View File

@@ -0,0 +1,8 @@
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
function useIsWindows() {
return useSelector((state: AppState) => state.system.status.item.isWindows);
}
export default useIsWindows;

View File

@@ -0,0 +1,8 @@
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
function useShowAdvancedSettings() {
return useSelector((state: AppState) => state.settings.advancedSettings);
}
export default useShowAdvancedSettings;

View File

@@ -1,149 +0,0 @@
// https://github.com/react-bootstrap/react-element-children
import React from 'react';
/**
* Iterates through children that are typically specified as `props.children`,
* but only maps over children that are "valid components".
*
* The mapFunction provided index will be normalised to the components mapped,
* so an invalid component would not increase the index.
*
* @param {?*} children Children tree container.
* @param {function(*, int)} func.
* @param {*} context Context for func.
* @return {object} Object containing the ordered map of results.
*/
export function map(children, func, context) {
let index = 0;
return React.Children.map(children, (child) => {
if (!React.isValidElement(child)) {
return child;
}
return func.call(context, child, index++);
});
}
/**
* Iterates through children that are "valid components".
*
* The provided forEachFunc(child, index) will be called for each
* leaf child with the index reflecting the position relative to "valid components".
*
* @param {?*} children Children tree container.
* @param {function(*, int)} func.
* @param {*} context Context for context.
*/
export function forEach(children, func, context) {
let index = 0;
React.Children.forEach(children, (child) => {
if (!React.isValidElement(child)) {
return;
}
func.call(context, child, index++);
});
}
/**
* Count the number of "valid components" in the Children container.
*
* @param {?*} children Children tree container.
* @returns {number}
*/
export function count(children) {
let result = 0;
React.Children.forEach(children, (child) => {
if (!React.isValidElement(child)) {
return;
}
++result;
});
return result;
}
/**
* Finds children that are typically specified as `props.children`,
* but only iterates over children that are "valid components".
*
* The provided forEachFunc(child, index) will be called for each
* leaf child with the index reflecting the position relative to "valid components".
*
* @param {?*} children Children tree container.
* @param {function(*, int)} func.
* @param {*} context Context for func.
* @returns {array} of children that meet the func return statement
*/
export function filter(children, func, context) {
const result = [];
forEach(children, (child, index) => {
if (func.call(context, child, index)) {
result.push(child);
}
});
return result;
}
export function find(children, func, context) {
let result = null;
forEach(children, (child, index) => {
if (result) {
return;
}
if (func.call(context, child, index)) {
result = child;
}
});
return result;
}
export function every(children, func, context) {
let result = true;
forEach(children, (child, index) => {
if (!result) {
return;
}
if (!func.call(context, child, index)) {
result = false;
}
});
return result;
}
export function some(children, func, context) {
let result = false;
forEach(children, (child, index) => {
if (result) {
return;
}
if (func.call(context, child, index)) {
result = true;
}
});
return result;
}
export function toArray(children) {
const result = [];
forEach(children, (child) => {
result.push(child);
});
return result;
}

View File

@@ -1,3 +0,0 @@
export default function getDisplayName(Component) {
return Component.displayName || Component.name || 'Component';
}

View File

@@ -10,7 +10,7 @@ import Popover from 'Components/Tooltip/Popover';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import EditImportListModal from 'Settings/ImportLists/ImportLists/EditImportListModal';
import { deleteImportList } from 'Store/Actions/Settings/importLists';
import ImportList from 'typings/ImportList';
import MovieCredit from 'typings/MovieCredit';
@@ -154,7 +154,7 @@ function MovieCastPoster(props: MovieCastPosterProps) {
{character}
</div>
<EditImportListModalConnector
<EditImportListModal
id={importListId}
isOpen={isEditImportListModalOpen}
onModalClose={setEditImportListModalClosed}

View File

@@ -10,7 +10,7 @@ import Popover from 'Components/Tooltip/Popover';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import EditImportListModal from 'Settings/ImportLists/ImportLists/EditImportListModal';
import { deleteImportList } from 'Store/Actions/Settings/importLists';
import ImportList from 'typings/ImportList';
import MovieCredit from 'typings/MovieCredit';
@@ -152,7 +152,7 @@ function MovieCrewPoster(props: MovieCrewPosterProps) {
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
<EditImportListModalConnector
<EditImportListModal
id={importListId}
isOpen={isEditImportListModalOpen}
onModalClose={setEditImportListModalClosed}

View File

@@ -284,7 +284,7 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
/>
<MovieIndexSelectAllButton
label="SelectAll"
label={translate('SelectAll')}
isSelectMode={isSelectMode}
overflowComponent={MovieIndexSelectAllMenuItem}
/>

View File

@@ -1,5 +1,6 @@
import React, { useCallback, useState } from 'react';
import React, { SyntheticEvent, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
@@ -141,8 +142,31 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
setIsDeleteMovieModalOpen(false);
}, [setIsDeleteMovieModalOpen]);
const [selectState, selectDispatch] = useSelect();
const onSelectPress = useCallback(
(event: SyntheticEvent<HTMLElement, MouseEvent>) => {
if (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey) {
window.open(`/movie/${tmdbId}`, '_blank');
return;
}
const shiftKey = event.nativeEvent.shiftKey;
selectDispatch({
type: 'toggleSelected',
id: movieId,
isSelected: !selectState.selectedState[movieId],
shiftKey,
});
},
[movieId, selectState.selectedState, selectDispatch, tmdbId]
);
const link = `/movie/${tmdbId}`;
const linkProps = isSelectMode ? { onPress: onSelectPress } : { to: link };
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
@@ -196,7 +220,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
<div className={styles.deleted} title={translate('Deleted')} />
) : null}
<Link className={styles.link} style={elementStyle} to={link}>
<Link className={styles.link} style={elementStyle} {...linkProps}>
<MoviePoster
style={elementStyle}
images={images}

View File

@@ -82,9 +82,9 @@ function RootFolderRow(props: RootFolderRowProps) {
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', { path })}
confirmLabel={translate('Delete')}
title={translate('RemoveRootFolder')}
message={translate('RemoveRootFolderMoviesMessageText', { path })}
confirmLabel={translate('Remove')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>

View File

@@ -1,70 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './AdvancedSettingsButton.css';
function AdvancedSettingsButton(props) {
const {
advancedSettings,
onAdvancedSettingsPress,
showLabel
} = props;
return (
<Link
className={styles.button}
title={advancedSettings ? translate('ShownClickToHide') : translate('HiddenClickToShow')}
onPress={onAdvancedSettingsPress}
>
<Icon
name={icons.ADVANCED_SETTINGS}
size={21}
/>
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
className={styles.indicatorBackground}
name={icons.CIRCLE}
size={16}
/>
<Icon
className={advancedSettings ? styles.enabled : styles.disabled}
name={advancedSettings ? icons.CHECK : icons.CLOSE}
size={10}
/>
</span>
{
showLabel ?
<div className={styles.labelContainer}>
<div className={styles.label}>
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
</div>
</div> :
null
}
</Link>
);
}
AdvancedSettingsButton.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
showLabel: PropTypes.bool.isRequired
};
AdvancedSettingsButton.defaultProps = {
showLabel: true
};
export default AdvancedSettingsButton;

View File

@@ -0,0 +1,67 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import translate from 'Utilities/String/translate';
import styles from './AdvancedSettingsButton.css';
interface AdvancedSettingsButtonProps {
showLabel: boolean;
}
function AdvancedSettingsButton({ showLabel }: AdvancedSettingsButtonProps) {
const showAdvancedSettings = useSelector(
(state: AppState) => state.settings.advancedSettings
);
const dispatch = useDispatch();
const handlePress = useCallback(() => {
dispatch(toggleAdvancedSettings());
}, [dispatch]);
return (
<Link
className={styles.button}
title={
showAdvancedSettings
? translate('ShownClickToHide')
: translate('HiddenClickToShow')
}
onPress={handlePress}
>
<Icon name={icons.ADVANCED_SETTINGS} size={21} />
<span
className={classNames(styles.indicatorContainer, 'fa-layers fa-fw')}
>
<Icon
className={styles.indicatorBackground}
name={icons.CIRCLE}
size={16}
/>
<Icon
className={showAdvancedSettings ? styles.enabled : styles.disabled}
name={showAdvancedSettings ? icons.CHECK : icons.CLOSE}
size={10}
/>
</span>
{showLabel ? (
<div className={styles.labelContainer}>
<div className={styles.label}>
{showAdvancedSettings
? translate('HideAdvanced')
: translate('ShowAdvanced')}
</div>
</div>
) : null}
</Link>
);
}
export default AdvancedSettingsButton;

View File

@@ -5,7 +5,7 @@ import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import ParseToolbarButton from 'Parse/ParseToolbarButton';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
@@ -13,9 +13,7 @@ import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCusto
function CustomFormatSettingsPage() {
return (
<PageContent title={translate('CustomFormatsSettings')}>
<SettingsToolbarConnector
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<SettingsToolbar
showSave={false}
additionalButtons={
<>

View File

@@ -5,7 +5,7 @@ import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
@@ -71,7 +71,7 @@ class DownloadClientSettings extends Component {
return (
<PageContent title={translate('DownloadClientSettings')}>
<SettingsToolbarConnector
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={

View File

@@ -38,7 +38,6 @@ class EditDownloadClientModalContent extends Component {
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteDownloadClientPress,
...otherProps
} = this.props;
@@ -198,8 +197,6 @@ class EditDownloadClientModalContent extends Component {
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
@@ -243,7 +240,6 @@ EditDownloadClientModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteDownloadClientPress: PropTypes.func
};

View File

@@ -6,8 +6,7 @@ import {
saveDownloadClient,
setDownloadClientFieldValue,
setDownloadClientValue,
testDownloadClient,
toggleAdvancedSettings
testDownloadClient
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
@@ -29,8 +28,7 @@ const mapDispatchToProps = {
setDownloadClientValue,
setDownloadClientFieldValue,
saveDownloadClient,
testDownloadClient,
toggleAdvancedSettings
testDownloadClient
};
class EditDownloadClientModalContentConnector extends Component {
@@ -63,10 +61,6 @@ class EditDownloadClientModalContentConnector extends Component {
this.props.testDownloadClient({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
@@ -76,7 +70,6 @@ class EditDownloadClientModalContentConnector extends Component {
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
@@ -94,7 +87,6 @@ EditDownloadClientModalContentConnector.propTypes = {
setDownloadClientFieldValue: PropTypes.func.isRequired,
saveDownloadClient: PropTypes.func.isRequired,
testDownloadClient: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -8,7 +8,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { kinds } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import AnalyticSettings from './AnalyticSettings';
import BackupSettings from './BackupSettings';
@@ -113,7 +113,7 @@ class GeneralSettings extends Component {
return (
<PageContent title={translate('GeneralSettings')}>
<SettingsToolbarConnector
<SettingsToolbar
{...otherProps}
/>

View File

@@ -30,7 +30,9 @@ export const authenticationMethodOptions = [
key: 'basic',
get value() {
return translate('AuthBasic');
}
},
isDisabled: true,
isHidden: true
},
{
key: 'forms',

View File

@@ -46,7 +46,6 @@ function createImportListExclusionSelector(id?: number) {
const settings = selectSettings(mapping, pendingChanges, saveError);
return {
id,
isFetching,
error,
isSaving,
@@ -171,7 +170,7 @@ function EditImportListExclusionModalContent({
</ModalBody>
<ModalFooter>
{id && (
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
@@ -179,7 +178,7 @@ function EditImportListExclusionModalContent({
>
{translate('Delete')}
</Button>
)}
) : null}
<Button onPress={onModalClose}>{translate('Cancel')}</Button>

View File

@@ -1,123 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import ImportListExclusions from './ImportListExclusions/ImportListExclusions';
import ImportListsConnector from './ImportLists/ImportListsConnector';
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
import ImportListOptions from './Options/ImportListOptions';
class ImportListSettings extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false,
isManageImportListsOpen: false
};
}
//
// Listeners
setChildSave = (saveCallback) => {
this._saveCallback = saveCallback;
};
onChildStateChange = (payload) => {
this.setState(payload);
};
onManageImportListsPress = () => {
this.setState({ isManageImportListsOpen: true });
};
onManageImportListsModalClose = () => {
this.setState({ isManageImportListsOpen: false });
};
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
}
};
//
// Render
render() {
const {
isTestingAll,
dispatchTestAllImportLists
} = this.props;
const {
isSaving,
hasPendingChanges,
isManageImportListsOpen
} = this.state;
return (
<PageContent title={translate('ImportListSettings')}>
<SettingsToolbarConnector
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllLists')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={dispatchTestAllImportLists}
/>
<PageToolbarButton
label={translate('ManageLists')}
iconName={icons.MANAGE}
onPress={this.onManageImportListsPress}
/>
</Fragment>
}
onSavePress={this.onSavePress}
/>
<PageContentBody>
<ImportListsConnector />
<ImportListOptions
setChildSave={this.setChildSave}
onChildStateChange={this.onChildStateChange}
/>
<ImportListExclusions />
<ManageImportListsModal
isOpen={isManageImportListsOpen}
onModalClose={this.onManageImportListsModalClose}
/>
</PageContentBody>
</PageContent>
);
}
}
ImportListSettings.propTypes = {
isTestingAll: PropTypes.bool.isRequired,
dispatchTestAllImportLists: PropTypes.func.isRequired
};
export default ImportListSettings;

View File

@@ -0,0 +1,107 @@
import React, { useCallback, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbar from 'Settings/SettingsToolbar';
import { testAllImportLists } from 'Store/Actions/settingsActions';
import {
SaveCallback,
SettingsStateChange,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
import ImportListExclusions from './ImportListExclusions/ImportListExclusions';
import ImportLists from './ImportLists/ImportLists';
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
import ImportListOptions from './Options/ImportListOptions';
function ImportListSettings() {
const dispatch = useDispatch();
const isTestingAll = useSelector(
(state: AppState) => state.settings.importLists.isTestingAll
);
const saveOptions = useRef<() => void>();
const [isSaving, setIsSaving] = useState(false);
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const [isManageImportListsModalOpen, setIsManageImportListsModalOpen] =
useState(false);
const handleSetChildSave = useCallback((saveCallback: SaveCallback) => {
saveOptions.current = saveCallback;
}, []);
const handleChildStateChange = useCallback(
({ isSaving, hasPendingChanges }: SettingsStateChange) => {
setIsSaving(isSaving);
setHasPendingChanges(hasPendingChanges);
},
[]
);
const handleManageImportListsPress = useCallback(() => {
setIsManageImportListsModalOpen(true);
}, []);
const handleManageImportListsModalClose = useCallback(() => {
setIsManageImportListsModalOpen(false);
}, []);
const handleSavePress = useCallback(() => {
saveOptions.current?.();
}, []);
const handleTestAllIndexersPress = useCallback(() => {
dispatch(testAllImportLists());
}, [dispatch]);
return (
<PageContent title={translate('ImportListSettings')}>
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllLists')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={handleTestAllIndexersPress}
/>
<PageToolbarButton
label={translate('ManageLists')}
iconName={icons.MANAGE}
onPress={handleManageImportListsPress}
/>
</>
}
onSavePress={handleSavePress}
/>
<PageContentBody>
<ImportLists />
<ImportListOptions
setChildSave={handleSetChildSave}
onChildStateChange={handleChildStateChange}
/>
<ImportListExclusions />
<ManageImportListsModal
isOpen={isManageImportListsModalOpen}
onModalClose={handleManageImportListsModalClose}
/>
</PageContentBody>
</PageContent>
);
}
export default ImportListSettings;

View File

@@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { testAllImportLists } from 'Store/Actions/settingsActions';
import ImportListSettings from './ImportListSettings';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.importLists.isTestingAll,
(isTestingAll) => {
return {
isTestingAll
};
}
);
}
const mapDispatchToProps = {
dispatchTestAllImportLists: testAllImportLists
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListSettings);

View File

@@ -1,117 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddImportListPresetMenuItem from './AddImportListPresetMenuItem';
import styles from './AddImportListItem.css';
class AddImportListItem extends Component {
//
// Listeners
onImportListSelect = () => {
const {
implementation,
implementationName,
minRefreshInterval
} = this.props;
this.props.onImportListSelect({ implementation, implementationName, minRefreshInterval });
};
//
// Render
render() {
const {
implementation,
implementationName,
minRefreshInterval,
infoLink,
presets,
onImportListSelect
} = this.props;
const hasPresets = !!presets && !!presets.length;
return (
<div
className={styles.list}
>
<Link
className={styles.underlay}
onPress={this.onImportListSelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onImportListSelect}
>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
{translate('Presets')}
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddImportListPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
minRefreshInterval={minRefreshInterval}
onPress={onImportListSelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
}
AddImportListItem.propTypes = {
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
minRefreshInterval: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired,
presets: PropTypes.arrayOf(PropTypes.object),
onImportListSelect: PropTypes.func.isRequired
};
export default AddImportListItem;

View File

@@ -0,0 +1,91 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import { selectImportListSchema } from 'Store/Actions/settingsActions';
import ImportList from 'typings/ImportList';
import translate from 'Utilities/String/translate';
import AddImportListPresetMenuItem from './AddImportListPresetMenuItem';
import styles from './AddImportListItem.css';
interface AddImportListItemProps {
implementation: string;
implementationName: string;
minRefreshInterval: string;
infoLink: string;
presets?: ImportList[];
onImportListSelect: () => void;
}
function AddImportListItem({
implementation,
implementationName,
minRefreshInterval,
infoLink,
presets,
onImportListSelect,
}: AddImportListItemProps) {
const dispatch = useDispatch();
const hasPresets = !!(presets && presets.length);
const handleImportListSelect = useCallback(() => {
dispatch(
selectImportListSchema({
implementation,
implementationName,
})
);
onImportListSelect();
}, [implementation, implementationName, dispatch, onImportListSelect]);
return (
<div className={styles.list}>
<Link className={styles.underlay} onPress={handleImportListSelect} />
<div className={styles.overlay}>
<div className={styles.name}>{implementationName}</div>
<div className={styles.actions}>
{hasPresets && (
<span>
<Button size={sizes.SMALL} onPress={handleImportListSelect}>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
{translate('Presets')}
</Button>
<MenuContent>
{presets.map((preset) => {
return (
<AddImportListPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
minRefreshInterval={minRefreshInterval}
onPress={onImportListSelect}
/>
);
})}
</MenuContent>
</Menu>
</span>
)}
<Button to={infoLink} size={sizes.SMALL}>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
export default AddImportListItem;

View File

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

View File

@@ -0,0 +1,23 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddImportListModalContent, {
AddImportListModalContentProps,
} from './AddImportListModalContent';
interface AddImportListModalProps extends AddImportListModalContentProps {
isOpen: boolean;
}
function AddImportListModal({
isOpen,
onModalClose,
...otherProps
}: AddImportListModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<AddImportListModalContent {...otherProps} onModalClose={onModalClose} />
</Modal>
);
}
export default AddImportListModal;

View File

@@ -1,115 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import AddImportListItem from './AddImportListItem';
import styles from './AddImportListModalContent.css';
class AddImportListModalContent extends Component {
//
// Render
render() {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
listGroups,
onImportListSelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddImportList')}
</ModalHeader>
<ModalBody>
{
isSchemaFetching ?
<LoadingIndicator /> :
null
}
{
!isSchemaFetching && !!schemaError ?
<Alert kind={kinds.DANGER}>
{translate('AddListError')}
</Alert> :
null
}
{
isSchemaPopulated && !schemaError ?
<div>
<Alert kind={kinds.INFO}>
<div>
{translate('SupportedListsMovie')}
</div>
<div>
{translate('SupportedListsMoreInfo')}
</div>
</Alert>
{
Object.keys(listGroups).map((key) => {
return (
<FieldSet key={key} legend={translate('TypeOfList', {
typeOfList: titleCase(key)
})}
>
<div className={styles.lists}>
{
listGroups[key].map((list) => {
return (
<AddImportListItem
key={list.implementation}
implementation={list.implementation}
{...list}
onImportListSelect={onImportListSelect}
/>
);
})
}
</div>
</FieldSet>
);
})
}
</div> :
null
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddImportListModalContent.propTypes = {
isSchemaFetching: PropTypes.bool.isRequired,
isSchemaPopulated: PropTypes.bool.isRequired,
schemaError: PropTypes.object,
listGroups: PropTypes.object.isRequired,
onImportListSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddImportListModalContent;

View File

@@ -0,0 +1,108 @@
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
import ImportList from 'typings/ImportList';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
import AddImportListItem from './AddImportListItem';
import styles from './AddImportListModalContent.css';
export interface AddImportListModalContentProps {
onImportListSelect: () => void;
onModalClose: () => void;
}
function AddImportListModalContent({
onImportListSelect,
onModalClose,
}: AddImportListModalContentProps) {
const dispatch = useDispatch();
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
useSelector((state: AppState) => state.settings.importLists);
const listGroups = useMemo(() => {
const result = schema.reduce<Record<string, ImportList[]>>((acc, item) => {
if (!acc[item.listType]) {
acc[item.listType] = [];
}
acc[item.listType].push(item);
return acc;
}, {});
// Sort the lists by listOrder after grouping them
Object.keys(result).forEach((key) => {
result[key].sort((a, b) => {
return a.listOrder - b.listOrder;
});
});
return result;
}, [schema]);
useEffect(() => {
dispatch(fetchImportListSchema());
}, [dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AddImportList')}</ModalHeader>
<ModalBody>
{isSchemaFetching ? <LoadingIndicator /> : null}
{!isSchemaFetching && !!schemaError ? (
<Alert kind={kinds.DANGER}>{translate('AddListError')}</Alert>
) : null}
{isSchemaPopulated && !schemaError ? (
<div>
<Alert kind={kinds.INFO}>
<div>{translate('SupportedListsMovie')}</div>
<div>{translate('SupportedListsMoreInfo')}</div>
</Alert>
{Object.keys(listGroups).map((key) => {
return (
<FieldSet
key={key}
legend={translate('TypeOfList', {
typeOfList: titleCase(key),
})}
>
<div className={styles.lists}>
{listGroups[key].map((list) => {
return (
<AddImportListItem
key={list.implementation}
{...list}
implementation={list.implementation}
onImportListSelect={onImportListSelect}
/>
);
})}
</div>
</FieldSet>
);
})}
</div>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default AddImportListModalContent;

View File

@@ -1,76 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchImportListSchema, selectImportListSchema } from 'Store/Actions/settingsActions';
import AddImportListModalContent from './AddImportListModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.importLists,
(importLists) => {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
} = importLists;
const listGroups = _.chain(schema)
.sortBy((o) => o.listOrder)
.groupBy('listType')
.value();
return {
isSchemaFetching,
isSchemaPopulated,
schemaError,
listGroups
};
}
);
}
const mapDispatchToProps = {
fetchImportListSchema,
selectImportListSchema
};
class AddImportListModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchImportListSchema();
}
//
// Listeners
onImportListSelect = ({ implementation, implementationName, name, minRefreshInterval }) => {
this.props.selectImportListSchema({ implementation, implementationName, presetName: name, minRefreshInterval });
this.props.onModalClose({ listSelected: true });
};
//
// Render
render() {
return (
<AddImportListModalContent
{...this.props}
onImportListSelect={this.onImportListSelect}
/>
);
}
}
AddImportListModalContentConnector.propTypes = {
fetchImportListSchema: PropTypes.func.isRequired,
selectImportListSchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddImportListModalContentConnector);

View File

@@ -1,57 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddImportListPresetMenuItem extends Component {
//
// Listeners
onPress = () => {
const {
name,
implementation,
implementationName,
minRefreshInterval
} = this.props;
this.props.onPress({
name,
implementation,
implementationName,
minRefreshInterval
});
};
//
// Render
render() {
const {
name,
implementation,
implementationName,
minRefreshInterval,
...otherProps
} = this.props;
return (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddImportListPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
minRefreshInterval: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddImportListPresetMenuItem;

View File

@@ -0,0 +1,44 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem';
import { selectImportListSchema } from 'Store/Actions/settingsActions';
interface AddImportListPresetMenuItemProps
extends Omit<MenuItemProps, 'children'> {
name: string;
implementation: string;
implementationName: string;
minRefreshInterval: string;
onPress: () => void;
}
function AddImportListPresetMenuItem({
name,
implementation,
implementationName,
minRefreshInterval,
onPress,
...otherProps
}: AddImportListPresetMenuItemProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
dispatch(
selectImportListSchema({
implementation,
implementationName,
presetName: name,
})
);
onPress();
}, [name, implementation, implementationName, dispatch, onPress]);
return (
<MenuItem {...otherProps} onPress={handlePress}>
{name}
</MenuItem>
);
}
export default AddImportListPresetMenuItem;

View File

@@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import EditImportListModalContentConnector from './EditImportListModalContentConnector';
function EditImportListModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditImportListModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditImportListModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditImportListModal;

View File

@@ -0,0 +1,44 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
cancelSaveImportList,
cancelTestImportList,
} from 'Store/Actions/settingsActions';
import EditImportListModalContent, {
EditImportListModalContentProps,
} from './EditImportListModalContent';
const section = 'settings.importLists';
interface EditImportListModalProps extends EditImportListModalContentProps {
isOpen: boolean;
}
function EditImportListModal({
isOpen,
onModalClose,
...otherProps
}: EditImportListModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section }));
dispatch(cancelTestImportList({ section }));
dispatch(cancelSaveImportList({ section }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
<EditImportListModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditImportListModal;

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { cancelSaveImportList, cancelTestImportList } from 'Store/Actions/settingsActions';
import EditImportListModal from './EditImportListModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.importLists';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
},
dispatchCancelTestImportList() {
dispatch(cancelTestImportList({ section }));
},
dispatchCancelSaveImportList() {
dispatch(cancelSaveImportList({ section }));
}
};
}
class EditImportListModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.dispatchCancelTestImportList();
this.props.dispatchCancelSaveImportList();
this.props.onModalClose();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchCancelTestImportList,
dispatchCancelSaveImportList,
...otherProps
} = this.props;
return (
<EditImportListModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditImportListModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchCancelTestImportList: PropTypes.func.isRequired,
dispatchCancelSaveImportList: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditImportListModalConnector);

View File

@@ -1,308 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import styles from './EditImportListModalContent.css';
function EditImportListModalContent(props) {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onAdvancedSettingsPress,
onDeleteImportListPress,
...otherProps
} = props;
const {
id,
implementationName,
name,
enabled,
enableAuto,
minRefreshInterval,
monitor,
minimumAvailability,
qualityProfileId,
rootFolderPath,
searchOnAdd,
tags,
fields,
message
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? translate('EditImportListImplementation', { implementationName }) : translate('AddImportListImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{
isFetching ?
<LoadingIndicator /> :
null
}
{
!isFetching && !!error ?
<Alert kind={kinds.DANGER}>
{translate('AddListError')}
</Alert> :
null
}
{
!isFetching && !error ?
<Form
{...otherProps}
>
{
!!message &&
<Alert
className={styles.message}
kind={message.value.type}
>
{message.value.message}
</Alert>
}
<Alert
kind={kinds.INFO}
className={styles.message}
>
{translate('ListWillRefreshEveryInterval', {
refreshInterval: formatShortTimeSpan(minRefreshInterval.value)
})}
</Alert>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enabled"
helpText={translate('ListEnabledHelpText')}
{...enabled}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAuto"
helpText={translate('EnableAutomaticAddMovieHelpText')}
{...enableAuto}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Monitor')}</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor"
helpText={translate('ListMonitorMovieHelpText')}
{...monitor}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="searchOnAdd"
helpText={translate('ListSearchOnAddMovieHelpText')}
{...searchOnAdd}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...minimumAvailability}
onChange={onInputChange}
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
helpText={translate('ListQualityProfileHelpText')}
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
helpText={translate('ListRootFolderHelpText')}
{...rootFolderPath}
includeMissingValue={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RadarrTags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('ListTagsHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="importList"
providerData={item}
{...field}
onChange={onFieldChange}
/>
);
})
}
</Form> :
null
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteImportListPress}
>
{translate('Delete')}
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditImportListModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteImportListPress: PropTypes.func
};
export default EditImportListModalContent;

View File

@@ -0,0 +1,313 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import { ImportListAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import {
saveImportList,
setImportListFieldValue,
setImportListValue,
testImportList,
} from 'Store/Actions/settingsActions';
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
import ImportList from 'typings/ImportList';
import { InputChanged } from 'typings/inputs';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import styles from './EditImportListModalContent.css';
export interface EditImportListModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteImportListPress?: () => void;
}
function EditImportListModalContent({
id,
onModalClose,
onDeleteImportListPress,
}: EditImportListModalContentProps) {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
const {
isFetching,
isSaving,
isTesting = false,
error,
saveError,
item,
validationErrors,
validationWarnings,
} = useSelector(
createProviderSettingsSelectorHook<ImportList, ImportListAppState>(
'importLists',
id
)
);
const wasSaving = usePrevious(isSaving);
const {
implementationName,
name,
enabled,
enableAuto,
minRefreshInterval,
monitor,
minimumAvailability,
rootFolderPath,
qualityProfileId,
searchOnAdd,
tags,
fields,
} = item;
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setImportListValue(change));
},
[dispatch]
);
const handleFieldChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setImportListFieldValue(change));
},
[dispatch]
);
const handleTestPress = useCallback(() => {
dispatch(testImportList({ id }));
}, [id, dispatch]);
const handleSavePress = useCallback(() => {
dispatch(saveImportList({ id }));
}, [id, dispatch]);
useEffect(() => {
if (wasSaving && !isSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id
? translate('EditImportListImplementation', { implementationName })
: translate('AddImportListImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('AddListError')}</Alert>
) : null}
{!isFetching && !error ? (
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
<Alert kind={kinds.INFO} className={styles.message}>
{translate('ListWillRefreshEveryInterval', {
refreshInterval: formatShortTimeSpan(minRefreshInterval.value),
})}
</Alert>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enabled"
helpText={translate('ListEnabledHelpText')}
{...enabled}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAuto"
helpText={translate('EnableAutomaticAddMovieHelpText')}
{...enableAuto}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Monitor')}</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_MOVIES_SELECT}
name="monitor"
helpText={translate('ListMonitorMovieHelpText')}
{...monitor}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="searchOnAdd"
helpText={translate('ListSearchOnAddMovieHelpText')}
{...searchOnAdd}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon className={styles.labelIcon} name={icons.INFO} />
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...minimumAvailability}
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
helpText={translate('ListQualityProfileHelpText')}
{...qualityProfileId}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
helpText={translate('ListRootFolderHelpText')}
{...rootFolderPath}
includeMissingValue={true}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RadarrTags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('ListTagsHelpText')}
{...tags}
onChange={handleInputChange}
/>
</FormGroup>
{fields?.length ? (
<div>
{fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
{...field}
advancedSettings={showAdvancedSettings}
provider="importList"
providerData={item}
onChange={handleFieldChange}
/>
);
})}
</div>
) : null}
</Form>
) : null}
</ModalBody>
<ModalFooter>
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteImportListPress}
>
{translate('Delete')}
</Button>
) : null}
<AdvancedSettingsButton showLabel={false} />
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={handleTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
export default EditImportListModalContent;

View File

@@ -1,101 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
saveImportList,
setImportListFieldValue,
setImportListValue,
testImportList,
toggleAdvancedSettings
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditImportListModalContent from './EditImportListModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('importLists'),
(advancedSettings, importList) => {
return {
advancedSettings,
...importList
};
}
);
}
const mapDispatchToProps = {
setImportListValue,
setImportListFieldValue,
saveImportList,
testImportList,
toggleAdvancedSettings
};
class EditImportListModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setImportListValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setImportListFieldValue({ name, value });
};
onSavePress = () => {
this.props.saveImportList({ id: this.props.id });
};
onTestPress = () => {
this.props.testImportList({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
render() {
return (
<EditImportListModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditImportListModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setImportListValue: PropTypes.func.isRequired,
setImportListFieldValue: PropTypes.func.isRequired,
saveImportList: PropTypes.func.isRequired,
testImportList: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListModalContentConnector);

View File

@@ -4,6 +4,11 @@
width: 290px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
@@ -12,6 +17,12 @@
font-size: 24px;
}
.cloneButton {
composes: button from '~Components/Link/IconButton.css';
height: 36px;
}
.enabled {
display: flex;
flex-wrap: wrap;

View File

@@ -1,9 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'cloneButton': string;
'enabled': string;
'list': string;
'name': string;
'nameContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,143 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import EditImportListModalConnector from './EditImportListModalConnector';
import styles from './ImportList.css';
class ImportList extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditImportListModalOpen: false,
isDeleteImportListModalOpen: false
};
}
//
// Listeners
onEditImportListPress = () => {
this.setState({ isEditImportListModalOpen: true });
};
onEditImportListModalClose = () => {
this.setState({ isEditImportListModalOpen: false });
};
onDeleteImportListPress = () => {
this.setState({
isEditImportListModalOpen: false,
isDeleteImportListModalOpen: true
});
};
onDeleteImportListModalClose = () => {
this.setState({ isDeleteImportListModalOpen: false });
};
onConfirmDeleteImportList = () => {
this.props.onConfirmDeleteImportList(this.props.id);
};
//
// Render
render() {
const {
id,
name,
enabled,
enableAuto,
tags,
tagList,
minRefreshInterval
} = this.props;
return (
<Card
className={styles.list}
overlayContent={true}
onPress={this.onEditImportListPress}
>
<div className={styles.name}>
{name}
</div>
<div className={styles.enabled}>
{
enabled ?
<Label kind={kinds.SUCCESS}>
{translate('Enabled')}
</Label> :
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
{
enableAuto ?
<Label kind={kinds.SUCCESS}>
{translate('AutomaticAdd')}
</Label> :
null
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<div className={styles.enabled}>
<Label kind={kinds.DEFAULT} title='List Refresh Interval'>
{`${translate('Refresh')}: ${formatShortTimeSpan(minRefreshInterval)}`}
</Label>
</div>
<EditImportListModalConnector
id={id}
isOpen={this.state.isEditImportListModalOpen}
onModalClose={this.onEditImportListModalClose}
onDeleteImportListPress={this.onDeleteImportListPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteImportListModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteImportList}
onCancel={this.onDeleteImportListModalClose}
/>
</Card>
);
}
}
ImportList.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired,
enableAuto: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
minRefreshInterval: PropTypes.string.isRequired,
onConfirmDeleteImportList: PropTypes.func.isRequired
};
export default ImportList;

View File

@@ -0,0 +1,130 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props';
import { deleteImportList } from 'Store/Actions/settingsActions';
import useTags from 'Tags/useTags';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate';
import EditImportListModal from './EditImportListModal';
import styles from './ImportList.css';
interface ImportListProps {
id: number;
name: string;
enabled: boolean;
enableAuto: boolean;
tags: number[];
minRefreshInterval: string;
onCloneImportListPress: (id: number) => void;
}
function ImportList({
id,
name,
enabled,
enableAuto,
tags,
minRefreshInterval,
onCloneImportListPress,
}: ImportListProps) {
const dispatch = useDispatch();
const tagList = useTags();
const [isEditImportListModalOpen, setIsEditImportListModalOpen] =
useState(false);
const [isDeleteImportListModalOpen, setIsDeleteImportListModalOpen] =
useState(false);
const handleEditImportListPress = useCallback(() => {
setIsEditImportListModalOpen(true);
}, []);
const handleEditImportListModalClose = useCallback(() => {
setIsEditImportListModalOpen(false);
}, []);
const handleDeleteImportListPress = useCallback(() => {
setIsEditImportListModalOpen(false);
setIsDeleteImportListModalOpen(true);
}, []);
const handleDeleteImportListModalClose = useCallback(() => {
setIsDeleteImportListModalOpen(false);
}, []);
const handleConfirmDeleteImportList = useCallback(() => {
dispatch(deleteImportList({ id }));
}, [id, dispatch]);
const handleCloneImportListPress = useCallback(() => {
onCloneImportListPress(id);
}, [id, onCloneImportListPress]);
return (
<Card
className={styles.list}
overlayContent={true}
onPress={handleEditImportListPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>{name}</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneImportList')}
name={icons.CLONE}
onPress={handleCloneImportListPress}
/>
</div>
<div className={styles.enabled}>
{enabled ? (
<Label kind={kinds.SUCCESS}>{translate('Enabled')}</Label>
) : (
<Label kind={kinds.DISABLED} outline={true}>
{translate('Disabled')}
</Label>
)}
{enableAuto ? (
<Label kind={kinds.SUCCESS}>{translate('AutomaticAdd')}</Label>
) : null}
</div>
<TagList tags={tags} tagList={tagList} />
<div className={styles.enabled}>
<Label kind={kinds.DEFAULT} title={translate('ListRefreshInterval')}>
{`${translate('Refresh')}: ${formatShortTimeSpan(
minRefreshInterval
)}`}
</Label>
</div>
<EditImportListModal
id={id}
isOpen={isEditImportListModalOpen}
onModalClose={handleEditImportListModalClose}
onDeleteImportListPress={handleDeleteImportListPress}
/>
<ConfirmModal
isOpen={isDeleteImportListModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={handleConfirmDeleteImportList}
onCancel={handleDeleteImportListModalClose}
/>
</Card>
);
}
export default ImportList;

View File

@@ -1,118 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddImportListModal from './AddImportListModal';
import EditImportListModalConnector from './EditImportListModalConnector';
import ImportList from './ImportList';
import styles from './ImportLists.css';
class ImportLists extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddImportListModalOpen: false,
isEditImportListModalOpen: false
};
}
//
// Listeners
onAddImportListPress = () => {
this.setState({ isAddImportListModalOpen: true });
};
onAddImportListModalClose = ({ listSelected = false } = {}) => {
this.setState({
isAddImportListModalOpen: false,
isEditImportListModalOpen: listSelected
});
};
onEditImportListModalClose = () => {
this.setState({ isEditImportListModalOpen: false });
};
//
// Render
render() {
const {
items,
tagList,
onConfirmDeleteImportList,
...otherProps
} = this.props;
const {
isAddImportListModalOpen,
isEditImportListModalOpen
} = this.state;
return (
<FieldSet legend={translate('ImportLists')} >
<PageSectionContent
errorMessage={translate('ImportListsLoadError')}
{...otherProps}
>
<div className={styles.lists}>
{
items.map((item) => {
return (
<ImportList
key={item.id}
{...item}
tagList={tagList}
onConfirmDeleteImportList={onConfirmDeleteImportList}
/>
);
})
}
<Card
className={styles.addList}
onPress={this.onAddImportListPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<AddImportListModal
isOpen={isAddImportListModalOpen}
onModalClose={this.onAddImportListModalClose}
/>
<EditImportListModalConnector
isOpen={isEditImportListModalOpen}
onModalClose={this.onEditImportListModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
ImportLists.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteImportList: PropTypes.func.isRequired
};
export default ImportLists;

View File

@@ -0,0 +1,109 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ImportListAppState } from 'App/State/SettingsAppState';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import {
cloneImportList,
fetchImportLists,
} from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import ImportListModel from 'typings/ImportList';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import AddImportListModal from './AddImportListModal';
import EditImportListModal from './EditImportListModal';
import ImportList from './ImportList';
import styles from './ImportLists.css';
function ImportLists() {
const dispatch = useDispatch();
const { isFetching, isPopulated, items, error } = useSelector(
createSortedSectionSelector<ImportListModel, ImportListAppState>(
'settings.importLists',
sortByProp('name')
)
);
const [isAddImportListModalOpen, setIsAddImportListModalOpen] =
useState(false);
const [isEditImportListModalOpen, setIsEditImportListModalOpen] =
useState(false);
const handleAddImportListPress = useCallback(() => {
setIsAddImportListModalOpen(true);
}, []);
const handleAddImportListModalClose = useCallback(() => {
setIsAddImportListModalOpen(false);
}, []);
const handleImportListSelect = useCallback(() => {
setIsAddImportListModalOpen(false);
setIsEditImportListModalOpen(true);
}, []);
const handleEditImportListModalClose = useCallback(() => {
setIsEditImportListModalOpen(false);
}, []);
const handleCloneImportListPress = useCallback(
(id: number) => {
dispatch(cloneImportList({ id }));
setIsEditImportListModalOpen(true);
},
[dispatch]
);
useEffect(() => {
dispatch(fetchImportLists());
dispatch(fetchRootFolders());
}, [dispatch]);
return (
<FieldSet legend={translate('ImportLists')}>
<PageSectionContent
errorMessage={translate('ImportListsLoadError')}
error={error}
isFetching={isFetching}
isPopulated={isPopulated}
>
<div className={styles.lists}>
{items.map((item) => {
return (
<ImportList
key={item.id}
{...item}
onCloneImportListPress={handleCloneImportListPress}
/>
);
})}
<Card className={styles.addList} onPress={handleAddImportListPress}>
<div className={styles.center}>
<Icon name={icons.ADD} size={45} />
</div>
</Card>
</div>
<AddImportListModal
isOpen={isAddImportListModalOpen}
onImportListSelect={handleImportListSelect}
onModalClose={handleAddImportListModalClose}
/>
<EditImportListModal
isOpen={isEditImportListModalOpen}
onModalClose={handleEditImportListModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
export default ImportLists;

View File

@@ -1,67 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import ImportLists from './ImportLists';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.importLists', sortByProp('name')),
createTagsSelector(),
(importLists, tagList) => {
return {
...importLists,
tagList
};
}
);
}
const mapDispatchToProps = {
fetchImportLists,
deleteImportList,
fetchRootFolders
};
class ImportListsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchImportLists();
this.props.fetchRootFolders();
}
//
// Listeners
onConfirmDeleteImportList = (id) => {
this.props.deleteImportList({ id });
};
//
// Render
render() {
return (
<ImportLists
{...this.props}
onConfirmDeleteImportList={this.onConfirmDeleteImportList}
/>
);
}
}
ImportListsConnector.propTypes = {
fetchImportLists: PropTypes.func.isRequired,
deleteImportList: PropTypes.func.isRequired,
fetchRootFolders: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListsConnector);

View File

@@ -1,14 +1,14 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
@@ -20,53 +20,62 @@ import createSettingsSectionSelector from 'Store/Selectors/createSettingsSection
import translate from 'Utilities/String/translate';
const SECTION = 'importListOptions';
const cleanLibraryLevelOptions = [
{ key: 'disabled', value: () => translate('Disabled') },
{ key: 'logOnly', value: () => translate('LogOnly') },
{ key: 'keepAndUnmonitor', value: () => translate('KeepAndUnmonitorMovie') },
{ key: 'removeAndKeep', value: () => translate('RemoveMovieAndKeepFiles') },
const cleanLibraryLevelOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
{
key: 'logOnly',
get value() {
return translate('LogOnly');
},
},
{
key: 'keepAndUnmonitor',
get value() {
return translate('KeepAndUnmonitorMovie');
},
},
{
key: 'removeAndKeep',
get value() {
return translate('RemoveMovieAndKeepFiles');
},
},
{
key: 'removeAndDelete',
value: () => translate('RemoveMovieAndDeleteFiles'),
get value() {
return translate('RemoveMovieAndDeleteFiles');
},
},
];
function createImportListOptionsSelector() {
return createSelector(
(state: AppState) => state.settings.advancedSettings,
createSettingsSectionSelector(SECTION),
(advancedSettings, sectionSettings) => {
return {
advancedSettings,
save: sectionSettings.isSaving,
...sectionSettings,
};
}
);
}
interface ImportListOptionsPageProps {
interface ImportListOptionsProps {
setChildSave(saveCallback: () => void): void;
onChildStateChange(payload: unknown): void;
}
function ImportListOptions(props: ImportListOptionsPageProps) {
const { setChildSave, onChildStateChange } = props;
function ImportListOptions({
setChildSave,
onChildStateChange,
}: ImportListOptionsProps) {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
const {
isSaving,
hasPendingChanges,
advancedSettings,
isFetching,
error,
settings,
hasSettings,
} = useSelector(createImportListOptionsSelector());
} = useSelector(createSettingsSectionSelector(SECTION));
const { listSyncLevel } = settings;
const dispatch = useDispatch();
const onInputChange = useCallback(
({ name, value }: { name: string; value: unknown }) => {
// @ts-expect-error 'setImportListOptionsValue' isn't typed yet
@@ -80,7 +89,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
setChildSave(() => dispatch(saveImportListOptions()));
return () => {
dispatch(clearPendingChanges({ section: SECTION }));
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
};
}, [dispatch, setChildSave]);
@@ -91,16 +100,11 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
});
}, [onChildStateChange, isSaving, hasPendingChanges]);
const translatedLevelOptions = cleanLibraryLevelOptions.map(
({ key, value }) => {
return {
key,
value: value(),
};
}
);
if (!showAdvancedSettings) {
return null;
}
return advancedSettings ? (
return (
<FieldSet legend={translate('Options')}>
{isFetching ? <LoadingIndicator /> : null}
@@ -110,12 +114,12 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
{hasSettings && !isFetching && !error ? (
<Form>
<FormGroup advancedSettings={advancedSettings} isAdvanced={true}>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('CleanLibraryLevel')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="listSyncLevel"
values={translatedLevelOptions}
values={cleanLibraryLevelOptions}
helpText={translate('ListSyncLevelHelpText')}
onChange={onInputChange}
{...listSyncLevel}
@@ -124,7 +128,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
</Form>
) : null}
</FieldSet>
) : null;
);
}
export default ImportListOptions;

View File

@@ -1,120 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import IndexersConnector from './Indexers/IndexersConnector';
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
import IndexerOptionsConnector from './Options/IndexerOptionsConnector';
class IndexerSettings extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false,
isManageIndexersOpen: false
};
}
//
// Listeners
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
};
onChildStateChange = (payload) => {
this.setState(payload);
};
onManageIndexersPress = () => {
this.setState({ isManageIndexersOpen: true });
};
onManageIndexersModalClose = () => {
this.setState({ isManageIndexersOpen: false });
};
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
}
};
//
// Render
render() {
const {
isTestingAll,
dispatchTestAllIndexers
} = this.props;
const {
isSaving,
hasPendingChanges,
isManageIndexersOpen
} = this.state;
return (
<PageContent title={translate('IndexerSettings')}>
<SettingsToolbarConnector
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={dispatchTestAllIndexers}
/>
<PageToolbarButton
label={translate('ManageIndexers')}
iconName={icons.MANAGE}
onPress={this.onManageIndexersPress}
/>
</Fragment>
}
onSavePress={this.onSavePress}
/>
<PageContentBody>
<IndexersConnector />
<IndexerOptionsConnector
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<ManageIndexersModal
isOpen={isManageIndexersOpen}
onModalClose={this.onManageIndexersModalClose}
/>
</PageContentBody>
</PageContent>
);
}
}
IndexerSettings.propTypes = {
isTestingAll: PropTypes.bool.isRequired,
dispatchTestAllIndexers: PropTypes.func.isRequired
};
export default IndexerSettings;

View File

@@ -0,0 +1,104 @@
import React, { useCallback, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbar from 'Settings/SettingsToolbar';
import { testAllIndexers } from 'Store/Actions/settingsActions';
import {
SaveCallback,
SettingsStateChange,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
import Indexers from './Indexers/Indexers';
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
import IndexerOptions from './Options/IndexerOptions';
function IndexerSettings() {
const dispatch = useDispatch();
const isTestingAll = useSelector(
(state: AppState) => state.settings.indexers.isTestingAll
);
const saveOptions = useRef<() => void>();
const [isSaving, setIsSaving] = useState(false);
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const [isManageIndexersModalOpen, setIsManageIndexersModalOpen] =
useState(false);
const handleSetChildSave = useCallback((saveCallback: SaveCallback) => {
saveOptions.current = saveCallback;
}, []);
const handleChildStateChange = useCallback(
({ isSaving, hasPendingChanges }: SettingsStateChange) => {
setIsSaving(isSaving);
setHasPendingChanges(hasPendingChanges);
},
[]
);
const handleManageIndexersPress = useCallback(() => {
setIsManageIndexersModalOpen(true);
}, []);
const handleManageIndexersModalClose = useCallback(() => {
setIsManageIndexersModalOpen(false);
}, []);
const handleSavePress = useCallback(() => {
saveOptions.current?.();
}, []);
const handleTestAllIndexersPress = useCallback(() => {
dispatch(testAllIndexers());
}, [dispatch]);
return (
<PageContent title={translate('IndexerSettings')}>
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={handleTestAllIndexersPress}
/>
<PageToolbarButton
label={translate('ManageIndexers')}
iconName={icons.MANAGE}
onPress={handleManageIndexersPress}
/>
</>
}
onSavePress={handleSavePress}
/>
<PageContentBody>
<Indexers />
<IndexerOptions
setChildSave={handleSetChildSave}
onChildStateChange={handleChildStateChange}
/>
<ManageIndexersModal
isOpen={isManageIndexersModalOpen}
onModalClose={handleManageIndexersModalClose}
/>
</PageContentBody>
</PageContent>
);
}
export default IndexerSettings;

View File

@@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { testAllIndexers } from 'Store/Actions/settingsActions';
import IndexerSettings from './IndexerSettings';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers.isTestingAll,
(isTestingAll) => {
return {
isTestingAll
};
}
);
}
const mapDispatchToProps = {
dispatchTestAllIndexers: testAllIndexers
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSettings);

View File

@@ -1,113 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem';
import styles from './AddIndexerItem.css';
class AddIndexerItem extends Component {
//
// Listeners
onIndexerSelect = () => {
const {
implementation,
implementationName
} = this.props;
this.props.onIndexerSelect({ implementation, implementationName });
};
//
// Render
render() {
const {
implementation,
implementationName,
infoLink,
presets,
onIndexerSelect
} = this.props;
const hasPresets = !!presets && !!presets.length;
return (
<div
className={styles.indexer}
>
<Link
className={styles.underlay}
onPress={this.onIndexerSelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onIndexerSelect}
>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
{translate('Presets')}
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddIndexerPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
onPress={onIndexerSelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
}
AddIndexerItem.propTypes = {
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired,
presets: PropTypes.arrayOf(PropTypes.object),
onIndexerSelect: PropTypes.func.isRequired
};
export default AddIndexerItem;

View File

@@ -0,0 +1,88 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import { selectIndexerSchema } from 'Store/Actions/settingsActions';
import Indexer from 'typings/Indexer';
import translate from 'Utilities/String/translate';
import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem';
import styles from './AddIndexerItem.css';
interface AddIndexerItemProps {
implementation: string;
implementationName: string;
infoLink: string;
presets?: Indexer[];
onIndexerSelect: () => void;
}
function AddIndexerItem({
implementation,
implementationName,
infoLink,
presets,
onIndexerSelect,
}: AddIndexerItemProps) {
const dispatch = useDispatch();
const hasPresets = !!presets && !!presets.length;
const handleIndexerSelect = useCallback(() => {
dispatch(
selectIndexerSchema({
implementation,
implementationName,
})
);
onIndexerSelect();
}, [implementation, implementationName, dispatch, onIndexerSelect]);
return (
<div className={styles.indexer}>
<Link className={styles.underlay} onPress={handleIndexerSelect} />
<div className={styles.overlay}>
<div className={styles.name}>{implementationName}</div>
<div className={styles.actions}>
{hasPresets && (
<span>
<Button size={sizes.SMALL} onPress={handleIndexerSelect}>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
{translate('Presets')}
</Button>
<MenuContent>
{presets.map((preset) => {
return (
<AddIndexerPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
onPress={onIndexerSelect}
/>
);
})}
</MenuContent>
</Menu>
</span>
)}
<Button to={infoLink} size={sizes.SMALL}>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
export default AddIndexerItem;

View File

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

View File

@@ -0,0 +1,26 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddIndexerModalContent from './AddIndexerModalContent';
interface AddIndexerModalProps {
isOpen: boolean;
onIndexerSelect: () => void;
onModalClose: () => void;
}
function AddIndexerModal({
isOpen,
onIndexerSelect,
onModalClose,
}: AddIndexerModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<AddIndexerModalContent
onIndexerSelect={onIndexerSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default AddIndexerModal;

View File

@@ -1,122 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerItem from './AddIndexerItem';
import styles from './AddIndexerModalContent.css';
class AddIndexerModalContent extends Component {
//
// Render
render() {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
usenetIndexers,
torrentIndexers,
onIndexerSelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddIndexer')}
</ModalHeader>
<ModalBody>
{
isSchemaFetching &&
<LoadingIndicator />
}
{
!isSchemaFetching && !!schemaError &&
<Alert kind={kinds.DANGER}>
{translate('AddIndexerError')}
</Alert>
}
{
isSchemaPopulated && !schemaError &&
<div>
<Alert kind={kinds.INFO}>
<div>
{translate('SupportedIndexers')}
</div>
<div>
{translate('SupportedIndexersMoreInfo')}
</div>
</Alert>
<FieldSet legend={translate('Usenet')}>
<div className={styles.indexers}>
{
usenetIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
);
})
}
</div>
</FieldSet>
<FieldSet legend={translate('Torrents')}>
<div className={styles.indexers}>
{
torrentIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
);
})
}
</div>
</FieldSet>
</div>
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddIndexerModalContent.propTypes = {
isSchemaFetching: PropTypes.bool.isRequired,
isSchemaPopulated: PropTypes.bool.isRequired,
schemaError: PropTypes.object,
usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
onIndexerSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerModalContent;

View File

@@ -0,0 +1,116 @@
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import { fetchIndexerSchema } from 'Store/Actions/settingsActions';
import Indexer from 'typings/Indexer';
import translate from 'Utilities/String/translate';
import AddIndexerItem from './AddIndexerItem';
import styles from './AddIndexerModalContent.css';
interface AddIndexerModalContentProps {
onIndexerSelect: () => void;
onModalClose: () => void;
}
function AddIndexerModalContent({
onIndexerSelect,
onModalClose,
}: AddIndexerModalContentProps) {
const dispatch = useDispatch();
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
useSelector((state: AppState) => state.settings.indexers);
const { usenetIndexers, torrentIndexers } = useMemo(() => {
return schema.reduce<{
usenetIndexers: Indexer[];
torrentIndexers: Indexer[];
}>(
(acc, item) => {
if (item.protocol === 'usenet') {
acc.usenetIndexers.push(item);
} else if (item.protocol === 'torrent') {
acc.torrentIndexers.push(item);
}
return acc;
},
{
usenetIndexers: [],
torrentIndexers: [],
}
);
}, [schema]);
useEffect(() => {
dispatch(fetchIndexerSchema());
}, [dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
<ModalBody>
{isSchemaFetching ? <LoadingIndicator /> : null}
{!isSchemaFetching && !!schemaError ? (
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
) : null}
{isSchemaPopulated && !schemaError ? (
<div>
<Alert kind={kinds.INFO}>
<div>{translate('SupportedIndexers')}</div>
<div>{translate('SupportedIndexersMoreInfo')}</div>
</Alert>
<FieldSet legend={translate('Usenet')}>
<div className={styles.indexers}>
{usenetIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
{...indexer}
implementation={indexer.implementation}
onIndexerSelect={onIndexerSelect}
/>
);
})}
</div>
</FieldSet>
<FieldSet legend={translate('Torrents')}>
<div className={styles.indexers}>
{torrentIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
{...indexer}
implementation={indexer.implementation}
onIndexerSelect={onIndexerSelect}
/>
);
})}
</div>
</FieldSet>
</div>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default AddIndexerModalContent;

View File

@@ -1,75 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerSchema, selectIndexerSchema } from 'Store/Actions/settingsActions';
import AddIndexerModalContent from './AddIndexerModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers,
(indexers) => {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
} = indexers;
const usenetIndexers = _.filter(schema, { protocol: 'usenet' });
const torrentIndexers = _.filter(schema, { protocol: 'torrent' });
return {
isSchemaFetching,
isSchemaPopulated,
schemaError,
usenetIndexers,
torrentIndexers
};
}
);
}
const mapDispatchToProps = {
fetchIndexerSchema,
selectIndexerSchema
};
class AddIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexerSchema();
}
//
// Listeners
onIndexerSelect = ({ implementation, implementationName, name }) => {
this.props.selectIndexerSchema({ implementation, implementationName, presetName: name });
this.props.onModalClose({ indexerSelected: true });
};
//
// Render
render() {
return (
<AddIndexerModalContent
{...this.props}
onIndexerSelect={this.onIndexerSelect}
/>
);
}
}
AddIndexerModalContentConnector.propTypes = {
fetchIndexerSchema: PropTypes.func.isRequired,
selectIndexerSchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);

View File

@@ -1,53 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddIndexerPresetMenuItem extends Component {
//
// Listeners
onPress = () => {
const {
name,
implementation,
implementationName
} = this.props;
this.props.onPress({
name,
implementation,
implementationName
});
};
//
// Render
render() {
const {
name,
implementation,
implementationName,
...otherProps
} = this.props;
return (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddIndexerPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddIndexerPresetMenuItem;

View File

@@ -0,0 +1,42 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem';
import { selectIndexerSchema } from 'Store/Actions/settingsActions';
interface AddIndexerPresetMenuItemProps
extends Omit<MenuItemProps, 'children'> {
name: string;
implementation: string;
implementationName: string;
onPress: () => void;
}
function AddIndexerPresetMenuItem({
name,
implementation,
implementationName,
onPress,
...otherProps
}: AddIndexerPresetMenuItemProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
dispatch(
selectIndexerSchema({
implementation,
implementationName,
presetName: name,
})
);
onPress();
}, [name, implementation, implementationName, dispatch, onPress]);
return (
<MenuItem {...otherProps} onPress={handlePress}>
{name}
</MenuItem>
);
}
export default AddIndexerPresetMenuItem;

View File

@@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import EditIndexerModalContentConnector from './EditIndexerModalContentConnector';
function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditIndexerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditIndexerModal;

View File

@@ -0,0 +1,45 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
cancelSaveIndexer,
cancelTestIndexer,
} from 'Store/Actions/settingsActions';
import EditIndexerModalContent, {
EditIndexerModalContentProps,
} from './EditIndexerModalContent';
const section = 'settings.indexers';
interface EditIndexerModalProps extends EditIndexerModalContentProps {
isOpen: boolean;
}
function EditIndexerModal({
isOpen,
onModalClose,
...otherProps
}: EditIndexerModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section }));
dispatch(cancelTestIndexer({ section }));
dispatch(cancelSaveIndexer({ section }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditIndexerModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditIndexerModal;

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { cancelSaveIndexer, cancelTestIndexer } from 'Store/Actions/settingsActions';
import EditIndexerModal from './EditIndexerModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.indexers';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
},
dispatchCancelTestIndexer() {
dispatch(cancelTestIndexer({ section }));
},
dispatchCancelSaveIndexer() {
dispatch(cancelSaveIndexer({ section }));
}
};
}
class EditIndexerModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.dispatchCancelTestIndexer();
this.props.dispatchCancelSaveIndexer();
this.props.onModalClose();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchCancelTestIndexer,
dispatchCancelSaveIndexer,
...otherProps
} = this.props;
return (
<EditIndexerModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditIndexerModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchCancelTestIndexer: PropTypes.func.isRequired,
dispatchCancelSaveIndexer: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditIndexerModalConnector);

View File

@@ -1,255 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
function EditIndexerModalContent(props) {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onDeleteIndexerPress,
onAdvancedSettingsPress,
...otherProps
} = props;
const {
id,
implementationName,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
supportsRss,
supportsSearch,
tags,
fields,
priority,
protocol,
downloadClientId
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? translate('EditIndexerImplementation', { implementationName }) : translate('AddIndexerImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('AddIndexerError')}
</Alert>
}
{
!isFetching && !error &&
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableRss')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableRss"
helpText={supportsRss.value ? translate('EnableRssHelpText') : undefined}
helpTextWarning={supportsRss.value ? undefined : translate('RssIsNotSupportedWithThisIndexer')}
isDisabled={!supportsRss.value}
{...enableRss}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticSearch"
helpText={supportsSearch.value ? translate('EnableAutomaticSearchHelpText') : undefined}
helpTextWarning={supportsSearch.value ? undefined : translate('SearchIsNotSupportedWithThisIndexer')}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
helpText={supportsSearch.value ? translate('EnableInteractiveSearchHelpText') : undefined}
helpTextWarning={supportsSearch.value ? undefined : translate('SearchIsNotSupportedWithThisIndexer')}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={onFieldChange}
/>
);
})
}
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('IndexerPriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagMovieHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
{translate('Delete')}
</Button>
}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditIndexerModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired,
onDeleteIndexerPress: PropTypes.func
};
export default EditIndexerModalContent;

View File

@@ -0,0 +1,299 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IndexerAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import {
saveIndexer,
setIndexerFieldValue,
setIndexerValue,
testIndexer,
} from 'Store/Actions/settingsActions';
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
import Indexer from 'typings/Indexer';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
export interface EditIndexerModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteIndexerPress?: () => void;
}
function EditIndexerModalContent({
id,
onModalClose,
onDeleteIndexerPress,
}: EditIndexerModalContentProps) {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
const {
isFetching,
error,
isSaving,
isTesting = false,
saveError,
item,
validationErrors,
validationWarnings,
} = useSelector(
createProviderSettingsSelectorHook<Indexer, IndexerAppState>('indexers', id)
);
const wasSaving = usePrevious(isSaving);
const {
implementationName = '',
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
supportsRss,
supportsSearch,
tags,
fields,
priority,
protocol,
downloadClientId,
} = item;
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setIndexerValue(change));
},
[dispatch]
);
const handleFieldChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setIndexerFieldValue(change));
},
[dispatch]
);
const handleSavePress = useCallback(() => {
dispatch(saveIndexer({ id }));
}, [id, dispatch]);
const handleTestPress = useCallback(() => {
dispatch(testIndexer({ id }));
}, [id, dispatch]);
useEffect(() => {
if (!isSaving && wasSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id
? translate('EditIndexerImplementation', { implementationName })
: translate('AddIndexerImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
) : null}
{!isFetching && !error ? (
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableRss')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableRss"
helpText={
supportsRss.value ? translate('EnableRssHelpText') : undefined
}
helpTextWarning={
supportsRss.value
? undefined
: translate('RssIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsRss.value}
{...enableRss}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticSearch"
helpText={
supportsSearch.value
? translate('EnableAutomaticSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
helpText={
supportsSearch.value
? translate('EnableInteractiveSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}
onChange={handleInputChange}
/>
</FormGroup>
{fields?.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={showAdvancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={handleFieldChange}
/>
);
})}
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('IndexerPriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagMovieHelpText')}
{...tags}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
) : null}
</ModalBody>
<ModalFooter>
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
{translate('Delete')}
</Button>
) : null}
<AdvancedSettingsButton showLabel={false} />
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={handleTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
export default EditIndexerModalContent;

View File

@@ -1,95 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer, toggleAdvancedSettings } from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditIndexerModalContent from './EditIndexerModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('indexers'),
(advancedSettings, indexer) => {
return {
advancedSettings,
...indexer
};
}
);
}
const mapDispatchToProps = {
setIndexerValue,
setIndexerFieldValue,
saveIndexer,
testIndexer,
toggleAdvancedSettings
};
class EditIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setIndexerValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setIndexerFieldValue({ name, value });
};
onSavePress = () => {
this.props.saveIndexer({ id: this.props.id });
};
onTestPress = () => {
this.props.testIndexer({ id: this.props.id });
};
onAdvancedSettingsPress = () => {
this.props.toggleAdvancedSettings();
};
//
// Render
render() {
return (
<EditIndexerModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditIndexerModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setIndexerValue: PropTypes.func.isRequired,
setIndexerFieldValue: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,
saveIndexer: PropTypes.func.isRequired,
testIndexer: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector);

View File

@@ -1,181 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditIndexerModalConnector from './EditIndexerModalConnector';
import styles from './Indexer.css';
class Indexer extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditIndexerModalOpen: false,
isDeleteIndexerModalOpen: false
};
}
//
// Listeners
onEditIndexerPress = () => {
this.setState({ isEditIndexerModalOpen: true });
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
onDeleteIndexerPress = () => {
this.setState({
isEditIndexerModalOpen: false,
isDeleteIndexerModalOpen: true
});
};
onDeleteIndexerModalClose = () => {
this.setState({ isDeleteIndexerModalOpen: false });
};
onConfirmDeleteIndexer = () => {
this.props.onConfirmDeleteIndexer(this.props.id);
};
onCloneIndexerPress = () => {
const {
id,
onCloneIndexerPress
} = this.props;
onCloneIndexerPress(id);
};
//
// Render
render() {
const {
id,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
tags,
tagList,
supportsRss,
supportsSearch,
priority,
showPriority
} = this.props;
return (
<Card
className={styles.indexer}
overlayContent={true}
onPress={this.onEditIndexerPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneIndexer')}
name={icons.CLONE}
onPress={this.onCloneIndexerPress}
/>
</div>
<div className={styles.enabled}>
{
supportsRss && enableRss &&
<Label kind={kinds.SUCCESS}>
{translate('Rss')}
</Label>
}
{
supportsSearch && enableAutomaticSearch &&
<Label kind={kinds.SUCCESS}>
{translate('AutomaticSearch')}
</Label>
}
{
supportsSearch && enableInteractiveSearch &&
<Label kind={kinds.SUCCESS}>
{translate('InteractiveSearch')}
</Label>
}
{
showPriority &&
<Label kind={kinds.DEFAULT}>
{translate('Priority')}: {priority}
</Label>
}
{
!enableRss && !enableAutomaticSearch && !enableInteractiveSearch &&
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<EditIndexerModalConnector
id={id}
isOpen={this.state.isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
onDeleteIndexerPress={this.onDeleteIndexerPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteIndexerModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexer}
onCancel={this.onDeleteIndexerModalClose}
/>
</Card>
);
}
}
Indexer.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enableRss: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired,
enableInteractiveSearch: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
supportsRss: PropTypes.bool.isRequired,
supportsSearch: PropTypes.bool.isRequired,
onCloneIndexerPress: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired,
priority: PropTypes.number.isRequired,
showPriority: PropTypes.bool.isRequired
};
export default Indexer;

View File

@@ -0,0 +1,131 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props';
import { deleteIndexer } from 'Store/Actions/settingsActions';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import IndexerModel from 'typings/Indexer';
import translate from 'Utilities/String/translate';
import EditIndexerModal from './EditIndexerModal';
import styles from './Indexer.css';
interface IndexerProps extends IndexerModel {
showPriority: boolean;
onCloneIndexerPress: (id: number) => void;
}
function Indexer({
id,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
tags,
supportsRss,
supportsSearch,
priority,
showPriority,
onCloneIndexerPress,
}: IndexerProps) {
const dispatch = useDispatch();
const tagList = useSelector(createTagsSelector());
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
useState(false);
const handleEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true);
}, []);
const handleEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, []);
const handleDeleteIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(false);
setIsDeleteIndexerModalOpen(true);
}, []);
const handleDeleteIndexerModalClose = useCallback(() => {
setIsDeleteIndexerModalOpen(false);
}, []);
const handleConfirmDeleteIndexer = useCallback(() => {
dispatch(deleteIndexer({ id }));
}, [id, dispatch]);
const handleCloneIndexerPress = useCallback(() => {
onCloneIndexerPress(id);
}, [id, onCloneIndexerPress]);
return (
<Card
className={styles.indexer}
overlayContent={true}
onPress={handleEditIndexerPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>{name}</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneIndexer')}
name={icons.CLONE}
onPress={handleCloneIndexerPress}
/>
</div>
<div className={styles.enabled}>
{supportsRss && enableRss ? (
<Label kind={kinds.SUCCESS}>{translate('Rss')}</Label>
) : null}
{supportsSearch && enableAutomaticSearch ? (
<Label kind={kinds.SUCCESS}>{translate('AutomaticSearch')}</Label>
) : null}
{supportsSearch && enableInteractiveSearch ? (
<Label kind={kinds.SUCCESS}>{translate('InteractiveSearch')}</Label>
) : null}
{showPriority ? (
<Label kind={kinds.DEFAULT}>
{translate('Priority')}: {priority}
</Label>
) : null}
{!enableRss && !enableAutomaticSearch && !enableInteractiveSearch ? (
<Label kind={kinds.DISABLED} outline={true}>
{translate('Disabled')}
</Label>
) : null}
</div>
<TagList tags={tags} tagList={tagList} />
<EditIndexerModal
id={id}
isOpen={isEditIndexerModalOpen}
onModalClose={handleEditIndexerModalClose}
onDeleteIndexerPress={handleDeleteIndexerPress}
/>
<ConfirmModal
isOpen={isDeleteIndexerModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={handleConfirmDeleteIndexer}
onCancel={handleDeleteIndexerModalClose}
/>
</Card>
);
}
export default Indexer;

View File

@@ -1,129 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerModal from './AddIndexerModal';
import EditIndexerModalConnector from './EditIndexerModalConnector';
import Indexer from './Indexer';
import styles from './Indexers.css';
class Indexers extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: false
};
}
//
// Listeners
onAddIndexerPress = () => {
this.setState({ isAddIndexerModalOpen: true });
};
onCloneIndexerPress = (id) => {
this.props.dispatchCloneIndexer({ id });
this.setState({ isEditIndexerModalOpen: true });
};
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
this.setState({
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: indexerSelected
});
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
//
// Render
render() {
const {
items,
tagList,
dispatchCloneIndexer,
onConfirmDeleteIndexer,
...otherProps
} = this.props;
const {
isAddIndexerModalOpen,
isEditIndexerModalOpen
} = this.state;
const showPriority = items.some((index) => index.priority !== 25);
return (
<FieldSet legend={translate('Indexers')}>
<PageSectionContent
errorMessage={translate('IndexersLoadError')}
{...otherProps}
>
<div className={styles.indexers}>
{
items.map((item) => {
return (
<Indexer
key={item.id}
{...item}
tagList={tagList}
showPriority={showPriority}
onCloneIndexerPress={this.onCloneIndexerPress}
onConfirmDeleteIndexer={onConfirmDeleteIndexer}
/>
);
})
}
<Card
className={styles.addIndexer}
onPress={this.onAddIndexerPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onModalClose={this.onAddIndexerModalClose}
/>
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
Indexers.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired
};
export default Indexers;

View File

@@ -0,0 +1,105 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IndexerAppState } from 'App/State/SettingsAppState';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import { cloneIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import IndexerModel from 'typings/Indexer';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import AddIndexerModal from './AddIndexerModal';
import EditIndexerModal from './EditIndexerModal';
import Indexer from './Indexer';
import styles from './Indexers.css';
function Indexers() {
const dispatch = useDispatch();
const { isFetching, isPopulated, items, error } = useSelector(
createSortedSectionSelector<IndexerModel, IndexerAppState>(
'settings.indexers',
sortByProp('name')
)
);
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const showPriority = items.some((index) => index.priority !== 25);
const handleAddIndexerPress = useCallback(() => {
setIsAddIndexerModalOpen(true);
}, []);
const handleCloneIndexerPress = useCallback(
(id: number) => {
dispatch(cloneIndexer({ id }));
setIsEditIndexerModalOpen(true);
},
[dispatch]
);
const handleIndexerSelect = useCallback(() => {
setIsAddIndexerModalOpen(false);
setIsEditIndexerModalOpen(true);
}, []);
const handleAddIndexerModalClose = useCallback(() => {
setIsAddIndexerModalOpen(false);
}, []);
const handleEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, []);
useEffect(() => {
dispatch(fetchIndexers());
}, [dispatch]);
return (
<FieldSet legend={translate('Indexers')}>
<PageSectionContent
errorMessage={translate('IndexersLoadError')}
error={error}
isFetching={isFetching}
isPopulated={isPopulated}
>
<div className={styles.indexers}>
{items.map((item) => {
return (
<Indexer
key={item.id}
{...item}
showPriority={showPriority}
onCloneIndexerPress={handleCloneIndexerPress}
/>
);
})}
<Card className={styles.addIndexer} onPress={handleAddIndexerPress}>
<div className={styles.center}>
<Icon name={icons.ADD} size={45} />
</div>
</Card>
</div>
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onIndexerSelect={handleIndexerSelect}
onModalClose={handleAddIndexerModalClose}
/>
<EditIndexerModal
isOpen={isEditIndexerModalOpen}
onModalClose={handleEditIndexerModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
export default Indexers;

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import Indexers from './Indexers';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.indexers', sortByProp('name')),
createTagsSelector(),
(indexers, tagList) => {
return {
...indexers,
tagList
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexers: fetchIndexers,
dispatchDeleteIndexer: deleteIndexer,
dispatchCloneIndexer: cloneIndexer
};
class IndexersConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchIndexers();
}
//
// Listeners
onConfirmDeleteIndexer = (id) => {
this.props.dispatchDeleteIndexer({ id });
};
//
// Render
render() {
return (
<Indexers
{...this.props}
onConfirmDeleteIndexer={this.onConfirmDeleteIndexer}
/>
);
}
}
IndexersConnector.propTypes = {
dispatchFetchIndexers: PropTypes.func.isRequired,
dispatchDeleteIndexer: PropTypes.func.isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector);

View File

@@ -1,174 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function IndexerOptions(props) {
const {
advancedSettings,
isFetching,
error,
settings,
hasSettings,
onInputChange,
onWhitelistedSubtitleChange
} = props;
return (
<FieldSet legend={translate('Options')}>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>
{translate('IndexerOptionsLoadError')}
</Alert>
}
{
hasSettings && !isFetching && !error &&
<Form>
<FormGroup>
<FormLabel>{translate('MinimumAge')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumAge"
min={0}
unit="minutes"
helpText={translate('MinimumAgeHelpText')}
onChange={onInputChange}
{...settings.minimumAge}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Retention')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="retention"
min={0}
unit="days"
helpText={translate('RetentionHelpText')}
onChange={onInputChange}
{...settings.retention}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MaximumSize')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="maximumSize"
min={0}
unit="MB"
helpText={translate('MaximumSizeHelpText')}
onChange={onInputChange}
{...settings.maximumSize}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('PreferIndexerFlags')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="preferIndexerFlags"
helpText={translate('PreferIndexerFlagsHelpText')}
helpLink="https://wiki.servarr.com/radarr/settings#indexer-flags"
onChange={onInputChange}
{...settings.preferIndexerFlags}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AvailabilityDelay')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="availabilityDelay"
unit="days"
helpText={translate('AvailabilityDelayHelpText')}
onChange={onInputChange}
{...settings.availabilityDelay}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RssSyncInterval')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="rssSyncInterval"
min={0}
max={120}
unit="minutes"
helpText={translate('RssSyncIntervalHelpText')}
helpTextWarning={translate('RssSyncIntervalHelpTextWarning')}
helpLink="https://wiki.servarr.com/radarr/faq#how-does-radarr-work"
onChange={onInputChange}
{...settings.rssSyncInterval}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('WhitelistedSubtitleTags')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT_TAG}
name="whitelistedHardcodedSubs"
helpText={translate('WhitelistedHardcodedSubsHelpText')}
onChange={onWhitelistedSubtitleChange}
{...settings.whitelistedHardcodedSubs}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('AllowHardcodedSubs')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="allowHardcodedSubs"
helpText={translate('AllowHardcodedSubsHelpText')}
onChange={onInputChange}
{...settings.allowHardcodedSubs}
/>
</FormGroup>
</Form>
}
</FieldSet>
);
}
IndexerOptions.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onWhitelistedSubtitleChange: PropTypes.func.isRequired
};
export default IndexerOptions;

View File

@@ -0,0 +1,210 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
fetchIndexerOptions,
saveIndexerOptions,
setIndexerOptionsValue,
} from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { InputChanged } from 'typings/inputs';
import {
OnChildStateChange,
SetChildSave,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
const SECTION = 'indexerOptions';
interface IndexerOptionsProps {
setChildSave: SetChildSave;
onChildStateChange: OnChildStateChange;
}
function IndexerOptions({
setChildSave,
onChildStateChange,
}: IndexerOptionsProps) {
const dispatch = useDispatch();
const {
isFetching,
isPopulated,
isSaving,
error,
settings,
hasSettings,
hasPendingChanges,
} = useSelector(createSettingsSectionSelector(SECTION));
const showAdvancedSettings = useShowAdvancedSettings();
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions aren't typed
dispatch(setIndexerOptionsValue(change));
},
[dispatch]
);
const handleWhitelistedSubtitleChange = useCallback(
({ name, value }: InputChanged<string[] | null>) => {
// @ts-expect-error - actions aren't typed
dispatch(setIndexerOptionsValue({ name, value: value?.join(',') }));
},
[dispatch]
);
useEffect(() => {
dispatch(fetchIndexerOptions());
setChildSave(() => dispatch(saveIndexerOptions()));
}, [dispatch, setChildSave]);
useEffect(() => {
onChildStateChange({
isSaving,
hasPendingChanges,
});
}, [hasPendingChanges, isSaving, onChildStateChange]);
useEffect(() => {
return () => {
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
};
}, [dispatch]);
return (
<FieldSet legend={translate('Options')}>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('IndexerOptionsLoadError')}
</Alert>
) : null}
{hasSettings && isPopulated && !error ? (
<Form>
<FormGroup>
<FormLabel>{translate('MinimumAge')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumAge"
min={0}
unit="minutes"
helpText={translate('MinimumAgeHelpText')}
onChange={handleInputChange}
{...settings.minimumAge}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Retention')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="retention"
min={0}
unit="days"
helpText={translate('RetentionHelpText')}
onChange={handleInputChange}
{...settings.retention}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MaximumSize')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="maximumSize"
min={0}
unit="MB"
helpText={translate('MaximumSizeHelpText')}
onChange={handleInputChange}
{...settings.maximumSize}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('PreferIndexerFlags')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="preferIndexerFlags"
helpText={translate('PreferIndexerFlagsHelpText')}
helpLink="https://wiki.servarr.com/radarr/settings#indexer-flags"
onChange={handleInputChange}
{...settings.preferIndexerFlags}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AvailabilityDelay')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="availabilityDelay"
unit="days"
helpText={translate('AvailabilityDelayHelpText')}
onChange={handleInputChange}
{...settings.availabilityDelay}
/>
</FormGroup>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('RssSyncInterval')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="rssSyncInterval"
min={0}
max={120}
unit="minutes"
helpText={translate('RssSyncIntervalHelpText')}
helpTextWarning={translate('RssSyncIntervalHelpTextWarning')}
helpLink="https://wiki.servarr.com/radarr/faq#how-does-radarr-work"
onChange={handleInputChange}
{...settings.rssSyncInterval}
/>
</FormGroup>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('WhitelistedSubtitleTags')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT_TAG}
name="whitelistedHardcodedSubs"
helpText={translate('WhitelistedHardcodedSubsHelpText')}
onChange={handleWhitelistedSubtitleChange}
{...settings.whitelistedHardcodedSubs}
/>
</FormGroup>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('AllowHardcodedSubs')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="allowHardcodedSubs"
helpText={translate('AllowHardcodedSubsHelpText')}
onChange={handleInputChange}
{...settings.allowHardcodedSubs}
/>
</FormGroup>
</Form>
) : null}
</FieldSet>
);
}
export default IndexerOptions;

View File

@@ -1,106 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchIndexerOptions, saveIndexerOptions, setIndexerOptionsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import IndexerOptions from './IndexerOptions';
const SECTION = 'indexerOptions';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createSettingsSectionSelector(SECTION),
(advancedSettings, sectionSettings) => {
return {
advancedSettings,
...sectionSettings
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexerOptions: fetchIndexerOptions,
dispatchSetIndexerOptionsValue: setIndexerOptionsValue,
dispatchSaveIndexerOptions: saveIndexerOptions,
dispatchClearPendingChanges: clearPendingChanges
};
class IndexerOptionsConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
dispatchFetchIndexerOptions,
dispatchSaveIndexerOptions,
onChildMounted
} = this.props;
dispatchFetchIndexerOptions();
onChildMounted(dispatchSaveIndexerOptions);
}
componentDidUpdate(prevProps) {
const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetIndexerOptionsValue({ name, value });
};
onWhitelistedSubtitleChange = ({ name, value }) => {
this.props.dispatchSetIndexerOptionsValue({ name, value: value.join(',') });
};
//
// Render
render() {
return (
<IndexerOptions
onInputChange={this.onInputChange}
onWhitelistedSubtitleChange={this.onWhitelistedSubtitleChange}
{...this.props}
/>
);
}
}
IndexerOptionsConnector.propTypes = {
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
dispatchFetchIndexerOptions: PropTypes.func.isRequired,
dispatchSetIndexerOptionsValue: PropTypes.func.isRequired,
dispatchSaveIndexerOptions: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerOptionsConnector);

View File

@@ -1,500 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import RootFolders from 'RootFolder/RootFolders';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import Naming from './Naming/Naming';
import AddRootFolder from './RootFolder/AddRootFolder';
const rescanAfterRefreshOptions = [
{
key: 'always',
get value() {
return translate('Always');
}
},
{
key: 'afterManual',
get value() {
return translate('AfterManualRefresh');
}
},
{
key: 'never',
get value() {
return translate('Never');
}
}
];
const downloadPropersAndRepacksOptions = [
{
key: 'preferAndUpgrade',
get value() {
return translate('PreferAndUpgrade');
}
},
{
key: 'doNotUpgrade',
get value() {
return translate('DoNotUpgradeAutomatically');
}
},
{
key: 'doNotPrefer',
get value() {
return translate('DoNotPrefer');
}
}
];
const fileDateOptions = [
{
key: 'none',
get value() {
return translate('None');
}
},
{
key: 'cinemas',
get value() {
return translate('InCinemasDate');
}
},
{
key: 'release',
get value() {
return translate('PhysicalReleaseDate');
}
}
];
class MediaManagement extends Component {
//
// Render
render() {
const {
advancedSettings,
isFetching,
error,
settings,
hasSettings,
isWindows,
onInputChange,
onSavePress,
...otherProps
} = this.props;
return (
<PageContent title={translate('MediaManagementSettings')}>
<SettingsToolbarConnector
advancedSettings={advancedSettings}
{...otherProps}
onSavePress={onSavePress}
/>
<PageContentBody>
<Naming />
{
isFetching ?
<FieldSet legend={translate('NamingSettings')}>
<LoadingIndicator />
</FieldSet> : null
}
{
!isFetching && error ?
<FieldSet legend={translate('NamingSettings')}>
<Alert kind={kinds.DANGER}>
{translate('MediaManagementSettingsLoadError')}
</Alert>
</FieldSet> : null
}
{
hasSettings && !isFetching && !error ?
<Form
id="mediaManagementSettings"
{...otherProps}
>
{
advancedSettings ?
<FieldSet legend={translate('Folders')}>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('CreateEmptyMovieFolders')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
isDisabled={settings.deleteEmptyFolders.value && !settings.createEmptyMovieFolders.value}
name="createEmptyMovieFolders"
helpText={translate('CreateEmptyMovieFoldersHelpText')}
onChange={onInputChange}
{...settings.createEmptyMovieFolders}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('DeleteEmptyFolders')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
isDisabled={settings.createEmptyMovieFolders.value && !settings.deleteEmptyFolders.value}
name="deleteEmptyFolders"
helpText={translate('DeleteEmptyFoldersHelpText')}
onChange={onInputChange}
{...settings.deleteEmptyFolders}
/>
</FormGroup>
</FieldSet> : null
}
{
advancedSettings ?
<FieldSet
legend={translate('Importing')}
>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipFreeSpaceCheckWhenImporting"
helpText={translate('SkipFreeSpaceCheckHelpText')}
onChange={onInputChange}
{...settings.skipFreeSpaceCheckWhenImporting}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('MinimumFreeSpace')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
unit='MB'
name="minimumFreeSpaceWhenImporting"
helpText={translate('MinimumFreeSpaceHelpText')}
onChange={onInputChange}
{...settings.minimumFreeSpaceWhenImporting}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('UseHardlinksInsteadOfCopy')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="copyUsingHardlinks"
helpText={translate('CopyUsingHardlinksMovieHelpText')}
helpTextWarning={translate('CopyUsingHardlinksHelpTextWarning')}
onChange={onInputChange}
{...settings.copyUsingHardlinks}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('ImportUsingScript')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="useScriptImport"
helpText={translate('ImportUsingScriptHelpText')}
onChange={onInputChange}
{...settings.useScriptImport}
/>
</FormGroup>
{
settings.useScriptImport.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ImportScriptPath')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
includeFiles={true}
name="scriptImportPath"
helpText={translate('ImportScriptPathHelpText')}
onChange={onInputChange}
{...settings.scriptImportPath}
/>
</FormGroup> : null
}
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="importExtraFiles"
helpText={translate('ImportExtraFilesMovieHelpText')}
onChange={onInputChange}
{...settings.importExtraFiles}
/>
</FormGroup>
{
settings.importExtraFiles.value ?
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="extraFileExtensions"
helpTexts={[
translate('ExtraFileExtensionsHelpText'),
translate('ExtraFileExtensionsHelpTextsExamples')
]}
onChange={onInputChange}
{...settings.extraFileExtensions}
/>
</FormGroup> : null
}
</FieldSet> : null
}
<FieldSet
legend={translate('FileManagement')}
>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('IgnoreDeletedMovies')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="autoUnmonitorPreviouslyDownloadedMovies"
helpText={translate('AutoUnmonitorPreviouslyDownloadedMoviesHelpText')}
onChange={onInputChange}
{...settings.autoUnmonitorPreviouslyDownloadedMovies}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('DownloadPropersAndRepacks')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="downloadPropersAndRepacks"
helpTexts={[
translate('DownloadPropersAndRepacksHelpText'),
translate('DownloadPropersAndRepacksHelpTextCustomFormat')
]}
helpTextWarning={
settings.downloadPropersAndRepacks.value === 'doNotPrefer' ?
translate('DownloadPropersAndRepacksHelpTextWarning') :
undefined
}
values={downloadPropersAndRepacksOptions}
onChange={onInputChange}
{...settings.downloadPropersAndRepacks}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('AnalyseVideoFiles')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableMediaInfo"
helpText={translate('AnalyseVideoFilesHelpText')}
onChange={onInputChange}
{...settings.enableMediaInfo}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RescanMovieFolderAfterRefresh')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="rescanAfterRefresh"
helpText={translate('RescanAfterRefreshMovieHelpText')}
helpTextWarning={translate('RescanAfterRefreshHelpTextWarning')}
values={rescanAfterRefreshOptions}
onChange={onInputChange}
{...settings.rescanAfterRefresh}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ChangeFileDate')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="fileDate"
helpText={translate('ChangeFileDateHelpText')}
values={fileDateOptions}
onChange={onInputChange}
{...settings.fileDate}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RecyclingBin')}</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="recycleBin"
helpText={translate('RecyclingBinHelpText')}
onChange={onInputChange}
{...settings.recycleBin}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RecyclingBinCleanup')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="recycleBinCleanupDays"
helpText={translate('RecyclingBinCleanupHelpText')}
helpTextWarning={translate('RecyclingBinCleanupHelpTextWarning')}
min={0}
onChange={onInputChange}
{...settings.recycleBinCleanupDays}
/>
</FormGroup>
</FieldSet>
{
advancedSettings && !isWindows ?
<FieldSet
legend={translate('Permissions')}
>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('SetPermissions')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="setPermissionsLinux"
helpText={translate('SetPermissionsLinuxHelpText')}
helpTextWarning={translate('SetPermissionsLinuxHelpTextWarning')}
onChange={onInputChange}
{...settings.setPermissionsLinux}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ChmodFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.UMASK}
name="chmodFolder"
helpText={translate('ChmodFolderHelpText')}
helpTextWarning={translate('ChmodFolderHelpTextWarning')}
onChange={onInputChange}
{...settings.chmodFolder}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ChownGroup')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="chownGroup"
helpText={translate('ChownGroupHelpText')}
helpTextWarning={translate('ChownGroupHelpTextWarning')}
values={fileDateOptions}
onChange={onInputChange}
{...settings.chownGroup}
/>
</FormGroup>
</FieldSet> : null
}
</Form> : null
}
<FieldSet legend={translate('RootFolders')}>
<RootFolders />
<AddRootFolder />
</FieldSet>
</PageContentBody>
</PageContent>
);
}
}
MediaManagement.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired,
onSavePress: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired
};
export default MediaManagement;

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