mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-04-24 05:47:22 -04:00
New: App Sync Profiles
This commit is contained in:
parent
29c4849bef
commit
f64f8e915f
51 changed files with 1509 additions and 19 deletions
|
@ -0,0 +1,28 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.appProfiles,
|
||||
(appProfiles) => {
|
||||
const tagList = appProfiles.items.map((appProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = appProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
|
@ -47,6 +48,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||
const valueType = selectedFilterBuilderProp.valueType;
|
||||
|
||||
switch (valueType) {
|
||||
case filterBuilderValueTypes.APP_PROFILE:
|
||||
return AppProfileFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.BOOL:
|
||||
return BoolFilterBuilderRowValue;
|
||||
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(appProfiles, includeNoChange, includeMixed) => {
|
||||
const values = _.map(appProfiles.items, (appProfile) => {
|
||||
return {
|
||||
key: appProfile.id,
|
||||
value: appProfile.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class AppProfileSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
values
|
||||
} = this.props;
|
||||
|
||||
if (!value || !values.some((v) => v.key === value) ) {
|
||||
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key)));
|
||||
|
||||
if (firstValue) {
|
||||
this.onChange({ name, value: firstValue.key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value: parseInt(value) });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppProfileSelectInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AppProfileSelectInputConnector.defaultProps = {
|
||||
includeNoChange: false
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(AppProfileSelectInputConnector);
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import Link from 'Components/Link/Link';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AppProfileSelectInputConnector from './AppProfileSelectInputConnector';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
|
@ -29,6 +30,9 @@ import styles from './FormInputGroup.css';
|
|||
|
||||
function getComponent(type) {
|
||||
switch (type) {
|
||||
case inputTypes.APP_PROFILE_SELECT:
|
||||
return AppProfileSelectInputConnector;
|
||||
|
||||
case inputTypes.AUTO_COMPLETE:
|
||||
return AutoCompleteInput;
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
|||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchIndexers } from 'Store/Actions/indexerActions';
|
||||
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
|
||||
import { fetchGeneralSettings, fetchIndexerCategories, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
|
@ -49,6 +49,7 @@ const selectIsPopulated = createSelector(
|
|||
(state) => state.settings.ui.isPopulated,
|
||||
(state) => state.settings.general.isPopulated,
|
||||
(state) => state.settings.languages.isPopulated,
|
||||
(state) => state.settings.appProfiles.isPopulated,
|
||||
(state) => state.indexers.isPopulated,
|
||||
(state) => state.indexerStatus.isPopulated,
|
||||
(state) => state.settings.indexerCategories.isPopulated,
|
||||
|
@ -59,6 +60,7 @@ const selectIsPopulated = createSelector(
|
|||
uiSettingsIsPopulated,
|
||||
generalSettingsIsPopulated,
|
||||
languagesIsPopulated,
|
||||
appProfilesIsPopulated,
|
||||
indexersIsPopulated,
|
||||
indexerStatusIsPopulated,
|
||||
indexerCategoriesIsPopulated,
|
||||
|
@ -70,6 +72,7 @@ const selectIsPopulated = createSelector(
|
|||
uiSettingsIsPopulated &&
|
||||
generalSettingsIsPopulated &&
|
||||
languagesIsPopulated &&
|
||||
appProfilesIsPopulated &&
|
||||
indexersIsPopulated &&
|
||||
indexerStatusIsPopulated &&
|
||||
indexerCategoriesIsPopulated &&
|
||||
|
@ -84,6 +87,7 @@ const selectErrors = createSelector(
|
|||
(state) => state.settings.ui.error,
|
||||
(state) => state.settings.general.error,
|
||||
(state) => state.settings.languages.error,
|
||||
(state) => state.settings.appProfiles.error,
|
||||
(state) => state.indexers.error,
|
||||
(state) => state.indexerStatus.error,
|
||||
(state) => state.settings.indexerCategories.error,
|
||||
|
@ -94,6 +98,7 @@ const selectErrors = createSelector(
|
|||
uiSettingsError,
|
||||
generalSettingsError,
|
||||
languagesError,
|
||||
appProfilesError,
|
||||
indexersError,
|
||||
indexerStatusError,
|
||||
indexerCategoriesError,
|
||||
|
@ -105,6 +110,7 @@ const selectErrors = createSelector(
|
|||
uiSettingsError ||
|
||||
generalSettingsError ||
|
||||
languagesError ||
|
||||
appProfilesError ||
|
||||
indexersError ||
|
||||
indexerStatusError ||
|
||||
indexerCategoriesError ||
|
||||
|
@ -118,6 +124,7 @@ const selectErrors = createSelector(
|
|||
uiSettingsError,
|
||||
generalSettingsError,
|
||||
languagesError,
|
||||
appProfilesError,
|
||||
indexersError,
|
||||
indexerStatusError,
|
||||
indexerCategoriesError,
|
||||
|
@ -174,6 +181,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
dispatchFetchUISettings() {
|
||||
dispatch(fetchUISettings());
|
||||
},
|
||||
dispatchFetchAppProfiles() {
|
||||
dispatch(fetchAppProfiles());
|
||||
},
|
||||
dispatchFetchGeneralSettings() {
|
||||
dispatch(fetchGeneralSettings());
|
||||
},
|
||||
|
@ -207,6 +217,7 @@ class PageConnector extends Component {
|
|||
this.props.dispatchFetchCustomFilters();
|
||||
this.props.dispatchFetchTags();
|
||||
this.props.dispatchFetchLanguages();
|
||||
this.props.dispatchFetchAppProfiles();
|
||||
this.props.dispatchFetchIndexers();
|
||||
this.props.dispatchFetchIndexerStatus();
|
||||
this.props.dispatchFetchIndexerCategories();
|
||||
|
@ -232,6 +243,7 @@ class PageConnector extends Component {
|
|||
hasError,
|
||||
dispatchFetchTags,
|
||||
dispatchFetchLanguages,
|
||||
dispatchFetchAppProfiles,
|
||||
dispatchFetchIndexers,
|
||||
dispatchFetchIndexerStatus,
|
||||
dispatchFetchIndexerCategories,
|
||||
|
@ -272,6 +284,7 @@ PageConnector.propTypes = {
|
|||
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||
dispatchFetchAppProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerStatus: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerCategories: PropTypes.func.isRequired,
|
||||
|
|
|
@ -4,5 +4,6 @@ export const DATE = 'date';
|
|||
export const DEFAULT = 'default';
|
||||
export const INDEXER = 'indexer';
|
||||
export const PROTOCOL = 'protocol';
|
||||
export const APP_PROFILE = 'appProfile';
|
||||
export const MOVIE_STATUS = 'movieStatus';
|
||||
export const TAG = 'tag';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export const AUTO_COMPLETE = 'autoComplete';
|
||||
export const APP_PROFILE_SELECT = 'appProfileSelect';
|
||||
export const AVAILABILITY_SELECT = 'availabilitySelect';
|
||||
export const CAPTCHA = 'captcha';
|
||||
export const CARDIGANNCAPTCHA = 'cardigannCaptcha';
|
||||
|
@ -22,6 +23,7 @@ export const TAG_SELECT = 'tagSelect';
|
|||
|
||||
export const all = [
|
||||
AUTO_COMPLETE,
|
||||
APP_PROFILE_SELECT,
|
||||
AVAILABILITY_SELECT,
|
||||
CAPTCHA,
|
||||
CARDIGANNCAPTCHA,
|
||||
|
|
|
@ -42,6 +42,7 @@ function EditIndexerModalContent(props) {
|
|||
redirect,
|
||||
supportsRss,
|
||||
supportsRedirect,
|
||||
appProfileId,
|
||||
fields,
|
||||
priority
|
||||
} = item;
|
||||
|
@ -105,6 +106,17 @@ function EditIndexerModalContent(props) {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AppProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.APP_PROFILE_SELECT}
|
||||
name="appProfileId"
|
||||
{...appProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
fields ?
|
||||
fields.map((field) => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AppProfileSelectInputConnector from 'Components/Form/AppProfileSelectInputConnector';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
|
@ -22,6 +23,7 @@ class IndexerEditorFooter extends Component {
|
|||
|
||||
this.state = {
|
||||
enable: NO_CHANGE,
|
||||
appProfileId: NO_CHANGE,
|
||||
savingTags: false,
|
||||
isDeleteMovieModalOpen: false,
|
||||
isTagsModalOpen: false
|
||||
|
@ -37,6 +39,7 @@ class IndexerEditorFooter extends Component {
|
|||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
enable: NO_CHANGE,
|
||||
appProfileId: NO_CHANGE,
|
||||
savingTags: false
|
||||
});
|
||||
}
|
||||
|
@ -99,6 +102,7 @@ class IndexerEditorFooter extends Component {
|
|||
|
||||
const {
|
||||
enable,
|
||||
appProfileId,
|
||||
savingTags,
|
||||
isTagsModalOpen,
|
||||
isDeleteMovieModalOpen
|
||||
|
@ -127,6 +131,21 @@ class IndexerEditorFooter extends Component {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<IndexerEditorFooterLabel
|
||||
label={translate('AppProfile')}
|
||||
isSaving={isSaving && appProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<AppProfileSelectInputConnector
|
||||
name="appProfileId"
|
||||
value={appProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<IndexerEditorFooterLabel
|
||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createIndexerAppProfileSelector from 'Store/Selectors/createIndexerAppProfileSelector';
|
||||
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
|
||||
import createIndexerStatusSelector from 'Store/Selectors/createIndexerStatusSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
|
@ -19,11 +20,13 @@ function selectShowSearchAction() {
|
|||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createIndexerSelector(),
|
||||
createIndexerAppProfileSelector(),
|
||||
createIndexerStatusSelector(),
|
||||
selectShowSearchAction(),
|
||||
createUISettingsSelector(),
|
||||
(
|
||||
movie,
|
||||
appProfile,
|
||||
status,
|
||||
showSearchAction,
|
||||
uiSettings
|
||||
|
@ -40,6 +43,7 @@ function createMapStateToProps() {
|
|||
|
||||
return {
|
||||
...movie,
|
||||
appProfile,
|
||||
status,
|
||||
showSearchAction,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
|
|
|
@ -47,6 +47,15 @@ function IndexerIndexSortMenu(props) {
|
|||
{translate('Added')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="appProfileId"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('AppProfile')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="priority"
|
||||
sortKey={sortKey}
|
||||
|
|
|
@ -18,6 +18,12 @@
|
|||
flex: 0 0 90px;
|
||||
}
|
||||
|
||||
.appProfileId {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.capabilities {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
|
|
|
@ -25,6 +25,12 @@
|
|||
flex: 0 0 90px;
|
||||
}
|
||||
|
||||
.appProfileId {
|
||||
composes: cell;
|
||||
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.capabilities {
|
||||
composes: cell;
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ class IndexerIndexRow extends Component {
|
|||
privacy,
|
||||
priority,
|
||||
status,
|
||||
appProfile,
|
||||
added,
|
||||
capabilities,
|
||||
columns,
|
||||
|
@ -183,6 +184,17 @@ class IndexerIndexRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (column.name === 'appProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[column.name]}
|
||||
>
|
||||
{appProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (column.name === 'capabilities') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
|
@ -284,6 +296,7 @@ IndexerIndexRow.propTypes = {
|
|||
name: PropTypes.string.isRequired,
|
||||
enable: PropTypes.bool.isRequired,
|
||||
redirect: PropTypes.bool.isRequired,
|
||||
appProfile: PropTypes.object.isRequired,
|
||||
status: PropTypes.object,
|
||||
capabilities: PropTypes.object.isRequired,
|
||||
added: PropTypes.string.isRequired,
|
||||
|
|
|
@ -5,6 +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 AppProfilesConnector from 'Settings/Profiles/App/AppProfilesConnector';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ApplicationsConnector from './Applications/ApplicationsConnector';
|
||||
|
@ -45,6 +46,7 @@ class ApplicationSettings extends Component {
|
|||
|
||||
<PageContentBody>
|
||||
<ApplicationsConnector />
|
||||
<AppProfilesConnector />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
|
|
31
frontend/src/Settings/Profiles/App/AppProfile.css
Normal file
31
frontend/src/Settings/Profiles/App/AppProfile.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
.appProfile {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
155
frontend/src/Settings/Profiles/App/AppProfile.js
Normal file
155
frontend/src/Settings/Profiles/App/AppProfile.js
Normal file
|
@ -0,0 +1,155 @@
|
|||
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 { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditAppProfileModalConnector from './EditAppProfileModalConnector';
|
||||
import styles from './AppProfile.css';
|
||||
|
||||
class AppProfile extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditAppProfileModalOpen: false,
|
||||
isDeleteAppProfileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditAppProfilePress = () => {
|
||||
this.setState({ isEditAppProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAppProfileModalClose = () => {
|
||||
this.setState({ isEditAppProfileModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteAppProfilePress = () => {
|
||||
this.setState({
|
||||
isEditAppProfileModalOpen: false,
|
||||
isDeleteAppProfileModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteAppProfileModalClose = () => {
|
||||
this.setState({ isDeleteAppProfileModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeleteAppProfile = () => {
|
||||
this.props.onConfirmDeleteAppProfile(this.props.id);
|
||||
}
|
||||
|
||||
onCloneAppProfilePress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneAppProfilePress
|
||||
} = this.props;
|
||||
|
||||
onCloneAppProfilePress(id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enableRss,
|
||||
enableAutomaticSearch,
|
||||
enableInteractiveSearch,
|
||||
isDeleting
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.appProfile}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditAppProfilePress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneProfile')}
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneAppProfilePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
{
|
||||
<Label
|
||||
kind={enableRss ? kinds.SUCCESS : kinds.DISABLED}
|
||||
outline={!enableRss}
|
||||
>
|
||||
{translate('RSS')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
<Label
|
||||
kind={enableAutomaticSearch ? kinds.SUCCESS : kinds.DISABLED}
|
||||
outline={!enableAutomaticSearch}
|
||||
>
|
||||
{translate('AutomaticSearch')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
<Label
|
||||
kind={enableInteractiveSearch ? kinds.SUCCESS : kinds.DISABLED}
|
||||
outline={!enableInteractiveSearch}
|
||||
>
|
||||
{translate('InteractiveSearch')}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditAppProfileModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditAppProfileModalOpen}
|
||||
onModalClose={this.onEditAppProfileModalClose}
|
||||
onDeleteAppProfilePress={this.onDeleteAppProfilePress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteAppProfileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteAppProfile')}
|
||||
message={translate('AppProfileDeleteConfirm', [name])}
|
||||
confirmLabel={translate('Delete')}
|
||||
isSpinning={isDeleting}
|
||||
onConfirm={this.onConfirmDeleteAppProfile}
|
||||
onCancel={this.onDeleteAppProfileModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
enableRss: PropTypes.bool.isRequired,
|
||||
enableAutomaticSearch: PropTypes.bool.isRequired,
|
||||
enableInteractiveSearch: PropTypes.bool.isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteAppProfile: PropTypes.func.isRequired,
|
||||
onCloneAppProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AppProfile;
|
|
@ -0,0 +1,31 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAppProfileSelector from 'Store/Selectors/createAppProfileSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createAppProfileSelector(),
|
||||
(appProfile) => {
|
||||
return {
|
||||
name: appProfile.name
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function AppProfileNameConnector({ name, ...otherProps }) {
|
||||
return (
|
||||
<span>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
AppProfileNameConnector.propTypes = {
|
||||
appProfileId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(AppProfileNameConnector);
|
21
frontend/src/Settings/Profiles/App/AppProfiles.css
Normal file
21
frontend/src/Settings/Profiles/App/AppProfiles.css
Normal file
|
@ -0,0 +1,21 @@
|
|||
.appProfiles {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.addAppProfile {
|
||||
composes: appProfile from '~./AppProfile.css';
|
||||
|
||||
background-color: $cardAlternateBackgroundColor;
|
||||
color: $gray;
|
||||
text-align: center;
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 0;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
107
frontend/src/Settings/Profiles/App/AppProfiles.js
Normal file
107
frontend/src/Settings/Profiles/App/AppProfiles.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
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 AppProfile from './AppProfile';
|
||||
import EditAppProfileModalConnector from './EditAppProfileModalConnector';
|
||||
import styles from './AppProfiles.css';
|
||||
|
||||
class AppProfiles extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAppProfileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCloneAppProfilePress = (id) => {
|
||||
this.props.onCloneAppProfilePress(id);
|
||||
this.setState({ isAppProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAppProfilePress = () => {
|
||||
this.setState({ isAppProfileModalOpen: true });
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ isAppProfileModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isDeleting,
|
||||
onConfirmDeleteAppProfile,
|
||||
onCloneAppProfilePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('AppProfiles')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('UnableToLoadAppProfiles')}
|
||||
{...otherProps}c={true}
|
||||
>
|
||||
<div className={styles.appProfiles}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<AppProfile
|
||||
key={item.id}
|
||||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
onConfirmDeleteAppProfile={onConfirmDeleteAppProfile}
|
||||
onCloneAppProfilePress={this.onCloneAppProfilePress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addAppProfile}
|
||||
onPress={this.onEditAppProfilePress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<EditAppProfileModalConnector
|
||||
isOpen={this.state.isAppProfileModalOpen}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppProfiles.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteAppProfile: PropTypes.func.isRequired,
|
||||
onCloneAppProfilePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AppProfiles;
|
63
frontend/src/Settings/Profiles/App/AppProfilesConnector.js
Normal file
63
frontend/src/Settings/Profiles/App/AppProfilesConnector.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cloneAppProfile, deleteAppProfile, fetchAppProfiles } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import AppProfiles from './AppProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
||||
(appProfiles) => appProfiles
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchAppProfiles: fetchAppProfiles,
|
||||
dispatchDeleteAppProfile: deleteAppProfile,
|
||||
dispatchCloneAppProfile: cloneAppProfile
|
||||
};
|
||||
|
||||
class AppProfilesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchAppProfiles();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteAppProfile = (id) => {
|
||||
this.props.dispatchDeleteAppProfile({ id });
|
||||
}
|
||||
|
||||
onCloneAppProfilePress = (id) => {
|
||||
this.props.dispatchCloneAppProfile({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AppProfiles
|
||||
onConfirmDeleteAppProfile={this.onConfirmDeleteAppProfile}
|
||||
onCloneAppProfilePress={this.onCloneAppProfilePress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AppProfilesConnector.propTypes = {
|
||||
dispatchFetchAppProfiles: PropTypes.func.isRequired,
|
||||
dispatchDeleteAppProfile: PropTypes.func.isRequired,
|
||||
dispatchCloneAppProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AppProfilesConnector);
|
37
frontend/src/Settings/Profiles/App/EditAppProfileModal.js
Normal file
37
frontend/src/Settings/Profiles/App/EditAppProfileModal.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditAppProfileModalContentConnector from './EditAppProfileModalContentConnector';
|
||||
|
||||
class EditAppProfileModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditAppProfileModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAppProfileModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditAppProfileModal;
|
|
@ -0,0 +1,43 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditAppProfileModal from './EditAppProfileModal';
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditAppProfileModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'settings.appProfiles' });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditAppProfileModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAppProfileModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditAppProfileModalConnector);
|
|
@ -0,0 +1,3 @@
|
|||
.deleteButtonContainer {
|
||||
margin-right: auto;
|
||||
}
|
184
frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js
Normal file
184
frontend/src/Settings/Profiles/App/EditAppProfileModalContent.js
Normal file
|
@ -0,0 +1,184 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
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 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 translate from 'Utilities/String/translate';
|
||||
import styles from './EditAppProfileModalContent.css';
|
||||
|
||||
class EditAppProfileModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
isInUse,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
onDeleteAppProfilePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enableRss,
|
||||
enableInteractiveSearch,
|
||||
enableAutomaticSearch
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
|
||||
<ModalHeader>
|
||||
{id ? translate('EditAppProfile') : translate('AddAppProfile')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToAddANewAppProfilePleaseTryAgain')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!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"
|
||||
{...enableRss}
|
||||
helpText={translate('EnableRssHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('EnableInteractiveSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableInteractiveSearch"
|
||||
{...enableInteractiveSearch}
|
||||
helpText={translate('EnableInteractiveSearchHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('EnableAutomaticSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableAutomaticSearch"
|
||||
{...enableAutomaticSearch}
|
||||
helpText={translate('EnableAutomaticSearchHelpText')}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id ?
|
||||
<div
|
||||
className={styles.deleteButtonContainer}
|
||||
title={
|
||||
isInUse ?
|
||||
translate('AppProfileInUse') :
|
||||
undefined
|
||||
}
|
||||
>
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
isDisabled={isInUse}
|
||||
onPress={onDeleteAppProfilePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAppProfileModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
isInUse: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteAppProfilePress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditAppProfileModalContent;
|
|
@ -0,0 +1,82 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchAppProfileSchema, saveAppProfile, setAppProfileValue } from 'Store/Actions/settingsActions';
|
||||
import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditAppProfileModalContent from './EditAppProfileModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createProviderSettingsSelector('appProfiles'),
|
||||
createProfileInUseSelector('appProfileId'),
|
||||
(appProfile, isInUse) => {
|
||||
return {
|
||||
...appProfile,
|
||||
isInUse
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchAppProfileSchema,
|
||||
setAppProfileValue,
|
||||
saveAppProfile
|
||||
};
|
||||
|
||||
class EditAppProfileModalContentConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id && !this.props.isPopulated) {
|
||||
this.props.fetchAppProfileSchema();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setAppProfileValue({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveAppProfile({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditAppProfileModalContent
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditAppProfileModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setAppProfileValue: PropTypes.func.isRequired,
|
||||
fetchAppProfileSchema: PropTypes.func.isRequired,
|
||||
saveAppProfile: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditAppProfileModalContentConnector);
|
97
frontend/src/Store/Actions/Settings/appProfiles.js
Normal file
97
frontend/src/Store/Actions/Settings/appProfiles.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.appProfiles';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_APP_PROFILES = 'settings/appProfiles/fetchAppProfiles';
|
||||
export const FETCH_APP_PROFILE_SCHEMA = 'settings/appProfiles/fetchAppProfileSchema';
|
||||
export const SAVE_APP_PROFILE = 'settings/appProfiles/saveAppProfile';
|
||||
export const DELETE_APP_PROFILE = 'settings/appProfiles/deleteAppProfile';
|
||||
export const SET_APP_PROFILE_VALUE = 'settings/appProfiles/setAppProfileValue';
|
||||
export const CLONE_APP_PROFILE = 'settings/appProfiles/cloneAppProfile';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchAppProfiles = createThunk(FETCH_APP_PROFILES);
|
||||
export const fetchAppProfileSchema = createThunk(FETCH_APP_PROFILE_SCHEMA);
|
||||
export const saveAppProfile = createThunk(SAVE_APP_PROFILE);
|
||||
export const deleteAppProfile = createThunk(DELETE_APP_PROFILE);
|
||||
|
||||
export const setAppProfileValue = createAction(SET_APP_PROFILE_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const cloneAppProfile = createAction(CLONE_APP_PROFILE);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_APP_PROFILES]: createFetchHandler(section, '/appprofile'),
|
||||
[FETCH_APP_PROFILE_SCHEMA]: createFetchSchemaHandler(section, '/appprofile/schema'),
|
||||
[SAVE_APP_PROFILE]: createSaveProviderHandler(section, '/appprofile'),
|
||||
[DELETE_APP_PROFILE]: createRemoveItemHandler(section, '/appprofile')
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_APP_PROFILE_VALUE]: createSetSettingValueReducer(section),
|
||||
|
||||
[CLONE_APP_PROFILE]: function(state, { payload }) {
|
||||
const id = payload.id;
|
||||
const newState = getSectionState(state, section);
|
||||
const item = newState.items.find((i) => i.id === id);
|
||||
const pendingChanges = { ...item, id: 0 };
|
||||
delete pendingChanges.id;
|
||||
|
||||
pendingChanges.name = `${pendingChanges.name} - Copy`;
|
||||
newState.pendingChanges = pendingChanges;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
|
@ -74,6 +74,12 @@ export const defaultState = {
|
|||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'appProfileId',
|
||||
label: translate('AppProfile'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'added',
|
||||
label: translate('Added'),
|
||||
|
@ -138,6 +144,12 @@ export const defaultState = {
|
|||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.PROTOCOL
|
||||
},
|
||||
{
|
||||
name: 'appProfileId',
|
||||
label: translate('AppProfile'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.APP_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: translate('Tags'),
|
||||
|
|
|
@ -2,6 +2,7 @@ import { createAction } from 'redux-actions';
|
|||
import { handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import applications from './Settings/applications';
|
||||
import appProfiles from './Settings/appProfiles';
|
||||
import development from './Settings/development';
|
||||
import downloadClients from './Settings/downloadClients';
|
||||
import general from './Settings/general';
|
||||
|
@ -16,6 +17,7 @@ export * from './Settings/indexerCategories';
|
|||
export * from './Settings/languages';
|
||||
export * from './Settings/notifications';
|
||||
export * from './Settings/applications';
|
||||
export * from './Settings/appProfiles';
|
||||
export * from './Settings/development';
|
||||
export * from './Settings/ui';
|
||||
|
||||
|
@ -36,6 +38,7 @@ export const defaultState = {
|
|||
languages: languages.defaultState,
|
||||
notifications: notifications.defaultState,
|
||||
applications: applications.defaultState,
|
||||
appProfiles: appProfiles.defaultState,
|
||||
development: development.defaultState,
|
||||
ui: ui.defaultState
|
||||
};
|
||||
|
@ -64,6 +67,7 @@ export const actionHandlers = handleThunks({
|
|||
...languages.actionHandlers,
|
||||
...notifications.actionHandlers,
|
||||
...applications.actionHandlers,
|
||||
...appProfiles.actionHandlers,
|
||||
...development.actionHandlers,
|
||||
...ui.actionHandlers
|
||||
});
|
||||
|
@ -83,6 +87,7 @@ export const reducers = createHandleActions({
|
|||
...languages.reducers,
|
||||
...notifications.reducers,
|
||||
...applications.reducers,
|
||||
...appProfiles.reducers,
|
||||
...development.reducers,
|
||||
...ui.reducers
|
||||
|
||||
|
|
15
frontend/src/Store/Selectors/createAppProfileSelector.js
Normal file
15
frontend/src/Store/Selectors/createAppProfileSelector.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
function createAppProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { appProfileId }) => appProfileId,
|
||||
(state) => state.settings.appProfiles.items,
|
||||
(appProfileId, appProfiles) => {
|
||||
return appProfiles.find((profile) => {
|
||||
return profile.id === appProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createAppProfileSelector;
|
|
@ -0,0 +1,16 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import createIndexerSelector from './createIndexerSelector';
|
||||
|
||||
function createIndexerAppProfileSelector() {
|
||||
return createSelector(
|
||||
(state) => state.settings.appProfiles.items,
|
||||
createIndexerSelector(),
|
||||
(appProfiles, indexer = {}) => {
|
||||
return appProfiles.find((profile) => {
|
||||
return profile.id === indexer.appProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createIndexerAppProfileSelector;
|
23
frontend/src/Store/Selectors/createProfileInUseSelector.js
Normal file
23
frontend/src/Store/Selectors/createProfileInUseSelector.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllIndexersSelector from './createAllIndexersSelector';
|
||||
|
||||
function createProfileInUseSelector(profileProp) {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
createAllIndexersSelector(),
|
||||
(id, indexers) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_.some(indexers, { [profileProp]: id })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createProfileInUseSelector;
|
|
@ -32,8 +32,8 @@ module.exports = {
|
|||
|
||||
// Drag
|
||||
dragHandleWidth: '40px',
|
||||
qualityProfileItemHeight: '30px',
|
||||
qualityProfileItemDragSourcePadding: '4px',
|
||||
appProfileItemHeight: '30px',
|
||||
appProfileItemDragSourcePadding: '4px',
|
||||
|
||||
// Progress Bar
|
||||
progressBarSmallHeight: '5px',
|
||||
|
|
|
@ -137,9 +137,9 @@ namespace NzbDrone.Core.Applications.Lidarr
|
|||
{
|
||||
Id = id,
|
||||
Name = $"{indexer.Name} (Prowlarr)",
|
||||
EnableRss = indexer.Enable,
|
||||
EnableAutomaticSearch = indexer.Enable,
|
||||
EnableInteractiveSearch = indexer.Enable,
|
||||
EnableRss = indexer.AppProfile.Value.EnableRss,
|
||||
EnableAutomaticSearch = indexer.AppProfile.Value.EnableAutomaticSearch,
|
||||
EnableInteractiveSearch = indexer.AppProfile.Value.EnableInteractiveSearch,
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
|
|
|
@ -137,9 +137,9 @@ namespace NzbDrone.Core.Applications.Radarr
|
|||
{
|
||||
Id = id,
|
||||
Name = $"{indexer.Name} (Prowlarr)",
|
||||
EnableRss = indexer.Enable,
|
||||
EnableAutomaticSearch = indexer.Enable,
|
||||
EnableInteractiveSearch = indexer.Enable,
|
||||
EnableRss = indexer.AppProfile.Value.EnableRss,
|
||||
EnableAutomaticSearch = indexer.AppProfile.Value.EnableAutomaticSearch,
|
||||
EnableInteractiveSearch = indexer.AppProfile.Value.EnableInteractiveSearch,
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
|
|
|
@ -137,9 +137,9 @@ namespace NzbDrone.Core.Applications.Readarr
|
|||
{
|
||||
Id = id,
|
||||
Name = $"{indexer.Name} (Prowlarr)",
|
||||
EnableRss = indexer.Enable,
|
||||
EnableAutomaticSearch = indexer.Enable,
|
||||
EnableInteractiveSearch = indexer.Enable,
|
||||
EnableRss = indexer.AppProfile.Value.EnableRss,
|
||||
EnableAutomaticSearch = indexer.AppProfile.Value.EnableAutomaticSearch,
|
||||
EnableInteractiveSearch = indexer.AppProfile.Value.EnableInteractiveSearch,
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
|
|
|
@ -137,9 +137,9 @@ namespace NzbDrone.Core.Applications.Sonarr
|
|||
{
|
||||
Id = id,
|
||||
Name = $"{indexer.Name} (Prowlarr)",
|
||||
EnableRss = indexer.Enable,
|
||||
EnableAutomaticSearch = indexer.Enable,
|
||||
EnableInteractiveSearch = indexer.Enable,
|
||||
EnableRss = indexer.AppProfile.Value.EnableRss,
|
||||
EnableAutomaticSearch = indexer.AppProfile.Value.EnableAutomaticSearch,
|
||||
EnableInteractiveSearch = indexer.AppProfile.Value.EnableInteractiveSearch,
|
||||
Priority = indexer.Priority,
|
||||
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
|
||||
ConfigContract = schema.ConfigContract,
|
||||
|
|
21
src/NzbDrone.Core/Datastore/Migration/006_app_profiles.cs
Normal file
21
src/NzbDrone.Core/Datastore/Migration/006_app_profiles.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(6)]
|
||||
public class app_profiles : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Create.TableForModel("AppSyncProfiles")
|
||||
.WithColumn("Name").AsString().Unique()
|
||||
.WithColumn("EnableRss").AsBoolean().NotNullable()
|
||||
.WithColumn("EnableInteractiveSearch").AsBoolean().NotNullable()
|
||||
.WithColumn("EnableAutomaticSearch").AsBoolean().NotNullable();
|
||||
|
||||
Alter.Table("Indexers")
|
||||
.AddColumn("AppProfileId").AsInt32().NotNullable().WithDefaultValue(1);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ using NzbDrone.Core.Languages;
|
|||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Notifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Tags;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Update.History;
|
||||
|
@ -52,7 +53,8 @@ namespace NzbDrone.Core.Datastore
|
|||
.Ignore(i => i.SupportsSearch)
|
||||
.Ignore(i => i.SupportsRedirect)
|
||||
.Ignore(i => i.Capabilities)
|
||||
.Ignore(d => d.Tags);
|
||||
.Ignore(d => d.Tags)
|
||||
.HasOne(a => a.AppProfile, a => a.AppProfileId);
|
||||
|
||||
Mapper.Entity<DownloadClientDefinition>("DownloadClients").RegisterModel()
|
||||
.Ignore(x => x.ImplementationName)
|
||||
|
@ -86,6 +88,8 @@ namespace NzbDrone.Core.Datastore
|
|||
|
||||
Mapper.Entity<CustomFilter>("CustomFilters").RegisterModel();
|
||||
Mapper.Entity<UpdateHistory>("UpdateHistory").RegisterModel();
|
||||
|
||||
Mapper.Entity<AppSyncProfile>("AppSyncProfiles").RegisterModel();
|
||||
}
|
||||
|
||||
private static void RegisterMappers()
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Indexers.Cardigann;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
|
||||
namespace NzbDrone.Core.Indexers
|
||||
|
@ -21,6 +23,8 @@ namespace NzbDrone.Core.Indexers
|
|||
public int Priority { get; set; } = 25;
|
||||
public bool Redirect { get; set; }
|
||||
public DateTime Added { get; set; }
|
||||
public int AppProfileId { get; set; }
|
||||
public LazyLoaded<AppSyncProfile> AppProfile { get; set; }
|
||||
|
||||
public IndexerStatus Status { get; set; }
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
"About": "About",
|
||||
"AcceptConfirmationModal": "Accept Confirmation Modal",
|
||||
"Actions": "Actions",
|
||||
"AppProfile": "App Profile",
|
||||
"AddAppProfile": "Add App Sync Profile",
|
||||
"Added": "Added",
|
||||
"AddedToDownloadClient": "Release added to client",
|
||||
"AddIndexer": "Add Indexer",
|
||||
|
@ -24,6 +26,7 @@
|
|||
"ApplyTagsHelpTexts2": "Add: Add the tags the existing list of tags",
|
||||
"ApplyTagsHelpTexts3": "Remove: Remove the entered tags",
|
||||
"ApplyTagsHelpTexts4": "Replace: Replace the tags with the entered tags (enter no tags to clear all tags)",
|
||||
"AppProfiles": "App Profiles",
|
||||
"AreYouSureYouWantToResetYourAPIKey": "Are you sure you want to reset your API Key?",
|
||||
"Authentication": "Authentication",
|
||||
"AuthenticationMethodHelpText": "Require Username and Password to access Prowlarr",
|
||||
|
@ -82,7 +85,6 @@
|
|||
"DevelopmentSettings": "Development Settings",
|
||||
"Disabled": "Disabled",
|
||||
"Docker": "Docker",
|
||||
"IndexerObsoleteCheckMessage": "Indexers are obsolete or have been updated: {0}. Please remove and (or) re-add to Prowlarr",
|
||||
"DownloadClient": "Download Client",
|
||||
"DownloadClientCheckNoneAvailableMessage": "No download client is available",
|
||||
"DownloadClientCheckUnableToCommunicateMessage": "Unable to communicate with {0}.",
|
||||
|
@ -93,6 +95,7 @@
|
|||
"DownloadClientStatusCheckSingleClientMessage": "Download clients unavailable due to failures: {0}",
|
||||
"DownloadClientUnavailable": "Download client is unavailable",
|
||||
"Downloading": "Downloading",
|
||||
"EditAppProfile": "Edit App Profile",
|
||||
"EditIndexer": "Edit Indexer",
|
||||
"Enable": "Enable",
|
||||
"EnableAutoHelpText": "If enabled, Movies will be automatically added to Prowlarr from this list",
|
||||
|
@ -110,7 +113,8 @@
|
|||
"EnableInteractiveSearchHelpText": "Will be used when interactive search is used",
|
||||
"EnableInteractiveSearchHelpTextWarning": "Search is not supported with this indexer",
|
||||
"EnableMediaInfoHelpText": "Extract video information such as resolution, runtime and codec information from files. This requires Prowlarr to read parts of the file which may cause high disk or network activity during scans.",
|
||||
"EnableRSS": "Enable RSS",
|
||||
"EnableRss": "Enable RSS",
|
||||
"EnableRssHelpText": "Enable Rss feed for Indexer",
|
||||
"EnableSSL": "Enable SSL",
|
||||
"EnableSslHelpText": " Requires restart running as administrator to take effect",
|
||||
"Error": "Error",
|
||||
|
@ -149,6 +153,7 @@
|
|||
"IndexerHealthCheckNoIndexers": "No indexers enabled, Prowlarr will not return search results",
|
||||
"IndexerLongTermStatusCheckAllClientMessage": "All indexers are unavailable due to failures for more than 6 hours",
|
||||
"IndexerLongTermStatusCheckSingleClientMessage": "Indexers unavailable due to failures for more than 6 hours: {0}",
|
||||
"IndexerObsoleteCheckMessage": "Indexers are obsolete or have been updated: {0}. Please remove and (or) re-add to Prowlarr",
|
||||
"IndexerPriority": "Indexer Priority",
|
||||
"IndexerPriorityHelpText": "Indexer Priority from 1 (Highest) to 50 (Lowest). Default: 25.",
|
||||
"IndexerQuery": "Indexer Query",
|
||||
|
|
12
src/NzbDrone.Core/Profiles/AppSyncProfile.cs
Normal file
12
src/NzbDrone.Core/Profiles/AppSyncProfile.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Profiles
|
||||
{
|
||||
public class AppSyncProfile : ModelBase
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool EnableRss { get; set; }
|
||||
public bool EnableAutomaticSearch { get; set; }
|
||||
public bool EnableInteractiveSearch { get; set; }
|
||||
}
|
||||
}
|
33
src/NzbDrone.Core/Profiles/AppSyncProfileRepository.cs
Normal file
33
src/NzbDrone.Core/Profiles/AppSyncProfileRepository.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Profiles
|
||||
{
|
||||
public interface IAppProfileRepository : IBasicRepository<AppSyncProfile>
|
||||
{
|
||||
bool Exists(int id);
|
||||
}
|
||||
|
||||
public class AppSyncProfileRepository : BasicRepository<AppSyncProfile>, IAppProfileRepository
|
||||
{
|
||||
public AppSyncProfileRepository(IMainDatabase database,
|
||||
IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
|
||||
protected override List<AppSyncProfile> Query(SqlBuilder builder)
|
||||
{
|
||||
var profiles = base.Query(builder);
|
||||
|
||||
return profiles;
|
||||
}
|
||||
|
||||
public bool Exists(int id)
|
||||
{
|
||||
return Query(x => x.Id == id).Count == 1;
|
||||
}
|
||||
}
|
||||
}
|
104
src/NzbDrone.Core/Profiles/AppSyncProfileService.cs
Normal file
104
src/NzbDrone.Core/Profiles/AppSyncProfileService.cs
Normal file
|
@ -0,0 +1,104 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Lifecycle;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Profiles
|
||||
{
|
||||
public interface IProfileService
|
||||
{
|
||||
AppSyncProfile Add(AppSyncProfile profile);
|
||||
void Update(AppSyncProfile profile);
|
||||
void Delete(int id);
|
||||
List<AppSyncProfile> All();
|
||||
AppSyncProfile Get(int id);
|
||||
bool Exists(int id);
|
||||
AppSyncProfile GetDefaultProfile(string name);
|
||||
}
|
||||
|
||||
public class AppSyncProfileService : IProfileService,
|
||||
IHandle<ApplicationStartedEvent>
|
||||
{
|
||||
private readonly IAppProfileRepository _profileRepository;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AppSyncProfileService(IAppProfileRepository profileRepository,
|
||||
IIndexerFactory movieService,
|
||||
Logger logger)
|
||||
{
|
||||
_profileRepository = profileRepository;
|
||||
_indexerFactory = movieService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public AppSyncProfile Add(AppSyncProfile profile)
|
||||
{
|
||||
return _profileRepository.Insert(profile);
|
||||
}
|
||||
|
||||
public void Update(AppSyncProfile profile)
|
||||
{
|
||||
_profileRepository.Update(profile);
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
if (_indexerFactory.All().Any(c => c.AppProfileId == id))
|
||||
{
|
||||
throw new ProfileInUseException(id);
|
||||
}
|
||||
|
||||
_profileRepository.Delete(id);
|
||||
}
|
||||
|
||||
public List<AppSyncProfile> All()
|
||||
{
|
||||
return _profileRepository.All().ToList();
|
||||
}
|
||||
|
||||
public AppSyncProfile Get(int id)
|
||||
{
|
||||
return _profileRepository.Get(id);
|
||||
}
|
||||
|
||||
public bool Exists(int id)
|
||||
{
|
||||
return _profileRepository.Exists(id);
|
||||
}
|
||||
|
||||
public void Handle(ApplicationStartedEvent message)
|
||||
{
|
||||
if (All().Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Info("Setting up default app profile");
|
||||
|
||||
AddDefaultProfile("Standard");
|
||||
}
|
||||
|
||||
public AppSyncProfile GetDefaultProfile(string name)
|
||||
{
|
||||
var qualityProfile = new AppSyncProfile
|
||||
{
|
||||
Name = name,
|
||||
EnableAutomaticSearch = true,
|
||||
EnableInteractiveSearch = true,
|
||||
EnableRss = true
|
||||
};
|
||||
|
||||
return qualityProfile;
|
||||
}
|
||||
|
||||
private AppSyncProfile AddDefaultProfile(string name)
|
||||
{
|
||||
var profile = GetDefaultProfile(name);
|
||||
|
||||
return Add(profile);
|
||||
}
|
||||
}
|
||||
}
|
12
src/NzbDrone.Core/Profiles/ProfileInUseException.cs
Normal file
12
src/NzbDrone.Core/Profiles/ProfileInUseException.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using NzbDrone.Common.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Profiles
|
||||
{
|
||||
public class ProfileInUseException : NzbDroneException
|
||||
{
|
||||
public ProfileInUseException(int profileId)
|
||||
: base("Profile [{0}] is in use.", profileId)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
[HttpPut]
|
||||
public IActionResult SaveAll(IndexerEditorResource resource)
|
||||
{
|
||||
var indexersToUpdate = _indexerService.All().Where(x => resource.IndexerIds.Contains(x.Id));
|
||||
var indexersToUpdate = _indexerService.AllProviders(false).Select(x => (IndexerDefinition)x.Definition).Where(d => resource.IndexerIds.Contains(d.Id));
|
||||
|
||||
foreach (var indexer in indexersToUpdate)
|
||||
{
|
||||
|
@ -34,6 +34,11 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
indexer.Enable = bool.Parse(resource.Enable);
|
||||
}
|
||||
|
||||
if (resource.AppProfileId.HasValue)
|
||||
{
|
||||
indexer.AppProfileId = resource.AppProfileId.Value;
|
||||
}
|
||||
|
||||
if (resource.Tags != null)
|
||||
{
|
||||
var newTags = resource.Tags;
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
{
|
||||
public List<int> IndexerIds { get; set; }
|
||||
public string Enable { get; set; }
|
||||
public int? AppProfileId { get; set; }
|
||||
public List<int> Tags { get; set; }
|
||||
public ApplyTags ApplyTags { get; set; }
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
public bool SupportsRss { get; set; }
|
||||
public bool SupportsSearch { get; set; }
|
||||
public bool SupportsRedirect { get; set; }
|
||||
public int AppProfileId { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
public IndexerPrivacy Privacy { get; set; }
|
||||
public IndexerCapabilityResource Capabilities { get; set; }
|
||||
|
@ -65,6 +66,7 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
}
|
||||
}
|
||||
|
||||
resource.AppProfileId = definition.AppProfileId;
|
||||
resource.BaseUrl = definition.BaseUrl;
|
||||
resource.Description = definition.Description;
|
||||
resource.Language = definition.Language;
|
||||
|
@ -117,6 +119,7 @@ namespace Prowlarr.Api.V1.Indexers
|
|||
}
|
||||
}
|
||||
|
||||
definition.AppProfileId = resource.AppProfileId;
|
||||
definition.Enable = resource.Enable;
|
||||
definition.Redirect = resource.Redirect;
|
||||
definition.BaseUrl = resource.BaseUrl;
|
||||
|
|
57
src/Prowlarr.Api.V1/Profiles/App/AppProfileController.cs
Normal file
57
src/Prowlarr.Api.V1/Profiles/App/AppProfileController.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Http.REST.Attributes;
|
||||
using Prowlarr.Http;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Profiles.App
|
||||
{
|
||||
[V1ApiController]
|
||||
public class AppProfileController : RestController<AppProfileResource>
|
||||
{
|
||||
private readonly IProfileService _profileService;
|
||||
|
||||
public AppProfileController(IProfileService profileService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<AppProfileResource> Create(AppProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _profileService.Add(model);
|
||||
return Created(model.Id);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public void DeleteProfile(int id)
|
||||
{
|
||||
_profileService.Delete(id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<AppProfileResource> Update(AppProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
_profileService.Update(model);
|
||||
|
||||
return Accepted(model.Id);
|
||||
}
|
||||
|
||||
public override AppProfileResource GetResourceById(int id)
|
||||
{
|
||||
return _profileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<AppProfileResource> GetAll()
|
||||
{
|
||||
return _profileService.All().ToResource();
|
||||
}
|
||||
}
|
||||
}
|
57
src/Prowlarr.Api.V1/Profiles/App/AppProfileResource.cs
Normal file
57
src/Prowlarr.Api.V1/Profiles/App/AppProfileResource.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using Prowlarr.Http.REST;
|
||||
|
||||
namespace Prowlarr.Api.V1.Profiles.App
|
||||
{
|
||||
public class AppProfileResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool EnableRss { get; set; }
|
||||
public bool EnableInteractiveSearch { get; set; }
|
||||
public bool EnableAutomaticSearch { get; set; }
|
||||
}
|
||||
|
||||
public static class ProfileResourceMapper
|
||||
{
|
||||
public static AppProfileResource ToResource(this AppSyncProfile model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AppProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
EnableRss = model.EnableRss,
|
||||
EnableInteractiveSearch = model.EnableInteractiveSearch,
|
||||
EnableAutomaticSearch = model.EnableAutomaticSearch
|
||||
};
|
||||
}
|
||||
|
||||
public static AppSyncProfile ToModel(this AppProfileResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AppSyncProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
EnableRss = resource.EnableRss,
|
||||
EnableInteractiveSearch = resource.EnableInteractiveSearch,
|
||||
EnableAutomaticSearch = resource.EnableAutomaticSearch
|
||||
};
|
||||
}
|
||||
|
||||
public static List<AppProfileResource> ToResource(this IEnumerable<AppSyncProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
25
src/Prowlarr.Api.V1/Profiles/App/AppProfileSchemaModule.cs
Normal file
25
src/Prowlarr.Api.V1/Profiles/App/AppProfileSchemaModule.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using Prowlarr.Http;
|
||||
|
||||
namespace Prowlarr.Api.V1.Profiles.App
|
||||
{
|
||||
[V1ApiController("appprofile/schema")]
|
||||
public class QualityProfileSchemaController : Controller
|
||||
{
|
||||
private readonly IProfileService _profileService;
|
||||
|
||||
public QualityProfileSchemaController(IProfileService profileService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public AppProfileResource GetSchema()
|
||||
{
|
||||
AppSyncProfile qualityProfile = _profileService.GetDefaultProfile(string.Empty);
|
||||
|
||||
return qualityProfile.ToResource();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue