mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-16 21:15:33 -04:00
Compare commits
31 Commits
v5.26.0.10
...
v5.27.0.10
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dae5e86b2c | ||
|
|
71f032d175 | ||
|
|
5a6db29dbd | ||
|
|
2dac2dd35b | ||
|
|
b829638a77 | ||
|
|
b6b7f13839 | ||
|
|
a9ad197b75 | ||
|
|
1b28116a7e | ||
|
|
5870c88e1c | ||
|
|
0629832bd0 | ||
|
|
430897c710 | ||
|
|
9c42246eef | ||
|
|
489a86b253 | ||
|
|
9c8d3b679d | ||
|
|
b2e51d1613 | ||
|
|
a95b1f2992 | ||
|
|
ac33b15048 | ||
|
|
d28f03af28 | ||
|
|
73b99d0be2 | ||
|
|
15c34a61de | ||
|
|
b99c536306 | ||
|
|
2ebf391f85 | ||
|
|
3945a2eeb8 | ||
|
|
e6980df590 | ||
|
|
187dd79b9c | ||
|
|
22ef334de6 | ||
|
|
c9eb9b8b98 | ||
|
|
9c74c40fc6 | ||
|
|
8911cbe872 | ||
|
|
7e541d4653 | ||
|
|
1cc2237ac0 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -165,15 +165,12 @@ Thumbs.db
|
||||
/tools/Addins/*
|
||||
packages.config.md5sum
|
||||
|
||||
|
||||
# Common IntelliJ Platform excludes
|
||||
|
||||
# Ignore Rider projects completely for now
|
||||
.idea/
|
||||
|
||||
# ignore node_modules symlink
|
||||
node_modules
|
||||
node_modules.nosync
|
||||
|
||||
# API doc generation
|
||||
.config/
|
||||
|
||||
# Ignore Jetbrains IntelliJ Workspace Directories
|
||||
.idea/
|
||||
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.26.0'
|
||||
majorVersion: '5.27.0'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
|
||||
@@ -108,7 +108,7 @@ class ImportMovie extends Component {
|
||||
{
|
||||
!rootFoldersFetching && !!rootFoldersError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
{translate('RootFoldersLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class ImportMovieSelectFolder extends Component {
|
||||
{
|
||||
!isFetching && error ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
{translate('RootFoldersLoadError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import MovieIndex from 'Movie/Index/MovieIndex';
|
||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||
import ImportListSettings from 'Settings/ImportLists/ImportListSettings';
|
||||
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
||||
import MediaManagement from 'Settings/MediaManagement/MediaManagement';
|
||||
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||
import Profiles from 'Settings/Profiles/Profiles';
|
||||
@@ -99,10 +99,7 @@ function AppRoutes() {
|
||||
|
||||
<Route exact={true} path="/settings" component={Settings} />
|
||||
|
||||
<Route
|
||||
path="/settings/mediamanagement"
|
||||
component={MediaManagementConnector}
|
||||
/>
|
||||
<Route path="/settings/mediamanagement" component={MediaManagement} />
|
||||
|
||||
<Route path="/settings/profiles" component={Profiles} />
|
||||
|
||||
@@ -113,17 +110,14 @@ function AppRoutes() {
|
||||
component={CustomFormatSettingsPage}
|
||||
/>
|
||||
|
||||
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
|
||||
<Route path="/settings/indexers" component={IndexerSettings} />
|
||||
|
||||
<Route
|
||||
path="/settings/downloadclients"
|
||||
component={DownloadClientSettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/importlists"
|
||||
component={ImportListSettingsConnector}
|
||||
/>
|
||||
<Route path="/settings/importlists" component={ImportListSettings} />
|
||||
|
||||
<Route path="/settings/connect" component={NotificationSettings} />
|
||||
|
||||
|
||||
@@ -43,9 +43,15 @@ export interface AppSectionSchemaState<T> {
|
||||
isSchemaFetching: boolean;
|
||||
isSchemaPopulated: boolean;
|
||||
schemaError: Error;
|
||||
schema: {
|
||||
items: T[];
|
||||
};
|
||||
schema: T[];
|
||||
selectedSchema?: T;
|
||||
}
|
||||
|
||||
export interface AppSectionItemSchemaState<T> {
|
||||
isSchemaFetching: boolean;
|
||||
isSchemaPopulated: boolean;
|
||||
schemaError: Error;
|
||||
schema: T;
|
||||
}
|
||||
|
||||
export interface AppSectionItemState<T> {
|
||||
@@ -61,9 +67,10 @@ export interface AppSectionProviderState<T>
|
||||
AppSectionSaveState {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
isTesting?: boolean;
|
||||
error: Error;
|
||||
items: T[];
|
||||
pendingChanges: Partial<T>;
|
||||
pendingChanges?: Partial<T>;
|
||||
}
|
||||
|
||||
interface AppSectionState<T> {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemSchemaState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
PagedAppSectionState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Language from 'Language/Language';
|
||||
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import DelayProfile from 'typings/DelayProfile';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||
@@ -16,12 +19,34 @@ import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import General from 'typings/Settings/General';
|
||||
import IndexerOptions from 'typings/Settings/IndexerOptions';
|
||||
import MediaManagement from 'typings/Settings/MediaManagement';
|
||||
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||
import NamingExample from 'typings/Settings/NamingExample';
|
||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
import MetadataAppState from './MetadataAppState';
|
||||
|
||||
type Presets<T> = T & {
|
||||
presets: T[];
|
||||
};
|
||||
|
||||
export interface AutoTaggingAppState
|
||||
extends AppSectionState<AutoTagging>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface AutoTaggingSpecificationAppState
|
||||
extends AppSectionState<AutoTaggingSpecification>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState<AutoTaggingSpecification> {}
|
||||
|
||||
export interface DelayProfileAppState
|
||||
extends AppSectionState<DelayProfile>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
@@ -33,6 +58,10 @@ export interface GeneralAppState
|
||||
extends AppSectionItemState<General>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface MediaManagementAppState
|
||||
extends AppSectionItemState<MediaManagement>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface NamingAppState
|
||||
extends AppSectionItemState<NamingConfig>,
|
||||
AppSectionSaveState {}
|
||||
@@ -42,12 +71,20 @@ export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState<Presets<ImportList>> {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface IndexerOptionsAppState
|
||||
extends AppSectionItemState<IndexerOptions>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface IndexerAppState
|
||||
extends AppSectionState<Indexer>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState<Presets<Indexer>> {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
@@ -57,7 +94,7 @@ export interface NotificationAppState
|
||||
|
||||
export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
AppSectionItemSchemaState<QualityProfile> {}
|
||||
|
||||
export interface ReleaseProfilesAppState
|
||||
extends AppSectionState<ReleaseProfile>,
|
||||
@@ -88,15 +125,20 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
autoTaggings: AutoTaggingAppState;
|
||||
autoTaggingSpecifications: AutoTaggingSpecificationAppState;
|
||||
customFormats: CustomFormatAppState;
|
||||
delayProfiles: DelayProfileAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexerOptions: IndexerOptionsAppState;
|
||||
indexers: IndexerAppState;
|
||||
languages: LanguageSettingsAppState;
|
||||
mediaManagement: MediaManagementAppState;
|
||||
metadata: MetadataAppState;
|
||||
naming: NamingAppState;
|
||||
namingExamples: NamingExamplesAppState;
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface TagDetail extends ModelBase {
|
||||
indexerIds: number[];
|
||||
movieIds: number[];
|
||||
notificationIds: number[];
|
||||
restrictionIds: number[];
|
||||
releaseProfileIds: number[];
|
||||
}
|
||||
|
||||
export interface TagDetailAppState
|
||||
|
||||
@@ -196,7 +196,7 @@ class CollectionOverview extends Component {
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.status}>
|
||||
{`${missingMovies} missing movie(s)`}
|
||||
{translate('CountMissingMoviesFromLibrary', { count: missingMovies })}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
|
||||
@@ -5,18 +5,20 @@ import { ValidationError, ValidationWarning } from 'typings/pending';
|
||||
import styles from './Form.css';
|
||||
|
||||
export interface FormProps {
|
||||
id?: string;
|
||||
children: ReactNode;
|
||||
validationErrors?: ValidationError[];
|
||||
validationWarnings?: ValidationWarning[];
|
||||
}
|
||||
|
||||
function Form({
|
||||
id,
|
||||
children,
|
||||
validationErrors = [],
|
||||
validationWarnings = [],
|
||||
}: FormProps) {
|
||||
return (
|
||||
<div>
|
||||
<div id={id}>
|
||||
{validationErrors.length || validationWarnings.length ? (
|
||||
<div className={styles.validationFailures}>
|
||||
{validationErrors.map((error, index) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ function createQualityProfilesSelector(
|
||||
includeMixed: boolean
|
||||
) {
|
||||
return createSelector(
|
||||
createSortedSectionSelector(
|
||||
createSortedSectionSelector<QualityProfile, QualityProfilesAppState>(
|
||||
'settings.qualityProfiles',
|
||||
sortByProp<QualityProfile, 'name'>('name')
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
@@ -6,7 +7,7 @@ import { kinds } from 'Helpers/Props';
|
||||
interface PageSectionContentProps {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error?: object;
|
||||
error?: Error;
|
||||
errorMessage: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -18,7 +19,7 @@ function PageSectionContent({
|
||||
errorMessage,
|
||||
children,
|
||||
}: PageSectionContentProps) {
|
||||
if (isFetching) {
|
||||
if (isFetching && !isPopulated) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
|
||||
const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin);
|
||||
const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1;
|
||||
|
||||
interface PageToolbarSectionProps {
|
||||
export interface PageToolbarSectionProps {
|
||||
children?:
|
||||
| (ReactElement<PageToolbarButtonProps> | ReactElement<never>)
|
||||
| (ReactElement<PageToolbarButtonProps> | ReactElement<never>)[];
|
||||
| (ReactElement<PageToolbarButtonProps> | ReactElement<never> | null)
|
||||
| (ReactElement<PageToolbarButtonProps> | ReactElement<never> | null)[];
|
||||
alignContent?: Extract<Align, keyof typeof styles>;
|
||||
collapseButtons?: boolean;
|
||||
}
|
||||
@@ -80,8 +80,12 @@ function PageToolbarSection({
|
||||
if (buttonCount - 1 === maxButtons) {
|
||||
const overflowItems: PageToolbarButtonProps[] = [];
|
||||
|
||||
const buttonsWithoutSeparators = validChildren.filter(
|
||||
(child) => Object.keys(child.props).length > 0
|
||||
);
|
||||
|
||||
return {
|
||||
buttons: validChildren,
|
||||
buttons: buttonsWithoutSeparators,
|
||||
buttonCount,
|
||||
overflowItems,
|
||||
};
|
||||
|
||||
8
frontend/src/Helpers/Hooks/useIsWindows.ts
Normal file
8
frontend/src/Helpers/Hooks/useIsWindows.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function useIsWindows() {
|
||||
return useSelector((state: AppState) => state.system.status.item.isWindows);
|
||||
}
|
||||
|
||||
export default useIsWindows;
|
||||
8
frontend/src/Helpers/Hooks/useShowAdvancedSettings.ts
Normal file
8
frontend/src/Helpers/Hooks/useShowAdvancedSettings.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function useShowAdvancedSettings() {
|
||||
return useSelector((state: AppState) => state.settings.advancedSettings);
|
||||
}
|
||||
|
||||
export default useShowAdvancedSettings;
|
||||
@@ -1,149 +0,0 @@
|
||||
// https://github.com/react-bootstrap/react-element-children
|
||||
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Iterates through children that are typically specified as `props.children`,
|
||||
* but only maps over children that are "valid components".
|
||||
*
|
||||
* The mapFunction provided index will be normalised to the components mapped,
|
||||
* so an invalid component would not increase the index.
|
||||
*
|
||||
* @param {?*} children Children tree container.
|
||||
* @param {function(*, int)} func.
|
||||
* @param {*} context Context for func.
|
||||
* @return {object} Object containing the ordered map of results.
|
||||
*/
|
||||
export function map(children, func, context) {
|
||||
let index = 0;
|
||||
|
||||
return React.Children.map(children, (child) => {
|
||||
if (!React.isValidElement(child)) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return func.call(context, child, index++);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through children that are "valid components".
|
||||
*
|
||||
* The provided forEachFunc(child, index) will be called for each
|
||||
* leaf child with the index reflecting the position relative to "valid components".
|
||||
*
|
||||
* @param {?*} children Children tree container.
|
||||
* @param {function(*, int)} func.
|
||||
* @param {*} context Context for context.
|
||||
*/
|
||||
export function forEach(children, func, context) {
|
||||
let index = 0;
|
||||
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (!React.isValidElement(child)) {
|
||||
return;
|
||||
}
|
||||
|
||||
func.call(context, child, index++);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of "valid components" in the Children container.
|
||||
*
|
||||
* @param {?*} children Children tree container.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function count(children) {
|
||||
let result = 0;
|
||||
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (!React.isValidElement(child)) {
|
||||
return;
|
||||
}
|
||||
|
||||
++result;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds children that are typically specified as `props.children`,
|
||||
* but only iterates over children that are "valid components".
|
||||
*
|
||||
* The provided forEachFunc(child, index) will be called for each
|
||||
* leaf child with the index reflecting the position relative to "valid components".
|
||||
*
|
||||
* @param {?*} children Children tree container.
|
||||
* @param {function(*, int)} func.
|
||||
* @param {*} context Context for func.
|
||||
* @returns {array} of children that meet the func return statement
|
||||
*/
|
||||
export function filter(children, func, context) {
|
||||
const result = [];
|
||||
|
||||
forEach(children, (child, index) => {
|
||||
if (func.call(context, child, index)) {
|
||||
result.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function find(children, func, context) {
|
||||
let result = null;
|
||||
|
||||
forEach(children, (child, index) => {
|
||||
if (result) {
|
||||
return;
|
||||
}
|
||||
if (func.call(context, child, index)) {
|
||||
result = child;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function every(children, func, context) {
|
||||
let result = true;
|
||||
|
||||
forEach(children, (child, index) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
if (!func.call(context, child, index)) {
|
||||
result = false;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function some(children, func, context) {
|
||||
let result = false;
|
||||
|
||||
forEach(children, (child, index) => {
|
||||
if (result) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (func.call(context, child, index)) {
|
||||
result = true;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function toArray(children) {
|
||||
const result = [];
|
||||
|
||||
forEach(children, (child) => {
|
||||
result.push(child);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function getDisplayName(Component) {
|
||||
return Component.displayName || Component.name || 'Component';
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import Popover from 'Components/Tooltip/Popover';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||
import EditImportListModal from 'Settings/ImportLists/ImportLists/EditImportListModal';
|
||||
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import MovieCredit from 'typings/MovieCredit';
|
||||
@@ -154,7 +154,7 @@ function MovieCastPoster(props: MovieCastPosterProps) {
|
||||
{character}
|
||||
</div>
|
||||
|
||||
<EditImportListModalConnector
|
||||
<EditImportListModal
|
||||
id={importListId}
|
||||
isOpen={isEditImportListModalOpen}
|
||||
onModalClose={setEditImportListModalClosed}
|
||||
|
||||
@@ -10,7 +10,7 @@ import Popover from 'Components/Tooltip/Popover';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||
import EditImportListModal from 'Settings/ImportLists/ImportLists/EditImportListModal';
|
||||
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import MovieCredit from 'typings/MovieCredit';
|
||||
@@ -152,7 +152,7 @@ function MovieCrewPoster(props: MovieCrewPosterProps) {
|
||||
</div>
|
||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
|
||||
|
||||
<EditImportListModalConnector
|
||||
<EditImportListModal
|
||||
id={importListId}
|
||||
isOpen={isEditImportListModalOpen}
|
||||
onModalClose={setEditImportListModalClosed}
|
||||
|
||||
@@ -82,9 +82,9 @@ function RootFolderRow(props: RootFolderRowProps) {
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteRootFolder')}
|
||||
message={translate('DeleteRootFolderMessageText', { path })}
|
||||
confirmLabel={translate('Delete')}
|
||||
title={translate('RemoveRootFolder')}
|
||||
message={translate('RemoveRootFolderMoviesMessageText', { path })}
|
||||
confirmLabel={translate('Remove')}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onDeleteModalClose}
|
||||
/>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AdvancedSettingsButton.css';
|
||||
|
||||
function AdvancedSettingsButton(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
onAdvancedSettingsPress,
|
||||
showLabel
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.button}
|
||||
title={advancedSettings ? translate('ShownClickToHide') : translate('HiddenClickToShow')}
|
||||
onPress={onAdvancedSettingsPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.ADVANCED_SETTINGS}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<span
|
||||
className={classNames(
|
||||
styles.indicatorContainer,
|
||||
'fa-layers fa-fw'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className={styles.indicatorBackground}
|
||||
name={icons.CIRCLE}
|
||||
size={16}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
className={advancedSettings ? styles.enabled : styles.disabled}
|
||||
name={advancedSettings ? icons.CHECK : icons.CLOSE}
|
||||
size={10}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{
|
||||
showLabel ?
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{advancedSettings ? translate('HideAdvanced') : translate('ShowAdvanced')}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
AdvancedSettingsButton.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
showLabel: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AdvancedSettingsButton.defaultProps = {
|
||||
showLabel: true
|
||||
};
|
||||
|
||||
export default AdvancedSettingsButton;
|
||||
67
frontend/src/Settings/AdvancedSettingsButton.tsx
Normal file
67
frontend/src/Settings/AdvancedSettingsButton.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { toggleAdvancedSettings } from 'Store/Actions/settingsActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AdvancedSettingsButton.css';
|
||||
|
||||
interface AdvancedSettingsButtonProps {
|
||||
showLabel: boolean;
|
||||
}
|
||||
|
||||
function AdvancedSettingsButton({ showLabel }: AdvancedSettingsButtonProps) {
|
||||
const showAdvancedSettings = useSelector(
|
||||
(state: AppState) => state.settings.advancedSettings
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
dispatch(toggleAdvancedSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.button}
|
||||
title={
|
||||
showAdvancedSettings
|
||||
? translate('ShownClickToHide')
|
||||
: translate('HiddenClickToShow')
|
||||
}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<Icon name={icons.ADVANCED_SETTINGS} size={21} />
|
||||
|
||||
<span
|
||||
className={classNames(styles.indicatorContainer, 'fa-layers fa-fw')}
|
||||
>
|
||||
<Icon
|
||||
className={styles.indicatorBackground}
|
||||
name={icons.CIRCLE}
|
||||
size={16}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
className={showAdvancedSettings ? styles.enabled : styles.disabled}
|
||||
name={showAdvancedSettings ? icons.CHECK : icons.CLOSE}
|
||||
size={10}
|
||||
/>
|
||||
</span>
|
||||
|
||||
{showLabel ? (
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{showAdvancedSettings
|
||||
? translate('HideAdvanced')
|
||||
: translate('ShowAdvanced')}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdvancedSettingsButton;
|
||||
@@ -5,7 +5,7 @@ import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
|
||||
@@ -13,9 +13,7 @@ import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCusto
|
||||
function CustomFormatSettingsPage() {
|
||||
return (
|
||||
<PageContent title={translate('CustomFormatsSettings')}>
|
||||
<SettingsToolbarConnector
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<SettingsToolbar
|
||||
showSave={false}
|
||||
additionalButtons={
|
||||
<>
|
||||
|
||||
@@ -5,7 +5,7 @@ import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
|
||||
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
|
||||
@@ -71,7 +71,7 @@ class DownloadClientSettings extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title={translate('DownloadClientSettings')}>
|
||||
<SettingsToolbarConnector
|
||||
<SettingsToolbar
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
|
||||
@@ -38,7 +38,6 @@ class EditDownloadClientModalContent extends Component {
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteDownloadClientPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -198,8 +197,6 @@ class EditDownloadClientModalContent extends Component {
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
@@ -243,7 +240,6 @@ EditDownloadClientModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteDownloadClientPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
saveDownloadClient,
|
||||
setDownloadClientFieldValue,
|
||||
setDownloadClientValue,
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
testDownloadClient
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||
@@ -29,8 +28,7 @@ const mapDispatchToProps = {
|
||||
setDownloadClientValue,
|
||||
setDownloadClientFieldValue,
|
||||
saveDownloadClient,
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
testDownloadClient
|
||||
};
|
||||
|
||||
class EditDownloadClientModalContentConnector extends Component {
|
||||
@@ -63,10 +61,6 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
this.props.testDownloadClient({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -76,7 +70,6 @@ class EditDownloadClientModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
@@ -94,7 +87,6 @@ EditDownloadClientModalContentConnector.propTypes = {
|
||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||
saveDownloadClient: PropTypes.func.isRequired,
|
||||
testDownloadClient: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AnalyticSettings from './AnalyticSettings';
|
||||
import BackupSettings from './BackupSettings';
|
||||
@@ -113,7 +113,7 @@ class GeneralSettings extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title={translate('GeneralSettings')}>
|
||||
<SettingsToolbarConnector
|
||||
<SettingsToolbar
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ function createImportListExclusionSelector(id?: number) {
|
||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
@@ -171,7 +170,7 @@ function EditImportListExclusionModalContent({
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{id && (
|
||||
{id ? (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
@@ -179,7 +178,7 @@ function EditImportListExclusionModalContent({
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportListExclusions from './ImportListExclusions/ImportListExclusions';
|
||||
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
||||
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
|
||||
import ImportListOptions from './Options/ImportListOptions';
|
||||
|
||||
class ImportListSettings extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._saveCallback = null;
|
||||
|
||||
this.state = {
|
||||
isSaving: false,
|
||||
hasPendingChanges: false,
|
||||
isManageImportListsOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
setChildSave = (saveCallback) => {
|
||||
this._saveCallback = saveCallback;
|
||||
};
|
||||
|
||||
onChildStateChange = (payload) => {
|
||||
this.setState(payload);
|
||||
};
|
||||
|
||||
onManageImportListsPress = () => {
|
||||
this.setState({ isManageImportListsOpen: true });
|
||||
};
|
||||
|
||||
onManageImportListsModalClose = () => {
|
||||
this.setState({ isManageImportListsOpen: false });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
if (this._saveCallback) {
|
||||
this._saveCallback();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isTestingAll,
|
||||
dispatchTestAllImportLists
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
isManageImportListsOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('ImportListSettings')}>
|
||||
<SettingsToolbarConnector
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('TestAllLists')}
|
||||
iconName={icons.TEST}
|
||||
isSpinning={isTestingAll}
|
||||
onPress={dispatchTestAllImportLists}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManageLists')}
|
||||
iconName={icons.MANAGE}
|
||||
onPress={this.onManageImportListsPress}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<ImportListsConnector />
|
||||
|
||||
<ImportListOptions
|
||||
setChildSave={this.setChildSave}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
|
||||
<ImportListExclusions />
|
||||
|
||||
<ManageImportListsModal
|
||||
isOpen={isManageImportListsOpen}
|
||||
onModalClose={this.onManageImportListsModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportListSettings.propTypes = {
|
||||
isTestingAll: PropTypes.bool.isRequired,
|
||||
dispatchTestAllImportLists: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportListSettings;
|
||||
107
frontend/src/Settings/ImportLists/ImportListSettings.tsx
Normal file
107
frontend/src/Settings/ImportLists/ImportListSettings.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import { testAllImportLists } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
SaveCallback,
|
||||
SettingsStateChange,
|
||||
} from 'typings/Settings/SettingsState';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportListExclusions from './ImportListExclusions/ImportListExclusions';
|
||||
import ImportLists from './ImportLists/ImportLists';
|
||||
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
|
||||
import ImportListOptions from './Options/ImportListOptions';
|
||||
|
||||
function ImportListSettings() {
|
||||
const dispatch = useDispatch();
|
||||
const isTestingAll = useSelector(
|
||||
(state: AppState) => state.settings.importLists.isTestingAll
|
||||
);
|
||||
|
||||
const saveOptions = useRef<() => void>();
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [hasPendingChanges, setHasPendingChanges] = useState(false);
|
||||
const [isManageImportListsModalOpen, setIsManageImportListsModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleSetChildSave = useCallback((saveCallback: SaveCallback) => {
|
||||
saveOptions.current = saveCallback;
|
||||
}, []);
|
||||
|
||||
const handleChildStateChange = useCallback(
|
||||
({ isSaving, hasPendingChanges }: SettingsStateChange) => {
|
||||
setIsSaving(isSaving);
|
||||
setHasPendingChanges(hasPendingChanges);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleManageImportListsPress = useCallback(() => {
|
||||
setIsManageImportListsModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleManageImportListsModalClose = useCallback(() => {
|
||||
setIsManageImportListsModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
saveOptions.current?.();
|
||||
}, []);
|
||||
|
||||
const handleTestAllIndexersPress = useCallback(() => {
|
||||
dispatch(testAllImportLists());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('ImportListSettings')}>
|
||||
<SettingsToolbar
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('TestAllLists')}
|
||||
iconName={icons.TEST}
|
||||
isSpinning={isTestingAll}
|
||||
onPress={handleTestAllIndexersPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManageLists')}
|
||||
iconName={icons.MANAGE}
|
||||
onPress={handleManageImportListsPress}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onSavePress={handleSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<ImportLists />
|
||||
|
||||
<ImportListOptions
|
||||
setChildSave={handleSetChildSave}
|
||||
onChildStateChange={handleChildStateChange}
|
||||
/>
|
||||
|
||||
<ImportListExclusions />
|
||||
|
||||
<ManageImportListsModal
|
||||
isOpen={isManageImportListsModalOpen}
|
||||
onModalClose={handleManageImportListsModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportListSettings;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { testAllImportLists } from 'Store/Actions/settingsActions';
|
||||
import ImportListSettings from './ImportListSettings';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.importLists.isTestingAll,
|
||||
(isTestingAll) => {
|
||||
return {
|
||||
isTestingAll
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchTestAllImportLists: testAllImportLists
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListSettings);
|
||||
@@ -1,117 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddImportListPresetMenuItem from './AddImportListPresetMenuItem';
|
||||
import styles from './AddImportListItem.css';
|
||||
|
||||
class AddImportListItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onImportListSelect = () => {
|
||||
const {
|
||||
implementation,
|
||||
implementationName,
|
||||
minRefreshInterval
|
||||
} = this.props;
|
||||
|
||||
this.props.onImportListSelect({ implementation, implementationName, minRefreshInterval });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
implementation,
|
||||
implementationName,
|
||||
minRefreshInterval,
|
||||
infoLink,
|
||||
presets,
|
||||
onImportListSelect
|
||||
} = this.props;
|
||||
|
||||
const hasPresets = !!presets && !!presets.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.list}
|
||||
>
|
||||
<Link
|
||||
className={styles.underlay}
|
||||
onPress={this.onImportListSelect}
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>
|
||||
{implementationName}
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{
|
||||
hasPresets &&
|
||||
<span>
|
||||
<Button
|
||||
size={sizes.SMALL}
|
||||
onPress={this.onImportListSelect}
|
||||
>
|
||||
{translate('Custom')}
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button
|
||||
className={styles.presetsMenuButton}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('Presets')}
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{
|
||||
presets.map((preset) => {
|
||||
return (
|
||||
<AddImportListPresetMenuItem
|
||||
key={preset.name}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
implementationName={implementationName}
|
||||
minRefreshInterval={minRefreshInterval}
|
||||
onPress={onImportListSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
}
|
||||
|
||||
<Button
|
||||
to={infoLink}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddImportListItem.propTypes = {
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
minRefreshInterval: PropTypes.string.isRequired,
|
||||
infoLink: PropTypes.string.isRequired,
|
||||
presets: PropTypes.arrayOf(PropTypes.object),
|
||||
onImportListSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddImportListItem;
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { selectImportListSchema } from 'Store/Actions/settingsActions';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddImportListPresetMenuItem from './AddImportListPresetMenuItem';
|
||||
import styles from './AddImportListItem.css';
|
||||
|
||||
interface AddImportListItemProps {
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
minRefreshInterval: string;
|
||||
infoLink: string;
|
||||
presets?: ImportList[];
|
||||
onImportListSelect: () => void;
|
||||
}
|
||||
|
||||
function AddImportListItem({
|
||||
implementation,
|
||||
implementationName,
|
||||
minRefreshInterval,
|
||||
infoLink,
|
||||
presets,
|
||||
onImportListSelect,
|
||||
}: AddImportListItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
const hasPresets = !!(presets && presets.length);
|
||||
|
||||
const handleImportListSelect = useCallback(() => {
|
||||
dispatch(
|
||||
selectImportListSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
})
|
||||
);
|
||||
|
||||
onImportListSelect();
|
||||
}, [implementation, implementationName, dispatch, onImportListSelect]);
|
||||
|
||||
return (
|
||||
<div className={styles.list}>
|
||||
<Link className={styles.underlay} onPress={handleImportListSelect} />
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>{implementationName}</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{hasPresets && (
|
||||
<span>
|
||||
<Button size={sizes.SMALL} onPress={handleImportListSelect}>
|
||||
{translate('Custom')}
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
|
||||
{translate('Presets')}
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{presets.map((preset) => {
|
||||
return (
|
||||
<AddImportListPresetMenuItem
|
||||
key={preset.name}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
implementationName={implementationName}
|
||||
minRefreshInterval={minRefreshInterval}
|
||||
onPress={onImportListSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Button to={infoLink} size={sizes.SMALL}>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddImportListItem;
|
||||
@@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddImportListModalContentConnector from './AddImportListModalContentConnector';
|
||||
|
||||
function AddImportListModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddImportListModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddImportListModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddImportListModal;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddImportListModalContent, {
|
||||
AddImportListModalContentProps,
|
||||
} from './AddImportListModalContent';
|
||||
|
||||
interface AddImportListModalProps extends AddImportListModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function AddImportListModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: AddImportListModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<AddImportListModalContent {...otherProps} onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddImportListModal;
|
||||
@@ -1,115 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddImportListItem from './AddImportListItem';
|
||||
import styles from './AddImportListModalContent.css';
|
||||
|
||||
class AddImportListModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
listGroups,
|
||||
onImportListSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AddImportList')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isSchemaFetching ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isSchemaFetching && !!schemaError ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddListError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isSchemaPopulated && !schemaError ?
|
||||
<div>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('SupportedListsMovie')}
|
||||
</div>
|
||||
<div>
|
||||
{translate('SupportedListsMoreInfo')}
|
||||
</div>
|
||||
</Alert>
|
||||
{
|
||||
Object.keys(listGroups).map((key) => {
|
||||
return (
|
||||
<FieldSet key={key} legend={translate('TypeOfList', {
|
||||
typeOfList: titleCase(key)
|
||||
})}
|
||||
>
|
||||
<div className={styles.lists}>
|
||||
{
|
||||
listGroups[key].map((list) => {
|
||||
return (
|
||||
<AddImportListItem
|
||||
key={list.implementation}
|
||||
implementation={list.implementation}
|
||||
{...list}
|
||||
onImportListSelect={onImportListSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddImportListModalContent.propTypes = {
|
||||
isSchemaFetching: PropTypes.bool.isRequired,
|
||||
isSchemaPopulated: PropTypes.bool.isRequired,
|
||||
schemaError: PropTypes.object,
|
||||
listGroups: PropTypes.object.isRequired,
|
||||
onImportListSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddImportListModalContent;
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddImportListItem from './AddImportListItem';
|
||||
import styles from './AddImportListModalContent.css';
|
||||
|
||||
export interface AddImportListModalContentProps {
|
||||
onImportListSelect: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function AddImportListModalContent({
|
||||
onImportListSelect,
|
||||
onModalClose,
|
||||
}: AddImportListModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
|
||||
useSelector((state: AppState) => state.settings.importLists);
|
||||
|
||||
const listGroups = useMemo(() => {
|
||||
const result = schema.reduce<Record<string, ImportList[]>>((acc, item) => {
|
||||
if (!acc[item.listType]) {
|
||||
acc[item.listType] = [];
|
||||
}
|
||||
|
||||
acc[item.listType].push(item);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Sort the lists by listOrder after grouping them
|
||||
Object.keys(result).forEach((key) => {
|
||||
result[key].sort((a, b) => {
|
||||
return a.listOrder - b.listOrder;
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [schema]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchImportListSchema());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('AddImportList')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isSchemaFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isSchemaFetching && !!schemaError ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('AddListError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isSchemaPopulated && !schemaError ? (
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>{translate('SupportedListsMovie')}</div>
|
||||
<div>{translate('SupportedListsMoreInfo')}</div>
|
||||
</Alert>
|
||||
{Object.keys(listGroups).map((key) => {
|
||||
return (
|
||||
<FieldSet
|
||||
key={key}
|
||||
legend={translate('TypeOfList', {
|
||||
typeOfList: titleCase(key),
|
||||
})}
|
||||
>
|
||||
<div className={styles.lists}>
|
||||
{listGroups[key].map((list) => {
|
||||
return (
|
||||
<AddImportListItem
|
||||
key={list.implementation}
|
||||
{...list}
|
||||
implementation={list.implementation}
|
||||
onImportListSelect={onImportListSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FieldSet>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddImportListModalContent;
|
||||
@@ -1,76 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchImportListSchema, selectImportListSchema } from 'Store/Actions/settingsActions';
|
||||
import AddImportListModalContent from './AddImportListModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.importLists,
|
||||
(importLists) => {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema
|
||||
} = importLists;
|
||||
|
||||
const listGroups = _.chain(schema)
|
||||
.sortBy((o) => o.listOrder)
|
||||
.groupBy('listType')
|
||||
.value();
|
||||
|
||||
return {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
listGroups
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchImportListSchema,
|
||||
selectImportListSchema
|
||||
};
|
||||
|
||||
class AddImportListModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchImportListSchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onImportListSelect = ({ implementation, implementationName, name, minRefreshInterval }) => {
|
||||
this.props.selectImportListSchema({ implementation, implementationName, presetName: name, minRefreshInterval });
|
||||
this.props.onModalClose({ listSelected: true });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddImportListModalContent
|
||||
{...this.props}
|
||||
onImportListSelect={this.onImportListSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddImportListModalContentConnector.propTypes = {
|
||||
fetchImportListSchema: PropTypes.func.isRequired,
|
||||
selectImportListSchema: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddImportListModalContentConnector);
|
||||
@@ -1,57 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
|
||||
class AddImportListPresetMenuItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
name,
|
||||
implementation,
|
||||
implementationName,
|
||||
minRefreshInterval
|
||||
} = this.props;
|
||||
|
||||
this.props.onPress({
|
||||
name,
|
||||
implementation,
|
||||
implementationName,
|
||||
minRefreshInterval
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
implementation,
|
||||
implementationName,
|
||||
minRefreshInterval,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
{...otherProps}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddImportListPresetMenuItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
minRefreshInterval: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddImportListPresetMenuItem;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem';
|
||||
import { selectImportListSchema } from 'Store/Actions/settingsActions';
|
||||
|
||||
interface AddImportListPresetMenuItemProps
|
||||
extends Omit<MenuItemProps, 'children'> {
|
||||
name: string;
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
minRefreshInterval: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function AddImportListPresetMenuItem({
|
||||
name,
|
||||
implementation,
|
||||
implementationName,
|
||||
minRefreshInterval,
|
||||
onPress,
|
||||
...otherProps
|
||||
}: AddImportListPresetMenuItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
dispatch(
|
||||
selectImportListSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
presetName: name,
|
||||
})
|
||||
);
|
||||
|
||||
onPress();
|
||||
}, [name, implementation, implementationName, dispatch, onPress]);
|
||||
|
||||
return (
|
||||
<MenuItem {...otherProps} onPress={handlePress}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddImportListPresetMenuItem;
|
||||
@@ -1,27 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EditImportListModalContentConnector from './EditImportListModalContentConnector';
|
||||
|
||||
function EditImportListModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditImportListModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditImportListModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditImportListModal;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
cancelSaveImportList,
|
||||
cancelTestImportList,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import EditImportListModalContent, {
|
||||
EditImportListModalContentProps,
|
||||
} from './EditImportListModalContent';
|
||||
|
||||
const section = 'settings.importLists';
|
||||
|
||||
interface EditImportListModalProps extends EditImportListModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function EditImportListModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditImportListModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
dispatch(cancelTestImportList({ section }));
|
||||
dispatch(cancelSaveImportList({ section }));
|
||||
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditImportListModalContent
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditImportListModal;
|
||||
@@ -1,65 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { cancelSaveImportList, cancelTestImportList } from 'Store/Actions/settingsActions';
|
||||
import EditImportListModal from './EditImportListModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const section = 'settings.importLists';
|
||||
|
||||
return {
|
||||
dispatchClearPendingChanges() {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelTestImportList() {
|
||||
dispatch(cancelTestImportList({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelSaveImportList() {
|
||||
dispatch(cancelSaveImportList({ section }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EditImportListModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPendingChanges();
|
||||
this.props.dispatchCancelTestImportList();
|
||||
this.props.dispatchCancelSaveImportList();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchCancelTestImportList,
|
||||
dispatchCancelSaveImportList,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EditImportListModal
|
||||
{...otherProps}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditImportListModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchCancelTestImportList: PropTypes.func.isRequired,
|
||||
dispatchCancelSaveImportList: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(EditImportListModalConnector);
|
||||
@@ -1,308 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditImportListModalContent.css';
|
||||
|
||||
function EditImportListModalContent(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
isTesting,
|
||||
saveError,
|
||||
item,
|
||||
onInputChange,
|
||||
onFieldChange,
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteImportListPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
enabled,
|
||||
enableAuto,
|
||||
minRefreshInterval,
|
||||
monitor,
|
||||
minimumAvailability,
|
||||
qualityProfileId,
|
||||
rootFolderPath,
|
||||
searchOnAdd,
|
||||
tags,
|
||||
fields,
|
||||
message
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id ? translate('EditImportListImplementation', { implementationName }) : translate('AddImportListImplementation', { implementationName })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error ?
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddListError')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error ?
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
!!message &&
|
||||
<Alert
|
||||
className={styles.message}
|
||||
kind={message.value.type}
|
||||
>
|
||||
{message.value.message}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
<Alert
|
||||
kind={kinds.INFO}
|
||||
className={styles.message}
|
||||
>
|
||||
{translate('ListWillRefreshEveryInterval', {
|
||||
refreshInterval: formatShortTimeSpan(minRefreshInterval.value)
|
||||
})}
|
||||
</Alert>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Enable')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enabled"
|
||||
helpText={translate('ListEnabledHelpText')}
|
||||
{...enabled}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('EnableAutomaticAdd')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableAuto"
|
||||
helpText={translate('EnableAutomaticAddMovieHelpText')}
|
||||
{...enableAuto}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Monitor')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||
name="monitor"
|
||||
helpText={translate('ListMonitorMovieHelpText')}
|
||||
{...monitor}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="searchOnAdd"
|
||||
helpText={translate('ListSearchOnAddMovieHelpText')}
|
||||
{...searchOnAdd}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
{...minimumAvailability}
|
||||
onChange={onInputChange}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
helpText={translate('ListQualityProfileHelpText')}
|
||||
{...qualityProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
helpText={translate('ListRootFolderHelpText')}
|
||||
{...rootFolderPath}
|
||||
includeMissingValue={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RadarrTags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('ListTagsHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
fields.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
advancedSettings={advancedSettings}
|
||||
provider="importList"
|
||||
providerData={item}
|
||||
{...field}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
</Form> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteImportListPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
onPress={onTestPress}
|
||||
>
|
||||
{translate('Test')}
|
||||
</SpinnerErrorButton>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
EditImportListModalContent.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isTesting: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteImportListPress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditImportListModalContent;
|
||||
@@ -0,0 +1,313 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import { ImportListAppState } from 'App/State/SettingsAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import {
|
||||
saveImportList,
|
||||
setImportListFieldValue,
|
||||
setImportListValue,
|
||||
testImportList,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditImportListModalContent.css';
|
||||
|
||||
export interface EditImportListModalContentProps {
|
||||
id?: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteImportListPress?: () => void;
|
||||
}
|
||||
|
||||
function EditImportListModalContent({
|
||||
id,
|
||||
onModalClose,
|
||||
onDeleteImportListPress,
|
||||
}: EditImportListModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const showAdvancedSettings = useShowAdvancedSettings();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isSaving,
|
||||
isTesting = false,
|
||||
error,
|
||||
saveError,
|
||||
item,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
} = useSelector(
|
||||
createProviderSettingsSelectorHook<ImportList, ImportListAppState>(
|
||||
'importLists',
|
||||
id
|
||||
)
|
||||
);
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
const {
|
||||
implementationName,
|
||||
name,
|
||||
enabled,
|
||||
enableAuto,
|
||||
minRefreshInterval,
|
||||
monitor,
|
||||
minimumAvailability,
|
||||
rootFolderPath,
|
||||
qualityProfileId,
|
||||
searchOnAdd,
|
||||
tags,
|
||||
fields,
|
||||
} = item;
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setImportListValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setImportListFieldValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTestPress = useCallback(() => {
|
||||
dispatch(testImportList({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
dispatch(saveImportList({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasSaving && !isSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id
|
||||
? translate('EditImportListImplementation', { implementationName })
|
||||
: translate('AddImportListImplementation', { implementationName })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('AddListError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error ? (
|
||||
<Form
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
>
|
||||
<Alert kind={kinds.INFO} className={styles.message}>
|
||||
{translate('ListWillRefreshEveryInterval', {
|
||||
refreshInterval: formatShortTimeSpan(minRefreshInterval.value),
|
||||
})}
|
||||
</Alert>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Enable')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enabled"
|
||||
helpText={translate('ListEnabledHelpText')}
|
||||
{...enabled}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('EnableAutomaticAdd')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableAuto"
|
||||
helpText={translate('EnableAutomaticAddMovieHelpText')}
|
||||
{...enableAuto}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Monitor')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||
name="monitor"
|
||||
helpText={translate('ListMonitorMovieHelpText')}
|
||||
{...monitor}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="searchOnAdd"
|
||||
helpText={translate('ListSearchOnAddMovieHelpText')}
|
||||
{...searchOnAdd}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon className={styles.labelIcon} name={icons.INFO} />
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
{...minimumAvailability}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
helpText={translate('ListQualityProfileHelpText')}
|
||||
{...qualityProfileId}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
helpText={translate('ListRootFolderHelpText')}
|
||||
{...rootFolderPath}
|
||||
includeMissingValue={true}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RadarrTags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('ListTagsHelpText')}
|
||||
{...tags}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{fields?.length ? (
|
||||
<div>
|
||||
{fields.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
{...field}
|
||||
advancedSettings={showAdvancedSettings}
|
||||
provider="importList"
|
||||
providerData={item}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</Form>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{id ? (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteImportListPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<AdvancedSettingsButton showLabel={false} />
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
onPress={handleTestPress}
|
||||
>
|
||||
{translate('Test')}
|
||||
</SpinnerErrorButton>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={handleSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditImportListModalContent;
|
||||
@@ -1,101 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
saveImportList,
|
||||
setImportListFieldValue,
|
||||
setImportListValue,
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditImportListModalContent from './EditImportListModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('importLists'),
|
||||
(advancedSettings, importList) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...importList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setImportListValue,
|
||||
setImportListFieldValue,
|
||||
saveImportList,
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditImportListModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setImportListValue({ name, value });
|
||||
};
|
||||
|
||||
onFieldChange = ({ name, value }) => {
|
||||
this.props.setImportListFieldValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveImportList({ id: this.props.id });
|
||||
};
|
||||
|
||||
onTestPress = () => {
|
||||
this.props.testImportList({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditImportListModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditImportListModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setImportListValue: PropTypes.func.isRequired,
|
||||
setImportListFieldValue: PropTypes.func.isRequired,
|
||||
saveImportList: PropTypes.func.isRequired,
|
||||
testImportList: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListModalContentConnector);
|
||||
@@ -4,6 +4,11 @@
|
||||
width: 290px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
@@ -12,6 +17,12 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.enabled {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'cloneButton': string;
|
||||
'enabled': string;
|
||||
'list': string;
|
||||
'name': string;
|
||||
'nameContainer': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditImportListModalConnector from './EditImportListModalConnector';
|
||||
import styles from './ImportList.css';
|
||||
|
||||
class ImportList extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditImportListModalOpen: false,
|
||||
isDeleteImportListModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditImportListPress = () => {
|
||||
this.setState({ isEditImportListModalOpen: true });
|
||||
};
|
||||
|
||||
onEditImportListModalClose = () => {
|
||||
this.setState({ isEditImportListModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteImportListPress = () => {
|
||||
this.setState({
|
||||
isEditImportListModalOpen: false,
|
||||
isDeleteImportListModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteImportListModalClose = () => {
|
||||
this.setState({ isDeleteImportListModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmDeleteImportList = () => {
|
||||
this.props.onConfirmDeleteImportList(this.props.id);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enabled,
|
||||
enableAuto,
|
||||
tags,
|
||||
tagList,
|
||||
minRefreshInterval
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.list}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditImportListPress}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
{
|
||||
enabled ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('Enabled')}
|
||||
</Label> :
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
{translate('Disabled')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
enableAuto ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('AutomaticAdd')}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
<Label kind={kinds.DEFAULT} title='List Refresh Interval'>
|
||||
{`${translate('Refresh')}: ${formatShortTimeSpan(minRefreshInterval)}`}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<EditImportListModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditImportListModalOpen}
|
||||
onModalClose={this.onEditImportListModalClose}
|
||||
onDeleteImportListPress={this.onDeleteImportListPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteImportListModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteImportList')}
|
||||
message={translate('DeleteImportListMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteImportList}
|
||||
onCancel={this.onDeleteImportListModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportList.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
enableAuto: PropTypes.bool.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
minRefreshInterval: PropTypes.string.isRequired,
|
||||
onConfirmDeleteImportList: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportList;
|
||||
130
frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx
Normal file
130
frontend/src/Settings/ImportLists/ImportLists/ImportList.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { deleteImportList } from 'Store/Actions/settingsActions';
|
||||
import useTags from 'Tags/useTags';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditImportListModal from './EditImportListModal';
|
||||
import styles from './ImportList.css';
|
||||
|
||||
interface ImportListProps {
|
||||
id: number;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
enableAuto: boolean;
|
||||
tags: number[];
|
||||
minRefreshInterval: string;
|
||||
onCloneImportListPress: (id: number) => void;
|
||||
}
|
||||
|
||||
function ImportList({
|
||||
id,
|
||||
name,
|
||||
enabled,
|
||||
enableAuto,
|
||||
tags,
|
||||
minRefreshInterval,
|
||||
onCloneImportListPress,
|
||||
}: ImportListProps) {
|
||||
const dispatch = useDispatch();
|
||||
const tagList = useTags();
|
||||
|
||||
const [isEditImportListModalOpen, setIsEditImportListModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const [isDeleteImportListModalOpen, setIsDeleteImportListModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleEditImportListPress = useCallback(() => {
|
||||
setIsEditImportListModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditImportListModalClose = useCallback(() => {
|
||||
setIsEditImportListModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteImportListPress = useCallback(() => {
|
||||
setIsEditImportListModalOpen(false);
|
||||
setIsDeleteImportListModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteImportListModalClose = useCallback(() => {
|
||||
setIsDeleteImportListModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDeleteImportList = useCallback(() => {
|
||||
dispatch(deleteImportList({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleCloneImportListPress = useCallback(() => {
|
||||
onCloneImportListPress(id);
|
||||
}, [id, onCloneImportListPress]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.list}
|
||||
overlayContent={true}
|
||||
onPress={handleEditImportListPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneImportList')}
|
||||
name={icons.CLONE}
|
||||
onPress={handleCloneImportListPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
{enabled ? (
|
||||
<Label kind={kinds.SUCCESS}>{translate('Enabled')}</Label>
|
||||
) : (
|
||||
<Label kind={kinds.DISABLED} outline={true}>
|
||||
{translate('Disabled')}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
{enableAuto ? (
|
||||
<Label kind={kinds.SUCCESS}>{translate('AutomaticAdd')}</Label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<TagList tags={tags} tagList={tagList} />
|
||||
|
||||
<div className={styles.enabled}>
|
||||
<Label kind={kinds.DEFAULT} title="List Refresh Interval">
|
||||
{`${translate('Refresh')}: ${formatShortTimeSpan(
|
||||
minRefreshInterval
|
||||
)}`}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<EditImportListModal
|
||||
id={id}
|
||||
isOpen={isEditImportListModalOpen}
|
||||
onModalClose={handleEditImportListModalClose}
|
||||
onDeleteImportListPress={handleDeleteImportListPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteImportListModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteImportList')}
|
||||
message={translate('DeleteImportListMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={handleConfirmDeleteImportList}
|
||||
onCancel={handleDeleteImportListModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportList;
|
||||
@@ -1,118 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddImportListModal from './AddImportListModal';
|
||||
import EditImportListModalConnector from './EditImportListModalConnector';
|
||||
import ImportList from './ImportList';
|
||||
import styles from './ImportLists.css';
|
||||
|
||||
class ImportLists extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddImportListModalOpen: false,
|
||||
isEditImportListModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddImportListPress = () => {
|
||||
this.setState({ isAddImportListModalOpen: true });
|
||||
};
|
||||
|
||||
onAddImportListModalClose = ({ listSelected = false } = {}) => {
|
||||
this.setState({
|
||||
isAddImportListModalOpen: false,
|
||||
isEditImportListModalOpen: listSelected
|
||||
});
|
||||
};
|
||||
|
||||
onEditImportListModalClose = () => {
|
||||
this.setState({ isEditImportListModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
tagList,
|
||||
onConfirmDeleteImportList,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isAddImportListModalOpen,
|
||||
isEditImportListModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('ImportLists')} >
|
||||
<PageSectionContent
|
||||
errorMessage={translate('ImportListsLoadError')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.lists}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ImportList
|
||||
key={item.id}
|
||||
{...item}
|
||||
tagList={tagList}
|
||||
onConfirmDeleteImportList={onConfirmDeleteImportList}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addList}
|
||||
onPress={this.onAddImportListPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AddImportListModal
|
||||
isOpen={isAddImportListModalOpen}
|
||||
onModalClose={this.onAddImportListModalClose}
|
||||
/>
|
||||
|
||||
<EditImportListModalConnector
|
||||
isOpen={isEditImportListModalOpen}
|
||||
onModalClose={this.onEditImportListModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportLists.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteImportList: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportLists;
|
||||
109
frontend/src/Settings/ImportLists/ImportLists/ImportLists.tsx
Normal file
109
frontend/src/Settings/ImportLists/ImportLists/ImportLists.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { ImportListAppState } from 'App/State/SettingsAppState';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import {
|
||||
cloneImportList,
|
||||
fetchImportLists,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import ImportListModel from 'typings/ImportList';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddImportListModal from './AddImportListModal';
|
||||
import EditImportListModal from './EditImportListModal';
|
||||
import ImportList from './ImportList';
|
||||
import styles from './ImportLists.css';
|
||||
|
||||
function ImportLists() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isFetching, isPopulated, items, error } = useSelector(
|
||||
createSortedSectionSelector<ImportListModel, ImportListAppState>(
|
||||
'settings.importLists',
|
||||
sortByProp('name')
|
||||
)
|
||||
);
|
||||
|
||||
const [isAddImportListModalOpen, setIsAddImportListModalOpen] =
|
||||
useState(false);
|
||||
const [isEditImportListModalOpen, setIsEditImportListModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleAddImportListPress = useCallback(() => {
|
||||
setIsAddImportListModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleAddImportListModalClose = useCallback(() => {
|
||||
setIsAddImportListModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleImportListSelect = useCallback(() => {
|
||||
setIsAddImportListModalOpen(false);
|
||||
setIsEditImportListModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditImportListModalClose = useCallback(() => {
|
||||
setIsEditImportListModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleCloneImportListPress = useCallback(
|
||||
(id: number) => {
|
||||
dispatch(cloneImportList({ id }));
|
||||
setIsEditImportListModalOpen(true);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchImportLists());
|
||||
dispatch(fetchRootFolders());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('ImportLists')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('ImportListsLoadError')}
|
||||
error={error}
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
>
|
||||
<div className={styles.lists}>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<ImportList
|
||||
key={item.id}
|
||||
{...item}
|
||||
onCloneImportListPress={handleCloneImportListPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Card className={styles.addList} onPress={handleAddImportListPress}>
|
||||
<div className={styles.center}>
|
||||
<Icon name={icons.ADD} size={45} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AddImportListModal
|
||||
isOpen={isAddImportListModalOpen}
|
||||
onImportListSelect={handleImportListSelect}
|
||||
onModalClose={handleAddImportListModalClose}
|
||||
/>
|
||||
|
||||
<EditImportListModal
|
||||
isOpen={isEditImportListModalOpen}
|
||||
onModalClose={handleEditImportListModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportLists;
|
||||
@@ -1,67 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import ImportLists from './ImportLists';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.importLists', sortByProp('name')),
|
||||
createTagsSelector(),
|
||||
(importLists, tagList) => {
|
||||
return {
|
||||
...importLists,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchImportLists,
|
||||
deleteImportList,
|
||||
fetchRootFolders
|
||||
};
|
||||
|
||||
class ImportListsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchImportLists();
|
||||
this.props.fetchRootFolders();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteImportList = (id) => {
|
||||
this.props.deleteImportList({ id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportLists
|
||||
{...this.props}
|
||||
onConfirmDeleteImportList={this.onConfirmDeleteImportList}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportListsConnector.propTypes = {
|
||||
fetchImportLists: PropTypes.func.isRequired,
|
||||
deleteImportList: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListsConnector);
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
@@ -20,53 +20,62 @@ import createSettingsSectionSelector from 'Store/Selectors/createSettingsSection
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const SECTION = 'importListOptions';
|
||||
const cleanLibraryLevelOptions = [
|
||||
{ key: 'disabled', value: () => translate('Disabled') },
|
||||
{ key: 'logOnly', value: () => translate('LogOnly') },
|
||||
{ key: 'keepAndUnmonitor', value: () => translate('KeepAndUnmonitorMovie') },
|
||||
{ key: 'removeAndKeep', value: () => translate('RemoveMovieAndKeepFiles') },
|
||||
const cleanLibraryLevelOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'disabled',
|
||||
get value() {
|
||||
return translate('Disabled');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'logOnly',
|
||||
get value() {
|
||||
return translate('LogOnly');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'keepAndUnmonitor',
|
||||
get value() {
|
||||
return translate('KeepAndUnmonitorMovie');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'removeAndKeep',
|
||||
get value() {
|
||||
return translate('RemoveMovieAndKeepFiles');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'removeAndDelete',
|
||||
value: () => translate('RemoveMovieAndDeleteFiles'),
|
||||
get value() {
|
||||
return translate('RemoveMovieAndDeleteFiles');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function createImportListOptionsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.advancedSettings,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
save: sectionSettings.isSaving,
|
||||
...sectionSettings,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface ImportListOptionsPageProps {
|
||||
interface ImportListOptionsProps {
|
||||
setChildSave(saveCallback: () => void): void;
|
||||
onChildStateChange(payload: unknown): void;
|
||||
}
|
||||
|
||||
function ImportListOptions(props: ImportListOptionsPageProps) {
|
||||
const { setChildSave, onChildStateChange } = props;
|
||||
function ImportListOptions({
|
||||
setChildSave,
|
||||
onChildStateChange,
|
||||
}: ImportListOptionsProps) {
|
||||
const dispatch = useDispatch();
|
||||
const showAdvancedSettings = useShowAdvancedSettings();
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
settings,
|
||||
hasSettings,
|
||||
} = useSelector(createImportListOptionsSelector());
|
||||
} = useSelector(createSettingsSectionSelector(SECTION));
|
||||
|
||||
const { listSyncLevel } = settings;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }: { name: string; value: unknown }) => {
|
||||
// @ts-expect-error 'setImportListOptionsValue' isn't typed yet
|
||||
@@ -80,7 +89,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
|
||||
setChildSave(() => dispatch(saveImportListOptions()));
|
||||
|
||||
return () => {
|
||||
dispatch(clearPendingChanges({ section: SECTION }));
|
||||
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
|
||||
};
|
||||
}, [dispatch, setChildSave]);
|
||||
|
||||
@@ -91,16 +100,11 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
|
||||
});
|
||||
}, [onChildStateChange, isSaving, hasPendingChanges]);
|
||||
|
||||
const translatedLevelOptions = cleanLibraryLevelOptions.map(
|
||||
({ key, value }) => {
|
||||
return {
|
||||
key,
|
||||
value: value(),
|
||||
};
|
||||
}
|
||||
);
|
||||
if (!showAdvancedSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return advancedSettings ? (
|
||||
return (
|
||||
<FieldSet legend={translate('Options')}>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
@@ -110,12 +114,12 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
|
||||
|
||||
{hasSettings && !isFetching && !error ? (
|
||||
<Form>
|
||||
<FormGroup advancedSettings={advancedSettings} isAdvanced={true}>
|
||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('CleanLibraryLevel')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="listSyncLevel"
|
||||
values={translatedLevelOptions}
|
||||
values={cleanLibraryLevelOptions}
|
||||
helpText={translate('ListSyncLevelHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...listSyncLevel}
|
||||
@@ -124,7 +128,7 @@ function ImportListOptions(props: ImportListOptionsPageProps) {
|
||||
</Form>
|
||||
) : null}
|
||||
</FieldSet>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportListOptions;
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import IndexersConnector from './Indexers/IndexersConnector';
|
||||
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
|
||||
import IndexerOptionsConnector from './Options/IndexerOptionsConnector';
|
||||
|
||||
class IndexerSettings extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._saveCallback = null;
|
||||
|
||||
this.state = {
|
||||
isSaving: false,
|
||||
hasPendingChanges: false,
|
||||
isManageIndexersOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChildMounted = (saveCallback) => {
|
||||
this._saveCallback = saveCallback;
|
||||
};
|
||||
|
||||
onChildStateChange = (payload) => {
|
||||
this.setState(payload);
|
||||
};
|
||||
|
||||
onManageIndexersPress = () => {
|
||||
this.setState({ isManageIndexersOpen: true });
|
||||
};
|
||||
|
||||
onManageIndexersModalClose = () => {
|
||||
this.setState({ isManageIndexersOpen: false });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
if (this._saveCallback) {
|
||||
this._saveCallback();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isTestingAll,
|
||||
dispatchTestAllIndexers
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
isManageIndexersOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('IndexerSettings')}>
|
||||
<SettingsToolbarConnector
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('TestAllIndexers')}
|
||||
iconName={icons.TEST}
|
||||
isSpinning={isTestingAll}
|
||||
onPress={dispatchTestAllIndexers}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManageIndexers')}
|
||||
iconName={icons.MANAGE}
|
||||
onPress={this.onManageIndexersPress}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<IndexersConnector />
|
||||
|
||||
<IndexerOptionsConnector
|
||||
onChildMounted={this.onChildMounted}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
|
||||
<ManageIndexersModal
|
||||
isOpen={isManageIndexersOpen}
|
||||
onModalClose={this.onManageIndexersModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerSettings.propTypes = {
|
||||
isTestingAll: PropTypes.bool.isRequired,
|
||||
dispatchTestAllIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default IndexerSettings;
|
||||
104
frontend/src/Settings/Indexers/IndexerSettings.tsx
Normal file
104
frontend/src/Settings/Indexers/IndexerSettings.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import { testAllIndexers } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
SaveCallback,
|
||||
SettingsStateChange,
|
||||
} from 'typings/Settings/SettingsState';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Indexers from './Indexers/Indexers';
|
||||
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
|
||||
import IndexerOptions from './Options/IndexerOptions';
|
||||
|
||||
function IndexerSettings() {
|
||||
const dispatch = useDispatch();
|
||||
const isTestingAll = useSelector(
|
||||
(state: AppState) => state.settings.indexers.isTestingAll
|
||||
);
|
||||
|
||||
const saveOptions = useRef<() => void>();
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [hasPendingChanges, setHasPendingChanges] = useState(false);
|
||||
const [isManageIndexersModalOpen, setIsManageIndexersModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleSetChildSave = useCallback((saveCallback: SaveCallback) => {
|
||||
saveOptions.current = saveCallback;
|
||||
}, []);
|
||||
|
||||
const handleChildStateChange = useCallback(
|
||||
({ isSaving, hasPendingChanges }: SettingsStateChange) => {
|
||||
setIsSaving(isSaving);
|
||||
setHasPendingChanges(hasPendingChanges);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleManageIndexersPress = useCallback(() => {
|
||||
setIsManageIndexersModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleManageIndexersModalClose = useCallback(() => {
|
||||
setIsManageIndexersModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
saveOptions.current?.();
|
||||
}, []);
|
||||
|
||||
const handleTestAllIndexersPress = useCallback(() => {
|
||||
dispatch(testAllIndexers());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('IndexerSettings')}>
|
||||
<SettingsToolbar
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('TestAllIndexers')}
|
||||
iconName={icons.TEST}
|
||||
isSpinning={isTestingAll}
|
||||
onPress={handleTestAllIndexersPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManageIndexers')}
|
||||
iconName={icons.MANAGE}
|
||||
onPress={handleManageIndexersPress}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onSavePress={handleSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<Indexers />
|
||||
|
||||
<IndexerOptions
|
||||
setChildSave={handleSetChildSave}
|
||||
onChildStateChange={handleChildStateChange}
|
||||
/>
|
||||
|
||||
<ManageIndexersModal
|
||||
isOpen={isManageIndexersModalOpen}
|
||||
onModalClose={handleManageIndexersModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexerSettings;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { testAllIndexers } from 'Store/Actions/settingsActions';
|
||||
import IndexerSettings from './IndexerSettings';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.indexers.isTestingAll,
|
||||
(isTestingAll) => {
|
||||
return {
|
||||
isTestingAll
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchTestAllIndexers: testAllIndexers
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSettings);
|
||||
@@ -1,113 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem';
|
||||
import styles from './AddIndexerItem.css';
|
||||
|
||||
class AddIndexerItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerSelect = () => {
|
||||
const {
|
||||
implementation,
|
||||
implementationName
|
||||
} = this.props;
|
||||
|
||||
this.props.onIndexerSelect({ implementation, implementationName });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
implementation,
|
||||
implementationName,
|
||||
infoLink,
|
||||
presets,
|
||||
onIndexerSelect
|
||||
} = this.props;
|
||||
|
||||
const hasPresets = !!presets && !!presets.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.indexer}
|
||||
>
|
||||
<Link
|
||||
className={styles.underlay}
|
||||
onPress={this.onIndexerSelect}
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>
|
||||
{implementationName}
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{
|
||||
hasPresets &&
|
||||
<span>
|
||||
<Button
|
||||
size={sizes.SMALL}
|
||||
onPress={this.onIndexerSelect}
|
||||
>
|
||||
{translate('Custom')}
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button
|
||||
className={styles.presetsMenuButton}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('Presets')}
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{
|
||||
presets.map((preset) => {
|
||||
return (
|
||||
<AddIndexerPresetMenuItem
|
||||
key={preset.name}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
implementationName={implementationName}
|
||||
onPress={onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
}
|
||||
|
||||
<Button
|
||||
to={infoLink}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerItem.propTypes = {
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
infoLink: PropTypes.string.isRequired,
|
||||
presets: PropTypes.arrayOf(PropTypes.object),
|
||||
onIndexerSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerItem;
|
||||
88
frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx
Normal file
88
frontend/src/Settings/Indexers/Indexers/AddIndexerItem.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { selectIndexerSchema } from 'Store/Actions/settingsActions';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem';
|
||||
import styles from './AddIndexerItem.css';
|
||||
|
||||
interface AddIndexerItemProps {
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
infoLink: string;
|
||||
presets?: Indexer[];
|
||||
onIndexerSelect: () => void;
|
||||
}
|
||||
|
||||
function AddIndexerItem({
|
||||
implementation,
|
||||
implementationName,
|
||||
infoLink,
|
||||
presets,
|
||||
onIndexerSelect,
|
||||
}: AddIndexerItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
const hasPresets = !!presets && !!presets.length;
|
||||
|
||||
const handleIndexerSelect = useCallback(() => {
|
||||
dispatch(
|
||||
selectIndexerSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
})
|
||||
);
|
||||
|
||||
onIndexerSelect();
|
||||
}, [implementation, implementationName, dispatch, onIndexerSelect]);
|
||||
|
||||
return (
|
||||
<div className={styles.indexer}>
|
||||
<Link className={styles.underlay} onPress={handleIndexerSelect} />
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>{implementationName}</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{hasPresets && (
|
||||
<span>
|
||||
<Button size={sizes.SMALL} onPress={handleIndexerSelect}>
|
||||
{translate('Custom')}
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
|
||||
{translate('Presets')}
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{presets.map((preset) => {
|
||||
return (
|
||||
<AddIndexerPresetMenuItem
|
||||
key={preset.name}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
implementationName={implementationName}
|
||||
onPress={onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Button to={infoLink} size={sizes.SMALL}>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddIndexerItem;
|
||||
@@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
|
||||
|
||||
function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddIndexerModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddIndexerModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerModal;
|
||||
26
frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx
Normal file
26
frontend/src/Settings/Indexers/Indexers/AddIndexerModal.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||
|
||||
interface AddIndexerModalProps {
|
||||
isOpen: boolean;
|
||||
onIndexerSelect: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function AddIndexerModal({
|
||||
isOpen,
|
||||
onIndexerSelect,
|
||||
onModalClose,
|
||||
}: AddIndexerModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<AddIndexerModalContent
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddIndexerModal;
|
||||
@@ -1,122 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerItem from './AddIndexerItem';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
class AddIndexerModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
usenetIndexers,
|
||||
torrentIndexers,
|
||||
onIndexerSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AddIndexer')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isSchemaFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isSchemaFetching && !!schemaError &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddIndexerError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isSchemaPopulated && !schemaError &&
|
||||
<div>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('SupportedIndexers')}
|
||||
</div>
|
||||
<div>
|
||||
{translate('SupportedIndexersMoreInfo')}
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<FieldSet legend={translate('Usenet')}>
|
||||
<div className={styles.indexers}>
|
||||
{
|
||||
usenetIndexers.map((indexer) => {
|
||||
return (
|
||||
<AddIndexerItem
|
||||
key={indexer.implementation}
|
||||
implementation={indexer.implementation}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Torrents')}>
|
||||
<div className={styles.indexers}>
|
||||
{
|
||||
torrentIndexers.map((indexer) => {
|
||||
return (
|
||||
<AddIndexerItem
|
||||
key={indexer.implementation}
|
||||
implementation={indexer.implementation}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerModalContent.propTypes = {
|
||||
isSchemaFetching: PropTypes.bool.isRequired,
|
||||
isSchemaPopulated: PropTypes.bool.isRequired,
|
||||
schemaError: PropTypes.object,
|
||||
usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onIndexerSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerModalContent;
|
||||
@@ -0,0 +1,116 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { fetchIndexerSchema } from 'Store/Actions/settingsActions';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerItem from './AddIndexerItem';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
interface AddIndexerModalContentProps {
|
||||
onIndexerSelect: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function AddIndexerModalContent({
|
||||
onIndexerSelect,
|
||||
onModalClose,
|
||||
}: AddIndexerModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
|
||||
useSelector((state: AppState) => state.settings.indexers);
|
||||
|
||||
const { usenetIndexers, torrentIndexers } = useMemo(() => {
|
||||
return schema.reduce<{
|
||||
usenetIndexers: Indexer[];
|
||||
torrentIndexers: Indexer[];
|
||||
}>(
|
||||
(acc, item) => {
|
||||
if (item.protocol === 'usenet') {
|
||||
acc.usenetIndexers.push(item);
|
||||
} else if (item.protocol === 'torrent') {
|
||||
acc.torrentIndexers.push(item);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
usenetIndexers: [],
|
||||
torrentIndexers: [],
|
||||
}
|
||||
);
|
||||
}, [schema]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchIndexerSchema());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isSchemaFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isSchemaFetching && !!schemaError ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isSchemaPopulated && !schemaError ? (
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>{translate('SupportedIndexers')}</div>
|
||||
<div>{translate('SupportedIndexersMoreInfo')}</div>
|
||||
</Alert>
|
||||
|
||||
<FieldSet legend={translate('Usenet')}>
|
||||
<div className={styles.indexers}>
|
||||
{usenetIndexers.map((indexer) => {
|
||||
return (
|
||||
<AddIndexerItem
|
||||
key={indexer.implementation}
|
||||
{...indexer}
|
||||
implementation={indexer.implementation}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Torrents')}>
|
||||
<div className={styles.indexers}>
|
||||
{torrentIndexers.map((indexer) => {
|
||||
return (
|
||||
<AddIndexerItem
|
||||
key={indexer.implementation}
|
||||
{...indexer}
|
||||
implementation={indexer.implementation}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FieldSet>
|
||||
</div>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddIndexerModalContent;
|
||||
@@ -1,75 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerSchema, selectIndexerSchema } from 'Store/Actions/settingsActions';
|
||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.indexers,
|
||||
(indexers) => {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema
|
||||
} = indexers;
|
||||
|
||||
const usenetIndexers = _.filter(schema, { protocol: 'usenet' });
|
||||
const torrentIndexers = _.filter(schema, { protocol: 'torrent' });
|
||||
|
||||
return {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
usenetIndexers,
|
||||
torrentIndexers
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchIndexerSchema,
|
||||
selectIndexerSchema
|
||||
};
|
||||
|
||||
class AddIndexerModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchIndexerSchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerSelect = ({ implementation, implementationName, name }) => {
|
||||
this.props.selectIndexerSchema({ implementation, implementationName, presetName: name });
|
||||
this.props.onModalClose({ indexerSelected: true });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddIndexerModalContent
|
||||
{...this.props}
|
||||
onIndexerSelect={this.onIndexerSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerModalContentConnector.propTypes = {
|
||||
fetchIndexerSchema: PropTypes.func.isRequired,
|
||||
selectIndexerSchema: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);
|
||||
@@ -1,53 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
|
||||
class AddIndexerPresetMenuItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
name,
|
||||
implementation,
|
||||
implementationName
|
||||
} = this.props;
|
||||
|
||||
this.props.onPress({
|
||||
name,
|
||||
implementation,
|
||||
implementationName
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
implementation,
|
||||
implementationName,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
{...otherProps}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddIndexerPresetMenuItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddIndexerPresetMenuItem;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem';
|
||||
import { selectIndexerSchema } from 'Store/Actions/settingsActions';
|
||||
|
||||
interface AddIndexerPresetMenuItemProps
|
||||
extends Omit<MenuItemProps, 'children'> {
|
||||
name: string;
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function AddIndexerPresetMenuItem({
|
||||
name,
|
||||
implementation,
|
||||
implementationName,
|
||||
onPress,
|
||||
...otherProps
|
||||
}: AddIndexerPresetMenuItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
dispatch(
|
||||
selectIndexerSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
presetName: name,
|
||||
})
|
||||
);
|
||||
|
||||
onPress();
|
||||
}, [name, implementation, implementationName, dispatch, onPress]);
|
||||
|
||||
return (
|
||||
<MenuItem {...otherProps} onPress={handlePress}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddIndexerPresetMenuItem;
|
||||
@@ -1,27 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EditIndexerModalContentConnector from './EditIndexerModalContentConnector';
|
||||
|
||||
function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditIndexerModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditIndexerModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditIndexerModal;
|
||||
45
frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx
Normal file
45
frontend/src/Settings/Indexers/Indexers/EditIndexerModal.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
cancelSaveIndexer,
|
||||
cancelTestIndexer,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import EditIndexerModalContent, {
|
||||
EditIndexerModalContentProps,
|
||||
} from './EditIndexerModalContent';
|
||||
|
||||
const section = 'settings.indexers';
|
||||
|
||||
interface EditIndexerModalProps extends EditIndexerModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function EditIndexerModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditIndexerModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
dispatch(cancelTestIndexer({ section }));
|
||||
dispatch(cancelSaveIndexer({ section }));
|
||||
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditIndexerModalContent
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditIndexerModal;
|
||||
@@ -1,65 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { cancelSaveIndexer, cancelTestIndexer } from 'Store/Actions/settingsActions';
|
||||
import EditIndexerModal from './EditIndexerModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const section = 'settings.indexers';
|
||||
|
||||
return {
|
||||
dispatchClearPendingChanges() {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelTestIndexer() {
|
||||
dispatch(cancelTestIndexer({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelSaveIndexer() {
|
||||
dispatch(cancelSaveIndexer({ section }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EditIndexerModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPendingChanges();
|
||||
this.props.dispatchCancelTestIndexer();
|
||||
this.props.dispatchCancelSaveIndexer();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchCancelTestIndexer,
|
||||
dispatchCancelSaveIndexer,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EditIndexerModal
|
||||
{...otherProps}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditIndexerModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchCancelTestIndexer: PropTypes.func.isRequired,
|
||||
dispatchCancelSaveIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(EditIndexerModalConnector);
|
||||
@@ -1,255 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditIndexerModalContent.css';
|
||||
|
||||
function EditIndexerModalContent(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
isTesting,
|
||||
saveError,
|
||||
item,
|
||||
onInputChange,
|
||||
onFieldChange,
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onDeleteIndexerPress,
|
||||
onAdvancedSettingsPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
enableRss,
|
||||
enableAutomaticSearch,
|
||||
enableInteractiveSearch,
|
||||
supportsRss,
|
||||
supportsSearch,
|
||||
tags,
|
||||
fields,
|
||||
priority,
|
||||
protocol,
|
||||
downloadClientId
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id ? translate('EditIndexerImplementation', { implementationName }) : translate('AddIndexerImplementation', { implementationName })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddIndexerError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('EnableRss')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableRss"
|
||||
helpText={supportsRss.value ? translate('EnableRssHelpText') : undefined}
|
||||
helpTextWarning={supportsRss.value ? undefined : translate('RssIsNotSupportedWithThisIndexer')}
|
||||
isDisabled={!supportsRss.value}
|
||||
{...enableRss}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableAutomaticSearch"
|
||||
helpText={supportsSearch.value ? translate('EnableAutomaticSearchHelpText') : undefined}
|
||||
helpTextWarning={supportsSearch.value ? undefined : translate('SearchIsNotSupportedWithThisIndexer')}
|
||||
isDisabled={!supportsSearch.value}
|
||||
{...enableAutomaticSearch}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableInteractiveSearch"
|
||||
helpText={supportsSearch.value ? translate('EnableInteractiveSearchHelpText') : undefined}
|
||||
helpTextWarning={supportsSearch.value ? undefined : translate('SearchIsNotSupportedWithThisIndexer')}
|
||||
isDisabled={!supportsSearch.value}
|
||||
{...enableInteractiveSearch}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
fields.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
advancedSettings={advancedSettings}
|
||||
provider="indexer"
|
||||
providerData={item}
|
||||
{...field}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('IndexerPriority')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
helpText={translate('IndexerPriorityHelpText')}
|
||||
min={1}
|
||||
max={50}
|
||||
{...priority}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('DownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
|
||||
name="downloadClientId"
|
||||
helpText={translate('IndexerDownloadClientHelpText')}
|
||||
{...downloadClientId}
|
||||
includeAny={true}
|
||||
protocol={protocol.value}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('IndexerTagMovieHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteIndexerPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
onPress={onTestPress}
|
||||
>
|
||||
{translate('Test')}
|
||||
</SpinnerErrorButton>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
EditIndexerModalContent.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isTesting: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteIndexerPress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditIndexerModalContent;
|
||||
@@ -0,0 +1,299 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { IndexerAppState } from 'App/State/SettingsAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import {
|
||||
saveIndexer,
|
||||
setIndexerFieldValue,
|
||||
setIndexerValue,
|
||||
testIndexer,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditIndexerModalContent.css';
|
||||
|
||||
export interface EditIndexerModalContentProps {
|
||||
id?: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteIndexerPress?: () => void;
|
||||
}
|
||||
|
||||
function EditIndexerModalContent({
|
||||
id,
|
||||
onModalClose,
|
||||
onDeleteIndexerPress,
|
||||
}: EditIndexerModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const showAdvancedSettings = useShowAdvancedSettings();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
isTesting = false,
|
||||
saveError,
|
||||
item,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
} = useSelector(
|
||||
createProviderSettingsSelectorHook<Indexer, IndexerAppState>('indexers', id)
|
||||
);
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
const {
|
||||
implementationName = '',
|
||||
name,
|
||||
enableRss,
|
||||
enableAutomaticSearch,
|
||||
enableInteractiveSearch,
|
||||
supportsRss,
|
||||
supportsSearch,
|
||||
tags,
|
||||
fields,
|
||||
priority,
|
||||
protocol,
|
||||
downloadClientId,
|
||||
} = item;
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setIndexerValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setIndexerFieldValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
dispatch(saveIndexer({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleTestPress = useCallback(() => {
|
||||
dispatch(testIndexer({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && wasSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id
|
||||
? translate('EditIndexerImplementation', { implementationName })
|
||||
: translate('AddIndexerImplementation', { implementationName })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error ? (
|
||||
<Form
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('EnableRss')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableRss"
|
||||
helpText={
|
||||
supportsRss.value ? translate('EnableRssHelpText') : undefined
|
||||
}
|
||||
helpTextWarning={
|
||||
supportsRss.value
|
||||
? undefined
|
||||
: translate('RssIsNotSupportedWithThisIndexer')
|
||||
}
|
||||
isDisabled={!supportsRss.value}
|
||||
{...enableRss}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableAutomaticSearch"
|
||||
helpText={
|
||||
supportsSearch.value
|
||||
? translate('EnableAutomaticSearchHelpText')
|
||||
: undefined
|
||||
}
|
||||
helpTextWarning={
|
||||
supportsSearch.value
|
||||
? undefined
|
||||
: translate('SearchIsNotSupportedWithThisIndexer')
|
||||
}
|
||||
isDisabled={!supportsSearch.value}
|
||||
{...enableAutomaticSearch}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableInteractiveSearch"
|
||||
helpText={
|
||||
supportsSearch.value
|
||||
? translate('EnableInteractiveSearchHelpText')
|
||||
: undefined
|
||||
}
|
||||
helpTextWarning={
|
||||
supportsSearch.value
|
||||
? undefined
|
||||
: translate('SearchIsNotSupportedWithThisIndexer')
|
||||
}
|
||||
isDisabled={!supportsSearch.value}
|
||||
{...enableInteractiveSearch}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{fields?.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
advancedSettings={showAdvancedSettings}
|
||||
provider="indexer"
|
||||
providerData={item}
|
||||
{...field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('IndexerPriority')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
helpText={translate('IndexerPriorityHelpText')}
|
||||
min={1}
|
||||
max={50}
|
||||
{...priority}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('DownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
|
||||
name="downloadClientId"
|
||||
helpText={translate('IndexerDownloadClientHelpText')}
|
||||
{...downloadClientId}
|
||||
includeAny={true}
|
||||
protocol={protocol.value}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('IndexerTagMovieHelpText')}
|
||||
{...tags}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{id ? (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteIndexerPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<AdvancedSettingsButton showLabel={false} />
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
onPress={handleTestPress}
|
||||
>
|
||||
{translate('Test')}
|
||||
</SpinnerErrorButton>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={handleSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditIndexerModalContent;
|
||||
@@ -1,95 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer, toggleAdvancedSettings } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditIndexerModalContent from './EditIndexerModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('indexers'),
|
||||
(advancedSettings, indexer) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...indexer
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setIndexerValue,
|
||||
setIndexerFieldValue,
|
||||
saveIndexer,
|
||||
testIndexer,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditIndexerModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setIndexerValue({ name, value });
|
||||
};
|
||||
|
||||
onFieldChange = ({ name, value }) => {
|
||||
this.props.setIndexerFieldValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveIndexer({ id: this.props.id });
|
||||
};
|
||||
|
||||
onTestPress = () => {
|
||||
this.props.testIndexer({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditIndexerModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditIndexerModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setIndexerValue: PropTypes.func.isRequired,
|
||||
setIndexerFieldValue: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
saveIndexer: PropTypes.func.isRequired,
|
||||
testIndexer: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector);
|
||||
@@ -1,181 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditIndexerModalConnector from './EditIndexerModalConnector';
|
||||
import styles from './Indexer.css';
|
||||
|
||||
class Indexer extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditIndexerModalOpen: false,
|
||||
isDeleteIndexerModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditIndexerPress = () => {
|
||||
this.setState({ isEditIndexerModalOpen: true });
|
||||
};
|
||||
|
||||
onEditIndexerModalClose = () => {
|
||||
this.setState({ isEditIndexerModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteIndexerPress = () => {
|
||||
this.setState({
|
||||
isEditIndexerModalOpen: false,
|
||||
isDeleteIndexerModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteIndexerModalClose = () => {
|
||||
this.setState({ isDeleteIndexerModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmDeleteIndexer = () => {
|
||||
this.props.onConfirmDeleteIndexer(this.props.id);
|
||||
};
|
||||
|
||||
onCloneIndexerPress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneIndexerPress
|
||||
} = this.props;
|
||||
|
||||
onCloneIndexerPress(id);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enableRss,
|
||||
enableAutomaticSearch,
|
||||
enableInteractiveSearch,
|
||||
tags,
|
||||
tagList,
|
||||
supportsRss,
|
||||
supportsSearch,
|
||||
priority,
|
||||
showPriority
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.indexer}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditIndexerPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneIndexer')}
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneIndexerPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
|
||||
{
|
||||
supportsRss && enableRss &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('Rss')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsSearch && enableAutomaticSearch &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('AutomaticSearch')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsSearch && enableInteractiveSearch &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('InteractiveSearch')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
showPriority &&
|
||||
<Label kind={kinds.DEFAULT}>
|
||||
{translate('Priority')}: {priority}
|
||||
</Label>
|
||||
}
|
||||
{
|
||||
!enableRss && !enableAutomaticSearch && !enableInteractiveSearch &&
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
{translate('Disabled')}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<EditIndexerModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditIndexerModalOpen}
|
||||
onModalClose={this.onEditIndexerModalClose}
|
||||
onDeleteIndexerPress={this.onDeleteIndexerPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteIndexerModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteIndexer')}
|
||||
message={translate('DeleteIndexerMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteIndexer}
|
||||
onCancel={this.onDeleteIndexerModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Indexer.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
enableRss: PropTypes.bool.isRequired,
|
||||
enableAutomaticSearch: PropTypes.bool.isRequired,
|
||||
enableInteractiveSearch: PropTypes.bool.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
supportsRss: PropTypes.bool.isRequired,
|
||||
supportsSearch: PropTypes.bool.isRequired,
|
||||
onCloneIndexerPress: PropTypes.func.isRequired,
|
||||
onConfirmDeleteIndexer: PropTypes.func.isRequired,
|
||||
priority: PropTypes.number.isRequired,
|
||||
showPriority: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default Indexer;
|
||||
131
frontend/src/Settings/Indexers/Indexers/Indexer.tsx
Normal file
131
frontend/src/Settings/Indexers/Indexers/Indexer.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { deleteIndexer } from 'Store/Actions/settingsActions';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import IndexerModel from 'typings/Indexer';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditIndexerModal from './EditIndexerModal';
|
||||
import styles from './Indexer.css';
|
||||
|
||||
interface IndexerProps extends IndexerModel {
|
||||
showPriority: boolean;
|
||||
onCloneIndexerPress: (id: number) => void;
|
||||
}
|
||||
|
||||
function Indexer({
|
||||
id,
|
||||
name,
|
||||
enableRss,
|
||||
enableAutomaticSearch,
|
||||
enableInteractiveSearch,
|
||||
tags,
|
||||
supportsRss,
|
||||
supportsSearch,
|
||||
priority,
|
||||
showPriority,
|
||||
onCloneIndexerPress,
|
||||
}: IndexerProps) {
|
||||
const dispatch = useDispatch();
|
||||
const tagList = useSelector(createTagsSelector());
|
||||
|
||||
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
|
||||
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleEditIndexerPress = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditIndexerModalClose = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteIndexerPress = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(false);
|
||||
setIsDeleteIndexerModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteIndexerModalClose = useCallback(() => {
|
||||
setIsDeleteIndexerModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDeleteIndexer = useCallback(() => {
|
||||
dispatch(deleteIndexer({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleCloneIndexerPress = useCallback(() => {
|
||||
onCloneIndexerPress(id);
|
||||
}, [id, onCloneIndexerPress]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.indexer}
|
||||
overlayContent={true}
|
||||
onPress={handleEditIndexerPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneIndexer')}
|
||||
name={icons.CLONE}
|
||||
onPress={handleCloneIndexerPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
{supportsRss && enableRss ? (
|
||||
<Label kind={kinds.SUCCESS}>{translate('Rss')}</Label>
|
||||
) : null}
|
||||
|
||||
{supportsSearch && enableAutomaticSearch ? (
|
||||
<Label kind={kinds.SUCCESS}>{translate('AutomaticSearch')}</Label>
|
||||
) : null}
|
||||
|
||||
{supportsSearch && enableInteractiveSearch ? (
|
||||
<Label kind={kinds.SUCCESS}>{translate('InteractiveSearch')}</Label>
|
||||
) : null}
|
||||
|
||||
{showPriority ? (
|
||||
<Label kind={kinds.DEFAULT}>
|
||||
{translate('Priority')}: {priority}
|
||||
</Label>
|
||||
) : null}
|
||||
|
||||
{!enableRss && !enableAutomaticSearch && !enableInteractiveSearch ? (
|
||||
<Label kind={kinds.DISABLED} outline={true}>
|
||||
{translate('Disabled')}
|
||||
</Label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<TagList tags={tags} tagList={tagList} />
|
||||
|
||||
<EditIndexerModal
|
||||
id={id}
|
||||
isOpen={isEditIndexerModalOpen}
|
||||
onModalClose={handleEditIndexerModalClose}
|
||||
onDeleteIndexerPress={handleDeleteIndexerPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteIndexerModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteIndexer')}
|
||||
message={translate('DeleteIndexerMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={handleConfirmDeleteIndexer}
|
||||
onCancel={handleDeleteIndexerModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default Indexer;
|
||||
@@ -1,129 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerModal from './AddIndexerModal';
|
||||
import EditIndexerModalConnector from './EditIndexerModalConnector';
|
||||
import Indexer from './Indexer';
|
||||
import styles from './Indexers.css';
|
||||
|
||||
class Indexers extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddIndexerModalOpen: false,
|
||||
isEditIndexerModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddIndexerPress = () => {
|
||||
this.setState({ isAddIndexerModalOpen: true });
|
||||
};
|
||||
|
||||
onCloneIndexerPress = (id) => {
|
||||
this.props.dispatchCloneIndexer({ id });
|
||||
this.setState({ isEditIndexerModalOpen: true });
|
||||
};
|
||||
|
||||
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
|
||||
this.setState({
|
||||
isAddIndexerModalOpen: false,
|
||||
isEditIndexerModalOpen: indexerSelected
|
||||
});
|
||||
};
|
||||
|
||||
onEditIndexerModalClose = () => {
|
||||
this.setState({ isEditIndexerModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
tagList,
|
||||
dispatchCloneIndexer,
|
||||
onConfirmDeleteIndexer,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isAddIndexerModalOpen,
|
||||
isEditIndexerModalOpen
|
||||
} = this.state;
|
||||
|
||||
const showPriority = items.some((index) => index.priority !== 25);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Indexers')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('IndexersLoadError')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.indexers}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<Indexer
|
||||
key={item.id}
|
||||
{...item}
|
||||
tagList={tagList}
|
||||
showPriority={showPriority}
|
||||
onCloneIndexerPress={this.onCloneIndexerPress}
|
||||
onConfirmDeleteIndexer={onConfirmDeleteIndexer}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addIndexer}
|
||||
onPress={this.onAddIndexerPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AddIndexerModal
|
||||
isOpen={isAddIndexerModalOpen}
|
||||
onModalClose={this.onAddIndexerModalClose}
|
||||
/>
|
||||
|
||||
<EditIndexerModalConnector
|
||||
isOpen={isEditIndexerModalOpen}
|
||||
onModalClose={this.onEditIndexerModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Indexers.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchCloneIndexer: PropTypes.func.isRequired,
|
||||
onConfirmDeleteIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Indexers;
|
||||
105
frontend/src/Settings/Indexers/Indexers/Indexers.tsx
Normal file
105
frontend/src/Settings/Indexers/Indexers/Indexers.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { IndexerAppState } from 'App/State/SettingsAppState';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { cloneIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import IndexerModel from 'typings/Indexer';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddIndexerModal from './AddIndexerModal';
|
||||
import EditIndexerModal from './EditIndexerModal';
|
||||
import Indexer from './Indexer';
|
||||
import styles from './Indexers.css';
|
||||
|
||||
function Indexers() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isFetching, isPopulated, items, error } = useSelector(
|
||||
createSortedSectionSelector<IndexerModel, IndexerAppState>(
|
||||
'settings.indexers',
|
||||
sortByProp('name')
|
||||
)
|
||||
);
|
||||
|
||||
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
|
||||
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
|
||||
|
||||
const showPriority = items.some((index) => index.priority !== 25);
|
||||
|
||||
const handleAddIndexerPress = useCallback(() => {
|
||||
setIsAddIndexerModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCloneIndexerPress = useCallback(
|
||||
(id: number) => {
|
||||
dispatch(cloneIndexer({ id }));
|
||||
setIsEditIndexerModalOpen(true);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleIndexerSelect = useCallback(() => {
|
||||
setIsAddIndexerModalOpen(false);
|
||||
setIsEditIndexerModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleAddIndexerModalClose = useCallback(() => {
|
||||
setIsAddIndexerModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleEditIndexerModalClose = useCallback(() => {
|
||||
setIsEditIndexerModalOpen(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchIndexers());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Indexers')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('IndexersLoadError')}
|
||||
error={error}
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
>
|
||||
<div className={styles.indexers}>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<Indexer
|
||||
key={item.id}
|
||||
{...item}
|
||||
showPriority={showPriority}
|
||||
onCloneIndexerPress={handleCloneIndexerPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Card className={styles.addIndexer} onPress={handleAddIndexerPress}>
|
||||
<div className={styles.center}>
|
||||
<Icon name={icons.ADD} size={45} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AddIndexerModal
|
||||
isOpen={isAddIndexerModalOpen}
|
||||
onIndexerSelect={handleIndexerSelect}
|
||||
onModalClose={handleAddIndexerModalClose}
|
||||
/>
|
||||
|
||||
<EditIndexerModal
|
||||
isOpen={isEditIndexerModalOpen}
|
||||
onModalClose={handleEditIndexerModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default Indexers;
|
||||
@@ -1,65 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import Indexers from './Indexers';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.indexers', sortByProp('name')),
|
||||
createTagsSelector(),
|
||||
(indexers, tagList) => {
|
||||
return {
|
||||
...indexers,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchIndexers: fetchIndexers,
|
||||
dispatchDeleteIndexer: deleteIndexer,
|
||||
dispatchCloneIndexer: cloneIndexer
|
||||
};
|
||||
|
||||
class IndexersConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchIndexers();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteIndexer = (id) => {
|
||||
this.props.dispatchDeleteIndexer({ id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Indexers
|
||||
{...this.props}
|
||||
onConfirmDeleteIndexer={this.onConfirmDeleteIndexer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexersConnector.propTypes = {
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired,
|
||||
dispatchDeleteIndexer: PropTypes.func.isRequired,
|
||||
dispatchCloneIndexer: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector);
|
||||
@@ -1,174 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function IndexerOptions(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
settings,
|
||||
hasSettings,
|
||||
onInputChange,
|
||||
onWhitelistedSubtitleChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Options')}>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('IndexerOptionsLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
hasSettings && !isFetching && !error &&
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MinimumAge')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="minimumAge"
|
||||
min={0}
|
||||
unit="minutes"
|
||||
helpText={translate('MinimumAgeHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.minimumAge}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Retention')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="retention"
|
||||
min={0}
|
||||
unit="days"
|
||||
helpText={translate('RetentionHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.retention}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MaximumSize')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="maximumSize"
|
||||
min={0}
|
||||
unit="MB"
|
||||
helpText={translate('MaximumSizeHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.maximumSize}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PreferIndexerFlags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="preferIndexerFlags"
|
||||
helpText={translate('PreferIndexerFlagsHelpText')}
|
||||
helpLink="https://wiki.servarr.com/radarr/settings#indexer-flags"
|
||||
onChange={onInputChange}
|
||||
{...settings.preferIndexerFlags}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AvailabilityDelay')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="availabilityDelay"
|
||||
unit="days"
|
||||
helpText={translate('AvailabilityDelayHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.availabilityDelay}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('RssSyncInterval')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="rssSyncInterval"
|
||||
min={0}
|
||||
max={120}
|
||||
unit="minutes"
|
||||
helpText={translate('RssSyncIntervalHelpText')}
|
||||
helpTextWarning={translate('RssSyncIntervalHelpTextWarning')}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#how-does-radarr-work"
|
||||
onChange={onInputChange}
|
||||
{...settings.rssSyncInterval}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('WhitelistedSubtitleTags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT_TAG}
|
||||
name="whitelistedHardcodedSubs"
|
||||
helpText={translate('WhitelistedHardcodedSubsHelpText')}
|
||||
onChange={onWhitelistedSubtitleChange}
|
||||
{...settings.whitelistedHardcodedSubs}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('AllowHardcodedSubs')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="allowHardcodedSubs"
|
||||
helpText={translate('AllowHardcodedSubsHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.allowHardcodedSubs}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
IndexerOptions.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
hasSettings: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onWhitelistedSubtitleChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default IndexerOptions;
|
||||
210
frontend/src/Settings/Indexers/Options/IndexerOptions.tsx
Normal file
210
frontend/src/Settings/Indexers/Options/IndexerOptions.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
fetchIndexerOptions,
|
||||
saveIndexerOptions,
|
||||
setIndexerOptionsValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import {
|
||||
OnChildStateChange,
|
||||
SetChildSave,
|
||||
} from 'typings/Settings/SettingsState';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const SECTION = 'indexerOptions';
|
||||
|
||||
interface IndexerOptionsProps {
|
||||
setChildSave: SetChildSave;
|
||||
onChildStateChange: OnChildStateChange;
|
||||
}
|
||||
|
||||
function IndexerOptions({
|
||||
setChildSave,
|
||||
onChildStateChange,
|
||||
}: IndexerOptionsProps) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
isSaving,
|
||||
error,
|
||||
settings,
|
||||
hasSettings,
|
||||
hasPendingChanges,
|
||||
} = useSelector(createSettingsSectionSelector(SECTION));
|
||||
|
||||
const showAdvancedSettings = useShowAdvancedSettings();
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions aren't typed
|
||||
dispatch(setIndexerOptionsValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleWhitelistedSubtitleChange = useCallback(
|
||||
({ name, value }: InputChanged<string[] | null>) => {
|
||||
// @ts-expect-error - actions aren't typed
|
||||
dispatch(setIndexerOptionsValue({ name, value: value?.join(',') }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchIndexerOptions());
|
||||
setChildSave(() => dispatch(saveIndexerOptions()));
|
||||
}, [dispatch, setChildSave]);
|
||||
|
||||
useEffect(() => {
|
||||
onChildStateChange({
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
});
|
||||
}, [hasPendingChanges, isSaving, onChildStateChange]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('Options')}>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('IndexerOptionsLoadError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{hasSettings && isPopulated && !error ? (
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MinimumAge')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="minimumAge"
|
||||
min={0}
|
||||
unit="minutes"
|
||||
helpText={translate('MinimumAgeHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.minimumAge}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Retention')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="retention"
|
||||
min={0}
|
||||
unit="days"
|
||||
helpText={translate('RetentionHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.retention}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MaximumSize')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="maximumSize"
|
||||
min={0}
|
||||
unit="MB"
|
||||
helpText={translate('MaximumSizeHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.maximumSize}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PreferIndexerFlags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="preferIndexerFlags"
|
||||
helpText={translate('PreferIndexerFlagsHelpText')}
|
||||
helpLink="https://wiki.servarr.com/radarr/settings#indexer-flags"
|
||||
onChange={handleInputChange}
|
||||
{...settings.preferIndexerFlags}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AvailabilityDelay')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="availabilityDelay"
|
||||
unit="days"
|
||||
helpText={translate('AvailabilityDelayHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.availabilityDelay}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('RssSyncInterval')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="rssSyncInterval"
|
||||
min={0}
|
||||
max={120}
|
||||
unit="minutes"
|
||||
helpText={translate('RssSyncIntervalHelpText')}
|
||||
helpTextWarning={translate('RssSyncIntervalHelpTextWarning')}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#how-does-radarr-work"
|
||||
onChange={handleInputChange}
|
||||
{...settings.rssSyncInterval}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('WhitelistedSubtitleTags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT_TAG}
|
||||
name="whitelistedHardcodedSubs"
|
||||
helpText={translate('WhitelistedHardcodedSubsHelpText')}
|
||||
onChange={handleWhitelistedSubtitleChange}
|
||||
{...settings.whitelistedHardcodedSubs}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||
<FormLabel>{translate('AllowHardcodedSubs')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="allowHardcodedSubs"
|
||||
helpText={translate('AllowHardcodedSubsHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.allowHardcodedSubs}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
) : null}
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexerOptions;
|
||||
@@ -1,106 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { fetchIndexerOptions, saveIndexerOptions, setIndexerOptionsValue } from 'Store/Actions/settingsActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import IndexerOptions from './IndexerOptions';
|
||||
|
||||
const SECTION = 'indexerOptions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchIndexerOptions: fetchIndexerOptions,
|
||||
dispatchSetIndexerOptionsValue: setIndexerOptionsValue,
|
||||
dispatchSaveIndexerOptions: saveIndexerOptions,
|
||||
dispatchClearPendingChanges: clearPendingChanges
|
||||
};
|
||||
|
||||
class IndexerOptionsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
dispatchFetchIndexerOptions,
|
||||
dispatchSaveIndexerOptions,
|
||||
onChildMounted
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchIndexerOptions();
|
||||
onChildMounted(dispatchSaveIndexerOptions);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
hasPendingChanges,
|
||||
isSaving,
|
||||
onChildStateChange
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
prevProps.isSaving !== isSaving ||
|
||||
prevProps.hasPendingChanges !== hasPendingChanges
|
||||
) {
|
||||
onChildStateChange({
|
||||
isSaving,
|
||||
hasPendingChanges
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetIndexerOptionsValue({ name, value });
|
||||
};
|
||||
|
||||
onWhitelistedSubtitleChange = ({ name, value }) => {
|
||||
this.props.dispatchSetIndexerOptionsValue({ name, value: value.join(',') });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<IndexerOptions
|
||||
onInputChange={this.onInputChange}
|
||||
onWhitelistedSubtitleChange={this.onWhitelistedSubtitleChange}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerOptionsConnector.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
hasPendingChanges: PropTypes.bool.isRequired,
|
||||
dispatchFetchIndexerOptions: PropTypes.func.isRequired,
|
||||
dispatchSetIndexerOptionsValue: PropTypes.func.isRequired,
|
||||
dispatchSaveIndexerOptions: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
onChildMounted: PropTypes.func.isRequired,
|
||||
onChildStateChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerOptionsConnector);
|
||||
@@ -1,500 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import RootFolders from 'RootFolder/RootFolders';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Naming from './Naming/Naming';
|
||||
import AddRootFolder from './RootFolder/AddRootFolder';
|
||||
|
||||
const rescanAfterRefreshOptions = [
|
||||
{
|
||||
key: 'always',
|
||||
get value() {
|
||||
return translate('Always');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'afterManual',
|
||||
get value() {
|
||||
return translate('AfterManualRefresh');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'never',
|
||||
get value() {
|
||||
return translate('Never');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const downloadPropersAndRepacksOptions = [
|
||||
{
|
||||
key: 'preferAndUpgrade',
|
||||
get value() {
|
||||
return translate('PreferAndUpgrade');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'doNotUpgrade',
|
||||
get value() {
|
||||
return translate('DoNotUpgradeAutomatically');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'doNotPrefer',
|
||||
get value() {
|
||||
return translate('DoNotPrefer');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const fileDateOptions = [
|
||||
{
|
||||
key: 'none',
|
||||
get value() {
|
||||
return translate('None');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'cinemas',
|
||||
get value() {
|
||||
return translate('InCinemasDate');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'release',
|
||||
get value() {
|
||||
return translate('PhysicalReleaseDate');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class MediaManagement extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
settings,
|
||||
hasSettings,
|
||||
isWindows,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('MediaManagementSettings')}>
|
||||
<SettingsToolbarConnector
|
||||
advancedSettings={advancedSettings}
|
||||
{...otherProps}
|
||||
onSavePress={onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<Naming />
|
||||
|
||||
{
|
||||
isFetching ?
|
||||
<FieldSet legend={translate('NamingSettings')}>
|
||||
<LoadingIndicator />
|
||||
</FieldSet> : null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<FieldSet legend={translate('NamingSettings')}>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('MediaManagementSettingsLoadError')}
|
||||
</Alert>
|
||||
</FieldSet> : null
|
||||
}
|
||||
|
||||
{
|
||||
hasSettings && !isFetching && !error ?
|
||||
<Form
|
||||
id="mediaManagementSettings"
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
advancedSettings ?
|
||||
<FieldSet legend={translate('Folders')}>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('CreateEmptyMovieFolders')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
isDisabled={settings.deleteEmptyFolders.value && !settings.createEmptyMovieFolders.value}
|
||||
name="createEmptyMovieFolders"
|
||||
helpText={translate('CreateEmptyMovieFoldersHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.createEmptyMovieFolders}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('DeleteEmptyFolders')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
isDisabled={settings.createEmptyMovieFolders.value && !settings.deleteEmptyFolders.value}
|
||||
name="deleteEmptyFolders"
|
||||
helpText={translate('DeleteEmptyFoldersHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.deleteEmptyFolders}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet> : null
|
||||
}
|
||||
|
||||
{
|
||||
advancedSettings ?
|
||||
<FieldSet
|
||||
legend={translate('Importing')}
|
||||
>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipFreeSpaceCheckWhenImporting"
|
||||
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('MinimumFreeSpace')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
unit='MB'
|
||||
name="minimumFreeSpaceWhenImporting"
|
||||
helpText={translate('MinimumFreeSpaceHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.minimumFreeSpaceWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('UseHardlinksInsteadOfCopy')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="copyUsingHardlinks"
|
||||
helpText={translate('CopyUsingHardlinksMovieHelpText')}
|
||||
helpTextWarning={translate('CopyUsingHardlinksHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...settings.copyUsingHardlinks}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('ImportUsingScript')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="useScriptImport"
|
||||
helpText={translate('ImportUsingScriptHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.useScriptImport}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
settings.useScriptImport.value ?
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ImportScriptPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
includeFiles={true}
|
||||
name="scriptImportPath"
|
||||
helpText={translate('ImportScriptPathHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.scriptImportPath}
|
||||
/>
|
||||
</FormGroup> : null
|
||||
}
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="importExtraFiles"
|
||||
helpText={translate('ImportExtraFilesMovieHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.importExtraFiles}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
settings.importExtraFiles.value ?
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="extraFileExtensions"
|
||||
helpTexts={[
|
||||
translate('ExtraFileExtensionsHelpText'),
|
||||
translate('ExtraFileExtensionsHelpTextsExamples')
|
||||
]}
|
||||
onChange={onInputChange}
|
||||
{...settings.extraFileExtensions}
|
||||
/>
|
||||
</FormGroup> : null
|
||||
}
|
||||
</FieldSet> : null
|
||||
}
|
||||
|
||||
<FieldSet
|
||||
legend={translate('FileManagement')}
|
||||
>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('IgnoreDeletedMovies')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="autoUnmonitorPreviouslyDownloadedMovies"
|
||||
helpText={translate('AutoUnmonitorPreviouslyDownloadedMoviesHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.autoUnmonitorPreviouslyDownloadedMovies}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('DownloadPropersAndRepacks')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="downloadPropersAndRepacks"
|
||||
helpTexts={[
|
||||
translate('DownloadPropersAndRepacksHelpText'),
|
||||
translate('DownloadPropersAndRepacksHelpTextCustomFormat')
|
||||
]}
|
||||
helpTextWarning={
|
||||
settings.downloadPropersAndRepacks.value === 'doNotPrefer' ?
|
||||
translate('DownloadPropersAndRepacksHelpTextWarning') :
|
||||
undefined
|
||||
}
|
||||
values={downloadPropersAndRepacksOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.downloadPropersAndRepacks}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AnalyseVideoFiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableMediaInfo"
|
||||
helpText={translate('AnalyseVideoFilesHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.enableMediaInfo}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('RescanMovieFolderAfterRefresh')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="rescanAfterRefresh"
|
||||
helpText={translate('RescanAfterRefreshMovieHelpText')}
|
||||
helpTextWarning={translate('RescanAfterRefreshHelpTextWarning')}
|
||||
values={rescanAfterRefreshOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.rescanAfterRefresh}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ChangeFileDate')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="fileDate"
|
||||
helpText={translate('ChangeFileDateHelpText')}
|
||||
values={fileDateOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.fileDate}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('RecyclingBin')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
name="recycleBin"
|
||||
helpText={translate('RecyclingBinHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.recycleBin}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('RecyclingBinCleanup')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="recycleBinCleanupDays"
|
||||
helpText={translate('RecyclingBinCleanupHelpText')}
|
||||
helpTextWarning={translate('RecyclingBinCleanupHelpTextWarning')}
|
||||
min={0}
|
||||
onChange={onInputChange}
|
||||
{...settings.recycleBinCleanupDays}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
|
||||
{
|
||||
advancedSettings && !isWindows ?
|
||||
<FieldSet
|
||||
legend={translate('Permissions')}
|
||||
>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('SetPermissions')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="setPermissionsLinux"
|
||||
helpText={translate('SetPermissionsLinuxHelpText')}
|
||||
helpTextWarning={translate('SetPermissionsLinuxHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...settings.setPermissionsLinux}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ChmodFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.UMASK}
|
||||
name="chmodFolder"
|
||||
helpText={translate('ChmodFolderHelpText')}
|
||||
helpTextWarning={translate('ChmodFolderHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...settings.chmodFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ChownGroup')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="chownGroup"
|
||||
helpText={translate('ChownGroupHelpText')}
|
||||
helpTextWarning={translate('ChownGroupHelpTextWarning')}
|
||||
values={fileDateOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.chownGroup}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet> : null
|
||||
}
|
||||
</Form> : null
|
||||
}
|
||||
|
||||
<FieldSet legend={translate('RootFolders')}>
|
||||
<RootFolders />
|
||||
<AddRootFolder />
|
||||
</FieldSet>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MediaManagement.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
hasSettings: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MediaManagement;
|
||||
530
frontend/src/Settings/MediaManagement/MediaManagement.tsx
Normal file
530
frontend/src/Settings/MediaManagement/MediaManagement.tsx
Normal file
@@ -0,0 +1,530 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import RootFolders from 'RootFolder/RootFolders';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
fetchMediaManagementSettings,
|
||||
saveMediaManagementSettings,
|
||||
saveNamingSettings,
|
||||
setMediaManagementSettingsValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import useIsWindows from 'System/useIsWindows';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import isEmpty from 'Utilities/Object/isEmpty';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Naming from './Naming/Naming';
|
||||
import AddRootFolder from './RootFolder/AddRootFolder';
|
||||
|
||||
const SECTION = 'mediaManagement';
|
||||
|
||||
const rescanAfterRefreshOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'always',
|
||||
get value() {
|
||||
return translate('Always');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'afterManual',
|
||||
get value() {
|
||||
return translate('AfterManualRefresh');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'never',
|
||||
get value() {
|
||||
return translate('Never');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const downloadPropersAndRepacksOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'preferAndUpgrade',
|
||||
get value() {
|
||||
return translate('PreferAndUpgrade');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'doNotUpgrade',
|
||||
get value() {
|
||||
return translate('DoNotUpgradeAutomatically');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'doNotPrefer',
|
||||
get value() {
|
||||
return translate('DoNotPrefer');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const fileDateOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'none',
|
||||
get value() {
|
||||
return translate('None');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'cinemas',
|
||||
get value() {
|
||||
return translate('InCinemasDate');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'release',
|
||||
get value() {
|
||||
return translate('PhysicalReleaseDate');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function MediaManagement() {
|
||||
const dispatch = useDispatch();
|
||||
const showAdvancedSettings = useShowAdvancedSettings();
|
||||
const hasNamingPendingChanges = !isEmpty(
|
||||
useSelector((state: AppState) => state.settings.naming.pendingChanges)
|
||||
);
|
||||
const isWindows = useIsWindows();
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
isSaving,
|
||||
error,
|
||||
settings,
|
||||
hasSettings,
|
||||
hasPendingChanges,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
} = useSelector(createSettingsSectionSelector(SECTION));
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
dispatch(saveMediaManagementSettings());
|
||||
dispatch(saveNamingSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setMediaManagementSettingsValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchMediaManagementSettings());
|
||||
|
||||
return () => {
|
||||
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('MediaManagementSettings')}>
|
||||
<SettingsToolbar
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasNamingPendingChanges || hasPendingChanges}
|
||||
onSavePress={handleSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<Naming />
|
||||
|
||||
{isFetching ? (
|
||||
<FieldSet legend={translate('NamingSettings')}>
|
||||
<LoadingIndicator />
|
||||
</FieldSet>
|
||||
) : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<FieldSet legend={translate('NamingSettings')}>
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('MediaManagementSettingsLoadError')}
|
||||
</Alert>
|
||||
</FieldSet>
|
||||
) : null}
|
||||
|
||||
{hasSettings && isPopulated && !error ? (
|
||||
<Form
|
||||
id="mediaManagementSettings"
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
>
|
||||
{showAdvancedSettings ? (
|
||||
<FieldSet legend={translate('Folders')}>
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('CreateEmptyMovieFolders')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
isDisabled={
|
||||
settings.deleteEmptyFolders.value &&
|
||||
!settings.createEmptyMovieFolders.value
|
||||
}
|
||||
name="createEmptyMovieFolders"
|
||||
helpText={translate('CreateEmptyMovieFoldersHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.createEmptyMovieFolders}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('DeleteEmptyFolders')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
isDisabled={
|
||||
settings.createEmptyMovieFolders.value &&
|
||||
!settings.deleteEmptyFolders.value
|
||||
}
|
||||
name="deleteEmptyFolders"
|
||||
helpText={translate('DeleteEmptyMovieFoldersHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.deleteEmptyFolders}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
) : null}
|
||||
|
||||
{showAdvancedSettings ? (
|
||||
<FieldSet legend={translate('Importing')}>
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipFreeSpaceCheckWhenImporting"
|
||||
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('MinimumFreeSpace')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
unit="MB"
|
||||
name="minimumFreeSpaceWhenImporting"
|
||||
helpText={translate('MinimumFreeSpaceHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.minimumFreeSpaceWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>
|
||||
{translate('UseHardlinksInsteadOfCopy')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="copyUsingHardlinks"
|
||||
helpText={translate('CopyUsingHardlinksMovieHelpText')}
|
||||
helpTextWarning={translate(
|
||||
'CopyUsingHardlinksHelpTextWarning'
|
||||
)}
|
||||
onChange={handleInputChange}
|
||||
{...settings.copyUsingHardlinks}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('ImportUsingScript')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="useScriptImport"
|
||||
helpText={translate('ImportUsingScriptHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.useScriptImport}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{settings.useScriptImport.value ? (
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ImportScriptPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
includeFiles={true}
|
||||
name="scriptImportPath"
|
||||
helpText={translate('ImportScriptPathHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.scriptImportPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="importExtraFiles"
|
||||
helpText={translate('ImportExtraFilesMovieHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.importExtraFiles}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{settings.importExtraFiles.value ? (
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ImportExtraFiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="extraFileExtensions"
|
||||
helpTexts={[
|
||||
translate('ExtraFileExtensionsHelpText'),
|
||||
translate('ExtraFileExtensionsHelpTextsExamples'),
|
||||
]}
|
||||
onChange={handleInputChange}
|
||||
{...settings.extraFileExtensions}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
</FieldSet>
|
||||
) : null}
|
||||
|
||||
<FieldSet legend={translate('FileManagement')}>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('UnmonitorDeletedMovies')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="autoUnmonitorPreviouslyDownloadedMovies"
|
||||
helpText={translate('UnmonitorDeletedMoviesHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.autoUnmonitorPreviouslyDownloadedMovies}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('DownloadPropersAndRepacks')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="downloadPropersAndRepacks"
|
||||
helpTexts={[
|
||||
translate('DownloadPropersAndRepacksHelpText'),
|
||||
translate('DownloadPropersAndRepacksHelpTextCustomFormat'),
|
||||
]}
|
||||
helpTextWarning={
|
||||
settings.downloadPropersAndRepacks.value === 'doNotPrefer'
|
||||
? translate('DownloadPropersAndRepacksHelpTextWarning')
|
||||
: undefined
|
||||
}
|
||||
values={downloadPropersAndRepacksOptions}
|
||||
onChange={handleInputChange}
|
||||
{...settings.downloadPropersAndRepacks}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AnalyseVideoFiles')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableMediaInfo"
|
||||
helpText={translate('AnalyseVideoFilesHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.enableMediaInfo}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>
|
||||
{translate('RescanMovieFolderAfterRefresh')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="rescanAfterRefresh"
|
||||
helpText={translate('RescanAfterRefreshMovieHelpText')}
|
||||
helpTextWarning={translate(
|
||||
'RescanAfterRefreshHelpTextWarning'
|
||||
)}
|
||||
values={rescanAfterRefreshOptions}
|
||||
onChange={handleInputChange}
|
||||
{...settings.rescanAfterRefresh}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ChangeFileDate')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="fileDate"
|
||||
helpText={translate('ChangeFileDateHelpText')}
|
||||
values={fileDateOptions}
|
||||
onChange={handleInputChange}
|
||||
{...settings.fileDate}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('RecyclingBin')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
name="recycleBin"
|
||||
helpText={translate('RecyclingBinHelpText')}
|
||||
includeFiles={false}
|
||||
onChange={handleInputChange}
|
||||
{...settings.recycleBin}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('RecyclingBinCleanup')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="recycleBinCleanupDays"
|
||||
helpText={translate('RecyclingBinCleanupHelpText')}
|
||||
helpTextWarning={translate(
|
||||
'RecyclingBinCleanupHelpTextWarning'
|
||||
)}
|
||||
min={0}
|
||||
onChange={handleInputChange}
|
||||
{...settings.recycleBinCleanupDays}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
|
||||
{showAdvancedSettings && !isWindows ? (
|
||||
<FieldSet legend={translate('Permissions')}>
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('SetPermissions')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="setPermissionsLinux"
|
||||
helpText={translate('SetPermissionsLinuxHelpText')}
|
||||
helpTextWarning={translate(
|
||||
'SetPermissionsLinuxHelpTextWarning'
|
||||
)}
|
||||
onChange={handleInputChange}
|
||||
{...settings.setPermissionsLinux}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ChmodFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.UMASK}
|
||||
name="chmodFolder"
|
||||
helpText={translate('ChmodFolderHelpText')}
|
||||
helpTextWarning={translate('ChmodFolderHelpTextWarning')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.chmodFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ChownGroup')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="chownGroup"
|
||||
helpText={translate('ChownGroupHelpText')}
|
||||
helpTextWarning={translate('ChownGroupHelpTextWarning')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.chownGroup}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
) : null}
|
||||
</Form>
|
||||
) : null}
|
||||
|
||||
<FieldSet legend={translate('RootFolders')}>
|
||||
<RootFolders />
|
||||
<AddRootFolder />
|
||||
</FieldSet>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MediaManagement;
|
||||
@@ -1,86 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { fetchMediaManagementSettings, saveMediaManagementSettings, saveNamingSettings, setMediaManagementSettingsValue } from 'Store/Actions/settingsActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import MediaManagement from './MediaManagement';
|
||||
|
||||
const SECTION = 'mediaManagement';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.naming,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
createSystemStatusSelector(),
|
||||
(advancedSettings, namingSettings, sectionSettings, systemStatus) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...sectionSettings,
|
||||
hasPendingChanges: !_.isEmpty(namingSettings.pendingChanges) || sectionSettings.hasPendingChanges,
|
||||
isWindows: systemStatus.isWindows
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchMediaManagementSettings,
|
||||
setMediaManagementSettingsValue,
|
||||
saveMediaManagementSettings,
|
||||
saveNamingSettings,
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class MediaManagementConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchMediaManagementSettings();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setMediaManagementSettingsValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveMediaManagementSettings();
|
||||
this.props.saveNamingSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MediaManagement
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MediaManagementConnector.propTypes = {
|
||||
fetchMediaManagementSettings: PropTypes.func.isRequired,
|
||||
setMediaManagementSettingsValue: PropTypes.func.isRequired,
|
||||
saveMediaManagementSettings: PropTypes.func.isRequired,
|
||||
saveNamingSettings: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MediaManagementConnector);
|
||||
@@ -73,7 +73,7 @@ function Naming() {
|
||||
dispatch(fetchNamingExamples());
|
||||
|
||||
return () => {
|
||||
dispatch(clearPendingChanges({ section: SECTION }));
|
||||
dispatch(clearPendingChanges({ section: 'settings.naming' }));
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import styles from './Metadatas.css';
|
||||
|
||||
function createMetadatasSelector() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector<MetadataType>(
|
||||
createSortedSectionSelector<MetadataType, MetadataAppState>(
|
||||
'settings.metadata',
|
||||
sortByProp('name')
|
||||
),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Metadatas from './Metadata/Metadatas';
|
||||
import MetadataOptionsConnector from './Options/MetadataOptionsConnector';
|
||||
@@ -50,7 +50,7 @@ class MetadataSettings extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title={translate('MetadataSettings')}>
|
||||
<SettingsToolbarConnector
|
||||
<SettingsToolbar
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
onSavePress={this.onSavePress}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NotificationsConnector from './Notifications/NotificationsConnector';
|
||||
|
||||
function NotificationSettings() {
|
||||
return (
|
||||
<PageContent title={translate('ConnectSettings')}>
|
||||
<SettingsToolbarConnector
|
||||
<SettingsToolbar
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ function EditNotificationModalContent(props) {
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteNotificationPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
@@ -139,8 +138,6 @@ function EditNotificationModalContent(props) {
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
@@ -183,7 +180,6 @@ EditNotificationModalContent.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteNotificationPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
saveNotification,
|
||||
setNotificationFieldValues,
|
||||
setNotificationValue,
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
testNotification
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditNotificationModalContent from './EditNotificationModalContent';
|
||||
@@ -29,8 +28,7 @@ const mapDispatchToProps = {
|
||||
setNotificationValue,
|
||||
setNotificationFieldValues,
|
||||
saveNotification,
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
testNotification
|
||||
};
|
||||
|
||||
class EditNotificationModalContentConnector extends Component {
|
||||
@@ -63,10 +61,6 @@ class EditNotificationModalContentConnector extends Component {
|
||||
this.props.testNotification({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -76,7 +70,6 @@ class EditNotificationModalContentConnector extends Component {
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
@@ -94,7 +87,6 @@ EditNotificationModalContentConnector.propTypes = {
|
||||
setNotificationFieldValues: PropTypes.func.isRequired,
|
||||
saveNotification: PropTypes.func.isRequired,
|
||||
testNotification: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect } from 'react';
|
||||
import keyboardShortcuts from 'Components/keyboardShortcuts';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function PendingChangesModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
bindShortcut,
|
||||
unbindShortcut
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
bindShortcut('enter', onConfirm);
|
||||
|
||||
return () => unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onCancel}
|
||||
>
|
||||
<ModalContent onModalClose={onCancel}>
|
||||
<ModalHeader>{translate('UnsavedChanges')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{translate('PendingChangesMessage')}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
kind={kinds.DEFAULT}
|
||||
onPress={onCancel}
|
||||
>
|
||||
{translate('PendingChangesStayReview')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
autoFocus={true}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onConfirm}
|
||||
>
|
||||
{translate('PendingChangesDiscardChanges')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
PendingChangesModal.propTypes = {
|
||||
className: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired,
|
||||
unbindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
PendingChangesModal.defaultProps = {
|
||||
kind: kinds.PRIMARY
|
||||
};
|
||||
|
||||
export default keyboardShortcuts(PendingChangesModal);
|
||||
55
frontend/src/Settings/PendingChangesModal.tsx
Normal file
55
frontend/src/Settings/PendingChangesModal.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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 useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
interface PendingChangesModalProps {
|
||||
className?: string;
|
||||
isOpen: boolean;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
function PendingChangesModal({
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: PendingChangesModalProps) {
|
||||
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
bindShortcut('acceptConfirmModal', onConfirm);
|
||||
}
|
||||
|
||||
return () => unbindShortcut('acceptConfirmModal');
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onCancel}>
|
||||
<ModalContent onModalClose={onCancel}>
|
||||
<ModalHeader>{translate('UnsavedChanges')}</ModalHeader>
|
||||
|
||||
<ModalBody>{translate('PendingChangesMessage')}</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button kind={kinds.DEFAULT} onPress={onCancel}>
|
||||
{translate('PendingChangesStayReview')}
|
||||
</Button>
|
||||
|
||||
<Button autoFocus={true} kind={kinds.DANGER} onPress={onConfirm}>
|
||||
{translate('PendingChangesDiscardChanges')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default PendingChangesModal;
|
||||
@@ -3,7 +3,7 @@ import { DndProvider } from 'react-dnd-multi-backend';
|
||||
import HTML5toTouch from 'react-dnd-multi-backend/dist/esm/HTML5toTouch';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
|
||||
import QualityProfilesConnector from './Quality/QualityProfilesConnector';
|
||||
@@ -20,7 +20,7 @@ class Profiles extends Component {
|
||||
render() {
|
||||
return (
|
||||
<PageContent title={translate('Profiles')}>
|
||||
<SettingsToolbarConnector showSave={false} />
|
||||
<SettingsToolbar showSave={false} />
|
||||
|
||||
<PageContentBody>
|
||||
<DndProvider options={HTML5toTouch}>
|
||||
|
||||
@@ -51,7 +51,6 @@ function createReleaseProfileSelector(id?: number) {
|
||||
);
|
||||
|
||||
return {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
|
||||
@@ -24,19 +24,19 @@
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
.track {
|
||||
top: 9px;
|
||||
margin: 0 5px;
|
||||
height: 3px;
|
||||
background-color: var(--sliderAccentColor);
|
||||
box-shadow: 0 0 0 #000;
|
||||
|
||||
&:nth-child(3n+1) {
|
||||
&:nth-child(3n + 1) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
.thumb {
|
||||
top: 1px;
|
||||
z-index: 0 !important;
|
||||
width: 18px;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'bar': string;
|
||||
'handle': string;
|
||||
'megabytesPerMinute': string;
|
||||
'quality': string;
|
||||
'qualityDefinition': string;
|
||||
@@ -10,7 +8,9 @@ interface CssExports {
|
||||
'sizeLimit': string;
|
||||
'sizes': string;
|
||||
'slider': string;
|
||||
'thumb': string;
|
||||
'title': string;
|
||||
'track': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -55,6 +55,27 @@ class QualityDefinition extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
trackRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.track}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
thumbRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.thumb}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -174,6 +195,7 @@ class QualityDefinition extends Component {
|
||||
|
||||
<div className={styles.sizeLimit}>
|
||||
<ReactSlider
|
||||
className={styles.slider}
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
@@ -182,9 +204,9 @@ class QualityDefinition extends Component {
|
||||
withTracks={true}
|
||||
allowCross={false}
|
||||
snapDragDisabled={true}
|
||||
className={styles.slider}
|
||||
trackClassName={styles.bar}
|
||||
thumbClassName={styles.handle}
|
||||
pearling={true}
|
||||
renderThumb={this.thumbRenderer}
|
||||
renderTrack={this.trackRenderer}
|
||||
onChange={this.onSliderChange}
|
||||
onAfterChange={this.onAfterSliderChange}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
|
||||
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
|
||||
@@ -64,7 +64,7 @@ class Quality extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title={translate('QualitySettings')}>
|
||||
<SettingsToolbarConnector
|
||||
<SettingsToolbar
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
|
||||
@@ -3,21 +3,16 @@ import Link from 'Components/Link/Link';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SettingsToolbarConnector from './SettingsToolbarConnector';
|
||||
import SettingsToolbar from './SettingsToolbar';
|
||||
import styles from './Settings.css';
|
||||
|
||||
function Settings() {
|
||||
return (
|
||||
<PageContent title={translate('Settings')}>
|
||||
<SettingsToolbarConnector
|
||||
hasPendingChanges={false}
|
||||
/>
|
||||
<SettingsToolbar hasPendingChanges={false} />
|
||||
|
||||
<PageContentBody>
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/mediamanagement"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/mediamanagement">
|
||||
{translate('MediaManagement')}
|
||||
</Link>
|
||||
|
||||
@@ -25,10 +20,7 @@ function Settings() {
|
||||
{translate('MediaManagementSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/profiles"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/profiles">
|
||||
{translate('Profiles')}
|
||||
</Link>
|
||||
|
||||
@@ -36,10 +28,7 @@ function Settings() {
|
||||
{translate('ProfilesSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/quality"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/quality">
|
||||
{translate('Quality')}
|
||||
</Link>
|
||||
|
||||
@@ -47,10 +36,7 @@ function Settings() {
|
||||
{translate('QualitySettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/customformats"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/customformats">
|
||||
{translate('CustomFormats')}
|
||||
</Link>
|
||||
|
||||
@@ -58,10 +44,7 @@ function Settings() {
|
||||
{translate('CustomFormatsSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/indexers"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/indexers">
|
||||
{translate('Indexers')}
|
||||
</Link>
|
||||
|
||||
@@ -69,10 +52,7 @@ function Settings() {
|
||||
{translate('IndexersSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/downloadclients"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/downloadclients">
|
||||
{translate('DownloadClients')}
|
||||
</Link>
|
||||
|
||||
@@ -80,10 +60,7 @@ function Settings() {
|
||||
{translate('DownloadClientsSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/importlists"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/importlists">
|
||||
{translate('ImportLists')}
|
||||
</Link>
|
||||
|
||||
@@ -91,10 +68,7 @@ function Settings() {
|
||||
{translate('ImportListsSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/connect"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/connect">
|
||||
{translate('Connect')}
|
||||
</Link>
|
||||
|
||||
@@ -102,10 +76,7 @@ function Settings() {
|
||||
{translate('ConnectSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/metadata"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/metadata">
|
||||
{translate('Metadata')}
|
||||
</Link>
|
||||
|
||||
@@ -113,21 +84,13 @@ function Settings() {
|
||||
{translate('MetadataSettingsMovieSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/tags"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/tags">
|
||||
{translate('Tags')}
|
||||
</Link>
|
||||
|
||||
<div className={styles.summary}>
|
||||
{translate('TagsSettingsSummary')}
|
||||
</div>
|
||||
<div className={styles.summary}>{translate('TagsSettingsSummary')}</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/general"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/general">
|
||||
{translate('General')}
|
||||
</Link>
|
||||
|
||||
@@ -135,22 +98,14 @@ function Settings() {
|
||||
{translate('GeneralSettingsSummary')}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to="/settings/ui"
|
||||
>
|
||||
<Link className={styles.link} to="/settings/ui">
|
||||
{translate('Ui')}
|
||||
</Link>
|
||||
|
||||
<div className={styles.summary}>
|
||||
{translate('UiSettingsSummary')}
|
||||
</div>
|
||||
<div className={styles.summary}>{translate('UiSettingsSummary')}</div>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -1,106 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AdvancedSettingsButton from './AdvancedSettingsButton';
|
||||
import PendingChangesModal from './PendingChangesModal';
|
||||
|
||||
class SettingsToolbar extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.bindShortcut(shortcuts.SAVE_SETTINGS.key, this.saveSettings, { isGlobal: true });
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
saveSettings = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const {
|
||||
hasPendingChanges,
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
if (hasPendingChanges) {
|
||||
onSavePress();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
advancedSettings,
|
||||
showSave,
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
hasPendingLocation,
|
||||
additionalButtons,
|
||||
onSavePress,
|
||||
onConfirmNavigation,
|
||||
onCancelNavigation,
|
||||
onAdvancedSettingsPress
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
/>
|
||||
|
||||
{
|
||||
showSave &&
|
||||
<PageToolbarButton
|
||||
label={hasPendingChanges ? translate('SaveChanges') : translate('NoChanges')}
|
||||
iconName={icons.SAVE}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!hasPendingChanges}
|
||||
onPress={onSavePress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
additionalButtons
|
||||
}
|
||||
</PageToolbarSection>
|
||||
|
||||
<PendingChangesModal
|
||||
isOpen={hasPendingLocation}
|
||||
onConfirm={onConfirmNavigation}
|
||||
onCancel={onCancelNavigation}
|
||||
/>
|
||||
</PageToolbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToolbar.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
showSave: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
hasPendingLocation: PropTypes.bool.isRequired,
|
||||
hasPendingChanges: PropTypes.bool,
|
||||
additionalButtons: PropTypes.node,
|
||||
onSavePress: PropTypes.func,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onConfirmNavigation: PropTypes.func.isRequired,
|
||||
onCancelNavigation: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
SettingsToolbar.defaultProps = {
|
||||
showSave: true
|
||||
};
|
||||
|
||||
export default keyboardShortcuts(SettingsToolbar);
|
||||
149
frontend/src/Settings/SettingsToolbar.tsx
Normal file
149
frontend/src/Settings/SettingsToolbar.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Action, Location, UnregisterCallback } from 'history';
|
||||
import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AdvancedSettingsButton from './AdvancedSettingsButton';
|
||||
import PendingChangesModal from './PendingChangesModal';
|
||||
|
||||
interface SettingsToolbarProps {
|
||||
showSave?: boolean;
|
||||
isSaving?: boolean;
|
||||
hasPendingChanges?: boolean;
|
||||
// TODO: This should do type checking like PageToolbarSectionProps,
|
||||
// but this works for the time being.
|
||||
additionalButtons?: ReactElement | null;
|
||||
onSavePress?: () => void;
|
||||
}
|
||||
|
||||
function SettingsToolbar({
|
||||
showSave = true,
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
additionalButtons = null,
|
||||
onSavePress,
|
||||
}: SettingsToolbarProps) {
|
||||
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
|
||||
const history = useHistory();
|
||||
const [nextLocation, setNextLocation] = useState<Location | null>(null);
|
||||
const [nextLocationAction, setNextLocationAction] = useState<Action | null>(
|
||||
null
|
||||
);
|
||||
const hasConfirmed = useRef(false);
|
||||
const unblocker = useRef<UnregisterCallback>();
|
||||
|
||||
const handleConfirmNavigation = useCallback(() => {
|
||||
if (!nextLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = `${nextLocation.pathname}${nextLocation.search}`;
|
||||
|
||||
hasConfirmed.current = true;
|
||||
|
||||
if (nextLocationAction === 'PUSH') {
|
||||
history.push(path);
|
||||
} else {
|
||||
// Unfortunately back and forward both use POP,
|
||||
// which means we don't actually know which direction
|
||||
// the user wanted to go, assuming back.
|
||||
|
||||
history.goBack();
|
||||
}
|
||||
}, [nextLocation, nextLocationAction, history]);
|
||||
|
||||
const handleCancelNavigation = useCallback(() => {
|
||||
setNextLocation(null);
|
||||
setNextLocationAction(null);
|
||||
hasConfirmed.current = false;
|
||||
}, []);
|
||||
|
||||
const handleRouterLeaving = useCallback(
|
||||
(routerLocation: Location, routerAction: Action) => {
|
||||
if (hasConfirmed.current) {
|
||||
setNextLocation(null);
|
||||
setNextLocationAction(null);
|
||||
hasConfirmed.current = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasPendingChanges) {
|
||||
setNextLocation(routerLocation);
|
||||
setNextLocationAction(routerAction);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
[hasPendingChanges]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
unblocker.current = history.block(handleRouterLeaving);
|
||||
|
||||
return () => {
|
||||
unblocker.current?.();
|
||||
};
|
||||
}, [history, handleRouterLeaving]);
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut(
|
||||
'saveSettings',
|
||||
() => {
|
||||
if (hasPendingChanges) {
|
||||
onSavePress?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
isGlobal: true,
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unbindShortcut('saveSettings');
|
||||
};
|
||||
}, [hasPendingChanges, bindShortcut, unbindShortcut, onSavePress]);
|
||||
|
||||
return (
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<AdvancedSettingsButton showLabel={true} />
|
||||
{showSave ? (
|
||||
<PageToolbarButton
|
||||
label={
|
||||
hasPendingChanges
|
||||
? translate('SaveChanges')
|
||||
: translate('NoChanges')
|
||||
}
|
||||
iconName={icons.SAVE}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!hasPendingChanges}
|
||||
onPress={onSavePress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{additionalButtons}
|
||||
</PageToolbarSection>
|
||||
|
||||
<PendingChangesModal
|
||||
isOpen={nextLocation !== null}
|
||||
onConfirm={handleConfirmNavigation}
|
||||
onCancel={handleCancelNavigation}
|
||||
/>
|
||||
</PageToolbar>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsToolbar;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user