mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ada33dc065 | |||
| badb68b817 | |||
| 3bd1b3e972 | |||
| 6851de42a7 | |||
| dd0b7c91f9 | |||
| 45ac69e2d9 | |||
| 9ccf0ecdb1 | |||
| 48a3467572 | |||
| d0a10379f9 | |||
| caab5e3614 | |||
| 4e47695f89 | |||
| 83bd4d0686 | |||
| a75619c8ef | |||
| 28689006fb | |||
| 43b0589bea | |||
| c4aad5800c | |||
| 0c998dac5c | |||
| d41c0f0ab7 | |||
| 85b13b7e41 | |||
| 2a545a84b4 | |||
| 280083f4d7 | |||
| d6dcae3d6a | |||
| ebde4d3bc8 | |||
| 1ee30290ef | |||
| d303eae7c6 | |||
| 584910514a | |||
| a253181d7d | |||
| 7ea6918327 | |||
| 953d3ad3fb | |||
| b9f4073514 | |||
| 86a17e7984 | |||
| f38545f852 | |||
| a7720e829d | |||
| 3a4eac4d59 | |||
| 04f792c55a | |||
| ada326e4dd | |||
| cae58d620b | |||
| e84df18e8d | |||
| a51ae70938 | |||
| 7cc04245ec | |||
| 2caf3c6725 | |||
| 41ff9352b9 | |||
| d7b9b2ccb2 | |||
| e90a50a3aa | |||
| a0dd26c353 | |||
| 2286055d6a | |||
| 0a5a4e0a6f | |||
| 619c38c493 | |||
| 0b8694c627 | |||
| e2793e56e9 | |||
| 68f61da321 | |||
| 8edb541e21 | |||
| d441becc74 | |||
| a97b2ee2ed | |||
| e70c61e24e | |||
| d1f96746e0 | |||
| 35893697bd | |||
| 540c150b93 | |||
| 48f819caee | |||
| 4ad7b60d9d | |||
| 7e4231fc0e | |||
| 94287d9427 | |||
| 8ec6b5dd4d | |||
| 4be43c9f2b | |||
| c388cf968b | |||
| b6b809f473 | |||
| 9dd31be7b3 | |||
| 25ab396a2c | |||
| 145cd74969 | |||
| b9c76d9bed | |||
| 63f16924b1 | |||
| a91a9f7fd9 | |||
| 4c8e9f204e | |||
| d64ee6681f | |||
| 2ecc57cd31 | |||
| 9620207503 | |||
| 0b090e5f39 | |||
| 51cb0920ed | |||
| a90d6682d3 | |||
| db62eddf5a | |||
| ac2b2e6215 | |||
| 9581dd9764 | |||
| 6c459c744a | |||
| 4676ecfce9 | |||
| ef92af9dd8 | |||
| b144482d68 | |||
| 173b1d6a4c | |||
| 5f624a147b | |||
| af066da4ff | |||
| 937ebcdac3 | |||
| 67f5199667 | |||
| 38cd130da5 | |||
| ed340be2b1 | |||
| 34cfb58b39 | |||
| 3d0f22ca7c | |||
| 2510f44c25 | |||
| c0bf75cae3 | |||
| a63ab1ddd6 | |||
| 41cb020ff0 | |||
| d660309b5a | |||
| 222c19e4b3 | |||
| b08981dee0 | |||
| 4a9c0b2240 |
+2
-2
@@ -9,14 +9,14 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.3.4'
|
majorVersion: '5.4.5'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.417'
|
dotnetVersion: '6.0.417'
|
||||||
nodeVersion: '16.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const monitoredOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true
|
isDisabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'monitored',
|
key: 'monitored',
|
||||||
@@ -42,7 +42,7 @@ const searchOnAddOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true
|
isDisabled: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'yes',
|
key: 'yes',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface CommandBody {
|
|||||||
trigger: string;
|
trigger: string;
|
||||||
suppressMessages: boolean;
|
suppressMessages: boolean;
|
||||||
movieId?: number;
|
movieId?: number;
|
||||||
|
movieIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Command extends ModelBase {
|
interface Command extends ModelBase {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { maxBy } from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
@@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
if (id) {
|
if (id) {
|
||||||
dispatchSetFilter({ selectedFilterKey: id });
|
dispatchSetFilter({ selectedFilterKey: id });
|
||||||
} else {
|
} else {
|
||||||
const last = customFilters[customFilters.length -1];
|
const last = maxBy(customFilters, 'id');
|
||||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
labelErrors: [
|
labelErrors: [
|
||||||
{
|
{
|
||||||
message: 'Label is required'
|
message: translate('LabelIsRequired')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Custom Filter
|
{translate('CustomFilter')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className={styles.labelContainer}>
|
<div className={styles.labelContainer}>
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>
|
||||||
Label
|
{translate('Label')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.labelInputContainer}>
|
<div className={styles.labelInputContainer}>
|
||||||
@@ -166,9 +167,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>{translate('Filters')}</div>
|
||||||
{translate('Filters')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.rows}>
|
<div className={styles.rows}>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
|||||||
dispatchSetFilter
|
dispatchSetFilter
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// Assume that delete and then unmounting means the delete was successful.
|
// Assume that delete and then unmounting means the deletion was successful.
|
||||||
// Moving this check to a ancestor would be more accurate, but would have
|
// Moving this check to an ancestor would be more accurate, but would have
|
||||||
// more boilerplate.
|
// more boilerplate.
|
||||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function AvailabilitySelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ function AvailabilitySelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ function createMapStateToProps() {
|
|||||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||||
return {
|
return {
|
||||||
key: downloadClient.id,
|
key: downloadClient.id,
|
||||||
value: downloadClient.name
|
value: downloadClient.name,
|
||||||
|
hint: `(${downloadClient.id})`
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
.isDisabled {
|
.isDisabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownArrowContainer {
|
.dropdownArrowContainer {
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ FormInputGroup.propTypes = {
|
|||||||
includeNoChange: PropTypes.bool,
|
includeNoChange: PropTypes.bool,
|
||||||
includeNoChangeDisabled: PropTypes.bool,
|
includeNoChangeDisabled: PropTypes.bool,
|
||||||
selectedValueOptions: PropTypes.object,
|
selectedValueOptions: PropTypes.object,
|
||||||
|
indexerFlags: PropTypes.number,
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
|||||||
@@ -4,22 +4,18 @@ import { createSelector } from 'reselect';
|
|||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
interface IndexerFlagsSelectInputProps {
|
|
||||||
name: string;
|
|
||||||
indexerFlags: number;
|
|
||||||
onChange(payload: object): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
||||||
createSelector(
|
createSelector(
|
||||||
(state: AppState) => state.settings.indexerFlags,
|
(state: AppState) => state.settings.indexerFlags,
|
||||||
(indexerFlags) => {
|
(indexerFlags) => {
|
||||||
const value = indexerFlags.items
|
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
|
||||||
.filter(
|
// eslint-disable-next-line no-bitwise
|
||||||
// eslint-disable-next-line no-bitwise
|
if ((selectedFlags & id) === id) {
|
||||||
(item) => (selectedFlags & item.id) === item.id
|
acc.push(id);
|
||||||
)
|
}
|
||||||
.map(({ id }) => id);
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const values = indexerFlags.items.map(({ id, name }) => ({
|
const values = indexerFlags.items.map(({ id, name }) => ({
|
||||||
key: id,
|
key: id,
|
||||||
@@ -33,6 +29,12 @@ const selectIndexerFlagsValues = (selectedFlags: number) =>
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface IndexerFlagsSelectInputProps {
|
||||||
|
name: string;
|
||||||
|
indexerFlags: number;
|
||||||
|
onChange(payload: object): void;
|
||||||
|
}
|
||||||
|
|
||||||
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
||||||
const { indexerFlags, onChange } = props;
|
const { indexerFlags, onChange } = props;
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
import monitorOptions from 'Utilities/Movie/monitorOptions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SelectInput from './SelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function MovieMonitoredSelectInput(props) {
|
function MovieMonitoredSelectInput(props) {
|
||||||
const values = [...monitorOptions];
|
const values = [...monitorOptions];
|
||||||
@@ -16,7 +16,7 @@ function MovieMonitoredSelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,12 +24,12 @@ function MovieMonitoredSelectInput(props) {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectInput
|
<EnhancedSelectInput
|
||||||
{...props}
|
{...props}
|
||||||
values={values}
|
values={values}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function createMapStateToProps() {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
value: translate('NoChange'),
|
||||||
disabled: includeNoChangeDisabled
|
isDisabled: includeNoChangeDisabled
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ function createMapStateToProps() {
|
|||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
value: '(Mixed)',
|
||||||
disabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class TextTagInputConnector extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<TagInput
|
<TagInput
|
||||||
|
delimiters={['Tab', 'Enter', ',']}
|
||||||
tagList={[]}
|
tagList={[]}
|
||||||
onTagAdd={this.onTagAdd}
|
onTagAdd={this.onTagAdd}
|
||||||
onTagDelete={this.onTagDelete}
|
onTagDelete={this.onTagDelete}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const selectAppProps = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectIsPopulated = createSelector(
|
const selectIsPopulated = createSelector(
|
||||||
|
(state) => state.movies.isPopulated,
|
||||||
(state) => state.customFilters.isPopulated,
|
(state) => state.customFilters.isPopulated,
|
||||||
(state) => state.tags.isPopulated,
|
(state) => state.tags.isPopulated,
|
||||||
(state) => state.settings.ui.isPopulated,
|
(state) => state.settings.ui.isPopulated,
|
||||||
@@ -56,6 +57,7 @@ const selectIsPopulated = createSelector(
|
|||||||
(state) => state.movieCollections.isPopulated,
|
(state) => state.movieCollections.isPopulated,
|
||||||
(state) => state.app.translations.isPopulated,
|
(state) => state.app.translations.isPopulated,
|
||||||
(
|
(
|
||||||
|
moviesIsPopulated,
|
||||||
customFiltersIsPopulated,
|
customFiltersIsPopulated,
|
||||||
tagsIsPopulated,
|
tagsIsPopulated,
|
||||||
uiSettingsIsPopulated,
|
uiSettingsIsPopulated,
|
||||||
@@ -68,6 +70,7 @@ const selectIsPopulated = createSelector(
|
|||||||
translationsIsPopulated
|
translationsIsPopulated
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
|
moviesIsPopulated &&
|
||||||
customFiltersIsPopulated &&
|
customFiltersIsPopulated &&
|
||||||
tagsIsPopulated &&
|
tagsIsPopulated &&
|
||||||
uiSettingsIsPopulated &&
|
uiSettingsIsPopulated &&
|
||||||
@@ -83,6 +86,7 @@ const selectIsPopulated = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const selectErrors = createSelector(
|
const selectErrors = createSelector(
|
||||||
|
(state) => state.movies.error,
|
||||||
(state) => state.customFilters.error,
|
(state) => state.customFilters.error,
|
||||||
(state) => state.tags.error,
|
(state) => state.tags.error,
|
||||||
(state) => state.settings.ui.error,
|
(state) => state.settings.ui.error,
|
||||||
@@ -94,6 +98,7 @@ const selectErrors = createSelector(
|
|||||||
(state) => state.movieCollections.error,
|
(state) => state.movieCollections.error,
|
||||||
(state) => state.app.translations.error,
|
(state) => state.app.translations.error,
|
||||||
(
|
(
|
||||||
|
moviesError,
|
||||||
customFiltersError,
|
customFiltersError,
|
||||||
tagsError,
|
tagsError,
|
||||||
uiSettingsError,
|
uiSettingsError,
|
||||||
@@ -106,6 +111,7 @@ const selectErrors = createSelector(
|
|||||||
translationsError
|
translationsError
|
||||||
) => {
|
) => {
|
||||||
const hasError = !!(
|
const hasError = !!(
|
||||||
|
moviesError ||
|
||||||
customFiltersError ||
|
customFiltersError ||
|
||||||
tagsError ||
|
tagsError ||
|
||||||
uiSettingsError ||
|
uiSettingsError ||
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
"start_url": "../../../../",
|
"start_url": "../../../../",
|
||||||
"theme_color": "#3a3f51",
|
"theme_color": "#3a3f51",
|
||||||
"background_color": "#3a3f51",
|
"background_color": "#3a3f51",
|
||||||
"display": "minimal-ui"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
|||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import DiscoverMovieTableOptionsConnector from './DiscoverMovieTableOptionsConnector';
|
import DiscoverMovieTableOptionsConnector from './DiscoverMovieTableOptionsConnector';
|
||||||
import styles from './DiscoverMovieHeader.css';
|
import styles from './DiscoverMovieHeader.css';
|
||||||
|
|
||||||
@@ -98,6 +99,7 @@ class DiscoverMovieHeader extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
name={icons.RECOMMENDED}
|
name={icons.RECOMMENDED}
|
||||||
size={12}
|
size={12}
|
||||||
|
title={translate('Recommendation')}
|
||||||
/>
|
/>
|
||||||
</VirtualTableHeaderCell>
|
</VirtualTableHeaderCell>
|
||||||
);
|
);
|
||||||
@@ -115,6 +117,7 @@ class DiscoverMovieHeader extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
name={icons.TRENDING}
|
name={icons.TRENDING}
|
||||||
size={12}
|
size={12}
|
||||||
|
title={translate('Trending')}
|
||||||
/>
|
/>
|
||||||
</VirtualTableHeaderCell>
|
</VirtualTableHeaderCell>
|
||||||
);
|
);
|
||||||
@@ -132,6 +135,7 @@ class DiscoverMovieHeader extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
name={icons.POPULAR}
|
name={icons.POPULAR}
|
||||||
size={12}
|
size={12}
|
||||||
|
title={translate('Popular')}
|
||||||
/>
|
/>
|
||||||
</VirtualTableHeaderCell>
|
</VirtualTableHeaderCell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
export default function useModalOpenState(
|
||||||
|
initialState: boolean
|
||||||
|
): [boolean, () => void, () => void] {
|
||||||
|
const [isOpen, setOpen] = useState(initialState);
|
||||||
|
|
||||||
|
const setModalOpen = useCallback(() => {
|
||||||
|
setOpen(true);
|
||||||
|
}, [setOpen]);
|
||||||
|
|
||||||
|
const setModalClosed = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, [setOpen]);
|
||||||
|
|
||||||
|
return [isOpen, setModalOpen, setModalClosed];
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
|
||||||
|
|
||||||
|
interface SelectIndexerFlagsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
indexerFlags: number;
|
||||||
|
modalTitle: string;
|
||||||
|
onIndexerFlagsSelect(indexerFlags: number): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectIndexerFlagsModal(props: SelectIndexerFlagsModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
indexerFlags,
|
||||||
|
modalTitle,
|
||||||
|
onIndexerFlagsSelect,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<SelectIndexerFlagsModalContent
|
||||||
|
indexerFlags={indexerFlags}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onIndexerFlagsSelect={onIndexerFlagsSelect}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectIndexerFlagsModal;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.modalBody {
|
||||||
|
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'modalBody': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import 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, scrollDirections } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './SelectIndexerFlagsModalContent.css';
|
||||||
|
|
||||||
|
interface SelectIndexerFlagsModalContentProps {
|
||||||
|
indexerFlags: number;
|
||||||
|
modalTitle: string;
|
||||||
|
onIndexerFlagsSelect(indexerFlags: number): void;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectIndexerFlagsModalContent(
|
||||||
|
props: SelectIndexerFlagsModalContentProps
|
||||||
|
) {
|
||||||
|
const { modalTitle, onIndexerFlagsSelect, onModalClose } = props;
|
||||||
|
const [indexerFlags, setIndexerFlags] = useState(props.indexerFlags);
|
||||||
|
|
||||||
|
const onIndexerFlagsChange = useCallback(
|
||||||
|
({ value }: { value: number }) => {
|
||||||
|
setIndexerFlags(value);
|
||||||
|
},
|
||||||
|
[setIndexerFlags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onIndexerFlagsSelectWrapper = useCallback(() => {
|
||||||
|
onIndexerFlagsSelect(indexerFlags);
|
||||||
|
}, [indexerFlags, onIndexerFlagsSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('SetIndexerFlagsModalTitle', { modalTitle })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody
|
||||||
|
className={styles.modalBody}
|
||||||
|
scrollDirection={scrollDirections.NONE}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IndexerFlags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.INDEXER_FLAGS_SELECT}
|
||||||
|
name="indexerFlags"
|
||||||
|
indexerFlags={indexerFlags}
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={onIndexerFlagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.SUCCESS} onPress={onIndexerFlagsSelectWrapper}>
|
||||||
|
{translate('SetIndexerFlags')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SelectIndexerFlagsModalContent;
|
||||||
@@ -26,6 +26,7 @@ import usePrevious from 'Helpers/Hooks/usePrevious';
|
|||||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
|
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
|
||||||
import ImportMode from 'InteractiveImport/ImportMode';
|
import ImportMode from 'InteractiveImport/ImportMode';
|
||||||
|
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
|
||||||
import InteractiveImport, {
|
import InteractiveImport, {
|
||||||
InteractiveImportCommandOptions,
|
InteractiveImportCommandOptions,
|
||||||
} from 'InteractiveImport/InteractiveImport';
|
} from 'InteractiveImport/InteractiveImport';
|
||||||
@@ -59,7 +60,13 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|||||||
import InteractiveImportRow from './InteractiveImportRow';
|
import InteractiveImportRow from './InteractiveImportRow';
|
||||||
import styles from './InteractiveImportModalContent.css';
|
import styles from './InteractiveImportModalContent.css';
|
||||||
|
|
||||||
type SelectType = 'select' | 'movie' | 'releaseGroup' | 'quality' | 'language';
|
type SelectType =
|
||||||
|
| 'select'
|
||||||
|
| 'movie'
|
||||||
|
| 'releaseGroup'
|
||||||
|
| 'quality'
|
||||||
|
| 'language'
|
||||||
|
| 'indexerFlags';
|
||||||
|
|
||||||
type FilterExistingFiles = 'all' | 'new';
|
type FilterExistingFiles = 'all' | 'new';
|
||||||
|
|
||||||
@@ -113,6 +120,15 @@ const COLUMNS = [
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'indexerFlags',
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.FLAG,
|
||||||
|
title: () => translate('IndexerFlags'),
|
||||||
|
}),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'rejections',
|
name: 'rejections',
|
||||||
label: React.createElement(Icon, {
|
label: React.createElement(Icon, {
|
||||||
@@ -257,8 +273,18 @@ function InteractiveImportModalContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showIndexerFlags = items.some((item) => item.indexerFlags);
|
||||||
|
|
||||||
|
if (!showIndexerFlags) {
|
||||||
|
const indexerFlagsColumn = result.find((c) => c.name === 'indexerFlags');
|
||||||
|
|
||||||
|
if (indexerFlagsColumn) {
|
||||||
|
indexerFlagsColumn.isVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [showMovie]);
|
}, [showMovie, items]);
|
||||||
|
|
||||||
const selectedIds: number[] = useMemo(() => {
|
const selectedIds: number[] = useMemo(() => {
|
||||||
return getSelectedIds(selectedState);
|
return getSelectedIds(selectedState);
|
||||||
@@ -283,6 +309,10 @@ function InteractiveImportModalContent(
|
|||||||
key: 'language',
|
key: 'language',
|
||||||
value: translate('SelectLanguage'),
|
value: translate('SelectLanguage'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'indexerFlags',
|
||||||
|
value: translate('SelectIndexerFlags'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (allowMovieChange) {
|
if (allowMovieChange) {
|
||||||
@@ -416,7 +446,14 @@ function InteractiveImportModalContent(
|
|||||||
const isSelected = selectedIds.indexOf(item.id) > -1;
|
const isSelected = selectedIds.indexOf(item.id) > -1;
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
const { movie, releaseGroup, quality, languages, movieFileId } = item;
|
const {
|
||||||
|
movie,
|
||||||
|
releaseGroup,
|
||||||
|
quality,
|
||||||
|
languages,
|
||||||
|
indexerFlags,
|
||||||
|
movieFileId,
|
||||||
|
} = item;
|
||||||
|
|
||||||
if (!movie) {
|
if (!movie) {
|
||||||
setInteractiveImportErrorMessage(
|
setInteractiveImportErrorMessage(
|
||||||
@@ -450,6 +487,7 @@ function InteractiveImportModalContent(
|
|||||||
releaseGroup,
|
releaseGroup,
|
||||||
quality,
|
quality,
|
||||||
languages,
|
languages,
|
||||||
|
indexerFlags,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -463,6 +501,7 @@ function InteractiveImportModalContent(
|
|||||||
releaseGroup,
|
releaseGroup,
|
||||||
quality,
|
quality,
|
||||||
languages,
|
languages,
|
||||||
|
indexerFlags,
|
||||||
downloadId,
|
downloadId,
|
||||||
movieFileId,
|
movieFileId,
|
||||||
});
|
});
|
||||||
@@ -620,6 +659,22 @@ function InteractiveImportModalContent(
|
|||||||
[selectedIds, dispatch]
|
[selectedIds, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onIndexerFlagsSelect = useCallback(
|
||||||
|
(indexerFlags: number) => {
|
||||||
|
dispatch(
|
||||||
|
updateInteractiveImportItems({
|
||||||
|
ids: selectedIds,
|
||||||
|
indexerFlags,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||||
|
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[selectedIds, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(
|
const errorMessage = getErrorMessage(
|
||||||
error,
|
error,
|
||||||
translate('InteractiveImportLoadError')
|
translate('InteractiveImportLoadError')
|
||||||
@@ -794,6 +849,14 @@ function InteractiveImportModalContent(
|
|||||||
onModalClose={onSelectModalClose}
|
onModalClose={onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectIndexerFlagsModal
|
||||||
|
isOpen={selectModalOpen === 'indexerFlags'}
|
||||||
|
indexerFlags={0}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onIndexerFlagsSelect={onIndexerFlagsSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isConfirmDeleteModalOpen}
|
isOpen={isConfirmDeleteModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import Column from 'Components/Table/Column';
|
|||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
|
||||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||||
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
|
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
|
||||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
|
import IndexerFlags from 'Movie/IndexerFlags';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
@@ -30,7 +32,12 @@ import translate from 'Utilities/String/translate';
|
|||||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||||
import styles from './InteractiveImportRow.css';
|
import styles from './InteractiveImportRow.css';
|
||||||
|
|
||||||
type SelectType = 'movie' | 'releaseGroup' | 'quality' | 'language';
|
type SelectType =
|
||||||
|
| 'movie'
|
||||||
|
| 'releaseGroup'
|
||||||
|
| 'quality'
|
||||||
|
| 'language'
|
||||||
|
| 'indexerFlags';
|
||||||
|
|
||||||
type SelectedChangeProps = SelectStateInputProps & {
|
type SelectedChangeProps = SelectStateInputProps & {
|
||||||
hasMovieFileId: boolean;
|
hasMovieFileId: boolean;
|
||||||
@@ -47,6 +54,7 @@ interface InteractiveImportRowProps {
|
|||||||
size: number;
|
size: number;
|
||||||
customFormats?: object[];
|
customFormats?: object[];
|
||||||
customFormatScore?: number;
|
customFormatScore?: number;
|
||||||
|
indexerFlags: number;
|
||||||
rejections: Rejection[];
|
rejections: Rejection[];
|
||||||
columns: Column[];
|
columns: Column[];
|
||||||
movieFileId?: number;
|
movieFileId?: number;
|
||||||
@@ -69,6 +77,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
size,
|
size,
|
||||||
customFormats,
|
customFormats,
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
|
indexerFlags,
|
||||||
rejections,
|
rejections,
|
||||||
isSelected,
|
isSelected,
|
||||||
modalTitle,
|
modalTitle,
|
||||||
@@ -84,6 +93,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
() => columns.find((c) => c.name === 'movie')?.isVisible ?? false,
|
() => columns.find((c) => c.name === 'movie')?.isVisible ?? false,
|
||||||
[columns]
|
[columns]
|
||||||
);
|
);
|
||||||
|
const isIndexerFlagsColumnVisible = useMemo(
|
||||||
|
() => columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false,
|
||||||
|
[columns]
|
||||||
|
);
|
||||||
|
|
||||||
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||||
null
|
null
|
||||||
@@ -223,12 +236,34 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSelectIndexerFlagsPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('indexerFlags');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onIndexerFlagsSelect = useCallback(
|
||||||
|
(indexerFlags: number) => {
|
||||||
|
dispatch(
|
||||||
|
updateInteractiveImportItem({
|
||||||
|
id,
|
||||||
|
indexerFlags,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
|
||||||
|
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
selectRowAfterChange();
|
||||||
|
},
|
||||||
|
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||||
|
);
|
||||||
|
|
||||||
const movieTitle = movie ? movie.title : '';
|
const movieTitle = movie ? movie.title : '';
|
||||||
|
|
||||||
const showMoviePlaceholder = isSelected && !movie;
|
const showMoviePlaceholder = isSelected && !movie;
|
||||||
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
|
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
|
||||||
const showQualityPlaceholder = isSelected && !quality;
|
const showQualityPlaceholder = isSelected && !quality;
|
||||||
const showLanguagePlaceholder = isSelected && !languages;
|
const showLanguagePlaceholder = isSelected && !languages;
|
||||||
|
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -311,6 +346,28 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
{isIndexerFlagsColumnVisible ? (
|
||||||
|
<TableRowCellButton
|
||||||
|
title={translate('ClickToChangeIndexerFlags')}
|
||||||
|
onPress={onSelectIndexerFlagsPress}
|
||||||
|
>
|
||||||
|
{showIndexerFlagsPlaceholder ? (
|
||||||
|
<InteractiveImportRowCellPlaceholder isOptional={true} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{indexerFlags ? (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||||
|
title={translate('IndexerFlags')}
|
||||||
|
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TableRowCellButton>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
{rejections.length ? (
|
{rejections.length ? (
|
||||||
<Popover
|
<Popover
|
||||||
@@ -361,6 +418,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
onLanguagesSelect={onLanguagesSelect}
|
onLanguagesSelect={onLanguagesSelect}
|
||||||
onModalClose={onSelectModalClose}
|
onModalClose={onSelectModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<SelectIndexerFlagsModal
|
||||||
|
isOpen={selectModalOpen === 'indexerFlags'}
|
||||||
|
indexerFlags={indexerFlags ?? 0}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onIndexerFlagsSelect={onIndexerFlagsSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface InteractiveImportCommandOptions {
|
|||||||
releaseGroup?: string;
|
releaseGroup?: string;
|
||||||
quality: QualityModel;
|
quality: QualityModel;
|
||||||
languages: Language[];
|
languages: Language[];
|
||||||
|
indexerFlags: number;
|
||||||
downloadId?: string;
|
downloadId?: string;
|
||||||
movieFileId?: number;
|
movieFileId?: number;
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,7 @@ interface InteractiveImport extends ModelBase {
|
|||||||
movie?: Movie;
|
movie?: Movie;
|
||||||
qualityWeight: number;
|
qualityWeight: number;
|
||||||
customFormats: object[];
|
customFormats: object[];
|
||||||
|
indexerFlags: number;
|
||||||
rejections: Rejection[];
|
rejections: Rejection[];
|
||||||
movieFileId?: number;
|
movieFileId?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rejected,
|
.rejected,
|
||||||
.indexerFlags,
|
.indexerFlags {
|
||||||
.download {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ interface InteractiveSearchRowProps {
|
|||||||
customFormats: CustomFormat[];
|
customFormats: CustomFormat[];
|
||||||
customFormatScore: number;
|
customFormatScore: number;
|
||||||
mappedMovieId?: number;
|
mappedMovieId?: number;
|
||||||
rejections: string[];
|
|
||||||
indexerFlags: string[];
|
indexerFlags: string[];
|
||||||
|
rejections: string[];
|
||||||
downloadAllowed: boolean;
|
downloadAllowed: boolean;
|
||||||
isGrabbing: boolean;
|
isGrabbing: boolean;
|
||||||
isGrabbed: boolean;
|
isGrabbed: boolean;
|
||||||
@@ -125,8 +125,8 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||||||
customFormatScore,
|
customFormatScore,
|
||||||
customFormats,
|
customFormats,
|
||||||
mappedMovieId,
|
mappedMovieId,
|
||||||
rejections = [],
|
|
||||||
indexerFlags = [],
|
indexerFlags = [],
|
||||||
|
rejections = [],
|
||||||
downloadAllowed,
|
downloadAllowed,
|
||||||
isGrabbing = false,
|
isGrabbing = false,
|
||||||
isGrabbed = false,
|
isGrabbed = false,
|
||||||
@@ -276,7 +276,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||||||
customFormats.length
|
customFormats.length
|
||||||
)}
|
)}
|
||||||
tooltip={<MovieFormats formats={customFormats} />}
|
tooltip={<MovieFormats formats={customFormats} />}
|
||||||
position={tooltipPositions.TOP}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class DeleteMovieModalContent extends Component {
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="addImportExclusion"
|
name="addImportExclusion"
|
||||||
value={addImportExclusion}
|
value={addImportExclusion}
|
||||||
helpText={translate('AddImportExclusionHelpText')}
|
helpText={translate('AddListExclusionMovieHelpText')}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onChange={onDeleteOptionChange}
|
onChange={onDeleteOptionChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
@@ -142,10 +143,10 @@ class MovieCastPoster extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.title}>
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
{personName}
|
{personName}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.title}>
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
{character}
|
{character}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
@@ -142,10 +143,10 @@ class MovieCrewPoster extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.title}>
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
{personName}
|
{personName}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.title}>
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
{job}
|
{job}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
.links {
|
.links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import MovieIndexViewMenu from './Menus/MovieIndexViewMenu';
|
|||||||
import MovieIndexFooter from './MovieIndexFooter';
|
import MovieIndexFooter from './MovieIndexFooter';
|
||||||
import MovieIndexRefreshMovieButton from './MovieIndexRefreshMovieButton';
|
import MovieIndexRefreshMovieButton from './MovieIndexRefreshMovieButton';
|
||||||
import MovieIndexSearchButton from './MovieIndexSearchButton';
|
import MovieIndexSearchButton from './MovieIndexSearchButton';
|
||||||
|
import MovieIndexSearchMenuItem from './MovieIndexSearchMenuItem';
|
||||||
import MovieIndexOverviews from './Overview/MovieIndexOverviews';
|
import MovieIndexOverviews from './Overview/MovieIndexOverviews';
|
||||||
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
|
import MovieIndexOverviewOptionsModal from './Overview/Options/MovieIndexOverviewOptionsModal';
|
||||||
import MovieIndexPosters from './Posters/MovieIndexPosters';
|
import MovieIndexPosters from './Posters/MovieIndexPosters';
|
||||||
@@ -247,6 +248,7 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||||||
<MovieIndexSearchButton
|
<MovieIndexSearchButton
|
||||||
isSelectMode={isSelectMode}
|
isSelectMode={isSelectMode}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
overflowComponent={MovieIndexSearchMenuItem}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|||||||
interface MovieIndexSearchButtonProps {
|
interface MovieIndexSearchButtonProps {
|
||||||
isSelectMode: boolean;
|
isSelectMode: boolean;
|
||||||
selectedFilterKey: string;
|
selectedFilterKey: string;
|
||||||
|
overflowComponent: React.FunctionComponent<never>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useSelect } from 'App/SelectContext';
|
||||||
|
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||||
|
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
|
||||||
|
import { MOVIE_SEARCH } from 'Commands/commandNames';
|
||||||
|
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
|
||||||
|
interface MovieIndexSearchMenuItemProps {
|
||||||
|
isSelectMode: boolean;
|
||||||
|
selectedFilterKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieIndexSearchMenuItem(props: MovieIndexSearchMenuItemProps) {
|
||||||
|
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
|
||||||
|
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { isSelectMode, selectedFilterKey } = props;
|
||||||
|
const [selectState] = useSelect();
|
||||||
|
const { selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedMovieIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const moviesToSearch =
|
||||||
|
isSelectMode && selectedMovieIds.length > 0
|
||||||
|
? selectedMovieIds
|
||||||
|
: items.map((m) => m.id);
|
||||||
|
|
||||||
|
const searchIndexLabel =
|
||||||
|
selectedFilterKey === 'all'
|
||||||
|
? translate('SearchAll')
|
||||||
|
: translate('SearchFiltered');
|
||||||
|
|
||||||
|
const searchSelectLabel =
|
||||||
|
selectedMovieIds.length > 0
|
||||||
|
? translate('SearchSelected')
|
||||||
|
: translate('SearchAll');
|
||||||
|
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: MOVIE_SEARCH,
|
||||||
|
movieIds: moviesToSearch,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch, moviesToSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageToolbarOverflowMenuItem
|
||||||
|
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||||
|
isSpinning={isSearching}
|
||||||
|
isDisabled={!items.length}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieIndexSearchMenuItem;
|
||||||
@@ -98,7 +98,7 @@ function DeleteMovieModalContent(props: DeleteMovieModalContentProps) {
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="addImportExclusion"
|
name="addImportExclusion"
|
||||||
value={addImportExclusion}
|
value={addImportExclusion}
|
||||||
helpText={translate('AddImportExclusionHelpText')}
|
helpText={translate('AddListExclusionMovieHelpText')}
|
||||||
onChange={onDeleteOptionChange}
|
onChange={onDeleteOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const monitoredOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'monitored',
|
key: 'monitored',
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
|
||||||
|
|
||||||
|
interface IndexerFlagsProps {
|
||||||
|
indexerFlags: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
|
||||||
|
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
|
||||||
|
|
||||||
|
const flags = allIndexerFlags.items.filter(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(item) => (indexerFlags & item.id) === item.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return flags.length ? (
|
||||||
|
<ul>
|
||||||
|
{flags.map((flag, index) => {
|
||||||
|
return <li key={index}>{flag.name}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexerFlags;
|
||||||
@@ -57,3 +57,9 @@
|
|||||||
|
|
||||||
width: 55px;
|
width: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.indexerFlags {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface CssExports {
|
|||||||
'dateAdded': string;
|
'dateAdded': string;
|
||||||
'download': string;
|
'download': string;
|
||||||
'formats': string;
|
'formats': string;
|
||||||
|
'indexerFlags': string;
|
||||||
'language': string;
|
'language': string;
|
||||||
'languages': string;
|
'languages': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import IndexerFlags from 'Movie/IndexerFlags';
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
@@ -82,6 +85,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
qualityCutoffNotMet,
|
qualityCutoffNotMet,
|
||||||
customFormats,
|
customFormats,
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
|
indexerFlags,
|
||||||
languages,
|
languages,
|
||||||
dateAdded,
|
dateAdded,
|
||||||
columns
|
columns
|
||||||
@@ -143,12 +147,30 @@ class MovieFileEditorRow extends Component {
|
|||||||
customFormats.length
|
customFormats.length
|
||||||
)}
|
)}
|
||||||
tooltip={<MovieFormats formats={customFormats} />}
|
tooltip={<MovieFormats formats={customFormats} />}
|
||||||
position={tooltipPositions.TOP}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'indexerFlags') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.indexerFlags}
|
||||||
|
>
|
||||||
|
{indexerFlags ? (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||||
|
title={translate('IndexerFlags')}
|
||||||
|
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'languages') {
|
if (name === 'languages') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
@@ -363,6 +385,7 @@ MovieFileEditorRow.propTypes = {
|
|||||||
releaseGroup: PropTypes.string,
|
releaseGroup: PropTypes.string,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
|
indexerFlags: PropTypes.number.isRequired,
|
||||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
mediaInfo: PropTypes.object,
|
mediaInfo: PropTypes.object,
|
||||||
@@ -372,7 +395,8 @@ MovieFileEditorRow.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
MovieFileEditorRow.defaultProps = {
|
MovieFileEditorRow.defaultProps = {
|
||||||
customFormats: []
|
customFormats: [],
|
||||||
|
indexerFlags: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MovieFileEditorRow;
|
export default MovieFileEditorRow;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MovieFileEditorRow from './MovieFileEditorRow';
|
import MovieFileEditorRow from './MovieFileEditorRow';
|
||||||
import styles from './MovieFileEditorTableContent.css';
|
import styles from './MovieFileEditorTableContent.css';
|
||||||
@@ -15,6 +16,9 @@ class MovieFileEditorTableContent extends Component {
|
|||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
onSortPress,
|
||||||
onTableOptionChange
|
onTableOptionChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -31,6 +35,9 @@ class MovieFileEditorTableContent extends Component {
|
|||||||
!!items.length &&
|
!!items.length &&
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
onTableOptionChange={onTableOptionChange}
|
onTableOptionChange={onTableOptionChange}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -60,7 +67,10 @@ MovieFileEditorTableContent.propTypes = {
|
|||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string.isRequired,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
onTableOptionChange: PropTypes.func.isRequired,
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
|
onSortPress: PropTypes.func.isRequired,
|
||||||
onDeletePress: PropTypes.func.isRequired
|
onDeletePress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { deleteMovieFile, setMovieFilesTableOption, updateMovieFiles } from 'Store/Actions/movieFileActions';
|
import { deleteMovieFile, setMovieFilesSort, setMovieFilesTableOption } from 'Store/Actions/movieFileActions';
|
||||||
import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
import { fetchLanguages, fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
import getQualities from 'Utilities/Quality/getQualities';
|
import getQualities from 'Utilities/Quality/getQualities';
|
||||||
import MovieFileEditorTableContent from './MovieFileEditorTableContent';
|
import MovieFileEditorTableContent from './MovieFileEditorTableContent';
|
||||||
@@ -11,7 +12,7 @@ import MovieFileEditorTableContent from './MovieFileEditorTableContent';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { movieId }) => movieId,
|
(state, { movieId }) => movieId,
|
||||||
(state) => state.movieFiles,
|
createClientSideCollectionSelector('movieFiles'),
|
||||||
(state) => state.settings.languages,
|
(state) => state.settings.languages,
|
||||||
(state) => state.settings.qualityProfiles,
|
(state) => state.settings.qualityProfiles,
|
||||||
createMovieSelector(),
|
createMovieSelector(),
|
||||||
@@ -28,6 +29,8 @@ function createMapStateToProps() {
|
|||||||
return {
|
return {
|
||||||
items: filesForMovie,
|
items: filesForMovie,
|
||||||
columns: movieFiles.columns,
|
columns: movieFiles.columns,
|
||||||
|
sortKey: movieFiles.sortKey,
|
||||||
|
sortDirection: movieFiles.sortDirection,
|
||||||
isDeleting: movieFiles.isDeleting,
|
isDeleting: movieFiles.isDeleting,
|
||||||
isSaving: movieFiles.isSaving,
|
isSaving: movieFiles.isSaving,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -38,31 +41,13 @@ function createMapStateToProps() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
const mapDispatchToProps = {
|
||||||
return {
|
fetchQualityProfileSchema,
|
||||||
dispatchFetchQualityProfileSchema() {
|
fetchLanguages,
|
||||||
dispatch(fetchQualityProfileSchema());
|
deleteMovieFile,
|
||||||
},
|
setMovieFilesTableOption,
|
||||||
|
setMovieFilesSort
|
||||||
dispatchFetchLanguages() {
|
};
|
||||||
dispatch(fetchLanguages());
|
|
||||||
},
|
|
||||||
|
|
||||||
dispatchUpdateMovieFiles(updateProps) {
|
|
||||||
dispatch(updateMovieFiles(updateProps));
|
|
||||||
},
|
|
||||||
|
|
||||||
onTableOptionChange(payload) {
|
|
||||||
dispatch(setMovieFilesTableOption(payload));
|
|
||||||
},
|
|
||||||
|
|
||||||
onDeletePress(movieFileId) {
|
|
||||||
dispatch(deleteMovieFile({
|
|
||||||
id: movieFileId
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieFileEditorTableContentConnector extends Component {
|
class MovieFileEditorTableContentConnector extends Component {
|
||||||
|
|
||||||
@@ -70,24 +55,40 @@ class MovieFileEditorTableContentConnector extends Component {
|
|||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.dispatchFetchLanguages();
|
this.props.fetchLanguages();
|
||||||
this.props.dispatchFetchQualityProfileSchema();
|
this.props.fetchQualityProfileSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onDeletePress = (movieFileId) => {
|
||||||
|
this.props.deleteMovieFile({
|
||||||
|
id: movieFileId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setMovieFilesTableOption(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
onSortPress = (sortKey, sortDirection) => {
|
||||||
|
this.props.setMovieFilesSort({
|
||||||
|
sortKey,
|
||||||
|
sortDirection
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
|
||||||
dispatchFetchLanguages,
|
|
||||||
dispatchFetchQualityProfileSchema,
|
|
||||||
dispatchUpdateMovieFiles,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MovieFileEditorTableContent
|
<MovieFileEditorTableContent
|
||||||
{...otherProps}
|
{...this.props}
|
||||||
|
onDeletePress={this.onDeletePress}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -97,9 +98,11 @@ MovieFileEditorTableContentConnector.propTypes = {
|
|||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
fetchLanguages: PropTypes.func.isRequired,
|
||||||
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
|
fetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||||
dispatchUpdateMovieFiles: PropTypes.func.isRequired
|
deleteMovieFile: PropTypes.func.isRequired,
|
||||||
|
setMovieFilesTableOption: PropTypes.func.isRequired,
|
||||||
|
setMovieFilesSort: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieFileEditorTableContentConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(MovieFileEditorTableContentConnector);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface MovieFile extends ModelBase {
|
|||||||
languages: Language[];
|
languages: Language[];
|
||||||
quality: QualityModel;
|
quality: QualityModel;
|
||||||
customFormats: CustomFormat[];
|
customFormats: CustomFormat[];
|
||||||
|
indexerFlags: number;
|
||||||
mediaInfo: MediaInfo;
|
mediaInfo: MediaInfo;
|
||||||
qualityCutoffNotMet: boolean;
|
qualityCutoffNotMet: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class OrganizePreviewModalContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && error &&
|
!isFetching && error &&
|
||||||
<div>{translate('OrganizeLoadError')}</div>
|
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class CustomFormat extends Component {
|
|||||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteCustomFormat')}
|
title={translate('DeleteCustomFormat')}
|
||||||
message={translate('DeleteCustomFormatMessageText', { customFormatName: name })}
|
message={translate('DeleteCustomFormatMessageText', { name })}
|
||||||
confirmLabel={translate('Delete')}
|
confirmLabel={translate('Delete')}
|
||||||
isSpinning={isDeleting}
|
isSpinning={isDeleting}
|
||||||
onConfirm={this.onConfirmDeleteCustomFormat}
|
onConfirm={this.onConfirmDeleteCustomFormat}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class CustomFormats extends Component {
|
|||||||
<FieldSet legend={translate('CustomFormats')}>
|
<FieldSet legend={translate('CustomFormats')}>
|
||||||
<PageSectionContent
|
<PageSectionContent
|
||||||
errorMessage={translate('CustomFormatsLoadError')}
|
errorMessage={translate('CustomFormatsLoadError')}
|
||||||
{...otherProps}c={true}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div className={styles.customFormats}>
|
<div className={styles.customFormats}>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -151,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
|
|||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<FieldSet legend={translate('Conditions')}>
|
<FieldSet legend={translate('Conditions')}>
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
<div>
|
||||||
|
{translate('CustomFormatsSettingsTriggerInfo')}
|
||||||
|
</div>
|
||||||
|
</Alert>
|
||||||
<div className={styles.customFormats}>
|
<div className={styles.customFormats}>
|
||||||
{
|
{
|
||||||
specifications.map((tag) => {
|
specifications.map((tag) => {
|
||||||
|
|||||||
+2
-2
@@ -42,9 +42,9 @@ class AddSpecificationModalContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!isSchemaFetching && !!schemaError &&
|
!isSchemaFetching && !!schemaError &&
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('AddConditionError')}
|
{translate('AddConditionError')}
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class DownloadClient extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onDeleteDownloadClientModalClose= () => {
|
onDeleteDownloadClientModalClose = () => {
|
||||||
this.setState({ isDeleteDownloadClientModalOpen: false });
|
this.setState({ isDeleteDownloadClientModalOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+9
@@ -15,6 +15,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditDownloadClientModalContent.css';
|
import styles from './EditDownloadClientModalContent.css';
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
onModalClose,
|
onModalClose,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
|
onAdvancedSettingsPress,
|
||||||
onDeleteDownloadClientPress,
|
onDeleteDownloadClientPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -199,6 +201,12 @@ class EditDownloadClientModalContent extends Component {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
isSpinning={isTesting}
|
isSpinning={isTesting}
|
||||||
error={saveError}
|
error={saveError}
|
||||||
@@ -239,6 +247,7 @@ EditDownloadClientModalContent.propTypes = {
|
|||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||||
onDeleteDownloadClientPress: PropTypes.func
|
onDeleteDownloadClientPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
saveDownloadClient,
|
||||||
|
setDownloadClientFieldValue,
|
||||||
|
setDownloadClientValue,
|
||||||
|
testDownloadClient,
|
||||||
|
toggleAdvancedSettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||||
|
|
||||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||||||
setDownloadClientValue,
|
setDownloadClientValue,
|
||||||
setDownloadClientFieldValue,
|
setDownloadClientFieldValue,
|
||||||
saveDownloadClient,
|
saveDownloadClient,
|
||||||
testDownloadClient
|
testDownloadClient,
|
||||||
|
toggleAdvancedSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditDownloadClientModalContentConnector extends Component {
|
class EditDownloadClientModalContentConnector extends Component {
|
||||||
@@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
this.props.testDownloadClient({ id: this.props.id });
|
this.props.testDownloadClient({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAdvancedSettingsPress = () => {
|
||||||
|
this.props.toggleAdvancedSettings();
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||||||
{...this.props}
|
{...this.props}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
|
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
/>
|
/>
|
||||||
@@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
|
|||||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||||
saveDownloadClient: PropTypes.func.isRequired,
|
saveDownloadClient: PropTypes.func.isRequired,
|
||||||
testDownloadClient: PropTypes.func.isRequired,
|
testDownloadClient: PropTypes.func.isRequired,
|
||||||
|
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ const enableOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
|
|||||||
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@@ -55,7 +54,7 @@ function createRemotePathMappingSelector() {
|
|||||||
items
|
items
|
||||||
} = remotePathMappings;
|
} = remotePathMappings;
|
||||||
|
|
||||||
const mapping = id ? _.find(items, { id }) : newRemotePathMapping;
|
const mapping = id ? items.find((i) => i.id === id) : newRemotePathMapping;
|
||||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+4
-5
@@ -1,4 +1,3 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@@ -7,7 +6,7 @@ import { saveImportExclusion, setImportExclusionValue } from 'Store/Actions/sett
|
|||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import EditImportListExclusionModalContent from './EditImportListExclusionModalContent';
|
import EditImportListExclusionModalContent from './EditImportListExclusionModalContent';
|
||||||
|
|
||||||
const newImportExclusion = {
|
const newImportListExclusion = {
|
||||||
movieTitle: '',
|
movieTitle: '',
|
||||||
tmdbId: 0,
|
tmdbId: 0,
|
||||||
movieYear: 0
|
movieYear: 0
|
||||||
@@ -27,7 +26,7 @@ function createImportExclusionSelector() {
|
|||||||
items
|
items
|
||||||
} = importExclusions;
|
} = importExclusions;
|
||||||
|
|
||||||
const mapping = id ? _.find(items, { id }) : newImportExclusion;
|
const mapping = id ? items.find((i) => i.id === id) : newImportListExclusion;
|
||||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -66,10 +65,10 @@ class EditImportExclusionModalContentConnector extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!this.props.id) {
|
if (!this.props.id) {
|
||||||
Object.keys(newImportExclusion).forEach((name) => {
|
Object.keys(newImportListExclusion).forEach((name) => {
|
||||||
this.props.setImportExclusionValue({
|
this.props.setImportExclusionValue({
|
||||||
name,
|
name,
|
||||||
value: newImportExclusion[name]
|
value: newImportListExclusion[name]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
.movieTitle {
|
.movieTitle {
|
||||||
@add-mixin truncate;
|
@add-mixin truncate;
|
||||||
|
|
||||||
flex: 0 0 600px;
|
flex: 0 1 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tmdbId,
|
.tmdbId,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class ImportListExclusion extends Component {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={styles.tmdbId}>{tmdbId}</div>
|
<div className={styles.tmdbId}>{tmdbId}</div>
|
||||||
<div className={styles.movieTitle}>{movieTitle}</div>
|
<div className={styles.movieTitle} title={movieTitle}>{movieTitle}</div>
|
||||||
<div className={styles.movieYear}>{movieYear}</div>
|
<div className={styles.movieYear}>{movieYear}</div>
|
||||||
|
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
flex: 0 0 600px;
|
flex: 0 1 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tmdbId,
|
.tmdbId,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditImportListModalContent.css';
|
import styles from './EditImportListModalContent.css';
|
||||||
@@ -33,6 +34,7 @@ function EditImportListModalContent(props) {
|
|||||||
onModalClose,
|
onModalClose,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
|
onAdvancedSettingsPress,
|
||||||
onDeleteImportListPress,
|
onDeleteImportListPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
@@ -234,6 +236,12 @@ function EditImportListModalContent(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
isSpinning={isTesting}
|
isSpinning={isTesting}
|
||||||
error={saveError}
|
error={saveError}
|
||||||
@@ -274,6 +282,7 @@ EditImportListModalContent.propTypes = {
|
|||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||||
onDeleteImportListPress: PropTypes.func
|
onDeleteImportListPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
saveImportList,
|
||||||
|
setImportListFieldValue,
|
||||||
|
setImportListValue,
|
||||||
|
testImportList,
|
||||||
|
toggleAdvancedSettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditImportListModalContent from './EditImportListModalContent';
|
import EditImportListModalContent from './EditImportListModalContent';
|
||||||
|
|
||||||
@@ -33,7 +39,8 @@ const mapDispatchToProps = {
|
|||||||
setImportListValue,
|
setImportListValue,
|
||||||
setImportListFieldValue,
|
setImportListFieldValue,
|
||||||
saveImportList,
|
saveImportList,
|
||||||
testImportList
|
testImportList,
|
||||||
|
toggleAdvancedSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditImportListModalContentConnector extends Component {
|
class EditImportListModalContentConnector extends Component {
|
||||||
@@ -66,6 +73,10 @@ class EditImportListModalContentConnector extends Component {
|
|||||||
this.props.testImportList({ id: this.props.id });
|
this.props.testImportList({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAdvancedSettingsPress = () => {
|
||||||
|
this.props.toggleAdvancedSettings();
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -75,6 +86,7 @@ class EditImportListModalContentConnector extends Component {
|
|||||||
{...this.props}
|
{...this.props}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
|
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
/>
|
/>
|
||||||
@@ -92,6 +104,7 @@ EditImportListModalContentConnector.propTypes = {
|
|||||||
setImportListFieldValue: PropTypes.func.isRequired,
|
setImportListFieldValue: PropTypes.func.isRequired,
|
||||||
saveImportList: PropTypes.func.isRequired,
|
saveImportList: PropTypes.func.isRequired,
|
||||||
testImportList: PropTypes.func.isRequired,
|
testImportList: PropTypes.func.isRequired,
|
||||||
|
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class ImportList extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onDeleteImportListModalClose= () => {
|
onDeleteImportListModalClose = () => {
|
||||||
this.setState({ isDeleteImportListModalOpen: false });
|
this.setState({ isDeleteImportListModalOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -33,7 +33,7 @@ const enableOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ function ImportListOptions(props) {
|
|||||||
{
|
{
|
||||||
!isFetching && error &&
|
!isFetching && error &&
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('UnableToLoadListOptions')}
|
{translate('ListOptionsLoadError')}
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
@@ -66,9 +67,9 @@ function EditIndexerModalContent(props) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error &&
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('AddIndexerError')}
|
{translate('AddIndexerError')}
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class Indexer extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onDeleteIndexerModalClose= () => {
|
onDeleteIndexerModalClose = () => {
|
||||||
this.setState({ isDeleteIndexerModalOpen: false });
|
this.setState({ isDeleteIndexerModalOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ const enableOptions = [
|
|||||||
get value() {
|
get value() {
|
||||||
return translate('NoChange');
|
return translate('NoChange');
|
||||||
},
|
},
|
||||||
disabled: true,
|
isDisabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
width: 480px;
|
width: 490px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.large {
|
.large {
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
|
|
||||||
.token {
|
.token {
|
||||||
flex: 0 0 50%;
|
flex: 0 0 50%;
|
||||||
padding: 6px 16px;
|
padding: 6px;
|
||||||
background-color: var(--popoverTitleBorderColor);
|
background-color: var(--popoverTitleBackgroundColor);
|
||||||
font-family: $monoSpaceFontFamily;
|
font-family: $monoSpaceFontFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex: 0 0 50%;
|
flex: 0 0 50%;
|
||||||
padding: 6px 16px;
|
padding: 6px;
|
||||||
background-color: var(--popoverTitleBackgroundColor);
|
background-color: var(--popoverBodyBackgroundColor);
|
||||||
|
|
||||||
.footNote {
|
.footNote {
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import NotificationEventItems from './NotificationEventItems';
|
import NotificationEventItems from './NotificationEventItems';
|
||||||
import styles from './EditNotificationModalContent.css';
|
import styles from './EditNotificationModalContent.css';
|
||||||
@@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
|
|||||||
onModalClose,
|
onModalClose,
|
||||||
onSavePress,
|
onSavePress,
|
||||||
onTestPress,
|
onTestPress,
|
||||||
|
onAdvancedSettingsPress,
|
||||||
onDeleteNotificationPress,
|
onDeleteNotificationPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
@@ -136,6 +138,12 @@ function EditNotificationModalContent(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<AdvancedSettingsButton
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||||
|
showLabel={false}
|
||||||
|
/>
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<SpinnerErrorButton
|
||||||
isSpinning={isTesting}
|
isSpinning={isTesting}
|
||||||
error={saveError}
|
error={saveError}
|
||||||
@@ -175,6 +183,7 @@ EditNotificationModalContent.propTypes = {
|
|||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
onTestPress: PropTypes.func.isRequired,
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||||
onDeleteNotificationPress: PropTypes.func
|
onDeleteNotificationPress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
|
import {
|
||||||
|
saveNotification,
|
||||||
|
setNotificationFieldValue,
|
||||||
|
setNotificationValue,
|
||||||
|
testNotification,
|
||||||
|
toggleAdvancedSettings
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
import EditNotificationModalContent from './EditNotificationModalContent';
|
import EditNotificationModalContent from './EditNotificationModalContent';
|
||||||
|
|
||||||
@@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||||||
setNotificationValue,
|
setNotificationValue,
|
||||||
setNotificationFieldValue,
|
setNotificationFieldValue,
|
||||||
saveNotification,
|
saveNotification,
|
||||||
testNotification
|
testNotification,
|
||||||
|
toggleAdvancedSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditNotificationModalContentConnector extends Component {
|
class EditNotificationModalContentConnector extends Component {
|
||||||
@@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
|
|||||||
this.props.testNotification({ id: this.props.id });
|
this.props.testNotification({ id: this.props.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAdvancedSettingsPress = () => {
|
||||||
|
this.props.toggleAdvancedSettings();
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
|
|||||||
{...this.props}
|
{...this.props}
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onTestPress={this.onTestPress}
|
onTestPress={this.onTestPress}
|
||||||
|
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onFieldChange={this.onFieldChange}
|
onFieldChange={this.onFieldChange}
|
||||||
/>
|
/>
|
||||||
@@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
|
|||||||
setNotificationFieldValue: PropTypes.func.isRequired,
|
setNotificationFieldValue: PropTypes.func.isRequired,
|
||||||
saveNotification: PropTypes.func.isRequired,
|
saveNotification: PropTypes.func.isRequired,
|
||||||
testNotification: PropTypes.func.isRequired,
|
testNotification: PropTypes.func.isRequired,
|
||||||
|
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ class Notification extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onMovieDelete && !onMovieFileDelete && !onManualInteractionRequired ?
|
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onMovieAdded && !onMovieDelete && !onMovieFileDelete && !onManualInteractionRequired ?
|
||||||
<Label
|
<Label
|
||||||
kind={kinds.DISABLED}
|
kind={kinds.DISABLED}
|
||||||
outline={true}
|
outline={true}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@@ -33,7 +32,7 @@ function createDelayProfileSelector() {
|
|||||||
items
|
items
|
||||||
} = delayProfiles;
|
} = delayProfiles;
|
||||||
|
|
||||||
const profile = id ? _.find(items, { id }) : newDelayProfile;
|
const profile = id ? items.find((i) => i.id === id) : newDelayProfile;
|
||||||
const settings = selectSettings(profile, pendingChanges, saveError);
|
const settings = selectSettings(profile, pendingChanges, saveError);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
@@ -157,9 +158,9 @@ class EditQualityProfileModalContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error &&
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('AddQualityProfileError')}
|
{translate('AddQualityProfileError')}
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class QualityProfiles extends Component {
|
|||||||
<FieldSet legend={translate('QualityProfiles')}>
|
<FieldSet legend={translate('QualityProfiles')}>
|
||||||
<PageSectionContent
|
<PageSectionContent
|
||||||
errorMessage={translate('QualityProfilesLoadError')}
|
errorMessage={translate('QualityProfilesLoadError')}
|
||||||
{...otherProps}c={true}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div className={styles.qualityProfiles}>
|
<div className={styles.qualityProfiles}>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class ReleaseProfile extends Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onDeleteReleaseProfileModalClose= () => {
|
onDeleteReleaseProfileModalClose = () => {
|
||||||
this.setState({ isDeleteReleaseProfileModalOpen: false });
|
this.setState({ isDeleteReleaseProfileModalOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class ReleaseProfile extends Component {
|
|||||||
isDeleteReleaseProfileModalOpen
|
isDeleteReleaseProfileModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const indexer = indexerList.find((i) => i.id === indexerId);
|
const indexer = indexerId !== 0 && indexerList.find((i) => i.id === indexerId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||||
import { fetchTagDetails } from 'Store/Actions/tagActions';
|
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||||
import Tags from './Tags';
|
import Tags from './Tags';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
@@ -25,6 +25,7 @@ function createMapStateToProps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchTags: fetchTags,
|
||||||
dispatchFetchTagDetails: fetchTagDetails,
|
dispatchFetchTagDetails: fetchTagDetails,
|
||||||
dispatchFetchDelayProfiles: fetchDelayProfiles,
|
dispatchFetchDelayProfiles: fetchDelayProfiles,
|
||||||
dispatchFetchNotifications: fetchNotifications,
|
dispatchFetchNotifications: fetchNotifications,
|
||||||
@@ -41,6 +42,7 @@ class MetadatasConnector extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {
|
const {
|
||||||
|
dispatchFetchTags,
|
||||||
dispatchFetchTagDetails,
|
dispatchFetchTagDetails,
|
||||||
dispatchFetchDelayProfiles,
|
dispatchFetchDelayProfiles,
|
||||||
dispatchFetchNotifications,
|
dispatchFetchNotifications,
|
||||||
@@ -50,6 +52,7 @@ class MetadatasConnector extends Component {
|
|||||||
dispatchFetchDownloadClients
|
dispatchFetchDownloadClients
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
dispatchFetchTags();
|
||||||
dispatchFetchTagDetails();
|
dispatchFetchTagDetails();
|
||||||
dispatchFetchDelayProfiles();
|
dispatchFetchDelayProfiles();
|
||||||
dispatchFetchNotifications();
|
dispatchFetchNotifications();
|
||||||
@@ -72,6 +75,7 @@ class MetadatasConnector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MetadatasConnector.propTypes = {
|
MetadatasConnector.propTypes = {
|
||||||
|
dispatchFetchTags: PropTypes.func.isRequired,
|
||||||
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
||||||
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
|
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
|
||||||
dispatchFetchNotifications: PropTypes.func.isRequired,
|
dispatchFetchNotifications: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -31,19 +31,19 @@ export const firstDayOfWeekOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const weekColumnOptions = [
|
export const weekColumnOptions = [
|
||||||
{ key: 'ddd M/D', value: 'Tue 3/25' },
|
{ key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' },
|
||||||
{ key: 'ddd MM/DD', value: 'Tue 03/25' },
|
{ key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' },
|
||||||
{ key: 'ddd D/M', value: 'Tue 25/3' },
|
{ key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' },
|
||||||
{ key: 'ddd DD/MM', value: 'Tue 25/03' }
|
{ key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const shortDateFormatOptions = [
|
const shortDateFormatOptions = [
|
||||||
{ key: 'MMM D YYYY', value: 'Mar 25 2014' },
|
{ key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' },
|
||||||
{ key: 'DD MMM YYYY', value: '25 Mar 2014' },
|
{ key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' },
|
||||||
{ key: 'MM/D/YYYY', value: '03/25/2014' },
|
{ key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' },
|
||||||
{ key: 'MM/DD/YYYY', value: '03/25/2014' },
|
{ key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' },
|
||||||
{ key: 'DD/MM/YYYY', value: '25/03/2014' },
|
{ key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' },
|
||||||
{ key: 'YYYY-MM-DD', value: '2014-03-25' }
|
{ key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const longDateFormatOptions = [
|
const longDateFormatOptions = [
|
||||||
|
|||||||
@@ -83,21 +83,21 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'isRecommendation',
|
name: 'isRecommendation',
|
||||||
columnLabel: 'Recommedation',
|
columnLabel: () => translate('Recommendation'),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isModifiable: false
|
isModifiable: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'isTrending',
|
name: 'isTrending',
|
||||||
columnLabel: 'Trending',
|
columnLabel: () => translate('Trending'),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isModifiable: false
|
isModifiable: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'isPopular',
|
name: 'isPopular',
|
||||||
columnLabel: 'Popular',
|
columnLabel: () => translate('Popular'),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isModifiable: false
|
isModifiable: false
|
||||||
@@ -177,7 +177,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lists',
|
name: 'lists',
|
||||||
label: 'Lists',
|
label: () => translate('Lists'),
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
@@ -283,7 +283,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'popular',
|
key: 'popular',
|
||||||
label: 'Popular',
|
label: () => translate('Popular'),
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'isPopular',
|
key: 'isPopular',
|
||||||
@@ -294,7 +294,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'trending',
|
key: 'trending',
|
||||||
label: 'Trending',
|
label: () => translate('Trending'),
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'isTrending',
|
key: 'isTrending',
|
||||||
@@ -305,7 +305,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'newNotExcluded',
|
key: 'newNotExcluded',
|
||||||
label: 'New Non-Excluded',
|
label: () => translate('NewNonExcluded'),
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'isExisting',
|
key: 'isExisting',
|
||||||
@@ -332,7 +332,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'studio',
|
name: 'studio',
|
||||||
label: 'Studio',
|
label: () => translate('Studio'),
|
||||||
type: filterBuilderTypes.ARRAY,
|
type: filterBuilderTypes.ARRAY,
|
||||||
optionsSelector: function(items) {
|
optionsSelector: function(items) {
|
||||||
const tagList = items.reduce((acc, movie) => {
|
const tagList = items.reduce((acc, movie) => {
|
||||||
@@ -387,19 +387,19 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'inCinemas',
|
name: 'inCinemas',
|
||||||
label: 'In Cinemas',
|
label: () => translate('InCinemas'),
|
||||||
type: filterBuilderTypes.DATE,
|
type: filterBuilderTypes.DATE,
|
||||||
valueType: filterBuilderValueTypes.DATE
|
valueType: filterBuilderValueTypes.DATE
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'physicalRelease',
|
name: 'physicalRelease',
|
||||||
label: 'Physical Release',
|
label: () => translate('PhysicalRelease'),
|
||||||
type: filterBuilderTypes.DATE,
|
type: filterBuilderTypes.DATE,
|
||||||
valueType: filterBuilderValueTypes.DATE
|
valueType: filterBuilderValueTypes.DATE
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'digitalRelease',
|
name: 'digitalRelease',
|
||||||
label: 'Digital Release',
|
label: () => translate('DigitalRelease'),
|
||||||
type: filterBuilderTypes.DATE,
|
type: filterBuilderTypes.DATE,
|
||||||
valueType: filterBuilderValueTypes.DATE
|
valueType: filterBuilderValueTypes.DATE
|
||||||
},
|
},
|
||||||
@@ -410,7 +410,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'genres',
|
name: 'genres',
|
||||||
label: 'Genres',
|
label: () => translate('Genres'),
|
||||||
type: filterBuilderTypes.ARRAY,
|
type: filterBuilderTypes.ARRAY,
|
||||||
optionsSelector: function(items) {
|
optionsSelector: function(items) {
|
||||||
const tagList = items.reduce((acc, movie) => {
|
const tagList = items.reduce((acc, movie) => {
|
||||||
@@ -466,42 +466,42 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'certification',
|
name: 'certification',
|
||||||
label: 'Certification',
|
label: () => translate('Certification'),
|
||||||
type: filterBuilderTypes.EXACT
|
type: filterBuilderTypes.EXACT
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'lists',
|
name: 'lists',
|
||||||
label: 'Lists',
|
label: () => translate('Lists'),
|
||||||
type: filterBuilderTypes.ARRAY,
|
type: filterBuilderTypes.ARRAY,
|
||||||
valueType: filterBuilderValueTypes.IMPORTLIST
|
valueType: filterBuilderValueTypes.IMPORTLIST
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'isExcluded',
|
name: 'isExcluded',
|
||||||
label: 'On Excluded List',
|
label: () => translate('OnExcludedList'),
|
||||||
type: filterBuilderTypes.EXACT,
|
type: filterBuilderTypes.EXACT,
|
||||||
valueType: filterBuilderValueTypes.BOOL
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'isExisting',
|
name: 'isExisting',
|
||||||
label: 'Exists in Library',
|
label: () => translate('ExistsInLibrary'),
|
||||||
type: filterBuilderTypes.EXACT,
|
type: filterBuilderTypes.EXACT,
|
||||||
valueType: filterBuilderValueTypes.BOOL
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'isRecommendation',
|
name: 'isRecommendation',
|
||||||
label: 'Recommended',
|
label: () => translate('Recommended'),
|
||||||
type: filterBuilderTypes.EXACT,
|
type: filterBuilderTypes.EXACT,
|
||||||
valueType: filterBuilderValueTypes.BOOL
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'isTrending',
|
name: 'isTrending',
|
||||||
label: 'Trending',
|
label: () => translate('Trending'),
|
||||||
type: filterBuilderTypes.EXACT,
|
type: filterBuilderTypes.EXACT,
|
||||||
valueType: filterBuilderValueTypes.BOOL
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'isPopular',
|
name: 'isPopular',
|
||||||
label: 'Popular',
|
label: () => translate('Popular'),
|
||||||
type: filterBuilderTypes.EXACT,
|
type: filterBuilderTypes.EXACT,
|
||||||
valueType: filterBuilderValueTypes.BOOL
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export const actionHandlers = handleThunks({
|
|||||||
quality: item.quality,
|
quality: item.quality,
|
||||||
languages: item.languages,
|
languages: item.languages,
|
||||||
releaseGroup: item.releaseGroup,
|
releaseGroup: item.releaseGroup,
|
||||||
|
indexerFlags: item.indexerFlags,
|
||||||
downloadId: item.downloadId
|
downloadId: item.downloadId
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { createAction } from 'redux-actions';
|
|||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons, sortDirections } from 'Helpers/Props';
|
||||||
import movieEntities from 'Movie/movieEntities';
|
import movieEntities from 'Movie/movieEntities';
|
||||||
|
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||||
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
|
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
@@ -31,13 +32,16 @@ export const defaultState = {
|
|||||||
deleteError: null,
|
deleteError: null,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
saveError: null,
|
saveError: null,
|
||||||
|
sortKey: 'relativePath',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
items: [],
|
items: [],
|
||||||
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'relativePath',
|
name: 'relativePath',
|
||||||
label: () => translate('RelativePath'),
|
label: () => translate('RelativePath'),
|
||||||
isVisible: true
|
isVisible: true,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'videoCodec',
|
name: 'videoCodec',
|
||||||
@@ -67,7 +71,8 @@ export const defaultState = {
|
|||||||
{
|
{
|
||||||
name: 'size',
|
name: 'size',
|
||||||
label: () => translate('Size'),
|
label: () => translate('Size'),
|
||||||
isVisible: true
|
isVisible: true,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'languages',
|
name: 'languages',
|
||||||
@@ -96,12 +101,23 @@ export const defaultState = {
|
|||||||
name: icons.SCORE,
|
name: icons.SCORE,
|
||||||
title: () => translate('CustomFormatScore')
|
title: () => translate('CustomFormatScore')
|
||||||
}),
|
}),
|
||||||
isVisible: true
|
isVisible: true,
|
||||||
|
isSortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'indexerFlags',
|
||||||
|
columnLabel: () => translate('IndexerFlags'),
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.FLAG,
|
||||||
|
title: () => translate('IndexerFlags')
|
||||||
|
}),
|
||||||
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'dateAdded',
|
name: 'dateAdded',
|
||||||
label: () => translate('Added'),
|
label: () => translate('Added'),
|
||||||
isVisible: false
|
isVisible: false,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
@@ -114,7 +130,9 @@ export const defaultState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
'movieFiles.columns'
|
'movieFiles.columns',
|
||||||
|
'movieFiles.sortDirection',
|
||||||
|
'movieFiles.sortKey'
|
||||||
];
|
];
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -125,6 +143,7 @@ export const DELETE_MOVIE_FILE = 'movieFiles/deleteMovieFile';
|
|||||||
export const DELETE_MOVIE_FILES = 'movieFiles/deleteMovieFiles';
|
export const DELETE_MOVIE_FILES = 'movieFiles/deleteMovieFiles';
|
||||||
export const UPDATE_MOVIE_FILES = 'movieFiles/updateMovieFiles';
|
export const UPDATE_MOVIE_FILES = 'movieFiles/updateMovieFiles';
|
||||||
export const CLEAR_MOVIE_FILES = 'movieFiles/clearMovieFiles';
|
export const CLEAR_MOVIE_FILES = 'movieFiles/clearMovieFiles';
|
||||||
|
export const SET_MOVIE_FILES_SORT = 'movieFiles/setMovieFilesSort';
|
||||||
export const SET_MOVIE_FILES_TABLE_OPTION = 'movieFiles/setMovieFilesTableOption';
|
export const SET_MOVIE_FILES_TABLE_OPTION = 'movieFiles/setMovieFilesTableOption';
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -135,6 +154,7 @@ export const deleteMovieFile = createThunk(DELETE_MOVIE_FILE);
|
|||||||
export const deleteMovieFiles = createThunk(DELETE_MOVIE_FILES);
|
export const deleteMovieFiles = createThunk(DELETE_MOVIE_FILES);
|
||||||
export const updateMovieFiles = createThunk(UPDATE_MOVIE_FILES);
|
export const updateMovieFiles = createThunk(UPDATE_MOVIE_FILES);
|
||||||
export const clearMovieFiles = createAction(CLEAR_MOVIE_FILES);
|
export const clearMovieFiles = createAction(CLEAR_MOVIE_FILES);
|
||||||
|
export const setMovieFilesSort = createAction(SET_MOVIE_FILES_SORT);
|
||||||
export const setMovieFilesTableOption = createAction(SET_MOVIE_FILES_TABLE_OPTION);
|
export const setMovieFilesTableOption = createAction(SET_MOVIE_FILES_TABLE_OPTION);
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -327,6 +347,7 @@ export const actionHandlers = handleThunks({
|
|||||||
// Reducers
|
// Reducers
|
||||||
|
|
||||||
export const reducers = createHandleActions({
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
[SET_MOVIE_FILES_TABLE_OPTION]: createSetTableOptionReducer(section),
|
[SET_MOVIE_FILES_TABLE_OPTION]: createSetTableOptionReducer(section),
|
||||||
|
|
||||||
[CLEAR_MOVIE_FILES]: (state) => {
|
[CLEAR_MOVIE_FILES]: (state) => {
|
||||||
@@ -340,6 +361,8 @@ export const reducers = createHandleActions({
|
|||||||
saveError: null,
|
saveError: null,
|
||||||
items: []
|
items: []
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
[SET_MOVIE_FILES_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||||
|
|
||||||
}, defaultState, section);
|
}, defaultState, section);
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
|
||||||
|
function createMultiMoviesSelector(movieIds: number[]) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.movies.itemMap,
|
||||||
|
(state: AppState) => state.movies.items,
|
||||||
|
(itemMap, allMovies) => {
|
||||||
|
return movieIds.reduce((acc: Movie[], movieId) => {
|
||||||
|
const movie = allMovies[itemMap[movieId]];
|
||||||
|
|
||||||
|
if (movie) {
|
||||||
|
acc.push(movie);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createMultiMoviesSelector;
|
||||||
@@ -10,15 +10,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commandName {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userAgent {
|
|
||||||
color: #b0b0b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.queued,
|
.queued,
|
||||||
.started,
|
.started,
|
||||||
.ended {
|
.ended {
|
||||||
|
|||||||
@@ -2,14 +2,12 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
'commandName': string;
|
|
||||||
'duration': string;
|
'duration': string;
|
||||||
'ended': string;
|
'ended': string;
|
||||||
'queued': string;
|
'queued': string;
|
||||||
'started': string;
|
'started': string;
|
||||||
'trigger': string;
|
'trigger': string;
|
||||||
'triggerContent': string;
|
'triggerContent': string;
|
||||||
'userAgent': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -1,279 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import formatDate from 'Utilities/Date/formatDate';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './QueuedTaskRow.css';
|
|
||||||
|
|
||||||
function getStatusIconProps(status, message) {
|
|
||||||
const title = titleCase(status);
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case 'queued':
|
|
||||||
return {
|
|
||||||
name: icons.PENDING,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'started':
|
|
||||||
return {
|
|
||||||
name: icons.REFRESH,
|
|
||||||
isSpinning: true,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'completed':
|
|
||||||
return {
|
|
||||||
name: icons.CHECK,
|
|
||||||
kind: kinds.SUCCESS,
|
|
||||||
title: message === 'Completed' ? title : `${title}: ${message}`
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'failed':
|
|
||||||
return {
|
|
||||||
name: icons.FATAL,
|
|
||||||
kind: kinds.DANGER,
|
|
||||||
title: `${title}: ${message}`
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
return {
|
|
||||||
name: icons.UNKNOWN,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormattedDates(props) {
|
|
||||||
const {
|
|
||||||
queued,
|
|
||||||
started,
|
|
||||||
ended,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (showRelativeDates) {
|
|
||||||
return {
|
|
||||||
queuedAt: moment(queued).fromNow(),
|
|
||||||
startedAt: started ? moment(started).fromNow() : '-',
|
|
||||||
endedAt: ended ? moment(ended).fromNow() : '-'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
queuedAt: formatDate(queued, shortDateFormat),
|
|
||||||
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
|
||||||
endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class QueuedTaskRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
...getFormattedDates(props),
|
|
||||||
isCancelConfirmModalOpen: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this._updateTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setUpdateTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
queued,
|
|
||||||
started,
|
|
||||||
ended
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
queued !== prevProps.queued ||
|
|
||||||
started !== prevProps.started ||
|
|
||||||
ended !== prevProps.ended
|
|
||||||
) {
|
|
||||||
this.setState(getFormattedDates(this.props));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._updateTimeoutId) {
|
|
||||||
this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setUpdateTimer() {
|
|
||||||
this._updateTimeoutId = setTimeout(() => {
|
|
||||||
this.setState(getFormattedDates(this.props));
|
|
||||||
this.setUpdateTimer();
|
|
||||||
}, 30000);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onCancelPress = () => {
|
|
||||||
this.setState({
|
|
||||||
isCancelConfirmModalOpen: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onAbortCancel = () => {
|
|
||||||
this.setState({
|
|
||||||
isCancelConfirmModalOpen: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
trigger,
|
|
||||||
commandName,
|
|
||||||
queued,
|
|
||||||
started,
|
|
||||||
ended,
|
|
||||||
status,
|
|
||||||
duration,
|
|
||||||
message,
|
|
||||||
clientUserAgent,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
onCancelPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
queuedAt,
|
|
||||||
startedAt,
|
|
||||||
endedAt,
|
|
||||||
isCancelConfirmModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
let triggerIcon = icons.QUICK;
|
|
||||||
|
|
||||||
if (trigger === 'manual') {
|
|
||||||
triggerIcon = icons.INTERACTIVE;
|
|
||||||
} else if (trigger === 'scheduled') {
|
|
||||||
triggerIcon = icons.SCHEDULED;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableRowCell className={styles.trigger}>
|
|
||||||
<span className={styles.triggerContent}>
|
|
||||||
<Icon
|
|
||||||
name={triggerIcon}
|
|
||||||
title={titleCase(trigger)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Icon
|
|
||||||
{...getStatusIconProps(status, message)}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell>
|
|
||||||
<span className={styles.commandName}>
|
|
||||||
{commandName}
|
|
||||||
</span>
|
|
||||||
{
|
|
||||||
clientUserAgent ?
|
|
||||||
<span className={styles.userAgent} title={translate('TaskUserAgentTooltip')}>
|
|
||||||
{translate('From')}: {clientUserAgent}
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.queued}
|
|
||||||
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
{queuedAt}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.started}
|
|
||||||
title={formatDateTime(started, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
{startedAt}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.ended}
|
|
||||||
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
|
||||||
>
|
|
||||||
{endedAt}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.duration}>
|
|
||||||
{formatTimeSpan(duration)}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell
|
|
||||||
className={styles.actions}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
status === 'queued' &&
|
|
||||||
<IconButton
|
|
||||||
title={translate('RemovedFromTaskQueue')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
onPress={this.onCancelPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={isCancelConfirmModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('Cancel')}
|
|
||||||
message={translate('CancelPendingTask')}
|
|
||||||
confirmLabel={translate('YesCancel')}
|
|
||||||
cancelLabel={translate('NoLeaveIt')}
|
|
||||||
onConfirm={onCancelPress}
|
|
||||||
onCancel={this.onAbortCancel}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueuedTaskRow.propTypes = {
|
|
||||||
trigger: PropTypes.string.isRequired,
|
|
||||||
commandName: PropTypes.string.isRequired,
|
|
||||||
queued: PropTypes.string.isRequired,
|
|
||||||
started: PropTypes.string,
|
|
||||||
ended: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
duration: PropTypes.string,
|
|
||||||
message: PropTypes.string,
|
|
||||||
clientUserAgent: PropTypes.string,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onCancelPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueuedTaskRow;
|
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { CommandBody } from 'Commands/Command';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import { cancelCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import formatDate from 'Utilities/Date/formatDate';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueuedTaskRowNameCell from './QueuedTaskRowNameCell';
|
||||||
|
import styles from './QueuedTaskRow.css';
|
||||||
|
|
||||||
|
function getStatusIconProps(status: string, message: string | undefined) {
|
||||||
|
const title = titleCase(status);
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'queued':
|
||||||
|
return {
|
||||||
|
name: icons.PENDING,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'started':
|
||||||
|
return {
|
||||||
|
name: icons.REFRESH,
|
||||||
|
isSpinning: true,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'completed':
|
||||||
|
return {
|
||||||
|
name: icons.CHECK,
|
||||||
|
kind: kinds.SUCCESS,
|
||||||
|
title: message === 'Completed' ? title : `${title}: ${message}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'failed':
|
||||||
|
return {
|
||||||
|
name: icons.FATAL,
|
||||||
|
kind: kinds.DANGER,
|
||||||
|
title: `${title}: ${message}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
name: icons.UNKNOWN,
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedDates(
|
||||||
|
queued: string,
|
||||||
|
started: string | undefined,
|
||||||
|
ended: string | undefined,
|
||||||
|
showRelativeDates: boolean,
|
||||||
|
shortDateFormat: string
|
||||||
|
) {
|
||||||
|
if (showRelativeDates) {
|
||||||
|
return {
|
||||||
|
queuedAt: moment(queued).fromNow(),
|
||||||
|
startedAt: started ? moment(started).fromNow() : '-',
|
||||||
|
endedAt: ended ? moment(ended).fromNow() : '-',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
queuedAt: formatDate(queued, shortDateFormat),
|
||||||
|
startedAt: started ? formatDate(started, shortDateFormat) : '-',
|
||||||
|
endedAt: ended ? formatDate(ended, shortDateFormat) : '-',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueuedTimes {
|
||||||
|
queuedAt: string;
|
||||||
|
startedAt: string;
|
||||||
|
endedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueuedTaskRowProps {
|
||||||
|
id: number;
|
||||||
|
trigger: string;
|
||||||
|
commandName: string;
|
||||||
|
queued: string;
|
||||||
|
started?: string;
|
||||||
|
ended?: string;
|
||||||
|
status: string;
|
||||||
|
duration?: string;
|
||||||
|
message?: string;
|
||||||
|
body: CommandBody;
|
||||||
|
clientUserAgent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueuedTaskRow(props: QueuedTaskRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
trigger,
|
||||||
|
commandName,
|
||||||
|
queued,
|
||||||
|
started,
|
||||||
|
ended,
|
||||||
|
status,
|
||||||
|
duration,
|
||||||
|
message,
|
||||||
|
body,
|
||||||
|
clientUserAgent,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
|
||||||
|
useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [times, setTimes] = useState<QueuedTimes>(
|
||||||
|
getFormattedDates(
|
||||||
|
queued,
|
||||||
|
started,
|
||||||
|
ended,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isCancelConfirmModalOpen,
|
||||||
|
openCancelConfirmModal,
|
||||||
|
closeCancelConfirmModal,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const handleCancelPress = useCallback(() => {
|
||||||
|
dispatch(cancelCommand({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTimeTimeoutId.current = setTimeout(() => {
|
||||||
|
setTimes(
|
||||||
|
getFormattedDates(
|
||||||
|
queued,
|
||||||
|
started,
|
||||||
|
ended,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (updateTimeTimeoutId.current) {
|
||||||
|
clearTimeout(updateTimeTimeoutId.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [queued, started, ended, showRelativeDates, shortDateFormat, setTimes]);
|
||||||
|
|
||||||
|
const { queuedAt, startedAt, endedAt } = times;
|
||||||
|
|
||||||
|
let triggerIcon = icons.QUICK;
|
||||||
|
|
||||||
|
if (trigger === 'manual') {
|
||||||
|
triggerIcon = icons.INTERACTIVE;
|
||||||
|
} else if (trigger === 'scheduled') {
|
||||||
|
triggerIcon = icons.SCHEDULED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableRowCell className={styles.trigger}>
|
||||||
|
<span className={styles.triggerContent}>
|
||||||
|
<Icon name={triggerIcon} title={titleCase(trigger)} />
|
||||||
|
|
||||||
|
<Icon {...getStatusIconProps(status, message)} />
|
||||||
|
</span>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<QueuedTaskRowNameCell
|
||||||
|
commandName={commandName}
|
||||||
|
body={body}
|
||||||
|
clientUserAgent={clientUserAgent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.queued}
|
||||||
|
title={formatDateTime(queued, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{queuedAt}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.started}
|
||||||
|
title={formatDateTime(started, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{startedAt}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.ended}
|
||||||
|
title={formatDateTime(ended, longDateFormat, timeFormat)}
|
||||||
|
>
|
||||||
|
{endedAt}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.duration}>
|
||||||
|
{formatTimeSpan(duration)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.actions}>
|
||||||
|
{status === 'queued' && (
|
||||||
|
<IconButton
|
||||||
|
title={translate('RemovedFromTaskQueue')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
onPress={openCancelConfirmModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isCancelConfirmModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('Cancel')}
|
||||||
|
message={translate('CancelPendingTask')}
|
||||||
|
confirmLabel={translate('YesCancel')}
|
||||||
|
cancelLabel={translate('NoLeaveIt')}
|
||||||
|
onConfirm={handleCancelPress}
|
||||||
|
onCancel={closeCancelConfirmModal}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { cancelCommand } from 'Store/Actions/commandActions';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import QueuedTaskRow from './QueuedTaskRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(uiSettings) => {
|
|
||||||
return {
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onCancelPress() {
|
|
||||||
dispatch(cancelCommand({
|
|
||||||
id: props.id
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.commandName {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userAgent {
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'commandName': string;
|
||||||
|
'userAgent': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { CommandBody } from 'Commands/Command';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import createMultiMoviesSelector from 'Store/Selectors/createMultiMoviesSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './QueuedTaskRowNameCell.css';
|
||||||
|
|
||||||
|
export interface QueuedTaskRowNameCellProps {
|
||||||
|
commandName: string;
|
||||||
|
body: CommandBody;
|
||||||
|
clientUserAgent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueuedTaskRowNameCell(
|
||||||
|
props: QueuedTaskRowNameCellProps
|
||||||
|
) {
|
||||||
|
const { commandName, body, clientUserAgent } = props;
|
||||||
|
const movieIds = [...(body.movieIds ?? [])];
|
||||||
|
|
||||||
|
if (body.movieId) {
|
||||||
|
movieIds.push(body.movieId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const movies = useSelector(createMultiMoviesSelector(movieIds));
|
||||||
|
const sortedMovies = movies.sort((a, b) =>
|
||||||
|
a.sortTitle.localeCompare(b.sortTitle)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell>
|
||||||
|
<span className={styles.commandName}>
|
||||||
|
{commandName}
|
||||||
|
{sortedMovies.length ? (
|
||||||
|
<span> - {sortedMovies.map((m) => m.title).join(', ')}</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{clientUserAgent ? (
|
||||||
|
<span
|
||||||
|
className={styles.userAgent}
|
||||||
|
title={translate('TaskUserAgentTooltip')}
|
||||||
|
>
|
||||||
|
{translate('From')}: {clientUserAgent}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QueuedTaskRowConnector from './QueuedTaskRowConnector';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'trigger',
|
|
||||||
label: () => translate('Trigger'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'commandName',
|
|
||||||
label: () => translate('Name'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'queued',
|
|
||||||
label: () => translate('Queued'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'started',
|
|
||||||
label: () => translate('Started'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ended',
|
|
||||||
label: () => translate('Ended'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'duration',
|
|
||||||
label: () => translate('Duration'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function QueuedTasks(props) {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
items
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('Queue')}>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated &&
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<QueuedTaskRowConnector
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
}
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QueuedTasks.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.array.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueuedTasks;
|
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import { fetchCommands } from 'Store/Actions/commandActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueuedTaskRow from './QueuedTaskRow';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'trigger',
|
||||||
|
label: '',
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'commandName',
|
||||||
|
label: () => translate('Name'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'queued',
|
||||||
|
label: () => translate('Queued'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'started',
|
||||||
|
label: () => translate('Started'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ended',
|
||||||
|
label: () => translate('Ended'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'duration',
|
||||||
|
label: () => translate('Duration'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function QueuedTasks() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isFetching, isPopulated, items } = useSelector(
|
||||||
|
(state: AppState) => state.commands
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchCommands());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('Queue')}>
|
||||||
|
{isFetching && !isPopulated && <LoadingIndicator />}
|
||||||
|
|
||||||
|
{isPopulated && (
|
||||||
|
<Table columns={columns}>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return <QueuedTaskRow key={item.id} {...item} />;
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchCommands } from 'Store/Actions/commandActions';
|
|
||||||
import QueuedTasks from './QueuedTasks';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.commands,
|
|
||||||
(commands) => {
|
|
||||||
return commands;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchCommands: fetchCommands
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueuedTasksConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<QueuedTasks
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueuedTasksConnector.propTypes = {
|
|
||||||
dispatchFetchCommands: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
|
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QueuedTasksConnector from './Queued/QueuedTasksConnector';
|
import QueuedTasks from './Queued/QueuedTasks';
|
||||||
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
|
||||||
|
|
||||||
function Tasks() {
|
function Tasks() {
|
||||||
@@ -10,7 +10,7 @@ function Tasks() {
|
|||||||
<PageContent title={translate('Tasks')}>
|
<PageContent title={translate('Tasks')}>
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
<ScheduledTasksConnector />
|
<ScheduledTasksConnector />
|
||||||
<QueuedTasksConnector />
|
<QueuedTasks />
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ export default function getQueueStatusText(queueStatus, queueState) {
|
|||||||
status = titleCase(queueStatus);
|
status = titleCase(queueStatus);
|
||||||
break;
|
break;
|
||||||
case 'delay':
|
case 'delay':
|
||||||
|
case 'downloadClientUnavailable':
|
||||||
status = translate('Pending');
|
status = translate('Pending');
|
||||||
break;
|
break;
|
||||||
case 'DownloadClientUnavailable':
|
|
||||||
case 'warning':
|
case 'warning':
|
||||||
status = translate('Error');
|
status = translate('Error');
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
|
|
||||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||||
<meta name="theme-color" content="#595959" />
|
<meta name="theme-color" content="#595959" />
|
||||||
<!-- Windows Phone -->
|
<!-- Windows Phone -->
|
||||||
<meta name="msapplication-navbutton-color" content="#3a3f51" />
|
<meta name="msapplication-navbutton-color" content="#464b51" />
|
||||||
|
<!-- Android/Apple Phone -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
|
||||||
<meta name="description" content="Radarr" />
|
<meta name="description" content="Radarr" />
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
||||||
|
|
||||||
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
<!-- Chrome, Safari, Opera, and Firefox OS -->
|
||||||
<meta name="theme-color" content="#595959" />
|
<meta name="theme-color" content="#595959" />
|
||||||
<!-- Windows Phone -->
|
<!-- Windows Phone -->
|
||||||
<meta name="msapplication-navbutton-color" content="#464b51" />
|
<meta name="msapplication-navbutton-color" content="#464b51" />
|
||||||
|
<!-- Android/Apple Phone -->
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
|
|
||||||
<meta name="description" content="Radarr (Preview)" />
|
<meta name="description" content="Radarr (Preview)" />
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@
|
|||||||
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
"lint": "eslint --config frontend/.eslintrc.js --ignore-path frontend/.eslintignore frontend/",
|
||||||
"lint-fix": "yarn lint --fix",
|
"lint-fix": "yarn lint --fix",
|
||||||
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
|
"stylelint-linux": "stylelint $(find frontend -name '*.css') --config frontend/.stylelintrc",
|
||||||
"stylelint-windows": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
"stylelint-windows": "stylelint \"frontend/**/*.css\" --config frontend/.stylelintrc"
|
||||||
},
|
},
|
||||||
"repository": "https://github.com/Radarr/Radarr",
|
"repository": "https://github.com/Radarr/Radarr",
|
||||||
"author": "Team Radarr",
|
"author": "Team Radarr",
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
"worker-loader": "3.0.8"
|
"worker-loader": "3.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "16.17.0",
|
"node": "20.11.1",
|
||||||
"yarn": "1.22.19"
|
"yarn": "1.22.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
|
[TestCase(@"https://baconbits.org/feeds.php?feed=torrents_tv&user=12345&auth=2b51db35e1910123321025a12b9933d2&passkey=mySecret&authkey=2b51db35e1910123321025a12b9933d2")]
|
||||||
[TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
|
[TestCase(@"http://127.0.0.1:9117/dl/indexername?jackett_apikey=flwjiefewklfjacketmySecretsdfldskjfsdlk&path=we0re9f0sdfbase64sfdkfjsdlfjk&file=The+Torrent+File+Name.torrent")]
|
||||||
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
|
[TestCase(@"http://nzb.su/getnzb/2b51db35e1912ffc138825a12b9933d2.nzb&i=37292&r=2b51db35e1910123321025a12b9933d2")]
|
||||||
|
[TestCase(@"https://b-hd.me/torrent/download/auto.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
|
||||||
|
[TestCase(@"https://b-hd.me/torrent/download/a-slug-in-the-url.343756.is1t1pl127p1sfwur8h4kgyhg1wcsn05")]
|
||||||
|
|
||||||
// NzbGet
|
// NzbGet
|
||||||
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
[TestCase(@"{ ""Name"" : ""ControlUsername"", ""Value"" : ""mySecret"" }, { ""Name"" : ""ControlPassword"", ""Value"" : ""mySecret"" }, ")]
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using NLog;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http.Proxy;
|
using NzbDrone.Common.Http.Proxy;
|
||||||
@@ -28,11 +31,14 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
|
||||||
private readonly ICached<CredentialCache> _credentialCache;
|
private readonly ICached<CredentialCache> _credentialCache;
|
||||||
|
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
|
||||||
ICreateManagedWebProxy createManagedWebProxy,
|
ICreateManagedWebProxy createManagedWebProxy,
|
||||||
ICertificateValidationService certificateValidationService,
|
ICertificateValidationService certificateValidationService,
|
||||||
IUserAgentBuilder userAgentBuilder,
|
IUserAgentBuilder userAgentBuilder,
|
||||||
ICacheManager cacheManager)
|
ICacheManager cacheManager,
|
||||||
|
Logger logger)
|
||||||
{
|
{
|
||||||
_proxySettingsProvider = proxySettingsProvider;
|
_proxySettingsProvider = proxySettingsProvider;
|
||||||
_createManagedWebProxy = createManagedWebProxy;
|
_createManagedWebProxy = createManagedWebProxy;
|
||||||
@@ -41,6 +47,8 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
|
|
||||||
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
|
||||||
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
|
_credentialCache = cacheManager.GetCache<CredentialCache>(typeof(ManagedHttpDispatcher), "credentialcache");
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
public async Task<HttpResponse> GetResponseAsync(HttpRequest request, CookieContainer cookies)
|
||||||
@@ -246,7 +254,27 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
return _credentialCache.Get("credentialCache", () => new CredentialCache());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
private bool HasRoutableIPv4Address()
|
||||||
|
{
|
||||||
|
// Get all IPv4 addresses from all interfaces and return true if there are any with non-loopback addresses
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||||
|
|
||||||
|
return networkInterfaces.Any(ni =>
|
||||||
|
ni.OperationalStatus == OperationalStatus.Up &&
|
||||||
|
ni.GetIPProperties().UnicastAddresses.Any(ip =>
|
||||||
|
ip.Address.AddressFamily == AddressFamily.InterNetwork &&
|
||||||
|
!IPAddress.IsLoopback(ip.Address)));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Debug(e, "Caught exception while GetAllNetworkInterfaces assuming IPv4 connectivity: {0}", e.Message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
|
||||||
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
|
||||||
@@ -269,10 +297,10 @@ namespace NzbDrone.Common.Http.Dispatchers
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
|
// Do not retry IPv6 if a routable IPv4 address is available, otherwise continue to attempt IPv6 connections.
|
||||||
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
|
var routableIPv4 = HasRoutableIPv4Address();
|
||||||
// but in the interest of keeping this implementation simple, this is acceptable.
|
_logger.Info("IPv4 is available: {0}, IPv6 will be {1}", routableIPv4, routableIPv4 ? "disabled" : "left enabled");
|
||||||
useIPv6 = false;
|
useIPv6 = !routableIPv4;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
new (@"/fetch/[a-z0-9]{32}/(?<secret>[a-z0-9]{32})", RegexOptions.Compiled),
|
||||||
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new (@"getnzb.*?(?<=\?|&)(r)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new (@"\b(\w*)?(_?(?<!use|get_)token|username|passwo?rd)=(?<secret>[^&=]+?)(?= |&|$|;)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
new (@"-hd.me/torrent/[a-z0-9-]\.[0-9]+\.(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
// Trackers Announce Keys; Designed for Qbit Json; should work for all in theory
|
||||||
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new (@"announce(\.php)?(/|%2f|%3fpasskey%3d)(?<secret>[a-z0-9]{16,})|(?<secret>[a-z0-9]{16,})(/|%2f)announce", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.Framework
|
|||||||
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
|
||||||
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
|
||||||
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
|
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
|
||||||
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>()));
|
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger));
|
||||||
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
|
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
|
||||||
Mocker.SetConstant<IRadarrCloudRequestBuilder>(new RadarrCloudRequestBuilder());
|
Mocker.SetConstant<IRadarrCloudRequestBuilder>(new RadarrCloudRequestBuilder());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
|
|||||||
Path.Combine(_movie.Path, "Scenes", "file7.mkv").AsOsAgnostic(),
|
Path.Combine(_movie.Path, "Scenes", "file7.mkv").AsOsAgnostic(),
|
||||||
Path.Combine(_movie.Path, "Shorts", "file8.mkv").AsOsAgnostic(),
|
Path.Combine(_movie.Path, "Shorts", "file8.mkv").AsOsAgnostic(),
|
||||||
Path.Combine(_movie.Path, "Trailers", "file9.mkv").AsOsAgnostic(),
|
Path.Combine(_movie.Path, "Trailers", "file9.mkv").AsOsAgnostic(),
|
||||||
|
Path.Combine(_movie.Path, "Other", "file9.mkv").AsOsAgnostic(),
|
||||||
Path.Combine(_movie.Path, "The Count of Monte Cristo (2002) (1080p BluRay x265 10bit Tigole).mkv").AsOsAgnostic(),
|
Path.Combine(_movie.Path, "The Count of Monte Cristo (2002) (1080p BluRay x265 10bit Tigole).mkv").AsOsAgnostic(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Movie.Title.2019.1080p.AMZN.WEB-Rip.DDP.5.1.HEVC", null)]
|
[TestCase("Movie.Title.2019.1080p.AMZN.WEB-Rip.DDP.5.1.HEVC", null)]
|
||||||
[TestCase("Movie Name (2017) [2160p REMUX] [HEVC DV HYBRID HDR10+ Dolby TrueHD Atmos 7 1 24-bit Audio English] [Data Lass]", null)]
|
[TestCase("Movie Name (2017) [2160p REMUX] [HEVC DV HYBRID HDR10+ Dolby TrueHD Atmos 7 1 24-bit Audio English] [Data Lass]", null)]
|
||||||
[TestCase("Movie Name (2017) [2160p REMUX] [HEVC DV HYBRID HDR10+ Dolby TrueHD Atmos 7 1 24-bit Audio English]-DataLass", "DataLass")]
|
[TestCase("Movie Name (2017) [2160p REMUX] [HEVC DV HYBRID HDR10+ Dolby TrueHD Atmos 7 1 24-bit Audio English]-DataLass", "DataLass")]
|
||||||
public void should_parse_expected_release_group(string title, string expected)
|
[TestCase("Movie Name (2017) (Showtime) (1080p.BD.DD5.1.x265-TheSickle[TAoE])", "TheSickle")]
|
||||||
|
public void should_parse_release_group(string title, string expected)
|
||||||
{
|
{
|
||||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||||
}
|
}
|
||||||
@@ -116,6 +117,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
|||||||
[TestCase("Movie Title (2011) [BluRay] [1080p] [YTS.MX] [YIFY]", "YIFY")]
|
[TestCase("Movie Title (2011) [BluRay] [1080p] [YTS.MX] [YIFY]", "YIFY")]
|
||||||
[TestCase("Movie Title (2014) [BluRay] [1080p] [YIFY] [YTS]", "YTS")]
|
[TestCase("Movie Title (2014) [BluRay] [1080p] [YIFY] [YTS]", "YTS")]
|
||||||
[TestCase("Movie Title (2018) [BluRay] [1080p] [YIFY] [YTS.LT]", "YTS.LT")]
|
[TestCase("Movie Title (2018) [BluRay] [1080p] [YIFY] [YTS.LT]", "YTS.LT")]
|
||||||
|
[TestCase("Movie Title (2016) (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 RZeroX) QxR", "RZeroX")]
|
||||||
|
[TestCase("Movie Title (2016) (1080p AMZN WEB-DL x265 HEVC 10bit EAC3 5 1 Garshasp) QxR", "Garshasp")]
|
||||||
public void should_parse_exception_release_group(string title, string expected)
|
public void should_parse_exception_release_group(string title, string expected)
|
||||||
{
|
{
|
||||||
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
Parser.Parser.ParseReleaseGroup(title).Should().Be(expected);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user