diff --git a/Requestrr.WebApi/ClientApp/package-lock.json b/Requestrr.WebApi/ClientApp/package-lock.json index 34e95c0b..0f854e96 100644 --- a/Requestrr.WebApi/ClientApp/package-lock.json +++ b/Requestrr.WebApi/ClientApp/package-lock.json @@ -1,6 +1,6 @@ { "name": "Requestrr", - "version": "2.0.5", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/Requestrr.WebApi/ClientApp/package.json b/Requestrr.WebApi/ClientApp/package.json index 50aef77e..e52f2243 100644 --- a/Requestrr.WebApi/ClientApp/package.json +++ b/Requestrr.WebApi/ClientApp/package.json @@ -1,6 +1,6 @@ { "name": "Requestrr", - "version": "2.0.5", + "version": "2.1.0", "description": "Requestrr is a server designed to faciliate the request of media through chat applications.", "main": "index.js", "author": "Darkalfx", diff --git a/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr.jsx b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/Movies/OverseerrMovie.jsx similarity index 78% rename from Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr.jsx rename to Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/Movies/OverseerrMovie.jsx index 0eaca1ce..0104a9f8 100644 --- a/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr.jsx +++ b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/Movies/OverseerrMovie.jsx @@ -2,10 +2,11 @@ import React from "react"; import Loader from 'react-loader-spinner' import { connect } from 'react-redux'; import { Alert } from "reactstrap"; -import { testOverseerrSettings } from "../../store/actions/MovieClientsActions" -import ValidatedTextbox from "../Inputs/ValidatedTextbox" -import Textbox from "../Inputs/Textbox" -import Dropdown from "../Inputs/Dropdown" +import { testOverseerrMovieSettings } from "../../../../store/actions/OverseerrClientRadarrActions" +import { setOverseerrMovieConnectionSettings } from "../../../../store/actions/OverseerrClientRadarrActions" +import ValidatedTextbox from "../../../Inputs/ValidatedTextbox" +import Dropdown from "../../../Inputs/Dropdown" +import OverseerrMovieCategoryList from "./OverseerrMovieCategoryList" import { FormGroup, @@ -14,7 +15,7 @@ import { Col } from "reactstrap"; -class Overseerr extends React.Component { +class OverseerrMovie extends React.Component { constructor(props) { super(props); @@ -49,6 +50,10 @@ class Overseerr extends React.Component { this.updateStateFromProps(this.props); } + componentDidUpdate(prevProps) { + this.onValidate(); + } + updateStateFromProps = props => { this.setState({ isTestingSettings: false, @@ -66,7 +71,8 @@ class Overseerr extends React.Component { isDefaultApiUserIDValid: true, useSSL: props.settings.useSSL, apiVersion: props.settings.version, - }); + isValid: false, + }, this.onValueChange); } onUseSSLChanged = event => { @@ -102,7 +108,7 @@ class Overseerr extends React.Component { port: this.state.port, apiKey: this.state.apiKey, useSSL: this.state.useSSL, - defaultApiUserID: this.state.defaultApiUserID, + DefaultApiUserID: this.state.DefaultApiUserID, version: this.state.apiVersion, }) .then(data => { @@ -132,6 +138,14 @@ class Overseerr extends React.Component { } onValueChange() { + this.props.setConnectionSettings({ + hostname: this.state.hostname, + port: this.state.port, + apiKey: this.state.apiKey, + useSSL: this.state.useSSL, + version: this.state.apiVersion, + }); + this.props.onChange({ client: this.state.client, hostname: this.state.hostname, @@ -139,9 +153,6 @@ class Overseerr extends React.Component { apiKey: this.state.apiKey, defaultApiUserID: this.state.defaultApiUserID, useSSL: this.state.useSSL, - qualityProfile: this.state.qualityProfile, - path: this.state.path, - profile: this.state.profile, version: this.state.apiVersion, }); @@ -149,7 +160,43 @@ class Overseerr extends React.Component { } onValidate() { - this.props.onValidate(this.state.isApiKeyValid && this.state.isHostnameValid && this.state.isPortValid && this.state.isDefaultApiUserIDValid); + let isValid = this.state.isApiKeyValid + && this.state.isHostnameValid + && this.state.isPortValid + && this.state.isDefaultApiUserIDValid + && (this.props.settings.categories.length == 0 || (this.props.settings.categories.every(x => this.validateCategory(x)) && this.props.settings.isRadarrServiceSettingsValid)); + + if (isValid !== this.state.isValid) { + this.setState({ isValid: isValid }, + () => this.props.onValidate(isValid)); + } + } + + validateCategory(category) { + if (!/\S/.test(category.name)) { + return false; + } + else if (/^[\w-]{1,32}$/.test(category.name)) { + var names = this.props.settings.categories.map(x => x.name); + + if (new Set(names).size !== names.length) { + return false; + } + else if (this.props.settings.radarrServiceSettings.radarrServices.every(x => x.id !== category.serviceId)) { + return false; + } + else { + var radarrService = this.props.settings.radarrServiceSettings.radarrServices.filter(x => x.id === category.serviceId)[0]; + + if (radarrService.profiles.length == 0 || radarrService.rootPaths.length == 0) { + return false; + } + } + } + else { + return false; + } + return true; } render() { @@ -283,14 +330,21 @@ class Overseerr extends React.Component { -
+ ); } } +const mapPropsToState = state => { + return { + settings: state.movies.overseerr + } +}; + const mapPropsToAction = { - testSettings: testOverseerrSettings, + testSettings: testOverseerrMovieSettings, + setConnectionSettings: setOverseerrMovieConnectionSettings, }; -export default connect(null, mapPropsToAction)(Overseerr); \ No newline at end of file +export default connect(mapPropsToState, mapPropsToAction)(OverseerrMovie); \ No newline at end of file diff --git a/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/Movies/OverseerrMovieCategory.jsx b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/Movies/OverseerrMovieCategory.jsx new file mode 100644 index 00000000..c723c23b --- /dev/null +++ b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/Movies/OverseerrMovieCategory.jsx @@ -0,0 +1,320 @@ +import React from "react"; +import Loader from 'react-loader-spinner' +import { connect } from 'react-redux'; +import { Alert } from "reactstrap"; +import { loadRadarrServiceSettings } from "../../../../store/actions/OverseerrClientRadarrActions" +import { setOverseerrMovieCategory } from "../../../../store/actions/OverseerrClientRadarrActions" +import { removeOverseerrMovieCategory } from "../../../../store/actions/OverseerrClientRadarrActions" +import ValidatedTextbox from "../../../Inputs/ValidatedTextbox" +import Dropdown from "../../../Inputs/Dropdown" +import MultiDropdown from "../../../Inputs/MultiDropdown" + +import { + FormGroup, + Input, + Row, + Col, + Collapse +} from "reactstrap"; + +class OverseerrMovieCategory extends React.Component { + constructor(props) { + super(props); + + this.state = { + nameErrorMessage: "", + isNameValid: true, + isOpen: false, + }; + + this.validateName = this.validateName.bind(this); + this.setCategory = this.setCategory.bind(this); + this.deleteCategory = this.deleteCategory.bind(this); + } + + componentDidMount() { + if (this.props.category.wasCreated) { + this.setState({ isOpen: true }); + } + + if (this.props.canConnect) { + this.props.loadServiceSettings(false); + } + } + + componentDidUpdate(prevProps, prevState) { + var previousNames = prevProps.overseerr.categories.map(x => x.name); + var currentNames = this.props.overseerr.categories.map(x => x.name); + + if (!(previousNames.length == currentNames.length && currentNames.every((value, index) => previousNames[index] == value))) { + this.validateName(this.props.category.name) + } + + if (this.props.canConnect) { + this.props.loadServiceSettings(false); + } + + if (prevProps.isSaving != this.props.isSaving) { + this.setState({ + isOpen: false, + }); + } + } + + validateNonEmptyString = value => { + return /\S/.test(value); + } + + validateName(value) { + var state = { isNameValid: true }; + + if (!/\S/.test(value)) { + state = { + ...state, + nameErrorMessage: "A category name is required.", + isNameValid: false, + }; + } + else if (/^[\w-]{1,32}$/.test(value)) { + if (this.props.overseerr.categories.map(x => x.id).includes(this.props.category.id) && this.props.overseerr.categories.filter(c => typeof c.id !== 'undefined' && c.id != this.props.category.id && c.name.toLowerCase().trim() == value.toLowerCase().trim()).length > 0) { + state = { + nameErrorMessage: "All categories must have different names.", + isNameValid: false, + }; + } + } + else { + state = { + nameErrorMessage: "Invalid categorie names, make sure they only contain alphanumeric characters, dashes and underscores. (No spaces, etc)", + isNameValid: false, + }; + } + + this.setState(state); + + return state.isNameValid; + } + + setCategory(fieldChanged, data) { + this.props.setOverseerrCategory(this.props.category.id, fieldChanged, data); + } + + deleteCategory() { + this.setState({ + isOpen: false, + }, () => setTimeout(() => this.props.removeOverseerrCategory(this.props.category.id), 150)); + } + + render() { + return ( + <> + + +
+
+ {this.props.category.name} +
+
+ + + + + + + + +
+ + + this.setCategory("name", newName)} + onValidate={isValid => this.setState({ isNameValid: isValid })} /> + + +
+ { return { name: x.name, value: x.id } })} + onChange={newServiceId => this.setCategory("serviceId", newServiceId)} /> + +
+ { + this.props.overseerr.radarrServiceSettings.radarrServices.length === 0 ? ( + + Could not find any radarr instances. + ) + : null + } + +
+ + +
+ x.id == this.props.category.serviceId) ? this.props.overseerr.radarrServiceSettings.radarrServices.filter(x => x.id == this.props.category.serviceId)[0].rootPaths.map(x => { return { name: x.name, value: x.name } }) : []} + onChange={newRootFolder => this.setCategory("rootFolder", newRootFolder)} /> + +
+ { + (this.props.overseerr.radarrServiceSettings.radarrServices.some(x => x.id == this.props.category.serviceId) ? this.props.overseerr.radarrServiceSettings.radarrServices.filter(x => x.id == this.props.category.serviceId)[0].rootPaths.map(x => { return { name: x.name, value: x.name } }) : []).length === 0 ? ( + + Could not find any paths. + ) + : null + } + + +
+ x.id == this.props.category.serviceId) ? this.props.overseerr.radarrServiceSettings.radarrServices.filter(x => x.id == this.props.category.serviceId)[0].profiles.map(x => { return { name: x.name, value: x.id } }) : []} + onChange={newProfileId => this.setCategory("profileId", newProfileId)} /> + +
+ { + (this.props.overseerr.radarrServiceSettings.radarrServices.some(x => x.id == this.props.category.serviceId) ? this.props.overseerr.radarrServiceSettings.radarrServices.filter(x => x.id == this.props.category.serviceId)[0].profiles.map(x => { return { name: x.name, value: x.id } }) : []).length === 0 ? ( + + Could not find any profiles. + ) + : null + } + +
+ + +
+ x.id == this.props.category.serviceId) ? this.props.overseerr.radarrServiceSettings.radarrServices.filter(x => x.id == this.props.category.serviceId)[0].tags : []).filter(x => this.props.category.tags.includes(x.id))} + items={this.props.overseerr.radarrServiceSettings.radarrServices.some(x => x.id == this.props.category.serviceId) ? this.props.overseerr.radarrServiceSettings.radarrServices.filter(x => x.id == this.props.category.serviceId)[0].tags : []} + onChange={newTags => this.setCategory("tags", newTags.map(x => x.id))} /> + +
+ { + !this.props.overseerr.isRadarrServiceSettingsValid ? ( + + Could not load tags, cannot reach Overseerr. + ) + : null + } + +
+ + + + this.setCategory("is4K", !this.props.category.is4K)} + checked={this.props.category.is4K} + /> + + + + + + + + + +
+
+ + + + ); + } +} + +const mapPropsToState = state => { + return { + overseerr: state.movies.overseerr + } +}; + +const mapPropsToAction = { + loadServiceSettings: loadRadarrServiceSettings, + setOverseerrCategory: setOverseerrMovieCategory, + removeOverseerrCategory: removeOverseerrMovieCategory, +}; + +export default connect(mapPropsToState, mapPropsToAction)(OverseerrMovieCategory); \ No newline at end of file diff --git a/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/Movies/OverseerrMovieCategoryList.jsx b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/Movies/OverseerrMovieCategoryList.jsx new file mode 100644 index 00000000..3e4e9c88 --- /dev/null +++ b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/Movies/OverseerrMovieCategoryList.jsx @@ -0,0 +1,100 @@ +import React from "react"; +import { connect } from 'react-redux'; +import { addOverseerrMovieCategory } from "../../../../store/actions/OverseerrClientRadarrActions" +import OverseerrMovieCategory from "./OverseerrMovieCategory"; + +// reactstrap components +import { + Button, + Card, + CardHeader, + CardBody, + FormGroup, + Form, + Input, + Container, + Row, + Col, + UncontrolledTooltip, +} from "reactstrap"; + +class OverseerrMovieCategoryList extends React.Component { + constructor(props) { + super(props); + this.createOverseerrCategory = this.createOverseerrCategory.bind(this); + } + + createOverseerrCategory() { + var newId = Math.floor((Math.random() * 900) + 1); + + while (this.props.overseerr.categories.map(x => x.id).includes(newId)) { + newId = Math.floor((Math.random() * 900) + 1); + } + + var newCategory = { + id: newId, + name: "new-category", + serviceId: this.props.overseerr.radarrServiceSettings.radarrServices.length > 0 ? this.props.overseerr.radarrServiceSettings.radarrServices[0].id : -1, + profileId: -1, + rootFolder: "", + tags: [], + wasCreated: true + }; + + this.props.addOverseerrCategory(newCategory); + } + + render() { + return ( + <> +
+
+ Overseerr Category Settings +
+
+
+ + + + + + + + + {this.props.overseerr.categories.map((category, key) => { + return ( + + + ) + })} + + + + +
CategoryActions
+ + + +
+
+
+
+ + ); + } +} + +const mapPropsToState = state => { + return { + overseerr: state.movies.overseerr + } +}; + +const mapPropsToAction = { + addOverseerrCategory: addOverseerrMovieCategory, +}; + +export default connect(mapPropsToState, mapPropsToAction)(OverseerrMovieCategoryList); \ No newline at end of file diff --git a/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/TvShows/OverseerrTvShow.jsx b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/TvShows/OverseerrTvShow.jsx new file mode 100644 index 00000000..9ea9967b --- /dev/null +++ b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/TvShows/OverseerrTvShow.jsx @@ -0,0 +1,350 @@ +import React from "react"; +import Loader from 'react-loader-spinner' +import { connect } from 'react-redux'; +import { Alert } from "reactstrap"; +import { testOverseerrTvShowSettings } from "../../../../store/actions/OverseerrClientSonarrActions" +import { setOverseerrTvShowConnectionSettings } from "../../../../store/actions/OverseerrClientSonarrActions" +import ValidatedTextbox from "../../../Inputs/ValidatedTextbox" +import Dropdown from "../../../Inputs/Dropdown" +import OverseerrTvShowCategoryList from "./OverseerrTvShowCategoryList" + +import { + FormGroup, + Input, + Row, + Col +} from "reactstrap"; + +class OverseerrTvShow extends React.Component { + constructor(props) { + super(props); + + this.state = { + isTestingSettings: false, + testSettingsRequested: false, + testSettingsSuccess: false, + testSettingsError: "", + hostname: "", + isHostnameValid: false, + port: "7878", + isPortValid: false, + apiKey: "", + isApiKeyValid: false, + defaultApiUserID: "", + isDefaultApiUserIDValid: true, + useSSL: "", + apiVersion: "", + }; + + this.onTestSettings = this.onTestSettings.bind(this); + this.onUseSSLChanged = this.onUseSSLChanged.bind(this); + this.onValueChange = this.onValueChange.bind(this); + this.onValidate = this.onValidate.bind(this); + this.updateStateFromProps = this.updateStateFromProps.bind(this); + this.validateNonEmptyString = this.validateNonEmptyString.bind(this); + this.validatePort = this.validatePort.bind(this); + this.validateDefaultUserId = this.validateDefaultUserId.bind(this); + } + + componentDidMount() { + this.updateStateFromProps(this.props); + } + + componentDidUpdate(prevProps) { + this.onValidate(); + } + + updateStateFromProps = props => { + this.setState({ + isTestingSettings: false, + TestingSettings: false, + testSettingsRequested: false, + testSettingsSuccess: false, + testSettingsError: "", + hostname: props.settings.hostname, + isHostnameValid: false, + port: props.settings.port, + isPortValid: false, + apiKey: props.settings.apiKey, + isApiKeyValid: false, + defaultApiUserID: props.settings.defaultApiUserID, + isDefaultApiUserIDValid: true, + useSSL: props.settings.useSSL, + apiVersion: props.settings.version, + isValid: false, + }, this.onValueChange); + } + + onUseSSLChanged = event => { + this.setState({ + useSSL: !this.state.useSSL + }, this.onValueChange); + } + + validateNonEmptyString = value => { + return /\S/.test(value); + } + + validatePort = value => { + return /^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/.test(value); + } + + validateDefaultUserId = value => { + return (!value || value.length === 0 || /^\s*$/.test(value)) || (/^[1-9]\d*$/).test(value); + } + + onTestSettings = e => { + e.preventDefault(); + + if (!this.state.isTestingSettings + && this.state.isHostnameValid + && this.state.isPortValid + && this.state.isDefaultApiUserIDValid + && this.state.isApiKeyValid) { + this.setState({ isTestingSettings: true }); + + this.props.testSettings({ + hostname: this.state.hostname, + port: this.state.port, + apiKey: this.state.apiKey, + useSSL: this.state.useSSL, + DefaultApiUserID: this.state.DefaultApiUserID, + version: this.state.apiVersion, + }) + .then(data => { + this.setState({ isTestingSettings: false }); + + if (data.ok) { + this.setState({ + testSettingsRequested: true, + testSettingsError: "", + testSettingsSuccess: true + }); + } + else { + var error = "An unknown error occurred while testing the settings"; + + if (typeof (data.error) === "string") + error = data.error; + + this.setState({ + testSettingsRequested: true, + testSettingsError: error, + testSettingsSuccess: false + }); + } + }); + } + } + + onValueChange() { + this.props.setConnectionSettings({ + hostname: this.state.hostname, + port: this.state.port, + apiKey: this.state.apiKey, + useSSL: this.state.useSSL, + version: this.state.apiVersion, + }); + + this.props.onChange({ + client: this.state.client, + hostname: this.state.hostname, + port: this.state.port, + apiKey: this.state.apiKey, + defaultApiUserID: this.state.defaultApiUserID, + useSSL: this.state.useSSL, + version: this.state.apiVersion, + }); + + this.onValidate(); + } + + onValidate() { + let isValid = this.state.isApiKeyValid + && this.state.isHostnameValid + && this.state.isPortValid + && this.state.isDefaultApiUserIDValid + && (this.props.settings.categories.length == 0 || (this.props.settings.categories.every(x => this.validateCategory(x)) && this.props.settings.isSonarrServiceSettingsValid)); + + if (isValid !== this.state.isValid) { + this.setState({ isValid: isValid }, + () => this.props.onValidate(isValid)); + } + } + + validateCategory(category) { + if (!/\S/.test(category.name)) { + return false; + } + else if (/^[\w-]{1,32}$/.test(category.name)) { + var names = this.props.settings.categories.map(x => x.name); + + if (new Set(names).size !== names.length) { + return false; + } + else if (this.props.settings.sonarrServiceSettings.sonarrServices.every(x => x.id !== category.serviceId)) { + return false; + } + else { + var sonarrService = this.props.settings.sonarrServiceSettings.sonarrServices.filter(x => x.id === category.serviceId)[0]; + + if (sonarrService.profiles.length == 0 || sonarrService.rootPaths.length == 0) { + return false; + } + } + } + else { + return false; + } + return true; + } + + render() { + return ( + <> +
+
+ Overseerr Connection Settings +
+
+
+ + + this.setState({ apiVersion: newApiVersion }, this.onValueChange)} /> + + + this.setState({ apiKey: newApiKey }, this.onValueChange)} + onValidate={isValid => this.setState({ isApiKeyValid: isValid }, this.onValidate)} /> + + + + + this.setState({ hostname: newHostname }, this.onValueChange)} + onValidate={isValid => this.setState({ isHostnameValid: isValid }, this.onValidate)} /> + + + this.setState({ port: newPort }, this.onValueChange)} + onValidate={isValid => this.setState({ isPortValid: isValid }, this.onValidate)} /> + + + + + this.setState({ defaultApiUserID: newDefaultApiUserID }, this.onValueChange)} + onValidate={isValid => this.setState({ isDefaultApiUserIDValid: isValid }, this.onValidate)} /> + + + + + + + + + + + + Click here to view how configure Overseerr permissions with the bot + + + + + + { + this.state.testSettingsRequested && !this.state.isTestingSettings ? + !this.state.testSettingsSuccess ? ( + + {this.state.testSettingsError} + ) + : + The specified settings are valid. + + : null + } + + + + + + + + + + +
+ + + ); + } +} + +const mapPropsToState = state => { + return { + settings: state.tvShows.overseerr + } +}; + +const mapPropsToAction = { + testSettings: testOverseerrTvShowSettings, + setConnectionSettings: setOverseerrTvShowConnectionSettings, +}; + +export default connect(mapPropsToState, mapPropsToAction)(OverseerrTvShow); \ No newline at end of file diff --git a/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/TvShows/OverseerrTvShowCategory.jsx b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/TvShows/OverseerrTvShowCategory.jsx new file mode 100644 index 00000000..39685e71 --- /dev/null +++ b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/TvShows/OverseerrTvShowCategory.jsx @@ -0,0 +1,351 @@ +import React from "react"; +import Loader from 'react-loader-spinner' +import { connect } from 'react-redux'; +import { Alert } from "reactstrap"; +import { loadSonarrServiceSettings } from "../../../../store/actions/OverseerrClientSonarrActions" +import { setOverseerrTvShowCategory } from "../../../../store/actions/OverseerrClientSonarrActions" +import { removeOverseerrTvShowCategory } from "../../../../store/actions/OverseerrClientSonarrActions" +import ValidatedTextbox from "../../../Inputs/ValidatedTextbox" +import Dropdown from "../../../Inputs/Dropdown" +import MultiDropdown from "../../../Inputs/MultiDropdown" + +import { + FormGroup, + Input, + Row, + Col, + Collapse +} from "reactstrap"; + +class OverseerrTvShowCategory extends React.Component { + constructor(props) { + super(props); + + this.state = { + nameErrorMessage: "", + isNameValid: true, + isOpen: false, + }; + + this.validateName = this.validateName.bind(this); + this.setCategory = this.setCategory.bind(this); + this.deleteCategory = this.deleteCategory.bind(this); + } + + componentDidMount() { + if (this.props.category.wasCreated) { + this.setState({ isOpen: true }); + } + + if (this.props.canConnect) { + this.props.loadServiceSettings(false); + } + } + + componentDidUpdate(prevProps, prevState) { + var previousNames = prevProps.overseerr.categories.map(x => x.name); + var currentNames = this.props.overseerr.categories.map(x => x.name); + + if (!(previousNames.length == currentNames.length && currentNames.every((value, index) => previousNames[index] == value))) { + this.validateName(this.props.category.name) + } + + if (this.props.canConnect) { + this.props.loadServiceSettings(false); + } + + if (prevProps.isSaving != this.props.isSaving) { + this.setState({ + isOpen: false, + }); + } + } + + validateNonEmptyString = value => { + return /\S/.test(value); + } + + validateName(value) { + var state = { isNameValid: true }; + + if (!/\S/.test(value)) { + state = { + ...state, + nameErrorMessage: "A category name is required.", + isNameValid: false, + }; + } + else if (/^[\w-]{1,32}$/.test(value)) { + if (this.props.overseerr.categories.map(x => x.id).includes(this.props.category.id) && this.props.overseerr.categories.filter(c => typeof c.id !== 'undefined' && c.id != this.props.category.id && c.name.toLowerCase().trim() == value.toLowerCase().trim()).length > 0) { + state = { + nameErrorMessage: "All categories must have different names.", + isNameValid: false, + }; + } + } + else { + state = { + nameErrorMessage: "Invalid categorie names, make sure they only contain alphanumeric characters, dashes and underscores. (No spaces, etc)", + isNameValid: false, + }; + } + + this.setState(state); + + return state.isNameValid; + } + + setCategory(fieldChanged, data) { + this.props.setOverseerrCategory(this.props.category.id, fieldChanged, data); + } + + deleteCategory() { + this.setState({ + isOpen: false, + }, () => setTimeout(() => this.props.removeOverseerrCategory(this.props.category.id), 150)); + } + + render() { + return ( + <> + + +
+
+ {this.props.category.name} +
+
+ + + + + + + + +
+ + + this.setCategory("name", newName)} + onValidate={isValid => this.setState({ isNameValid: isValid })} /> + + +
+ { return { name: x.name, value: x.id } })} + onChange={newServiceId => this.setCategory("serviceId", newServiceId)} /> + +
+ { + this.props.overseerr.sonarrServiceSettings.sonarrServices.length === 0 ? ( + + Could not find any sonarr instances. + ) + : null + } + +
+ + +
+ x.id == this.props.category.serviceId) ? this.props.overseerr.sonarrServiceSettings.sonarrServices.filter(x => x.id == this.props.category.serviceId)[0].rootPaths.map(x => { return { name: x.name, value: x.name } }) : []} + onChange={newRootFolder => this.setCategory("rootFolder", newRootFolder)} /> + +
+ { + (this.props.overseerr.sonarrServiceSettings.sonarrServices.some(x => x.id == this.props.category.serviceId) ? this.props.overseerr.sonarrServiceSettings.sonarrServices.filter(x => x.id == this.props.category.serviceId)[0].rootPaths.map(x => { return { name: x.name, value: x.name } }) : []).length === 0 ? ( + + Could not find any paths. + ) + : null + } + + +
+ x.id == this.props.category.serviceId) ? this.props.overseerr.sonarrServiceSettings.sonarrServices.filter(x => x.id == this.props.category.serviceId)[0].profiles.map(x => { return { name: x.name, value: x.id } }) : []} + onChange={newProfileId => this.setCategory("profileId", newProfileId)} /> + +
+ { + (this.props.overseerr.sonarrServiceSettings.sonarrServices.some(x => x.id == this.props.category.serviceId) ? this.props.overseerr.sonarrServiceSettings.sonarrServices.filter(x => x.id == this.props.category.serviceId)[0].profiles.map(x => { return { name: x.name, value: x.id } }) : []).length === 0 ? ( + + Could not find any profiles. + ) + : null + } + +
+ + +
+ x.id == this.props.category.serviceId) ? this.props.overseerr.sonarrServiceSettings.sonarrServices.filter(x => x.id == this.props.category.serviceId)[0].languageProfiles.map(x => { return { name: x.name, value: x.id } }) : []} + onChange={newLanguageId => this.setCategory("languageProfileId", newLanguageId)} /> + +
+ { + (this.props.overseerr.sonarrServiceSettings.sonarrServices.some(x => x.id == this.props.category.serviceId) ? this.props.overseerr.sonarrServiceSettings.sonarrServices.filter(x => x.id == this.props.category.serviceId)[0].languageProfiles.map(x => { return { name: x.name, value: x.id } }) : []).length === 0 ? ( + + Could not find any languages. + ) + : null + } + + +
+ x.id == this.props.category.serviceId) ? this.props.overseerr.sonarrServiceSettings.sonarrServices.filter(x => x.id == this.props.category.serviceId)[0].tags : []).filter(x => this.props.category.tags.includes(x.id))} + items={this.props.overseerr.sonarrServiceSettings.sonarrServices.some(x => x.id == this.props.category.serviceId) ? this.props.overseerr.sonarrServiceSettings.sonarrServices.filter(x => x.id == this.props.category.serviceId)[0].tags : []} + onChange={newTags => this.setCategory("tags", newTags.map(x => x.id))} /> + +
+ { + !this.props.overseerr.isSonarrServiceSettingsValid ? ( + + Could not load tags, cannot reach Overseerr. + ) + : null + } + +
+ + + + this.setCategory("is4K", !this.props.category.is4K)} + checked={this.props.category.is4K} + /> + + + + + + + + + +
+
+ + + + ); + } +} + +const mapPropsToState = state => { + return { + overseerr: state.tvShows.overseerr + } +}; + +const mapPropsToAction = { + loadServiceSettings: loadSonarrServiceSettings, + setOverseerrCategory: setOverseerrTvShowCategory, + removeOverseerrCategory: removeOverseerrTvShowCategory, +}; + +export default connect(mapPropsToState, mapPropsToAction)(OverseerrTvShowCategory); \ No newline at end of file diff --git a/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/TvShows/OverseerrTvShowCategoryList.jsx b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/TvShows/OverseerrTvShowCategoryList.jsx new file mode 100644 index 00000000..1c8faf56 --- /dev/null +++ b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Overseerr/TvShows/OverseerrTvShowCategoryList.jsx @@ -0,0 +1,100 @@ +import React from "react"; +import { connect } from 'react-redux'; +import { addOverseerrTvShowCategory } from "../../../../store/actions/OverseerrClientSonarrActions" +import OverseerrTvShowCategory from "./OverseerrTvShowCategory"; + +// reactstrap components +import { + Button, + Card, + CardHeader, + CardBody, + FormGroup, + Form, + Input, + Container, + Row, + Col, + UncontrolledTooltip, +} from "reactstrap"; + +class OverseerrTvShowCategoryList extends React.Component { + constructor(props) { + super(props); + this.createOverseerrCategory = this.createOverseerrCategory.bind(this); + } + + createOverseerrCategory() { + var newId = Math.floor((Math.random() * 900) + 1); + + while (this.props.overseerr.categories.map(x => x.id).includes(newId)) { + newId = Math.floor((Math.random() * 900) + 1); + } + + var newCategory = { + id: newId, + name: "new-category", + serviceId: this.props.overseerr.sonarrServiceSettings.sonarrServices.length > 0 ? this.props.overseerr.sonarrServiceSettings.sonarrServices[0].id : -1, + profileId: -1, + rootFolder: "", + tags: [], + wasCreated: true + }; + + this.props.addOverseerrCategory(newCategory); + } + + render() { + return ( + <> +
+
+ Overseerr Category Settings +
+
+
+ + + + + + + + + {this.props.overseerr.categories.map((category, key) => { + return ( + + + ) + })} + + + + +
CategoryActions
+ + + +
+
+
+
+ + ); + } +} + +const mapPropsToState = state => { + return { + overseerr: state.tvShows.overseerr + } +}; + +const mapPropsToAction = { + addOverseerrCategory: addOverseerrTvShowCategory, +}; + +export default connect(mapPropsToState, mapPropsToAction)(OverseerrTvShowCategoryList); \ No newline at end of file diff --git a/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Radarr/RadarrCategory.jsx b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Radarr/RadarrCategory.jsx index 9fbbdb64..b2d3f98a 100644 --- a/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Radarr/RadarrCategory.jsx +++ b/Requestrr.WebApi/ClientApp/src/components/DownloadClients/Radarr/RadarrCategory.jsx @@ -24,26 +24,18 @@ class RadarrCategory extends React.Component { super(props); this.state = { - arePathsValid: true, - areProfilesValid: true, - areTagsValid: true, nameErrorMessage: "", isNameValid: true, isOpen: false, }; this.validateName = this.validateName.bind(this); - this.setPaths = this.setPaths.bind(this); - this.setProfiles = this.setProfiles.bind(this); - this.setTags = this.setTags.bind(this); this.setCategory = this.setCategory.bind(this); this.deleteCategory = this.deleteCategory.bind(this); } componentDidMount() { - var category = { ...this.props.category }; - - if (category.wasCreated) { + if (this.props.category.wasCreated) { this.setState({ isOpen: true }); } @@ -52,22 +44,14 @@ class RadarrCategory extends React.Component { this.props.loadRootPaths(false); this.props.loadTags(false); } - - category = this.setPaths(category); - category = this.setProfiles(category); - category = this.setTags(category); - - this.setCategory(category) } componentDidUpdate(prevProps, prevState) { - var category = { ...this.props.category }; - var previousNames = prevProps.radarr.categories.map(x => x.name); var currentNames = this.props.radarr.categories.map(x => x.name); if (!(previousNames.length == currentNames.length && currentNames.every((value, index) => previousNames[index] == value))) { - this.validateName(category.name) + this.validateName(this.props.category.name) } if (this.props.canConnect) { @@ -76,22 +60,6 @@ class RadarrCategory extends React.Component { this.props.loadTags(false); } - if (!(prevProps.radarr.tags.length == this.props.radarr.tags.length && prevProps.radarr.tags.reduce((a, b, i) => a && this.props.radarr.tags[i], true))) { - category = this.setTags(category); - } - - if (!(prevProps.radarr.profiles.length == this.props.radarr.profiles.length && prevProps.radarr.profiles.reduce((a, b, i) => a && this.props.radarr.profiles[i], true))) { - category = this.setProfiles(category); - } - - if (!(prevProps.radarr.paths.length == this.props.radarr.paths.length && prevProps.radarr.paths.reduce((a, b, i) => a && this.props.radarr.paths[i], true))) { - category = this.setPaths(category); - } - - if (JSON.stringify(category) !== JSON.stringify(this.props.category)) { - this.setCategory(category) - } - if (prevProps.isSaving != this.props.isSaving) { this.setState({ isOpen: false, @@ -99,38 +67,6 @@ class RadarrCategory extends React.Component { } } - setPaths(category) { - if (this.props.radarr.paths.length > 0) { - var defaultPathId = this.props.radarr.paths[0].path; - var pathValue = this.props.radarr.paths.map(x => x.path).includes(category.rootFolder) ? category.rootFolder : defaultPathId; - - category = { ...category, rootFolder: pathValue }; - } - - return category; - } - - setProfiles(category) { - if (this.props.radarr.profiles.length > 0) { - var defaultProfileId = this.props.radarr.profiles[0].id; - var profileValue = this.props.radarr.profiles.map(x => x.id).includes(category.profileId) ? category.profileId : defaultProfileId; - - category = { ...category, profileId: profileValue }; - } - - return category; - } - - setTags(category) { - if (this.props.radarr.tags.length > 0) { - var tagsValue = category.tags.filter(x => this.props.radarr.tags.map(x => x.id).includes(x)); - - category = { ...category, tags: tagsValue }; - } - - return category; - } - validateNonEmptyString = value => { return /\S/.test(value); } @@ -164,9 +100,9 @@ class RadarrCategory extends React.Component { return state.isNameValid; } - - setCategory(category) { - this.props.setRadarrCategory(category); + + setCategory(fieldChanged, data) { + this.props.setRadarrCategory(this.props.category.id, fieldChanged, data); } deleteCategory() { @@ -207,7 +143,7 @@ class RadarrCategory extends React.Component { isSubmitted={this.props.isSubmitted || this.state.isNameValid} value={this.props.category.name} validation={this.validateName} - onChange={newName => this.setCategory({ ...this.props.category, name: newName })} + onChange={newName => this.setCategory("name", newName)} onValidate={isValid => this.setState({ isNameValid: isValid })} /> @@ -218,7 +154,7 @@ class RadarrCategory extends React.Component { name="Path" value={this.props.category.rootFolder} items={this.props.radarr.paths.map(x => { return { name: x.path, value: x.path } })} - onChange={newPath => this.setCategory({ ...this.props.category, rootFolder: newPath })} /> + onChange={newPath => this.setCategory("rootFolder", newPath)} />