Merge branch 'develop' of https://github.com/Sonarr/Sonarr into issue-5148
This commit is contained in:
commit
cbf4c9f9cb
|
@ -268,7 +268,7 @@ dotnet_diagnostic.CA5397.severity = suggestion
|
||||||
|
|
||||||
dotnet_diagnostic.SYSLIB0006.severity = none
|
dotnet_diagnostic.SYSLIB0006.severity = none
|
||||||
|
|
||||||
[*.{js,html,js,hbs,less,css}]
|
[*.{js,html,hbs,less,css,ts,tsx}]
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
|
@ -11,6 +11,7 @@ on:
|
||||||
- ".github/workflows/api_docs.yml"
|
- ".github/workflows/api_docs.yml"
|
||||||
- "docs.sh"
|
- "docs.sh"
|
||||||
- "src/Sonarr.Api.*/**"
|
- "src/Sonarr.Api.*/**"
|
||||||
|
- "src/Sonarr.Http/**"
|
||||||
- "src/**/*.csproj"
|
- "src/**/*.csproj"
|
||||||
- "src/*"
|
- "src/*"
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ module.exports = (env) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
entry: {
|
entry: {
|
||||||
index: 'index.js'
|
index: 'index.ts'
|
||||||
},
|
},
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
|
@ -67,23 +67,23 @@ module.exports = (env) => {
|
||||||
output: {
|
output: {
|
||||||
path: distFolder,
|
path: distFolder,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
filename: '[name].js',
|
filename: '[name]-[contenthash].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
optimization: {
|
optimization: {
|
||||||
moduleIds: 'deterministic',
|
moduleIds: 'deterministic',
|
||||||
chunkIds: 'named',
|
chunkIds: isProduction ? 'deterministic' : 'named'
|
||||||
splitChunks: {
|
|
||||||
chunks: 'initial',
|
|
||||||
name: 'vendors'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
performance: {
|
performance: {
|
||||||
hints: false
|
hints: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
experiments: {
|
||||||
|
topLevelAwait: true
|
||||||
|
},
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
__DEV__: !isProduction,
|
__DEV__: !isProduction,
|
||||||
|
@ -97,7 +97,8 @@ module.exports = (env) => {
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: 'frontend/src/index.ejs',
|
template: 'frontend/src/index.ejs',
|
||||||
filename: 'index.html',
|
filename: 'index.html',
|
||||||
publicPath: '/'
|
publicPath: '/',
|
||||||
|
inject: false
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new FileManagerPlugin({
|
new FileManagerPlugin({
|
||||||
|
|
|
@ -4,13 +4,14 @@ import IconButton from 'Components/Link/IconButton';
|
||||||
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 Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import episodeEntities from 'Episode/episodeEntities';
|
import episodeEntities from 'Episode/episodeEntities';
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
||||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||||
|
@ -210,7 +211,14 @@ class HistoryRow extends Component {
|
||||||
key={name}
|
key={name}
|
||||||
className={styles.customFormatScore}
|
className={styles.customFormatScore}
|
||||||
>
|
>
|
||||||
{formatPreferredWordScore(customFormatScore)}
|
<Tooltip
|
||||||
|
anchor={formatPreferredWordScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -294,4 +302,8 @@ HistoryRow.propTypes = {
|
||||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
HistoryRow.defaultProps = {
|
||||||
|
customFormats: []
|
||||||
|
};
|
||||||
|
|
||||||
export default HistoryRow;
|
export default HistoryRow;
|
||||||
|
|
|
@ -16,6 +16,12 @@
|
||||||
width: 150px;
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customFormatScore {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'actions': string;
|
'actions': string;
|
||||||
|
'customFormatScore': string;
|
||||||
'progress': string;
|
'progress': string;
|
||||||
'protocol': string;
|
'protocol': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
|
|
|
@ -8,15 +8,17 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
||||||
import QueueStatusCell from './QueueStatusCell';
|
import QueueStatusCell from './QueueStatusCell';
|
||||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||||
import TimeleftCell from './TimeleftCell';
|
import TimeleftCell from './TimeleftCell';
|
||||||
|
@ -91,6 +93,7 @@ class QueueRow extends Component {
|
||||||
languages,
|
languages,
|
||||||
quality,
|
quality,
|
||||||
customFormats,
|
customFormats,
|
||||||
|
customFormatScore,
|
||||||
protocol,
|
protocol,
|
||||||
indexer,
|
indexer,
|
||||||
outputPath,
|
outputPath,
|
||||||
|
@ -259,6 +262,24 @@ class QueueRow extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.customFormatScore}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatPreferredWordScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'protocol') {
|
if (name === 'protocol') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
|
@ -413,6 +434,7 @@ QueueRow.propTypes = {
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
protocol: PropTypes.string.isRequired,
|
protocol: PropTypes.string.isRequired,
|
||||||
indexer: PropTypes.string,
|
indexer: PropTypes.string,
|
||||||
outputPath: PropTypes.string,
|
outputPath: PropTypes.string,
|
||||||
|
@ -436,6 +458,7 @@ QueueRow.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
QueueRow.defaultProps = {
|
QueueRow.defaultProps = {
|
||||||
|
customFormats: [],
|
||||||
isGrabbing: false,
|
isGrabbing: false,
|
||||||
isRemoving: false
|
isRemoving: false
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ import NotFound from 'Components/NotFound';
|
||||||
import Switch from 'Components/Router/Switch';
|
import Switch from 'Components/Router/Switch';
|
||||||
import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector';
|
import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector';
|
||||||
import SeriesIndex from 'Series/Index/SeriesIndex';
|
import SeriesIndex from 'Series/Index/SeriesIndex';
|
||||||
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
|
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
|
@ -179,7 +179,7 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/customformats"
|
path="/settings/customformats"
|
||||||
component={CustomFormatSettingsConnector}
|
component={CustomFormatSettingsPage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -2,6 +2,7 @@ import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||||
import EpisodesAppState from './EpisodesAppState';
|
import EpisodesAppState from './EpisodesAppState';
|
||||||
|
import ParseAppState from './ParseAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
|
@ -44,6 +45,7 @@ interface AppState {
|
||||||
episodesSelection: EpisodesAppState;
|
episodesSelection: EpisodesAppState;
|
||||||
episodeFiles: EpisodeFilesAppState;
|
episodeFiles: EpisodeFilesAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
|
parse: ParseAppState;
|
||||||
seriesIndex: SeriesIndexAppState;
|
seriesIndex: SeriesIndexAppState;
|
||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
series: SeriesAppState;
|
series: SeriesAppState;
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import { AppSectionItemState } from 'App/State/AppSectionState';
|
||||||
|
import Episode from 'Episode/Episode';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import Series from 'Series/Series';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
|
||||||
|
export interface SeriesTitleInfo {
|
||||||
|
title: string;
|
||||||
|
titleWithoutYear: string;
|
||||||
|
year: number;
|
||||||
|
allTitles: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedEpisodeInfo {
|
||||||
|
releaseTitle: string;
|
||||||
|
seriesTitle: string;
|
||||||
|
seriesTitleInfo: SeriesTitleInfo;
|
||||||
|
quality: QualityModel;
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeNumbers: number[];
|
||||||
|
absoluteEpisodeNumbers: number[];
|
||||||
|
specialAbsoluteEpisodeNumbers: number[];
|
||||||
|
languages: Language[];
|
||||||
|
fullSeason: boolean;
|
||||||
|
isPartialSeason: boolean;
|
||||||
|
isMultiSeason: boolean;
|
||||||
|
isSeasonExtra: boolean;
|
||||||
|
special: boolean;
|
||||||
|
releaseHash: string;
|
||||||
|
seasonPart: number;
|
||||||
|
releaseGroup?: string;
|
||||||
|
releaseTokens: string;
|
||||||
|
airDate?: string;
|
||||||
|
isDaily: boolean;
|
||||||
|
isAbsoluteNumbering: boolean;
|
||||||
|
isPossibleSpecialEpisode: boolean;
|
||||||
|
isPossibleSceneSeasonSpecial: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParseModel extends ModelBase {
|
||||||
|
title: string;
|
||||||
|
parsedEpisodeInfo: ParsedEpisodeInfo;
|
||||||
|
series?: Series;
|
||||||
|
episodes: Episode[];
|
||||||
|
languages?: Language[];
|
||||||
|
customFormats?: CustomFormat[];
|
||||||
|
customFormatScore?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseAppState = AppSectionItemState<ParseModel>;
|
||||||
|
|
||||||
|
export default ParseAppState;
|
|
@ -578,7 +578,7 @@ EnhancedSelectInput.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
disabledClassName: PropTypes.string,
|
disabledClassName: PropTypes.string,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -69,7 +69,7 @@ class QualityProfileSelectInputConnector extends Component {
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
onChange = ({ name, value }) => {
|
||||||
this.props.onChange({ name, value: parseInt(value) });
|
this.props.onChange({ name, value: value === 'noChange' ? value : parseInt(value) });
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -7,6 +7,7 @@ function ErrorPage(props) {
|
||||||
const {
|
const {
|
||||||
version,
|
version,
|
||||||
isLocalStorageSupported,
|
isLocalStorageSupported,
|
||||||
|
translationsError,
|
||||||
seriesError,
|
seriesError,
|
||||||
customFiltersError,
|
customFiltersError,
|
||||||
tagsError,
|
tagsError,
|
||||||
|
@ -19,6 +20,8 @@ function ErrorPage(props) {
|
||||||
|
|
||||||
if (!isLocalStorageSupported) {
|
if (!isLocalStorageSupported) {
|
||||||
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
|
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
|
||||||
|
} else if (translationsError) {
|
||||||
|
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
|
||||||
} else if (seriesError) {
|
} else if (seriesError) {
|
||||||
errorMessage = getErrorMessage(seriesError, 'Failed to load series from API');
|
errorMessage = getErrorMessage(seriesError, 'Failed to load series from API');
|
||||||
} else if (customFiltersError) {
|
} else if (customFiltersError) {
|
||||||
|
@ -49,6 +52,7 @@ function ErrorPage(props) {
|
||||||
ErrorPage.propTypes = {
|
ErrorPage.propTypes = {
|
||||||
version: PropTypes.string.isRequired,
|
version: PropTypes.string.isRequired,
|
||||||
isLocalStorageSupported: PropTypes.bool.isRequired,
|
isLocalStorageSupported: PropTypes.bool.isRequired,
|
||||||
|
translationsError: PropTypes.object,
|
||||||
seriesError: PropTypes.object,
|
seriesError: PropTypes.object,
|
||||||
customFiltersError: PropTypes.object,
|
customFiltersError: PropTypes.object,
|
||||||
tagsError: PropTypes.object,
|
tagsError: PropTypes.object,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
import { fetchSeries } from 'Store/Actions/seriesActions';
|
import { fetchSeries } from 'Store/Actions/seriesActions';
|
||||||
import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||||
|
@ -52,6 +52,7 @@ const selectIsPopulated = createSelector(
|
||||||
(state) => state.settings.languages.isPopulated,
|
(state) => state.settings.languages.isPopulated,
|
||||||
(state) => state.settings.importLists.isPopulated,
|
(state) => state.settings.importLists.isPopulated,
|
||||||
(state) => state.system.status.isPopulated,
|
(state) => state.system.status.isPopulated,
|
||||||
|
(state) => state.app.translations.isPopulated,
|
||||||
(
|
(
|
||||||
seriesIsPopulated,
|
seriesIsPopulated,
|
||||||
customFiltersIsPopulated,
|
customFiltersIsPopulated,
|
||||||
|
@ -60,7 +61,8 @@ const selectIsPopulated = createSelector(
|
||||||
qualityProfilesIsPopulated,
|
qualityProfilesIsPopulated,
|
||||||
languagesIsPopulated,
|
languagesIsPopulated,
|
||||||
importListsIsPopulated,
|
importListsIsPopulated,
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated,
|
||||||
|
translationsIsPopulated
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
seriesIsPopulated &&
|
seriesIsPopulated &&
|
||||||
|
@ -70,7 +72,8 @@ const selectIsPopulated = createSelector(
|
||||||
qualityProfilesIsPopulated &&
|
qualityProfilesIsPopulated &&
|
||||||
languagesIsPopulated &&
|
languagesIsPopulated &&
|
||||||
importListsIsPopulated &&
|
importListsIsPopulated &&
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated &&
|
||||||
|
translationsIsPopulated
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -84,6 +87,7 @@ const selectErrors = createSelector(
|
||||||
(state) => state.settings.languages.error,
|
(state) => state.settings.languages.error,
|
||||||
(state) => state.settings.importLists.error,
|
(state) => state.settings.importLists.error,
|
||||||
(state) => state.system.status.error,
|
(state) => state.system.status.error,
|
||||||
|
(state) => state.app.translations.error,
|
||||||
(
|
(
|
||||||
seriesError,
|
seriesError,
|
||||||
customFiltersError,
|
customFiltersError,
|
||||||
|
@ -92,7 +96,8 @@ const selectErrors = createSelector(
|
||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
languagesError,
|
languagesError,
|
||||||
importListsError,
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError,
|
||||||
|
translationsError
|
||||||
) => {
|
) => {
|
||||||
const hasError = !!(
|
const hasError = !!(
|
||||||
seriesError ||
|
seriesError ||
|
||||||
|
@ -102,7 +107,8 @@ const selectErrors = createSelector(
|
||||||
qualityProfilesError ||
|
qualityProfilesError ||
|
||||||
languagesError ||
|
languagesError ||
|
||||||
importListsError ||
|
importListsError ||
|
||||||
systemStatusError
|
systemStatusError ||
|
||||||
|
translationsError
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -114,7 +120,8 @@ const selectErrors = createSelector(
|
||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
languagesError,
|
languagesError,
|
||||||
importListsError,
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError,
|
||||||
|
translationsError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -173,6 +180,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||||
dispatchFetchStatus() {
|
dispatchFetchStatus() {
|
||||||
dispatch(fetchStatus());
|
dispatch(fetchStatus());
|
||||||
},
|
},
|
||||||
|
dispatchFetchTranslations() {
|
||||||
|
dispatch(fetchTranslations());
|
||||||
|
},
|
||||||
onResize(dimensions) {
|
onResize(dimensions) {
|
||||||
dispatch(saveDimensions(dimensions));
|
dispatch(saveDimensions(dimensions));
|
||||||
},
|
},
|
||||||
|
@ -205,6 +215,7 @@ class PageConnector extends Component {
|
||||||
this.props.dispatchFetchImportLists();
|
this.props.dispatchFetchImportLists();
|
||||||
this.props.dispatchFetchUISettings();
|
this.props.dispatchFetchUISettings();
|
||||||
this.props.dispatchFetchStatus();
|
this.props.dispatchFetchStatus();
|
||||||
|
this.props.dispatchFetchTranslations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +240,7 @@ class PageConnector extends Component {
|
||||||
dispatchFetchImportLists,
|
dispatchFetchImportLists,
|
||||||
dispatchFetchUISettings,
|
dispatchFetchUISettings,
|
||||||
dispatchFetchStatus,
|
dispatchFetchStatus,
|
||||||
|
dispatchFetchTranslations,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -268,6 +280,7 @@ PageConnector.propTypes = {
|
||||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchTranslations: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { isLocked } from 'Utilities/scrollLock';
|
||||||
import styles from './PageContentBody.css';
|
import styles from './PageContentBody.css';
|
||||||
|
|
||||||
interface PageContentBodyProps {
|
interface PageContentBodyProps {
|
||||||
className: string;
|
className?: string;
|
||||||
innerClassName: string;
|
innerClassName?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
initialScrollTop?: number;
|
initialScrollTop?: number;
|
||||||
onScroll?: (payload: OnScroll) => void;
|
onScroll?: (payload: OnScroll) => void;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector';
|
import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import MessagesConnector from './Messages/MessagesConnector';
|
import MessagesConnector from './Messages/MessagesConnector';
|
||||||
import PageSidebarItem from './PageSidebarItem';
|
import PageSidebarItem from './PageSidebarItem';
|
||||||
import styles from './PageSidebar.css';
|
import styles from './PageSidebar.css';
|
||||||
|
@ -20,16 +21,22 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
|
||||||
const links = [
|
const links = [
|
||||||
{
|
{
|
||||||
iconName: icons.SERIES_CONTINUING,
|
iconName: icons.SERIES_CONTINUING,
|
||||||
title: 'Series',
|
get title() {
|
||||||
|
return translate('Series');
|
||||||
|
},
|
||||||
to: '/',
|
to: '/',
|
||||||
alias: '/series',
|
alias: '/series',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Add New',
|
get title() {
|
||||||
|
return translate('AddNew');
|
||||||
|
},
|
||||||
to: '/add/new'
|
to: '/add/new'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Library Import',
|
get title() {
|
||||||
|
return translate('LibraryImport');
|
||||||
|
},
|
||||||
to: '/add/import'
|
to: '/add/import'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -37,26 +44,36 @@ const links = [
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.CALENDAR,
|
iconName: icons.CALENDAR,
|
||||||
title: 'Calendar',
|
get title() {
|
||||||
|
return translate('Calendar');
|
||||||
|
},
|
||||||
to: '/calendar'
|
to: '/calendar'
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.ACTIVITY,
|
iconName: icons.ACTIVITY,
|
||||||
title: 'Activity',
|
get title() {
|
||||||
|
return translate('Activity');
|
||||||
|
},
|
||||||
to: '/activity/queue',
|
to: '/activity/queue',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Queue',
|
get title() {
|
||||||
|
return translate('Queue');
|
||||||
|
},
|
||||||
to: '/activity/queue',
|
to: '/activity/queue',
|
||||||
statusComponent: QueueStatusConnector
|
statusComponent: QueueStatusConnector
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'History',
|
get title() {
|
||||||
|
return translate('History');
|
||||||
|
},
|
||||||
to: '/activity/history'
|
to: '/activity/history'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Blocklist',
|
get title() {
|
||||||
|
return translate('Blocklist');
|
||||||
|
},
|
||||||
to: '/activity/blocklist'
|
to: '/activity/blocklist'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -64,15 +81,21 @@ const links = [
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.WARNING,
|
iconName: icons.WARNING,
|
||||||
title: 'Wanted',
|
get title() {
|
||||||
|
return translate('Wanted');
|
||||||
|
},
|
||||||
to: '/wanted/missing',
|
to: '/wanted/missing',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Missing',
|
get title() {
|
||||||
|
return translate('Missing');
|
||||||
|
},
|
||||||
to: '/wanted/missing'
|
to: '/wanted/missing'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Cutoff Unmet',
|
get title() {
|
||||||
|
return translate('CutoffUnmet');
|
||||||
|
},
|
||||||
to: '/wanted/cutoffunmet'
|
to: '/wanted/cutoffunmet'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -80,59 +103,87 @@ const links = [
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.SETTINGS,
|
iconName: icons.SETTINGS,
|
||||||
title: 'Settings',
|
get title() {
|
||||||
|
return translate('Settings');
|
||||||
|
},
|
||||||
to: '/settings',
|
to: '/settings',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Media Management',
|
get title() {
|
||||||
|
return translate('MediaManagement');
|
||||||
|
},
|
||||||
to: '/settings/mediamanagement'
|
to: '/settings/mediamanagement'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Profiles',
|
get title() {
|
||||||
|
return translate('Profiles');
|
||||||
|
},
|
||||||
to: '/settings/profiles'
|
to: '/settings/profiles'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Quality',
|
get title() {
|
||||||
|
return translate('Quality');
|
||||||
|
},
|
||||||
to: '/settings/quality'
|
to: '/settings/quality'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Custom Formats',
|
get title() {
|
||||||
|
return translate('CustomFormats');
|
||||||
|
},
|
||||||
to: '/settings/customformats'
|
to: '/settings/customformats'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Indexers',
|
get title() {
|
||||||
|
return translate('Indexers');
|
||||||
|
},
|
||||||
to: '/settings/indexers'
|
to: '/settings/indexers'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Download Clients',
|
get title() {
|
||||||
|
return translate('DownloadClients');
|
||||||
|
},
|
||||||
to: '/settings/downloadclients'
|
to: '/settings/downloadclients'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Import Lists',
|
get title() {
|
||||||
|
return translate('ImportLists');
|
||||||
|
},
|
||||||
to: '/settings/importlists'
|
to: '/settings/importlists'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Connect',
|
get title() {
|
||||||
|
return translate('Connect');
|
||||||
|
},
|
||||||
to: '/settings/connect'
|
to: '/settings/connect'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Metadata',
|
get title() {
|
||||||
|
return translate('Metadata');
|
||||||
|
},
|
||||||
to: '/settings/metadata'
|
to: '/settings/metadata'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Metadata Source',
|
get title() {
|
||||||
|
return translate('MetadataSource');
|
||||||
|
},
|
||||||
to: '/settings/metadatasource'
|
to: '/settings/metadatasource'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tags',
|
get title() {
|
||||||
|
return translate('Tags');
|
||||||
|
},
|
||||||
to: '/settings/tags'
|
to: '/settings/tags'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'General',
|
get title() {
|
||||||
|
return translate('General');
|
||||||
|
},
|
||||||
to: '/settings/general'
|
to: '/settings/general'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'UI',
|
get title() {
|
||||||
|
return translate('UI');
|
||||||
|
},
|
||||||
to: '/settings/ui'
|
to: '/settings/ui'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -140,32 +191,46 @@ const links = [
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.SYSTEM,
|
iconName: icons.SYSTEM,
|
||||||
title: 'System',
|
get title() {
|
||||||
|
return translate('System');
|
||||||
|
},
|
||||||
to: '/system/status',
|
to: '/system/status',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Status',
|
get title() {
|
||||||
|
return translate('Status');
|
||||||
|
},
|
||||||
to: '/system/status',
|
to: '/system/status',
|
||||||
statusComponent: HealthStatusConnector
|
statusComponent: HealthStatusConnector
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tasks',
|
get title() {
|
||||||
|
return translate('Tasks');
|
||||||
|
},
|
||||||
to: '/system/tasks'
|
to: '/system/tasks'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Backup',
|
get title() {
|
||||||
|
return translate('Backup');
|
||||||
|
},
|
||||||
to: '/system/backup'
|
to: '/system/backup'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Updates',
|
get title() {
|
||||||
|
return translate('Updates');
|
||||||
|
},
|
||||||
to: '/system/updates'
|
to: '/system/updates'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Events',
|
get title() {
|
||||||
|
return translate('Events');
|
||||||
|
},
|
||||||
to: '/system/events'
|
to: '/system/events'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Log Files',
|
get title() {
|
||||||
|
return translate('LogFiles');
|
||||||
|
},
|
||||||
to: '/system/logs/files'
|
to: '/system/logs/files'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
faBookReader as fasBookReader,
|
faBookReader as fasBookReader,
|
||||||
faBroadcastTower as fasBroadcastTower,
|
faBroadcastTower as fasBroadcastTower,
|
||||||
faBug as fasBug,
|
faBug as fasBug,
|
||||||
|
faCalculator as fasCalculator,
|
||||||
faCalendarAlt as fasCalendarAlt,
|
faCalendarAlt as fasCalendarAlt,
|
||||||
faCaretDown as fasCaretDown,
|
faCaretDown as fasCaretDown,
|
||||||
faCheck as fasCheck,
|
faCheck as fasCheck,
|
||||||
|
@ -174,6 +175,7 @@ export const PAGE_PREVIOUS = fasBackward;
|
||||||
export const PAGE_NEXT = fasForward;
|
export const PAGE_NEXT = fasForward;
|
||||||
export const PAGE_LAST = fasFastForward;
|
export const PAGE_LAST = fasFastForward;
|
||||||
export const PARENT = fasLevelUpAlt;
|
export const PARENT = fasLevelUpAlt;
|
||||||
|
export const PARSE = fasCalculator;
|
||||||
export const PAUSED = fasPause;
|
export const PAUSED = fasPause;
|
||||||
export const PENDING = farClock;
|
export const PENDING = farClock;
|
||||||
export const PROFILE = fasUser;
|
export const PROFILE = fasUser;
|
||||||
|
|
|
@ -30,6 +30,8 @@ import {
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import Rejection from 'typings/Rejection';
|
import Rejection from 'typings/Rejection';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||||
import styles from './InteractiveImportRow.css';
|
import styles from './InteractiveImportRow.css';
|
||||||
|
|
||||||
|
@ -57,6 +59,7 @@ interface InteractiveImportRowProps {
|
||||||
languages?: Language[];
|
languages?: Language[];
|
||||||
size: number;
|
size: number;
|
||||||
customFormats?: object[];
|
customFormats?: object[];
|
||||||
|
customFormatScore?: number;
|
||||||
rejections: Rejection[];
|
rejections: Rejection[];
|
||||||
columns: Column[];
|
columns: Column[];
|
||||||
episodeFileId?: number;
|
episodeFileId?: number;
|
||||||
|
@ -80,6 +83,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
size,
|
size,
|
||||||
customFormats,
|
customFormats,
|
||||||
|
customFormatScore,
|
||||||
rejections,
|
rejections,
|
||||||
isReprocessing,
|
isReprocessing,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
@ -427,8 +431,8 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
{customFormats?.length ? (
|
{customFormats?.length ? (
|
||||||
<Popover
|
<Popover
|
||||||
anchor={<Icon name={icons.INTERACTIVE} />}
|
anchor={formatPreferredWordScore(customFormatScore)}
|
||||||
title="Formats"
|
title={translate('CustomFormats')}
|
||||||
body={
|
body={
|
||||||
<div className={styles.customFormatTooltip}>
|
<div className={styles.customFormatTooltip}>
|
||||||
<EpisodeFormats formats={customFormats} />
|
<EpisodeFormats formats={customFormats} />
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
.inputContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputIconContainer {
|
||||||
|
width: 58px;
|
||||||
|
height: 46px;
|
||||||
|
border: 1px solid var(--inputBorderColor);
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
background-color: var(--inputIconContainerBackgroundColor);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
composes: input from '~Components/Form/TextInput.css';
|
||||||
|
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearButton {
|
||||||
|
border: 1px solid var(--inputBorderColor);
|
||||||
|
border-left: none;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: $largeFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'clearButton': string;
|
||||||
|
'helpText': string;
|
||||||
|
'input': string;
|
||||||
|
'inputContainer': string;
|
||||||
|
'inputIconContainer': string;
|
||||||
|
'message': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,111 @@
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { clear, fetch } from 'Store/Actions/parseActions';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import ParseResult from './ParseResult';
|
||||||
|
import parseStateSelector from './parseStateSelector';
|
||||||
|
import styles from './Parse.css';
|
||||||
|
|
||||||
|
function Parse() {
|
||||||
|
const { isFetching, error, item } = useSelector(parseStateSelector());
|
||||||
|
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
const trimmedValue = value.trim();
|
||||||
|
|
||||||
|
setTitle(value);
|
||||||
|
|
||||||
|
if (trimmedValue === '') {
|
||||||
|
dispatch(clear());
|
||||||
|
} else {
|
||||||
|
dispatch(fetch({ title: trimmedValue }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setTitle, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClearPress = useCallback(() => {
|
||||||
|
setTitle('');
|
||||||
|
dispatch(clear());
|
||||||
|
}, [setTitle, dispatch]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
return () => {
|
||||||
|
dispatch(clear());
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title="Parse">
|
||||||
|
<PageContentBody>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<div className={styles.inputIconContainer}>
|
||||||
|
<Icon name={icons.PARSE} size={20} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
className={styles.input}
|
||||||
|
name="title"
|
||||||
|
value={title}
|
||||||
|
placeholder="eg. Series.Title.S01E05.720p.HDTV-RlsGroup"
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button className={styles.clearButton} onPress={onClearPress}>
|
||||||
|
<Icon name={icons.REMOVE} size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && !!error ? (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.helpText}>
|
||||||
|
Error parsing, please try again.
|
||||||
|
</div>
|
||||||
|
<div>{getErrorMessage(error)}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isFetching && title && !error && !item.parsedEpisodeInfo ? (
|
||||||
|
<div className={styles.message}>
|
||||||
|
Unable to parse the provided title, please try again.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isFetching && !error && item.parsedEpisodeInfo ? (
|
||||||
|
<ParseResult item={item} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{title ? null : (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.helpText}>
|
||||||
|
Enter a release title in the input above
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Sonarr will attempt to parse the title and show you details about
|
||||||
|
it
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Parse;
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ParseModalContent from './ParseModalContent';
|
||||||
|
|
||||||
|
interface ParseModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParseModal(props: ParseModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<ParseModalContent onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ParseModal;
|
|
@ -0,0 +1,45 @@
|
||||||
|
.inputContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputIconContainer {
|
||||||
|
width: 58px;
|
||||||
|
height: 46px;
|
||||||
|
border: 1px solid var(--inputBorderColor);
|
||||||
|
border-right: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
background-color: var(--inputIconContainerBackgroundColor);
|
||||||
|
text-align: center;
|
||||||
|
line-height: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
composes: input from '~Components/Form/TextInput.css';
|
||||||
|
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearButton {
|
||||||
|
border: 1px solid var(--inputBorderColor);
|
||||||
|
border-left: none;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: $largeFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'clearButton': string;
|
||||||
|
'helpText': string;
|
||||||
|
'input': string;
|
||||||
|
'inputContainer': string;
|
||||||
|
'inputIconContainer': string;
|
||||||
|
'message': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,125 @@
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { clear, fetch } from 'Store/Actions/parseActions';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import ParseResult from './ParseResult';
|
||||||
|
import parseStateSelector from './parseStateSelector';
|
||||||
|
import styles from './ParseModalContent.css';
|
||||||
|
|
||||||
|
interface ParseModalContentProps {
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParseModalContent(props: ParseModalContentProps) {
|
||||||
|
const { onModalClose } = props;
|
||||||
|
const { isFetching, error, item } = useSelector(parseStateSelector());
|
||||||
|
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const onInputChange = useCallback(
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
const trimmedValue = value.trim();
|
||||||
|
|
||||||
|
setTitle(value);
|
||||||
|
|
||||||
|
if (trimmedValue === '') {
|
||||||
|
dispatch(clear());
|
||||||
|
} else {
|
||||||
|
dispatch(fetch({ title: trimmedValue }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setTitle, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClearPress = useCallback(() => {
|
||||||
|
setTitle('');
|
||||||
|
dispatch(clear());
|
||||||
|
}, [setTitle, dispatch]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
return () => {
|
||||||
|
dispatch(clear());
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('TestParsing')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.inputContainer}>
|
||||||
|
<div className={styles.inputIconContainer}>
|
||||||
|
<Icon name={icons.PARSE} size={20} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
className={styles.input}
|
||||||
|
name="title"
|
||||||
|
value={title}
|
||||||
|
placeholder="eg. Series.Title.S01E05.720p.HDTV-RlsGroup"
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button className={styles.clearButton} onPress={onClearPress}>
|
||||||
|
<Icon name={icons.REMOVE} size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && !!error ? (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.helpText}>
|
||||||
|
Error parsing, please try again.
|
||||||
|
</div>
|
||||||
|
<div>{getErrorMessage(error)}</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isFetching && title && !error && !item.parsedEpisodeInfo ? (
|
||||||
|
<div className={styles.message}>
|
||||||
|
Unable to parse the provided title, please try again.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isFetching && !error && item.parsedEpisodeInfo ? (
|
||||||
|
<ParseResult item={item} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{title ? null : (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.helpText}>
|
||||||
|
Enter a release title in the input above
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Sonarr will attempt to parse the title and show you details about
|
||||||
|
it
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ParseModalContent;
|
|
@ -0,0 +1,8 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'column': string;
|
||||||
|
'container': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,243 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ParseModel } from 'App/State/ParseAppState';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import ParseResultItem from './ParseResultItem';
|
||||||
|
import styles from './ParseResult.css';
|
||||||
|
|
||||||
|
interface ParseResultProps {
|
||||||
|
item: ParseModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParseResult(props: ParseResultProps) {
|
||||||
|
const { item } = props;
|
||||||
|
const {
|
||||||
|
customFormats,
|
||||||
|
customFormatScore,
|
||||||
|
episodes,
|
||||||
|
languages,
|
||||||
|
parsedEpisodeInfo,
|
||||||
|
series,
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
const {
|
||||||
|
releaseTitle,
|
||||||
|
seriesTitle,
|
||||||
|
seriesTitleInfo,
|
||||||
|
releaseGroup,
|
||||||
|
releaseHash,
|
||||||
|
seasonNumber,
|
||||||
|
episodeNumbers,
|
||||||
|
absoluteEpisodeNumbers,
|
||||||
|
special,
|
||||||
|
fullSeason,
|
||||||
|
isMultiSeason,
|
||||||
|
isPartialSeason,
|
||||||
|
isDaily,
|
||||||
|
airDate,
|
||||||
|
quality,
|
||||||
|
} = parsedEpisodeInfo;
|
||||||
|
|
||||||
|
const finalLanguages = languages ?? parsedEpisodeInfo.languages;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FieldSet legend={translate('Release')}>
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('ReleaseTitle')}
|
||||||
|
data={releaseTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem title={translate('SeriesTitle')} data={seriesTitle} />
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('Year')}
|
||||||
|
data={seriesTitleInfo.year > 0 ? seriesTitleInfo.year : '-'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('AllTitles')}
|
||||||
|
data={
|
||||||
|
seriesTitleInfo.allTitles?.length > 0
|
||||||
|
? seriesTitleInfo.allTitles.join(', ')
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('ReleaseGroup')}
|
||||||
|
data={releaseGroup ?? '-'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('ReleaseHash')}
|
||||||
|
data={releaseHash ? releaseHash : '-'}
|
||||||
|
/>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('EpisodeInfo')}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.column}>
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('SeasonNumber')}
|
||||||
|
data={
|
||||||
|
seasonNumber === 0 && absoluteEpisodeNumbers.length
|
||||||
|
? '-'
|
||||||
|
: seasonNumber
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('EpisodeNumbers')}
|
||||||
|
data={episodeNumbers.join(', ') || '-'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('AbsoluteEpisodeNumbers')}
|
||||||
|
data={
|
||||||
|
absoluteEpisodeNumbers.length
|
||||||
|
? absoluteEpisodeNumbers.join(', ')
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('Daily')}
|
||||||
|
data={isDaily ? 'True' : 'False'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('AirDate')}
|
||||||
|
data={airDate ?? '-'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.column}>
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('Special')}
|
||||||
|
data={special ? 'True' : 'False'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('FullSeason')}
|
||||||
|
data={fullSeason ? 'True' : 'False'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('MultiSeason')}
|
||||||
|
data={isMultiSeason ? 'True' : 'False'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('PartialSeason')}
|
||||||
|
data={isPartialSeason ? 'True' : 'False'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('Quality')}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.column}>
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('Quality')}
|
||||||
|
data={quality.quality.name}
|
||||||
|
/>
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('Proper')}
|
||||||
|
data={
|
||||||
|
quality.revision.version > 1 && !quality.revision.isRepack
|
||||||
|
? 'True'
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('Repack')}
|
||||||
|
data={quality.revision.isRepack ? 'True' : '-'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.column}>
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('Version')}
|
||||||
|
data={
|
||||||
|
quality.revision.version > 1 ? quality.revision.version : '-'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('Real')}
|
||||||
|
data={quality.revision.real ? 'True' : '-'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('Languages')}>
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('Languages')}
|
||||||
|
data={finalLanguages.map((l) => l.name).join(', ')}
|
||||||
|
/>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('Details')}>
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('MatchedToSeries')}
|
||||||
|
data={
|
||||||
|
series ? (
|
||||||
|
<SeriesTitleLink
|
||||||
|
titleSlug={series.titleSlug}
|
||||||
|
title={series.title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('MatchedToSeason')}
|
||||||
|
data={episodes.length ? episodes[0].seasonNumber : '-'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('MatchedToEpisodes')}
|
||||||
|
data={
|
||||||
|
episodes.length ? (
|
||||||
|
<div>
|
||||||
|
{episodes.map((e) => {
|
||||||
|
return (
|
||||||
|
<div key={e.id}>
|
||||||
|
{e.episodeNumber}
|
||||||
|
{series?.seriesType === 'anime' && e.absoluteEpisodeNumber
|
||||||
|
? ` (${e.absoluteEpisodeNumber})`
|
||||||
|
: ''}{' '}
|
||||||
|
{` - ${e.title}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('CustomFormats')}
|
||||||
|
data={<EpisodeFormats formats={customFormats} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseResultItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={customFormatScore}
|
||||||
|
/>
|
||||||
|
</FieldSet>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ParseResult;
|
|
@ -0,0 +1,21 @@
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-right: 20px;
|
||||||
|
width: 250px;
|
||||||
|
text-align: right;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $breakpointSmall) {
|
||||||
|
.item {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'item': string;
|
||||||
|
'title': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import styles from './ParseResultItem.css';
|
||||||
|
|
||||||
|
interface ParseResultItemProps {
|
||||||
|
title: string;
|
||||||
|
data: string | number | ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParseResultItem(props: ParseResultItemProps) {
|
||||||
|
const { title, data } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.item}>
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
<div>{data}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ParseResultItem;
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { Fragment, useCallback, useState } from 'react';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import ParseModal from 'Parse/ParseModal';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function ParseToolbarButton() {
|
||||||
|
const [isParseModalOpen, setIsParseModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const onOpenParseModalPress = useCallback(() => {
|
||||||
|
setIsParseModalOpen(true);
|
||||||
|
}, [setIsParseModalOpen]);
|
||||||
|
|
||||||
|
const onParseModalClose = useCallback(() => {
|
||||||
|
setIsParseModalOpen(false);
|
||||||
|
}, [setIsParseModalOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('TestParsing')}
|
||||||
|
iconName={icons.PARSE}
|
||||||
|
onPress={onOpenParseModalPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ParseModal isOpen={isParseModalOpen} onModalClose={onParseModalClose} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ParseToolbarButton;
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import ParseAppState from 'App/State/ParseAppState';
|
||||||
|
|
||||||
|
export default function parseStateSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.parse,
|
||||||
|
(parse: ParseAppState) => {
|
||||||
|
return parse;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -56,3 +56,9 @@
|
||||||
|
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.customFormatScore {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'audio': string;
|
'audio': string;
|
||||||
'audioLanguages': string;
|
'audioLanguages': string;
|
||||||
|
'customFormatScore': string;
|
||||||
'episodeNumber': string;
|
'episodeNumber': string;
|
||||||
'episodeNumberAnime': string;
|
'episodeNumberAnime': string;
|
||||||
'languages': string;
|
'languages': string;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
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 Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeNumber from 'Episode/EpisodeNumber';
|
import EpisodeNumber from 'Episode/EpisodeNumber';
|
||||||
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
|
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
|
||||||
|
@ -12,7 +13,9 @@ import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
||||||
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
|
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
|
||||||
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
|
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
|
||||||
|
import { tooltipPositions } from 'Helpers/Props';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
||||||
import formatRuntime from 'Utilities/Number/formatRuntime';
|
import formatRuntime from 'Utilities/Number/formatRuntime';
|
||||||
import styles from './EpisodeRow.css';
|
import styles from './EpisodeRow.css';
|
||||||
|
|
||||||
|
@ -72,6 +75,7 @@ class EpisodeRow extends Component {
|
||||||
episodeFileSize,
|
episodeFileSize,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
customFormats,
|
customFormats,
|
||||||
|
customFormatScore,
|
||||||
alternateTitles,
|
alternateTitles,
|
||||||
columns
|
columns
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -193,6 +197,24 @@ class EpisodeRow extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.customFormatScore}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatPreferredWordScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'languages') {
|
if (name === 'languages') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
@ -355,6 +377,7 @@ EpisodeRow.propTypes = {
|
||||||
episodeFileSize: PropTypes.number,
|
episodeFileSize: PropTypes.number,
|
||||||
releaseGroup: PropTypes.string,
|
releaseGroup: PropTypes.string,
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
customFormatScore: PropTypes.number.isRequired,
|
||||||
mediaInfo: PropTypes.object,
|
mediaInfo: PropTypes.object,
|
||||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|
|
@ -19,6 +19,7 @@ function createMapStateToProps() {
|
||||||
episodeFileSize: episodeFile ? episodeFile.size : null,
|
episodeFileSize: episodeFile ? episodeFile.size : null,
|
||||||
releaseGroup: episodeFile ? episodeFile.releaseGroup : null,
|
releaseGroup: episodeFile ? episodeFile.releaseGroup : null,
|
||||||
customFormats: episodeFile ? episodeFile.customFormats : [],
|
customFormats: episodeFile ? episodeFile.customFormats : [],
|
||||||
|
customFormatScore: episodeFile ? episodeFile.customFormatScore : 0,
|
||||||
alternateTitles: series.alternateTitles
|
alternateTitles: series.alternateTitles
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -248,6 +248,8 @@ class SeriesDetails extends Component {
|
||||||
expandIcon = icons.EXPAND;
|
expandIcon = icons.EXPAND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fanartUrl = getFanartUrl(images);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title={title}>
|
<PageContent title={title}>
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
|
@ -327,9 +329,11 @@ class SeriesDetails extends Component {
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div
|
<div
|
||||||
className={styles.backdrop}
|
className={styles.backdrop}
|
||||||
style={{
|
style={
|
||||||
backgroundImage: `url(${getFanartUrl(images)})`
|
fanartUrl ?
|
||||||
}}
|
{ backgroundImage: `url(${fanartUrl})` } :
|
||||||
|
null
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className={styles.backdropOverlay} />
|
<div className={styles.backdropOverlay} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -235,7 +235,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
|
||||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button onPress={onSavePressWrapper}>
|
<Button onPress={onSavePressWrapper}>
|
||||||
{translate('Apply Changes')}
|
{translate('ApplyChanges')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
import Series from 'Series/Series';
|
import Series from 'Series/Series';
|
||||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './TagsModalContent.css';
|
import styles from './TagsModalContent.css';
|
||||||
|
|
||||||
interface TagsModalContentProps {
|
interface TagsModalContentProps {
|
||||||
|
@ -73,12 +74,12 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>Tags</ModalHeader>
|
<ModalHeader>{translate('Tags')}</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Tags</FormLabel>
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
|
@ -89,7 +90,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Apply Tags</FormLabel>
|
<FormLabel>{translate('ApplyTags')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
|
@ -97,17 +98,17 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
value={applyTags}
|
value={applyTags}
|
||||||
values={applyTagsOptions}
|
values={applyTagsOptions}
|
||||||
helpTexts={[
|
helpTexts={[
|
||||||
'How to apply tags to the selected series',
|
translate('ApplyTagsHelpTextHowToApplySeries'),
|
||||||
'Add: Add the tags the existing list of tags',
|
translate('ApplyTagsHelpTextAdd'),
|
||||||
'Remove: Remove the entered tags',
|
translate('ApplyTagsHelpTextRemove'),
|
||||||
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
|
translate('ApplyTagsHelpTextReplace'),
|
||||||
]}
|
]}
|
||||||
onChange={onApplyTagsChange}
|
onChange={onApplyTagsChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Result</FormLabel>
|
<FormLabel>{translate('Result')}</FormLabel>
|
||||||
|
|
||||||
<div className={styles.result}>
|
<div className={styles.result}>
|
||||||
{seriesTags.map((id) => {
|
{seriesTags.map((id) => {
|
||||||
|
@ -124,7 +125,11 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
title={removeTag ? 'Removing tag' : 'Existing tag'}
|
title={
|
||||||
|
removeTag
|
||||||
|
? translate('RemovingTag')
|
||||||
|
: translate('ExistingTag')
|
||||||
|
}
|
||||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
|
@ -148,7 +153,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
title={'Adding tag'}
|
title={translate('AddingTag')}
|
||||||
kind={kinds.SUCCESS}
|
kind={kinds.SUCCESS}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
|
@ -162,10 +167,10 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>Cancel</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||||
Apply
|
{translate('Apply')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -23,6 +23,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
||||||
import NoSeries from 'Series/NoSeries';
|
import NoSeries from 'Series/NoSeries';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { fetchQueueDetails } from 'Store/Actions/queueActions';
|
import { fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
|
@ -246,6 +247,9 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
||||||
isSelectMode={isSelectMode}
|
isSelectMode={isSelectMode}
|
||||||
overflowComponent={SeriesIndexSelectAllMenuItem}
|
overflowComponent={SeriesIndexSelectAllMenuItem}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
<ParseToolbarButton />
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
<PageToolbarSection
|
<PageToolbarSection
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { DndProvider } from 'react-dnd';
|
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
|
||||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
|
||||||
|
|
||||||
class CustomFormatSettingsConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PageContent title="Custom Format Settings">
|
|
||||||
<SettingsToolbarConnector
|
|
||||||
showSave={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
<DndProvider backend={HTML5Backend}>
|
|
||||||
<CustomFormatsConnector />
|
|
||||||
</DndProvider>
|
|
||||||
</PageContentBody>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CustomFormatSettingsConnector;
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import { DndProvider } from 'react-dnd';
|
||||||
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import ParseToolbarButton from 'Parse/ParseToolbarButton';
|
||||||
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
|
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||||
|
|
||||||
|
function CustomFormatSettingsPage() {
|
||||||
|
return (
|
||||||
|
<PageContent title="Custom Format Settings">
|
||||||
|
<SettingsToolbarConnector
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
showSave={false}
|
||||||
|
additionalButtons={
|
||||||
|
<Fragment>
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<ParseToolbarButton />
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{/* TODO: Upgrade react-dnd to get typings, we're 2 major versions behind */}
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<DndProvider backend={HTML5Backend}>
|
||||||
|
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<CustomFormatsConnector />
|
||||||
|
</DndProvider>
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomFormatSettingsPage;
|
|
@ -6,6 +6,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
|
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
|
||||||
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
|
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
|
||||||
import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector';
|
import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector';
|
||||||
|
@ -85,7 +86,7 @@ class DownloadClientSettings extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Manage Clients"
|
label={translate('ManageClients')}
|
||||||
iconName={icons.MANAGE}
|
iconName={icons.MANAGE}
|
||||||
onPress={this.onManageDownloadClientsPress}
|
onPress={this.onManageDownloadClientsPress}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||||
import Card from 'Components/Card';
|
import Card from 'Components/Card';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TagList from 'Components/TagList';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import EditDownloadClientModalConnector from './EditDownloadClientModalConnector';
|
import EditDownloadClientModalConnector from './EditDownloadClientModalConnector';
|
||||||
import styles from './DownloadClient.css';
|
import styles from './DownloadClient.css';
|
||||||
|
@ -55,7 +56,9 @@ class DownloadClient extends Component {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
enable,
|
enable,
|
||||||
priority
|
priority,
|
||||||
|
tags,
|
||||||
|
tagList
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -93,6 +96,11 @@ class DownloadClient extends Component {
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TagList
|
||||||
|
tags={tags}
|
||||||
|
tagList={tagList}
|
||||||
|
/>
|
||||||
|
|
||||||
<EditDownloadClientModalConnector
|
<EditDownloadClientModalConnector
|
||||||
id={id}
|
id={id}
|
||||||
isOpen={this.state.isEditDownloadClientModalOpen}
|
isOpen={this.state.isEditDownloadClientModalOpen}
|
||||||
|
@ -119,6 +127,8 @@ DownloadClient.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
enable: PropTypes.bool.isRequired,
|
enable: PropTypes.bool.isRequired,
|
||||||
priority: PropTypes.number.isRequired,
|
priority: PropTypes.number.isRequired,
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ class DownloadClients extends Component {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
onConfirmDeleteDownloadClient,
|
onConfirmDeleteDownloadClient,
|
||||||
|
tagList,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -70,6 +71,7 @@ class DownloadClients extends Component {
|
||||||
<DownloadClient
|
<DownloadClient
|
||||||
key={item.id}
|
key={item.id}
|
||||||
{...item}
|
{...item}
|
||||||
|
tagList={tagList}
|
||||||
onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient}
|
onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -108,6 +110,7 @@ DownloadClients.propTypes = {
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,13 +4,20 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
|
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import DownloadClients from './DownloadClients';
|
import DownloadClients from './DownloadClients';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector('settings.downloadClients', sortByName),
|
createSortedSectionSelector('settings.downloadClients', sortByName),
|
||||||
(downloadClients) => downloadClients
|
createTagsSelector(),
|
||||||
|
(downloadClients, tagList) => {
|
||||||
|
return {
|
||||||
|
...downloadClients,
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,7 @@ class EditDownloadClientModalContent extends Component {
|
||||||
removeCompletedDownloads,
|
removeCompletedDownloads,
|
||||||
removeFailedDownloads,
|
removeFailedDownloads,
|
||||||
fields,
|
fields,
|
||||||
|
tags,
|
||||||
message
|
message
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
|
@ -137,6 +138,18 @@ class EditDownloadClientModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Tags</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
helpText="Only use this download client for series with at least one matching tag. Leave blank to use with all series."
|
||||||
|
{...tags}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FieldSet
|
<FieldSet
|
||||||
size={sizes.SMALL}
|
size={sizes.SMALL}
|
||||||
legend="Completed Download Handling"
|
legend="Completed Download Handling"
|
||||||
|
|
|
@ -27,9 +27,25 @@ interface ManageDownloadClientsEditModalContentProps {
|
||||||
const NO_CHANGE = 'noChange';
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
const enableOptions = [
|
const enableOptions = [
|
||||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
{
|
||||||
{ key: 'enabled', value: 'Enabled' },
|
key: NO_CHANGE,
|
||||||
{ key: 'disabled', value: 'Disabled' },
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Enabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Disabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function ManageDownloadClientsEditModalContent(
|
function ManageDownloadClientsEditModalContent(
|
||||||
|
@ -97,7 +113,9 @@ function ManageDownloadClientsEditModalContent(
|
||||||
setRemoveFailedDownloads(value);
|
setRemoveFailedDownloads(value);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn('EditDownloadClientsModalContent Unknown Input');
|
console.warn(
|
||||||
|
`EditDownloadClientsModalContent Unknown Input: '${name}'`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
|
@ -162,7 +180,7 @@ function ManageDownloadClientsEditModalContent(
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
<ModalFooter className={styles.modalFooter}>
|
||||||
<div className={styles.selected}>
|
<div className={styles.selected}>
|
||||||
{translate('{count} download clients selected', {
|
{translate('CountDownloadClientsSelected', {
|
||||||
count: selectedCount,
|
count: selectedCount,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -170,7 +188,7 @@ function ManageDownloadClientsEditModalContent(
|
||||||
<div>
|
<div>
|
||||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button onPress={save}>{translate('Apply Changes')}</Button>
|
<Button onPress={save}>{translate('ApplyChanges')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
@ -20,9 +21,11 @@ import {
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
import ManageDownloadClientsEditModal from './Edit/ManageDownloadClientsEditModal';
|
import ManageDownloadClientsEditModal from './Edit/ManageDownloadClientsEditModal';
|
||||||
import ManageDownloadClientsModalRow from './ManageDownloadClientsModalRow';
|
import ManageDownloadClientsModalRow from './ManageDownloadClientsModalRow';
|
||||||
|
import TagsModal from './Tags/TagsModal';
|
||||||
import styles from './ManageDownloadClientsModalContent.css';
|
import styles from './ManageDownloadClientsModalContent.css';
|
||||||
|
|
||||||
// TODO: This feels janky to do, but not sure of a better way currently
|
// TODO: This feels janky to do, but not sure of a better way currently
|
||||||
|
@ -33,37 +36,55 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
get label() {
|
||||||
|
return translate('Name');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'implementation',
|
name: 'implementation',
|
||||||
label: 'Implementation',
|
get label() {
|
||||||
|
return translate('Implementation');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enable',
|
name: 'enable',
|
||||||
label: 'Enabled',
|
get label() {
|
||||||
|
return translate('Enabled');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'priority',
|
name: 'priority',
|
||||||
label: 'Priority',
|
get label() {
|
||||||
|
return translate('Priority');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'removeCompletedDownloads',
|
name: 'removeCompletedDownloads',
|
||||||
label: 'Remove Completed',
|
get label() {
|
||||||
|
return translate('RemoveCompleted');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'removeFailedDownloads',
|
name: 'removeFailedDownloads',
|
||||||
label: 'Remove Failed',
|
get label() {
|
||||||
|
return translate('RemoveFailed');
|
||||||
|
},
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
label: 'Tags',
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
|
@ -92,6 +113,8 @@ function ManageDownloadClientsModalContent(
|
||||||
|
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
|
||||||
|
const [isSavingTags, setIsSavingTags] = useState(false);
|
||||||
|
|
||||||
const [selectState, setSelectState] = useSelectState();
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
|
||||||
|
@ -138,6 +161,30 @@ function ManageDownloadClientsModalContent(
|
||||||
[selectedIds, dispatch]
|
[selectedIds, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onTagsPress = useCallback(() => {
|
||||||
|
setIsTagsModalOpen(true);
|
||||||
|
}, [setIsTagsModalOpen]);
|
||||||
|
|
||||||
|
const onTagsModalClose = useCallback(() => {
|
||||||
|
setIsTagsModalOpen(false);
|
||||||
|
}, [setIsTagsModalOpen]);
|
||||||
|
|
||||||
|
const onApplyTagsPress = useCallback(
|
||||||
|
(tags: number[], applyTags: string) => {
|
||||||
|
setIsSavingTags(true);
|
||||||
|
setIsTagsModalOpen(false);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
bulkEditDownloadClients({
|
||||||
|
ids: selectedIds,
|
||||||
|
tags,
|
||||||
|
applyTags,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[selectedIds, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
const onSelectAllChange = useCallback(
|
const onSelectAllChange = useCallback(
|
||||||
({ value }: SelectStateInputProps) => {
|
({ value }: SelectStateInputProps) => {
|
||||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
|
@ -158,17 +205,24 @@ function ManageDownloadClientsModalContent(
|
||||||
[items, setSelectState]
|
[items, setSelectState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error, 'Unable to load import lists.');
|
const errorMessage = getErrorMessage(
|
||||||
|
error,
|
||||||
|
'Unable to load download clients.'
|
||||||
|
);
|
||||||
const anySelected = selectedCount > 0;
|
const anySelected = selectedCount > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>Manage Import Lists</ModalHeader>
|
<ModalHeader>{translate('ManageDownloadClients')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{isFetching ? <LoadingIndicator /> : null}
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !items.length && (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoDownloadClientsFound')}</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
<Table
|
<Table
|
||||||
columns={COLUMNS}
|
columns={COLUMNS}
|
||||||
|
@ -203,7 +257,7 @@ function ManageDownloadClientsModalContent(
|
||||||
isDisabled={!anySelected}
|
isDisabled={!anySelected}
|
||||||
onPress={onDeletePress}
|
onPress={onDeletePress}
|
||||||
>
|
>
|
||||||
Delete
|
{translate('Delete')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
|
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
|
@ -211,11 +265,19 @@ function ManageDownloadClientsModalContent(
|
||||||
isDisabled={!anySelected}
|
isDisabled={!anySelected}
|
||||||
onPress={onEditPress}
|
onPress={onEditPress}
|
||||||
>
|
>
|
||||||
Edit
|
{translate('Edit')}
|
||||||
|
</SpinnerButton>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
isSpinning={isSaving && isSavingTags}
|
||||||
|
isDisabled={!anySelected}
|
||||||
|
onPress={onTagsPress}
|
||||||
|
>
|
||||||
|
Set Tags
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onPress={onModalClose}>Close</Button>
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|
||||||
<ManageDownloadClientsEditModal
|
<ManageDownloadClientsEditModal
|
||||||
|
@ -225,12 +287,21 @@ function ManageDownloadClientsModalContent(
|
||||||
downloadClientIds={selectedIds}
|
downloadClientIds={selectedIds}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TagsModal
|
||||||
|
isOpen={isTagsModalOpen}
|
||||||
|
ids={selectedIds}
|
||||||
|
onApplyTagsPress={onApplyTagsPress}
|
||||||
|
onModalClose={onTagsModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title="Delete Download Clients(s)"
|
title={translate('DeleteSelectedDownloadClients')}
|
||||||
message={`Are you sure you want to delete ${selectedIds.length} download clients(s)?`}
|
message={translate('DeleteSelectedDownloadClientsMessageText', {
|
||||||
confirmLabel="Delete"
|
count: selectedIds.length,
|
||||||
|
})}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={onConfirmDelete}
|
onConfirm={onConfirmDelete}
|
||||||
onCancel={onDeleteModalClose}
|
onCancel={onDeleteModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ManageDownloadClientsModalRow.css';
|
import styles from './ManageDownloadClientsModalRow.css';
|
||||||
|
|
||||||
interface ManageDownloadClientsModalRowProps {
|
interface ManageDownloadClientsModalRowProps {
|
||||||
|
@ -14,6 +18,7 @@ interface ManageDownloadClientsModalRowProps {
|
||||||
removeCompletedDownloads: boolean;
|
removeCompletedDownloads: boolean;
|
||||||
removeFailedDownloads: boolean;
|
removeFailedDownloads: boolean;
|
||||||
implementation: string;
|
implementation: string;
|
||||||
|
tags: number[];
|
||||||
columns: Column[];
|
columns: Column[];
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onSelectedChange(result: SelectStateInputProps): void;
|
onSelectedChange(result: SelectStateInputProps): void;
|
||||||
|
@ -31,6 +36,7 @@ function ManageDownloadClientsModalRow(
|
||||||
removeCompletedDownloads,
|
removeCompletedDownloads,
|
||||||
removeFailedDownloads,
|
removeFailedDownloads,
|
||||||
implementation,
|
implementation,
|
||||||
|
tags,
|
||||||
onSelectedChange,
|
onSelectedChange,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -58,17 +64,23 @@ function ManageDownloadClientsModalRow(
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.enable}>
|
<TableRowCell className={styles.enable}>
|
||||||
{enable ? 'Yes' : 'No'}
|
<Label kind={enable ? kinds.SUCCESS : kinds.DISABLED} outline={!enable}>
|
||||||
|
{enable ? translate('Yes') : translate('No')}
|
||||||
|
</Label>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.priority}>{priority}</TableRowCell>
|
<TableRowCell className={styles.priority}>{priority}</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.removeCompletedDownloads}>
|
<TableRowCell className={styles.removeCompletedDownloads}>
|
||||||
{removeCompletedDownloads ? 'Yes' : 'No'}
|
{removeCompletedDownloads ? translate('Yes') : translate('No')}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.removeFailedDownloads}>
|
<TableRowCell className={styles.removeFailedDownloads}>
|
||||||
{removeFailedDownloads ? 'Yes' : 'No'}
|
{removeFailedDownloads ? translate('Yes') : translate('No')}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.tags}>
|
||||||
|
<TagListConnector tags={tags} />
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import TagsModalContent from './TagsModalContent';
|
||||||
|
|
||||||
|
interface TagsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
ids: number[];
|
||||||
|
onApplyTagsPress: (tags: number[], applyTags: string) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagsModal(props: TagsModalProps) {
|
||||||
|
const { isOpen, onModalClose, ...otherProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<TagsModalContent {...otherProps} onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagsModal;
|
|
@ -0,0 +1,12 @@
|
||||||
|
.renameIcon {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
9
frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.css.d.ts
vendored
Normal file
9
frontend/src/Settings/DownloadClients/DownloadClients/Manage/Tags/TagsModalContent.css.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'message': string;
|
||||||
|
'renameIcon': string;
|
||||||
|
'result': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,200 @@
|
||||||
|
import { uniq } from 'lodash';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||||
|
import { Tag } from 'App/State/TagsAppState';
|
||||||
|
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 Label from 'Components/Label';
|
||||||
|
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, sizes } from 'Helpers/Props';
|
||||||
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './TagsModalContent.css';
|
||||||
|
|
||||||
|
interface TagsModalContentProps {
|
||||||
|
ids: number[];
|
||||||
|
onApplyTagsPress: (tags: number[], applyTags: string) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TagsModalContent(props: TagsModalContentProps) {
|
||||||
|
const { ids, onModalClose, onApplyTagsPress } = props;
|
||||||
|
|
||||||
|
const allDownloadClients: DownloadClientAppState = useSelector(
|
||||||
|
(state: AppState) => state.settings.downloadClients
|
||||||
|
);
|
||||||
|
const tagList: Tag[] = useSelector(createTagsSelector());
|
||||||
|
|
||||||
|
const [tags, setTags] = useState<number[]>([]);
|
||||||
|
const [applyTags, setApplyTags] = useState('add');
|
||||||
|
|
||||||
|
const downloadClientsTags = useMemo(() => {
|
||||||
|
const tags = ids.reduce((acc: number[], id) => {
|
||||||
|
const s = allDownloadClients.items.find(
|
||||||
|
(s: DownloadClient) => s.id === id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (s) {
|
||||||
|
acc.push(...s.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return uniq(tags);
|
||||||
|
}, [ids, allDownloadClients]);
|
||||||
|
|
||||||
|
const onTagsChange = useCallback(
|
||||||
|
({ value }: { value: number[] }) => {
|
||||||
|
setTags(value);
|
||||||
|
},
|
||||||
|
[setTags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onApplyTagsChange = useCallback(
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
setApplyTags(value);
|
||||||
|
},
|
||||||
|
[setApplyTags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onApplyPress = useCallback(() => {
|
||||||
|
onApplyTagsPress(tags, applyTags);
|
||||||
|
}, [tags, applyTags, onApplyTagsPress]);
|
||||||
|
|
||||||
|
const applyTagsOptions = [
|
||||||
|
{
|
||||||
|
key: 'add',
|
||||||
|
get value() {
|
||||||
|
return translate('Add');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'remove',
|
||||||
|
get value() {
|
||||||
|
return translate('Remove');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'replace',
|
||||||
|
get value() {
|
||||||
|
return translate('Replace');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('Tags')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
value={tags}
|
||||||
|
onChange={onTagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ApplyTags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="applyTags"
|
||||||
|
value={applyTags}
|
||||||
|
values={applyTagsOptions}
|
||||||
|
helpTexts={[
|
||||||
|
translate('ApplyTagsHelpTextHowToApplyDownloadClients'),
|
||||||
|
translate('ApplyTagsHelpTextAdd'),
|
||||||
|
translate('ApplyTagsHelpTextRemove'),
|
||||||
|
translate('ApplyTagsHelpTextReplace'),
|
||||||
|
]}
|
||||||
|
onChange={onApplyTagsChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Result')}</FormLabel>
|
||||||
|
|
||||||
|
<div className={styles.result}>
|
||||||
|
{downloadClientsTags.map((id) => {
|
||||||
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTag =
|
||||||
|
(applyTags === 'remove' && tags.indexOf(id) > -1) ||
|
||||||
|
(applyTags === 'replace' && tags.indexOf(id) === -1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={tag.id}
|
||||||
|
title={
|
||||||
|
removeTag
|
||||||
|
? translate('RemovingTag')
|
||||||
|
: translate('ExistingTag')
|
||||||
|
}
|
||||||
|
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{(applyTags === 'add' || applyTags === 'replace') &&
|
||||||
|
tags.map((id) => {
|
||||||
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadClientsTags.indexOf(id) > -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={tag.id}
|
||||||
|
title={translate('AddingTag')}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||||
|
{translate('Apply')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagsModalContent;
|
|
@ -6,6 +6,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
|
import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
|
||||||
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
||||||
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
|
import ManageImportListsModal from './ImportLists/Manage/ManageImportListsModal';
|
||||||
|
@ -81,7 +82,7 @@ class ImportListSettings extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Manage Lists"
|
label={translate('ManageLists')}
|
||||||
iconName={icons.MANAGE}
|
iconName={icons.MANAGE}
|
||||||
onPress={this.onManageImportListsPress}
|
onPress={this.onManageImportListsPress}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -26,9 +26,25 @@ interface ManageImportListsEditModalContentProps {
|
||||||
const NO_CHANGE = 'noChange';
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
const autoAddOptions = [
|
const autoAddOptions = [
|
||||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
{
|
||||||
{ key: 'enabled', value: 'Enabled' },
|
key: NO_CHANGE,
|
||||||
{ key: 'disabled', value: 'Disabled' },
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Enabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Disabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function ManageImportListsEditModalContent(
|
function ManageImportListsEditModalContent(
|
||||||
|
@ -87,7 +103,7 @@ function ManageImportListsEditModalContent(
|
||||||
setRootFolderPath(value);
|
setRootFolderPath(value);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn('EditImportListModalContent Unknown Input');
|
console.warn(`EditImportListModalContent Unknown Input: '${name}'`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
|
@ -142,7 +158,9 @@ function ManageImportListsEditModalContent(
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
<ModalFooter className={styles.modalFooter}>
|
||||||
<div className={styles.selected}>
|
<div className={styles.selected}>
|
||||||
{translate('{count} import lists selected', { count: selectedCount })}
|
{translate('CountImportListsSelected', {
|
||||||
|
count: selectedCount,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { ImportListAppState } from 'App/State/SettingsAppState';
|
import { ImportListAppState } from 'App/State/SettingsAppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
@ -20,6 +21,7 @@ import {
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
import ManageImportListsEditModal from './Edit/ManageImportListsEditModal';
|
import ManageImportListsEditModal from './Edit/ManageImportListsEditModal';
|
||||||
import ManageImportListsModalRow from './ManageImportListsModalRow';
|
import ManageImportListsModalRow from './ManageImportListsModalRow';
|
||||||
|
@ -34,37 +36,49 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
get label() {
|
||||||
|
return translate('Name');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'implementation',
|
name: 'implementation',
|
||||||
label: 'Implementation',
|
get label() {
|
||||||
|
return translate('Implementation');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'qualityProfileId',
|
name: 'qualityProfileId',
|
||||||
label: 'Quality Profile',
|
get label() {
|
||||||
|
return translate('QualityProfile');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'rootFolderPath',
|
name: 'rootFolderPath',
|
||||||
label: 'Root Folder',
|
get label() {
|
||||||
|
return translate('RootFolder');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableAutomaticAdd',
|
name: 'enableAutomaticAdd',
|
||||||
label: 'Auto Add',
|
get label() {
|
||||||
|
return translate('AutoAdd');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
label: 'Tags',
|
get label() {
|
||||||
|
return translate('Tags');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
|
@ -190,12 +204,16 @@ function ManageImportListsModalContent(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>Manage Import Lists</ModalHeader>
|
<ModalHeader>{translate('ManageImportLists')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{isFetching ? <LoadingIndicator /> : null}
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !items.length && (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoImportListsFound')}</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
<Table
|
<Table
|
||||||
columns={COLUMNS}
|
columns={COLUMNS}
|
||||||
|
@ -230,7 +248,7 @@ function ManageImportListsModalContent(
|
||||||
isDisabled={!anySelected}
|
isDisabled={!anySelected}
|
||||||
onPress={onDeletePress}
|
onPress={onDeletePress}
|
||||||
>
|
>
|
||||||
Delete
|
{translate('Delete')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
|
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
|
@ -238,7 +256,7 @@ function ManageImportListsModalContent(
|
||||||
isDisabled={!anySelected}
|
isDisabled={!anySelected}
|
||||||
onPress={onEditPress}
|
onPress={onEditPress}
|
||||||
>
|
>
|
||||||
Edit
|
{translate('Edit')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
|
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
|
@ -246,11 +264,11 @@ function ManageImportListsModalContent(
|
||||||
isDisabled={!anySelected}
|
isDisabled={!anySelected}
|
||||||
onPress={onTagsPress}
|
onPress={onTagsPress}
|
||||||
>
|
>
|
||||||
Set Tags
|
{translate('SetTags')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onPress={onModalClose}>Close</Button>
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|
||||||
<ManageImportListsEditModal
|
<ManageImportListsEditModal
|
||||||
|
@ -270,9 +288,11 @@ function ManageImportListsModalContent(
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title="Delete Import List(s)"
|
title={translate('DeleteSelectedImportLists')}
|
||||||
message={`Are you sure you want to delete ${selectedIds.length} import list(s)?`}
|
message={translate('DeleteSelectedImportListsMessageText', {
|
||||||
confirmLabel="Delete"
|
count: selectedIds.length,
|
||||||
|
})}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={onConfirmDelete}
|
onConfirm={onConfirmDelete}
|
||||||
onCancel={onDeleteModalClose}
|
onCancel={onDeleteModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './TagsModalContent.css';
|
import styles from './TagsModalContent.css';
|
||||||
|
|
||||||
interface TagsModalContentProps {
|
interface TagsModalContentProps {
|
||||||
|
@ -36,7 +37,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
const [tags, setTags] = useState<number[]>([]);
|
const [tags, setTags] = useState<number[]>([]);
|
||||||
const [applyTags, setApplyTags] = useState('add');
|
const [applyTags, setApplyTags] = useState('add');
|
||||||
|
|
||||||
const seriesTags = useMemo(() => {
|
const importListsTags = useMemo(() => {
|
||||||
const tags = ids.reduce((acc: number[], id) => {
|
const tags = ids.reduce((acc: number[], id) => {
|
||||||
const s = allImportLists.items.find((s: ImportList) => s.id === id);
|
const s = allImportLists.items.find((s: ImportList) => s.id === id);
|
||||||
|
|
||||||
|
@ -69,19 +70,34 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
}, [tags, applyTags, onApplyTagsPress]);
|
}, [tags, applyTags, onApplyTagsPress]);
|
||||||
|
|
||||||
const applyTagsOptions = [
|
const applyTagsOptions = [
|
||||||
{ key: 'add', value: 'Add' },
|
{
|
||||||
{ key: 'remove', value: 'Remove' },
|
key: 'add',
|
||||||
{ key: 'replace', value: 'Replace' },
|
get value() {
|
||||||
|
return translate('Add');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'remove',
|
||||||
|
get value() {
|
||||||
|
return translate('Remove');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'replace',
|
||||||
|
get value() {
|
||||||
|
return translate('Replace');
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>Tags</ModalHeader>
|
<ModalHeader>{translate('Tags')}</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Tags</FormLabel>
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
|
@ -92,7 +108,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Apply Tags</FormLabel>
|
<FormLabel>{translate('ApplyTags')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
|
@ -100,20 +116,20 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
value={applyTags}
|
value={applyTags}
|
||||||
values={applyTagsOptions}
|
values={applyTagsOptions}
|
||||||
helpTexts={[
|
helpTexts={[
|
||||||
'How to apply tags to the selected list',
|
translate('ApplyTagsHelpTextHowToApplyImportLists'),
|
||||||
'Add: Add the tags the existing list of tags',
|
translate('ApplyTagsHelpTextAdd'),
|
||||||
'Remove: Remove the entered tags',
|
translate('ApplyTagsHelpTextRemove'),
|
||||||
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
|
translate('ApplyTagsHelpTextReplace'),
|
||||||
]}
|
]}
|
||||||
onChange={onApplyTagsChange}
|
onChange={onApplyTagsChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Result</FormLabel>
|
<FormLabel>{translate('Result')}</FormLabel>
|
||||||
|
|
||||||
<div className={styles.result}>
|
<div className={styles.result}>
|
||||||
{seriesTags.map((id) => {
|
{importListsTags.map((id) => {
|
||||||
const tag = tagList.find((t) => t.id === id);
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
|
@ -127,7 +143,11 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
title={removeTag ? 'Removing tag' : 'Existing tag'}
|
title={
|
||||||
|
removeTag
|
||||||
|
? translate('RemovingTag')
|
||||||
|
: translate('ExistingTag')
|
||||||
|
}
|
||||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
|
@ -144,14 +164,14 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seriesTags.indexOf(id) > -1) {
|
if (importListsTags.indexOf(id) > -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
title={'Adding tag'}
|
title={translate('AddingTag')}
|
||||||
kind={kinds.SUCCESS}
|
kind={kinds.SUCCESS}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
|
@ -165,10 +185,10 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>Cancel</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||||
Apply
|
{translate('Apply')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import IndexersConnector from './Indexers/IndexersConnector';
|
import IndexersConnector from './Indexers/IndexersConnector';
|
||||||
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
|
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
|
||||||
import IndexerOptionsConnector from './Options/IndexerOptionsConnector';
|
import IndexerOptionsConnector from './Options/IndexerOptionsConnector';
|
||||||
|
@ -84,7 +85,7 @@ class IndexerSettings extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Manage Indexers"
|
label={translate('ManageIndexers')}
|
||||||
iconName={icons.MANAGE}
|
iconName={icons.MANAGE}
|
||||||
onPress={this.onManageIndexersPress}
|
onPress={this.onManageIndexersPress}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -27,9 +27,25 @@ interface ManageIndexersEditModalContentProps {
|
||||||
const NO_CHANGE = 'noChange';
|
const NO_CHANGE = 'noChange';
|
||||||
|
|
||||||
const enableOptions = [
|
const enableOptions = [
|
||||||
{ key: NO_CHANGE, value: 'No Change', disabled: true },
|
{
|
||||||
{ key: 'enabled', value: 'Enabled' },
|
key: NO_CHANGE,
|
||||||
{ key: 'disabled', value: 'Disabled' },
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Enabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'disabled',
|
||||||
|
get value() {
|
||||||
|
return translate('Disabled');
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function ManageIndexersEditModalContent(
|
function ManageIndexersEditModalContent(
|
||||||
|
@ -97,7 +113,7 @@ function ManageIndexersEditModalContent(
|
||||||
setPriority(value);
|
setPriority(value);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn('EditIndexersModalContent Unknown Input');
|
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
|
@ -111,7 +127,7 @@ function ManageIndexersEditModalContent(
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('EnableRss')}</FormLabel>
|
<FormLabel>{translate('EnableRSS')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
|
@ -162,13 +178,15 @@ function ManageIndexersEditModalContent(
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
<ModalFooter className={styles.modalFooter}>
|
||||||
<div className={styles.selected}>
|
<div className={styles.selected}>
|
||||||
{translate('{count} indexers selected', { count: selectedCount })}
|
{translate('CountIndexersSelected', {
|
||||||
|
count: selectedCount,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button onPress={save}>{translate('Apply Changes')}</Button>
|
<Button onPress={save}>{translate('ApplyChanges')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { IndexerAppState } from 'App/State/SettingsAppState';
|
import { IndexerAppState } from 'App/State/SettingsAppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
@ -20,6 +21,7 @@ import {
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
import ManageIndexersEditModal from './Edit/ManageIndexersEditModal';
|
import ManageIndexersEditModal from './Edit/ManageIndexersEditModal';
|
||||||
import ManageIndexersModalRow from './ManageIndexersModalRow';
|
import ManageIndexersModalRow from './ManageIndexersModalRow';
|
||||||
|
@ -34,43 +36,57 @@ type OnSelectedChangeCallback = React.ComponentProps<
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
{
|
{
|
||||||
name: 'name',
|
name: 'name',
|
||||||
label: 'Name',
|
get label() {
|
||||||
|
return translate('Name');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'implementation',
|
name: 'implementation',
|
||||||
label: 'Implementation',
|
get label() {
|
||||||
|
return translate('Implementation');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableRss',
|
name: 'enableRss',
|
||||||
label: 'Enable RSS',
|
get label() {
|
||||||
|
return translate('EnableRSS');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableAutomaticSearch',
|
name: 'enableAutomaticSearch',
|
||||||
label: 'Enable Automatic Search',
|
get label() {
|
||||||
|
return translate('EnableAutomaticSearch');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'enableInteractiveSearch',
|
name: 'enableInteractiveSearch',
|
||||||
label: 'Enable Interactive Search',
|
get label() {
|
||||||
|
return translate('EnableInteractiveSearch');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'priority',
|
name: 'priority',
|
||||||
label: 'Priority',
|
get label() {
|
||||||
|
return translate('Priority');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
label: 'Tags',
|
get label() {
|
||||||
|
return translate('Tags');
|
||||||
|
},
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
|
@ -189,17 +205,21 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
[items, setSelectState]
|
[items, setSelectState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error, 'Unable to load import lists.');
|
const errorMessage = getErrorMessage(error, 'Unable to load indexers.');
|
||||||
const anySelected = selectedCount > 0;
|
const anySelected = selectedCount > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>Manage Import Lists</ModalHeader>
|
<ModalHeader>{translate('ManageIndexers')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{isFetching ? <LoadingIndicator /> : null}
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
{error ? <div>{errorMessage}</div> : null}
|
{error ? <div>{errorMessage}</div> : null}
|
||||||
|
|
||||||
|
{isPopulated && !error && !items.length && (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoIndexersFound')}</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
{isPopulated && !!items.length && !isFetching && !isFetching ? (
|
||||||
<Table
|
<Table
|
||||||
columns={COLUMNS}
|
columns={COLUMNS}
|
||||||
|
@ -234,7 +254,7 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
isDisabled={!anySelected}
|
isDisabled={!anySelected}
|
||||||
onPress={onDeletePress}
|
onPress={onDeletePress}
|
||||||
>
|
>
|
||||||
Delete
|
{translate('Delete')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
|
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
|
@ -242,7 +262,7 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
isDisabled={!anySelected}
|
isDisabled={!anySelected}
|
||||||
onPress={onEditPress}
|
onPress={onEditPress}
|
||||||
>
|
>
|
||||||
Edit
|
{translate('Edit')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
|
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
|
@ -250,11 +270,11 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
isDisabled={!anySelected}
|
isDisabled={!anySelected}
|
||||||
onPress={onTagsPress}
|
onPress={onTagsPress}
|
||||||
>
|
>
|
||||||
Set Tags
|
{translate('SetTags')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onPress={onModalClose}>Close</Button>
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|
||||||
<ManageIndexersEditModal
|
<ManageIndexersEditModal
|
||||||
|
@ -274,9 +294,11 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title="Delete Import List(s)"
|
title={translate('DeleteSelectedIndexers')}
|
||||||
message={`Are you sure you want to delete ${selectedIds.length} import list(s)?`}
|
message={translate('DeleteSelectedIndexersMessageText', {
|
||||||
confirmLabel="Delete"
|
count: selectedIds.length,
|
||||||
|
})}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
onConfirm={onConfirmDelete}
|
onConfirm={onConfirmDelete}
|
||||||
onCancel={onDeleteModalClose}
|
onCancel={onDeleteModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ManageIndexersModalRow.css';
|
import styles from './ManageIndexersModalRow.css';
|
||||||
|
|
||||||
interface ManageIndexersModalRowProps {
|
interface ManageIndexersModalRowProps {
|
||||||
|
@ -59,15 +62,30 @@ function ManageIndexersModalRow(props: ManageIndexersModalRowProps) {
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.enableRss}>
|
<TableRowCell className={styles.enableRss}>
|
||||||
{enableRss ? 'Yes' : 'No'}
|
<Label
|
||||||
|
kind={enableRss ? kinds.SUCCESS : kinds.DISABLED}
|
||||||
|
outline={!enableRss}
|
||||||
|
>
|
||||||
|
{enableRss ? translate('Yes') : translate('No')}
|
||||||
|
</Label>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.enableAutomaticSearch}>
|
<TableRowCell className={styles.enableAutomaticSearch}>
|
||||||
{enableAutomaticSearch ? 'Yes' : 'No'}
|
<Label
|
||||||
|
kind={enableAutomaticSearch ? kinds.SUCCESS : kinds.DISABLED}
|
||||||
|
outline={!enableAutomaticSearch}
|
||||||
|
>
|
||||||
|
{enableAutomaticSearch ? translate('Yes') : translate('No')}
|
||||||
|
</Label>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.enableInteractiveSearch}>
|
<TableRowCell className={styles.enableInteractiveSearch}>
|
||||||
{enableInteractiveSearch ? 'Yes' : 'No'}
|
<Label
|
||||||
|
kind={enableInteractiveSearch ? kinds.SUCCESS : kinds.DISABLED}
|
||||||
|
outline={!enableInteractiveSearch}
|
||||||
|
>
|
||||||
|
{enableInteractiveSearch ? translate('Yes') : translate('No')}
|
||||||
|
</Label>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.priority}>{priority}</TableRowCell>
|
<TableRowCell className={styles.priority}>{priority}</TableRowCell>
|
||||||
|
|
|
@ -17,6 +17,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './TagsModalContent.css';
|
import styles from './TagsModalContent.css';
|
||||||
|
|
||||||
interface TagsModalContentProps {
|
interface TagsModalContentProps {
|
||||||
|
@ -36,7 +37,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
const [tags, setTags] = useState<number[]>([]);
|
const [tags, setTags] = useState<number[]>([]);
|
||||||
const [applyTags, setApplyTags] = useState('add');
|
const [applyTags, setApplyTags] = useState('add');
|
||||||
|
|
||||||
const seriesTags = useMemo(() => {
|
const indexersTags = useMemo(() => {
|
||||||
const tags = ids.reduce((acc: number[], id) => {
|
const tags = ids.reduce((acc: number[], id) => {
|
||||||
const s = allIndexers.items.find((s: Indexer) => s.id === id);
|
const s = allIndexers.items.find((s: Indexer) => s.id === id);
|
||||||
|
|
||||||
|
@ -69,19 +70,34 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
}, [tags, applyTags, onApplyTagsPress]);
|
}, [tags, applyTags, onApplyTagsPress]);
|
||||||
|
|
||||||
const applyTagsOptions = [
|
const applyTagsOptions = [
|
||||||
{ key: 'add', value: 'Add' },
|
{
|
||||||
{ key: 'remove', value: 'Remove' },
|
key: 'add',
|
||||||
{ key: 'replace', value: 'Replace' },
|
get value() {
|
||||||
|
return translate('Add');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'remove',
|
||||||
|
get value() {
|
||||||
|
return translate('Remove');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'replace',
|
||||||
|
get value() {
|
||||||
|
return translate('Replace');
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>Tags</ModalHeader>
|
<ModalHeader>{translate('Tags')}</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Tags</FormLabel>
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
|
@ -92,7 +108,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Apply Tags</FormLabel>
|
<FormLabel>{translate('ApplyTags')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
|
@ -100,20 +116,20 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
value={applyTags}
|
value={applyTags}
|
||||||
values={applyTagsOptions}
|
values={applyTagsOptions}
|
||||||
helpTexts={[
|
helpTexts={[
|
||||||
'How to apply tags to the selected indexer(s)',
|
translate('ApplyTagsHelpTextHowToApplyIndexers'),
|
||||||
'Add: Add the tags the existing list of tags',
|
translate('ApplyTagsHelpTextAdd'),
|
||||||
'Remove: Remove the entered tags',
|
translate('ApplyTagsHelpTextRemove'),
|
||||||
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)',
|
translate('ApplyTagsHelpTextReplace'),
|
||||||
]}
|
]}
|
||||||
onChange={onApplyTagsChange}
|
onChange={onApplyTagsChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Result</FormLabel>
|
<FormLabel>{translate('Result')}</FormLabel>
|
||||||
|
|
||||||
<div className={styles.result}>
|
<div className={styles.result}>
|
||||||
{seriesTags.map((id) => {
|
{indexersTags.map((id) => {
|
||||||
const tag = tagList.find((t) => t.id === id);
|
const tag = tagList.find((t) => t.id === id);
|
||||||
|
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
|
@ -127,7 +143,11 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
title={removeTag ? 'Removing tag' : 'Existing tag'}
|
title={
|
||||||
|
removeTag
|
||||||
|
? translate('RemovingTag')
|
||||||
|
: translate('ExistingTag')
|
||||||
|
}
|
||||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
|
@ -144,14 +164,14 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seriesTags.indexOf(id) > -1) {
|
if (indexersTags.indexOf(id) > -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
title={'Adding tag'}
|
title={translate('AddingTag')}
|
||||||
kind={kinds.SUCCESS}
|
kind={kinds.SUCCESS}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
|
@ -165,10 +185,10 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>Cancel</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||||
Apply
|
{translate('Apply')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
|
@ -134,6 +134,7 @@ const historyShape = {
|
||||||
};
|
};
|
||||||
|
|
||||||
SettingsToolbarConnector.propTypes = {
|
SettingsToolbarConnector.propTypes = {
|
||||||
|
showSave: PropTypes.bool,
|
||||||
hasPendingChanges: PropTypes.bool.isRequired,
|
hasPendingChanges: PropTypes.bool.isRequired,
|
||||||
history: PropTypes.shape(historyShape).isRequired,
|
history: PropTypes.shape(historyShape).isRequired,
|
||||||
onSavePress: PropTypes.func,
|
onSavePress: PropTypes.func,
|
||||||
|
|
|
@ -21,6 +21,7 @@ function TagDetailsModalContent(props) {
|
||||||
notifications,
|
notifications,
|
||||||
releaseProfiles,
|
releaseProfiles,
|
||||||
indexers,
|
indexers,
|
||||||
|
downloadClients,
|
||||||
autoTags,
|
autoTags,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
onDeleteTagPress
|
onDeleteTagPress
|
||||||
|
@ -179,6 +180,22 @@ function TagDetailsModalContent(props) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
downloadClients.length ?
|
||||||
|
<FieldSet legend="Download Clients">
|
||||||
|
{
|
||||||
|
downloadClients.map((item) => {
|
||||||
|
return (
|
||||||
|
<div key={item.id}>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</FieldSet> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
autoTags.length ?
|
autoTags.length ?
|
||||||
<FieldSet legend="Auto Tagging">
|
<FieldSet legend="Auto Tagging">
|
||||||
|
@ -228,6 +245,7 @@ TagDetailsModalContent.propTypes = {
|
||||||
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
downloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
autoTags: PropTypes.arrayOf(PropTypes.object).isRequired,
|
autoTags: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onDeleteTagPress: PropTypes.func.isRequired
|
onDeleteTagPress: PropTypes.func.isRequired
|
||||||
|
|
|
@ -77,6 +77,14 @@ function createMatchingIndexersSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createMatchingDownloadClientsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { downloadClientIds }) => downloadClientIds,
|
||||||
|
(state) => state.settings.downloadClients.items,
|
||||||
|
findMatchingItems
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function createMatchingAutoTagsSelector() {
|
function createMatchingAutoTagsSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { autoTagIds }) => autoTagIds,
|
(state, { autoTagIds }) => autoTagIds,
|
||||||
|
@ -93,8 +101,9 @@ function createMapStateToProps() {
|
||||||
createMatchingNotificationsSelector(),
|
createMatchingNotificationsSelector(),
|
||||||
createMatchingReleaseProfilesSelector(),
|
createMatchingReleaseProfilesSelector(),
|
||||||
createMatchingIndexersSelector(),
|
createMatchingIndexersSelector(),
|
||||||
|
createMatchingDownloadClientsSelector(),
|
||||||
createMatchingAutoTagsSelector(),
|
createMatchingAutoTagsSelector(),
|
||||||
(series, delayProfiles, importLists, notifications, releaseProfiles, indexers, autoTags) => {
|
(series, delayProfiles, importLists, notifications, releaseProfiles, indexers, downloadClients, autoTags) => {
|
||||||
return {
|
return {
|
||||||
series,
|
series,
|
||||||
delayProfiles,
|
delayProfiles,
|
||||||
|
@ -102,6 +111,7 @@ function createMapStateToProps() {
|
||||||
notifications,
|
notifications,
|
||||||
releaseProfiles,
|
releaseProfiles,
|
||||||
indexers,
|
indexers,
|
||||||
|
downloadClients,
|
||||||
autoTags
|
autoTags
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ class Tag extends Component {
|
||||||
notificationIds,
|
notificationIds,
|
||||||
restrictionIds,
|
restrictionIds,
|
||||||
indexerIds,
|
indexerIds,
|
||||||
|
downloadClientIds,
|
||||||
autoTagIds,
|
autoTagIds,
|
||||||
seriesIds
|
seriesIds
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -73,6 +74,7 @@ class Tag extends Component {
|
||||||
notificationIds.length ||
|
notificationIds.length ||
|
||||||
restrictionIds.length ||
|
restrictionIds.length ||
|
||||||
indexerIds.length ||
|
indexerIds.length ||
|
||||||
|
downloadClientIds.length ||
|
||||||
autoTagIds.length ||
|
autoTagIds.length ||
|
||||||
seriesIds.length
|
seriesIds.length
|
||||||
);
|
);
|
||||||
|
@ -121,6 +123,11 @@ class Tag extends Component {
|
||||||
count={indexerIds.length}
|
count={indexerIds.length}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<TagInUse
|
||||||
|
label="download client"
|
||||||
|
count={downloadClientIds.length}
|
||||||
|
/>
|
||||||
|
|
||||||
<TagInUse
|
<TagInUse
|
||||||
label="auto tagging"
|
label="auto tagging"
|
||||||
count={autoTagIds.length}
|
count={autoTagIds.length}
|
||||||
|
@ -146,6 +153,7 @@ class Tag extends Component {
|
||||||
notificationIds={notificationIds}
|
notificationIds={notificationIds}
|
||||||
restrictionIds={restrictionIds}
|
restrictionIds={restrictionIds}
|
||||||
indexerIds={indexerIds}
|
indexerIds={indexerIds}
|
||||||
|
downloadClientIds={downloadClientIds}
|
||||||
autoTagIds={autoTagIds}
|
autoTagIds={autoTagIds}
|
||||||
isOpen={isDetailsModalOpen}
|
isOpen={isDetailsModalOpen}
|
||||||
onModalClose={this.onDetailsModalClose}
|
onModalClose={this.onDetailsModalClose}
|
||||||
|
@ -174,6 +182,7 @@ Tag.propTypes = {
|
||||||
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
downloadClientIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
autoTagIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
autoTagIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
onConfirmDeleteTag: PropTypes.func.isRequired
|
onConfirmDeleteTag: PropTypes.func.isRequired
|
||||||
|
@ -185,6 +194,7 @@ Tag.defaultProps = {
|
||||||
notificationIds: [],
|
notificationIds: [],
|
||||||
restrictionIds: [],
|
restrictionIds: [],
|
||||||
indexerIds: [],
|
indexerIds: [],
|
||||||
|
downloadClientIds: [],
|
||||||
autoTagIds: [],
|
autoTagIds: [],
|
||||||
seriesIds: []
|
seriesIds: []
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import FieldSet from 'Components/FieldSet';
|
import FieldSet from 'Components/FieldSet';
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
import TagConnector from './TagConnector';
|
import TagConnector from './TagConnector';
|
||||||
import styles from './Tags.css';
|
import styles from './Tags.css';
|
||||||
|
|
||||||
|
@ -13,7 +15,9 @@ function Tags(props) {
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return (
|
return (
|
||||||
<div>No tags have been added yet</div>
|
<Alert kind={kinds.INFO}>
|
||||||
|
No tags have been added yet
|
||||||
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ 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 { fetchDelayProfiles, 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 } from 'Store/Actions/tagActions';
|
||||||
import Tags from './Tags';
|
import Tags from './Tags';
|
||||||
|
|
||||||
|
@ -30,7 +30,8 @@ const mapDispatchToProps = {
|
||||||
dispatchFetchImportLists: fetchImportLists,
|
dispatchFetchImportLists: fetchImportLists,
|
||||||
dispatchFetchNotifications: fetchNotifications,
|
dispatchFetchNotifications: fetchNotifications,
|
||||||
dispatchFetchReleaseProfiles: fetchReleaseProfiles,
|
dispatchFetchReleaseProfiles: fetchReleaseProfiles,
|
||||||
dispatchFetchIndexers: fetchIndexers
|
dispatchFetchIndexers: fetchIndexers,
|
||||||
|
dispatchFetchDownloadClients: fetchDownloadClients
|
||||||
};
|
};
|
||||||
|
|
||||||
class MetadatasConnector extends Component {
|
class MetadatasConnector extends Component {
|
||||||
|
@ -45,7 +46,8 @@ class MetadatasConnector extends Component {
|
||||||
dispatchFetchImportLists,
|
dispatchFetchImportLists,
|
||||||
dispatchFetchNotifications,
|
dispatchFetchNotifications,
|
||||||
dispatchFetchReleaseProfiles,
|
dispatchFetchReleaseProfiles,
|
||||||
dispatchFetchIndexers
|
dispatchFetchIndexers,
|
||||||
|
dispatchFetchDownloadClients
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
dispatchFetchTagDetails();
|
dispatchFetchTagDetails();
|
||||||
|
@ -54,6 +56,7 @@ class MetadatasConnector extends Component {
|
||||||
dispatchFetchNotifications();
|
dispatchFetchNotifications();
|
||||||
dispatchFetchReleaseProfiles();
|
dispatchFetchReleaseProfiles();
|
||||||
dispatchFetchIndexers();
|
dispatchFetchIndexers();
|
||||||
|
dispatchFetchDownloadClients();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -74,7 +77,8 @@ MetadatasConnector.propTypes = {
|
||||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||||
dispatchFetchNotifications: PropTypes.func.isRequired,
|
dispatchFetchNotifications: PropTypes.func.isRequired,
|
||||||
dispatchFetchReleaseProfiles: PropTypes.func.isRequired,
|
dispatchFetchReleaseProfiles: PropTypes.func.isRequired,
|
||||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
dispatchFetchIndexers: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchDownloadClients: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
||||||
|
|
|
@ -31,9 +31,8 @@ export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadCl
|
||||||
export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
|
export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
|
||||||
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
|
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
|
||||||
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
||||||
|
|
||||||
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
|
||||||
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
|
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
|
||||||
|
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -48,9 +47,8 @@ export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT);
|
||||||
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
|
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
|
||||||
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
|
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
|
||||||
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
|
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
|
||||||
|
|
||||||
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
|
||||||
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
|
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
|
||||||
|
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
||||||
|
|
||||||
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
|
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
|
@ -106,8 +104,8 @@ export default {
|
||||||
[TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'),
|
[TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'),
|
||||||
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
|
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
|
||||||
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient'),
|
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient'),
|
||||||
[BULK_DELETE_DOWNLOAD_CLIENTS]: createBulkRemoveItemHandler(section, '/downloadclient/bulk'),
|
[BULK_EDIT_DOWNLOAD_CLIENTS]: createBulkEditItemHandler(section, '/downloadclient/bulk'),
|
||||||
[BULK_EDIT_DOWNLOAD_CLIENTS]: createBulkEditItemHandler(section, '/downloadclient/bulk')
|
[BULK_DELETE_DOWNLOAD_CLIENTS]: createBulkRemoveItemHandler(section, '/downloadclient/bulk')
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -31,9 +31,8 @@ export const DELETE_IMPORT_LIST = 'settings/importlists/deleteImportList';
|
||||||
export const TEST_IMPORT_LIST = 'settings/importlists/testImportList';
|
export const TEST_IMPORT_LIST = 'settings/importlists/testImportList';
|
||||||
export const CANCEL_TEST_IMPORT_LIST = 'settings/importlists/cancelTestImportList';
|
export const CANCEL_TEST_IMPORT_LIST = 'settings/importlists/cancelTestImportList';
|
||||||
export const TEST_ALL_IMPORT_LISTS = 'settings/importlists/testAllImportLists';
|
export const TEST_ALL_IMPORT_LISTS = 'settings/importlists/testAllImportLists';
|
||||||
|
|
||||||
export const BULK_DELETE_IMPORT_LISTS = 'settings/importlists/bulkDeleteImportLists';
|
|
||||||
export const BULK_EDIT_IMPORT_LISTS = 'settings/importlists/bulkEditImportLists';
|
export const BULK_EDIT_IMPORT_LISTS = 'settings/importlists/bulkEditImportLists';
|
||||||
|
export const BULK_DELETE_IMPORT_LISTS = 'settings/importlists/bulkDeleteImportLists';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -48,9 +47,8 @@ export const deleteImportList = createThunk(DELETE_IMPORT_LIST);
|
||||||
export const testImportList = createThunk(TEST_IMPORT_LIST);
|
export const testImportList = createThunk(TEST_IMPORT_LIST);
|
||||||
export const cancelTestImportList = createThunk(CANCEL_TEST_IMPORT_LIST);
|
export const cancelTestImportList = createThunk(CANCEL_TEST_IMPORT_LIST);
|
||||||
export const testAllImportLists = createThunk(TEST_ALL_IMPORT_LISTS);
|
export const testAllImportLists = createThunk(TEST_ALL_IMPORT_LISTS);
|
||||||
|
|
||||||
export const bulkDeleteImportLists = createThunk(BULK_DELETE_IMPORT_LISTS);
|
|
||||||
export const bulkEditImportLists = createThunk(BULK_EDIT_IMPORT_LISTS);
|
export const bulkEditImportLists = createThunk(BULK_EDIT_IMPORT_LISTS);
|
||||||
|
export const bulkDeleteImportLists = createThunk(BULK_DELETE_IMPORT_LISTS);
|
||||||
|
|
||||||
export const setImportListValue = createAction(SET_IMPORT_LIST_VALUE, (payload) => {
|
export const setImportListValue = createAction(SET_IMPORT_LIST_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
|
@ -105,8 +103,8 @@ export default {
|
||||||
[TEST_IMPORT_LIST]: createTestProviderHandler(section, '/importlist'),
|
[TEST_IMPORT_LIST]: createTestProviderHandler(section, '/importlist'),
|
||||||
[CANCEL_TEST_IMPORT_LIST]: createCancelTestProviderHandler(section),
|
[CANCEL_TEST_IMPORT_LIST]: createCancelTestProviderHandler(section),
|
||||||
[TEST_ALL_IMPORT_LISTS]: createTestAllProvidersHandler(section, '/importlist'),
|
[TEST_ALL_IMPORT_LISTS]: createTestAllProvidersHandler(section, '/importlist'),
|
||||||
[BULK_DELETE_IMPORT_LISTS]: createBulkRemoveItemHandler(section, '/importlist/bulk'),
|
[BULK_EDIT_IMPORT_LISTS]: createBulkEditItemHandler(section, '/importlist/bulk'),
|
||||||
[BULK_EDIT_IMPORT_LISTS]: createBulkEditItemHandler(section, '/importlist/bulk')
|
[BULK_DELETE_IMPORT_LISTS]: createBulkRemoveItemHandler(section, '/importlist/bulk')
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
|
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||||
|
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||||
|
@ -11,8 +13,6 @@ import { createThunk } from 'Store/thunks';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
import createBulkEditItemHandler from '../Creators/createBulkEditItemHandler';
|
|
||||||
import createBulkRemoveItemHandler from '../Creators/createBulkRemoveItemHandler';
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Variables
|
// Variables
|
||||||
|
@ -34,9 +34,8 @@ export const DELETE_INDEXER = 'settings/indexers/deleteIndexer';
|
||||||
export const TEST_INDEXER = 'settings/indexers/testIndexer';
|
export const TEST_INDEXER = 'settings/indexers/testIndexer';
|
||||||
export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
|
export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
|
||||||
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
||||||
|
|
||||||
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
|
||||||
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
|
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
|
||||||
|
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -52,9 +51,8 @@ export const deleteIndexer = createThunk(DELETE_INDEXER);
|
||||||
export const testIndexer = createThunk(TEST_INDEXER);
|
export const testIndexer = createThunk(TEST_INDEXER);
|
||||||
export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
|
export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
|
||||||
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
|
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
|
||||||
|
|
||||||
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
|
||||||
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
|
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
|
||||||
|
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
||||||
|
|
||||||
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
|
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
|
@ -110,9 +108,8 @@ export default {
|
||||||
[TEST_INDEXER]: createTestProviderHandler(section, '/indexer'),
|
[TEST_INDEXER]: createTestProviderHandler(section, '/indexer'),
|
||||||
[CANCEL_TEST_INDEXER]: createCancelTestProviderHandler(section),
|
[CANCEL_TEST_INDEXER]: createCancelTestProviderHandler(section),
|
||||||
[TEST_ALL_INDEXERS]: createTestAllProvidersHandler(section, '/indexer'),
|
[TEST_ALL_INDEXERS]: createTestAllProvidersHandler(section, '/indexer'),
|
||||||
|
[BULK_EDIT_INDEXERS]: createBulkEditItemHandler(section, '/indexer/bulk'),
|
||||||
[BULK_DELETE_INDEXERS]: createBulkRemoveItemHandler(section, '/indexer/bulk'),
|
[BULK_DELETE_INDEXERS]: createBulkRemoveItemHandler(section, '/indexer/bulk')
|
||||||
[BULK_EDIT_INDEXERS]: createBulkEditItemHandler(section, '/indexer/bulk')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
|
import { fetchTranslations as fetchAppTranslations } from 'Utilities/String/translate';
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
|
||||||
function getDimensions(width, height) {
|
function getDimensions(width, height) {
|
||||||
|
@ -41,7 +42,12 @@ export const defaultState = {
|
||||||
isReconnecting: false,
|
isReconnecting: false,
|
||||||
isDisconnected: false,
|
isDisconnected: false,
|
||||||
isRestarting: false,
|
isRestarting: false,
|
||||||
isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen
|
isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen,
|
||||||
|
translations: {
|
||||||
|
isFetching: true,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -53,6 +59,7 @@ export const SAVE_DIMENSIONS = 'app/saveDimensions';
|
||||||
export const SET_VERSION = 'app/setVersion';
|
export const SET_VERSION = 'app/setVersion';
|
||||||
export const SET_APP_VALUE = 'app/setAppValue';
|
export const SET_APP_VALUE = 'app/setAppValue';
|
||||||
export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible';
|
export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible';
|
||||||
|
export const FETCH_TRANSLATIONS = 'app/fetchTranslations';
|
||||||
|
|
||||||
export const PING_SERVER = 'app/pingServer';
|
export const PING_SERVER = 'app/pingServer';
|
||||||
|
|
||||||
|
@ -66,6 +73,7 @@ export const setAppValue = createAction(SET_APP_VALUE);
|
||||||
export const showMessage = createAction(SHOW_MESSAGE);
|
export const showMessage = createAction(SHOW_MESSAGE);
|
||||||
export const hideMessage = createAction(HIDE_MESSAGE);
|
export const hideMessage = createAction(HIDE_MESSAGE);
|
||||||
export const pingServer = createThunk(PING_SERVER);
|
export const pingServer = createThunk(PING_SERVER);
|
||||||
|
export const fetchTranslations = createThunk(FETCH_TRANSLATIONS);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Helpers
|
// Helpers
|
||||||
|
@ -127,6 +135,17 @@ function pingServerAfterTimeout(getState, dispatch) {
|
||||||
export const actionHandlers = handleThunks({
|
export const actionHandlers = handleThunks({
|
||||||
[PING_SERVER]: function(getState, payload, dispatch) {
|
[PING_SERVER]: function(getState, payload, dispatch) {
|
||||||
pingServerAfterTimeout(getState, dispatch);
|
pingServerAfterTimeout(getState, dispatch);
|
||||||
|
},
|
||||||
|
[FETCH_TRANSLATIONS]: async function(getState, payload, dispatch) {
|
||||||
|
const isFetchingComplete = await fetchAppTranslations();
|
||||||
|
|
||||||
|
dispatch(setAppValue({
|
||||||
|
translations: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: isFetchingComplete,
|
||||||
|
error: isFetchingComplete ? null : 'Failed to load translations from API'
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import episodeEntities from 'Episode/episodeEntities';
|
import episodeEntities from 'Episode/episodeEntities';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { icons, sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import { updateItem } from './baseActions';
|
import { updateItem } from './baseActions';
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
import createFetchHandler from './Creators/createFetchHandler';
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
@ -109,6 +112,19 @@ export const defaultState = {
|
||||||
label: 'Formats',
|
label: 'Formats',
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormatScore',
|
||||||
|
get columnLabel() {
|
||||||
|
return translate('CustomFormatScore');
|
||||||
|
},
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.SCORE,
|
||||||
|
get title() {
|
||||||
|
return translate('CustomFormatScore');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
label: 'Status',
|
label: 'Status',
|
||||||
|
|
|
@ -14,6 +14,7 @@ import * as importSeries from './importSeriesActions';
|
||||||
import * as interactiveImportActions from './interactiveImportActions';
|
import * as interactiveImportActions from './interactiveImportActions';
|
||||||
import * as oAuth from './oAuthActions';
|
import * as oAuth from './oAuthActions';
|
||||||
import * as organizePreview from './organizePreviewActions';
|
import * as organizePreview from './organizePreviewActions';
|
||||||
|
import * as parse from './parseActions';
|
||||||
import * as paths from './pathActions';
|
import * as paths from './pathActions';
|
||||||
import * as providerOptions from './providerOptionActions';
|
import * as providerOptions from './providerOptionActions';
|
||||||
import * as queue from './queueActions';
|
import * as queue from './queueActions';
|
||||||
|
@ -44,6 +45,7 @@ export default [
|
||||||
interactiveImportActions,
|
interactiveImportActions,
|
||||||
oAuth,
|
oAuth,
|
||||||
organizePreview,
|
organizePreview,
|
||||||
|
parse,
|
||||||
paths,
|
paths,
|
||||||
providerOptions,
|
providerOptions,
|
||||||
queue,
|
queue,
|
||||||
|
|
|
@ -47,6 +47,10 @@ export const defaultState = {
|
||||||
|
|
||||||
quality: function(item, direction) {
|
quality: function(item, direction) {
|
||||||
return item.qualityWeight || 0;
|
return item.qualityWeight || 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
customFormats: function(item, direction) {
|
||||||
|
return item.customFormatScore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import { set, update } from './baseActions';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import createClearReducer from './Creators/Reducers/createClearReducer';
|
||||||
|
|
||||||
|
interface FetchPayload {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'parse';
|
||||||
|
let parseTimeout: number | null = null;
|
||||||
|
let abortCurrentRequest: (() => void) | null = null;
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
item: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH = 'parse/fetch';
|
||||||
|
export const CLEAR = 'parse/clear';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetch = createThunk(FETCH);
|
||||||
|
export const clear = createAction(CLEAR);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
[FETCH]: function (
|
||||||
|
_getState: () => AppState,
|
||||||
|
payload: FetchPayload,
|
||||||
|
dispatch: Dispatch
|
||||||
|
) {
|
||||||
|
if (parseTimeout) {
|
||||||
|
clearTimeout(parseTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTimeout = window.setTimeout(async () => {
|
||||||
|
dispatch(set({ section, isFetching: true }));
|
||||||
|
|
||||||
|
if (abortCurrentRequest) {
|
||||||
|
abortCurrentRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { request, abortRequest } = createAjaxRequest({
|
||||||
|
url: '/parse',
|
||||||
|
data: {
|
||||||
|
title: payload.title,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await request;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
batchActions([
|
||||||
|
update({ section, data }),
|
||||||
|
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: true,
|
||||||
|
error: null,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isAdding: false,
|
||||||
|
isAdded: false,
|
||||||
|
addError: error,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
abortCurrentRequest = abortRequest;
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions(
|
||||||
|
{
|
||||||
|
[CLEAR]: createClearReducer(section, defaultState),
|
||||||
|
},
|
||||||
|
defaultState,
|
||||||
|
section
|
||||||
|
);
|
|
@ -1,7 +1,9 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import Icon from 'Components/Icon';
|
||||||
|
import { icons, sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||||
|
@ -104,6 +106,15 @@ export const defaultState = {
|
||||||
isSortable: false,
|
isSortable: false,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormatScore',
|
||||||
|
columnLabel: 'Custom Format Score',
|
||||||
|
label: React.createElement(Icon, {
|
||||||
|
name: icons.SCORE,
|
||||||
|
title: 'Custom format score'
|
||||||
|
}),
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'protocol',
|
name: 'protocol',
|
||||||
label: 'Protocol',
|
label: 'Protocol',
|
||||||
|
|
|
@ -4,23 +4,25 @@ import AppState from 'App/State/AppState';
|
||||||
type GetState = () => AppState;
|
type GetState = () => AppState;
|
||||||
type Thunk = (
|
type Thunk = (
|
||||||
getState: GetState,
|
getState: GetState,
|
||||||
identity: unknown,
|
identityFn: never,
|
||||||
dispatch: Dispatch
|
dispatch: Dispatch
|
||||||
) => unknown;
|
) => unknown;
|
||||||
|
|
||||||
const thunks: Record<string, Thunk> = {};
|
const thunks: Record<string, Thunk> = {};
|
||||||
|
|
||||||
function identity(payload: unknown) {
|
function identity<T, TResult>(payload: T): TResult {
|
||||||
return payload;
|
return payload as unknown as TResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createThunk(type: string, identityFunction = identity) {
|
export function createThunk(type: string, identityFunction = identity) {
|
||||||
return function (payload: unknown = {}) {
|
return function <T>(payload?: T) {
|
||||||
return function (dispatch: Dispatch, getState: GetState) {
|
return function (dispatch: Dispatch, getState: GetState) {
|
||||||
const thunk = thunks[type];
|
const thunk = thunks[type];
|
||||||
|
|
||||||
if (thunk) {
|
if (thunk) {
|
||||||
return thunk(getState, identityFunction(payload), dispatch);
|
const finalPayload = payload ?? {};
|
||||||
|
|
||||||
|
return thunk(getState, identityFunction(finalPayload), dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Error(`Thunk handler has not been registered for ${type}`);
|
throw Error(`Thunk handler has not been registered for ${type}`);
|
||||||
|
|
|
@ -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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
@ -11,7 +12,7 @@ import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import LogsTableRow from './LogsTableRow';
|
import LogsTableRow from './LogsTableRow';
|
||||||
|
|
||||||
function LogsTable(props) {
|
function LogsTable(props) {
|
||||||
|
@ -81,9 +82,9 @@ function LogsTable(props) {
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !error && !items.length &&
|
isPopulated && !error && !items.length &&
|
||||||
<div>
|
<Alert kind={kinds.INFO}>
|
||||||
No events found
|
No events found
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,7 +11,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
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 { icons } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import LogsNavMenu from '../LogsNavMenu';
|
import LogsNavMenu from '../LogsNavMenu';
|
||||||
import LogFilesTableRow from './LogFilesTableRow';
|
import LogFilesTableRow from './LogFilesTableRow';
|
||||||
|
|
||||||
|
@ -117,7 +117,9 @@ class LogFiles extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !items.length &&
|
!isFetching && !items.length &&
|
||||||
<div>No log files</div>
|
<Alert kind={kinds.INFO}>
|
||||||
|
No log files
|
||||||
|
</Alert>
|
||||||
}
|
}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
@ -59,7 +60,9 @@ class Updates extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
noUpdates &&
|
noUpdates &&
|
||||||
<div>No updates are available</div>
|
<Alert kind={kinds.INFO}>
|
||||||
|
No updates are available
|
||||||
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
|
||||||
|
|
||||||
function getTranslations() {
|
|
||||||
return createAjaxRequest({
|
|
||||||
global: false,
|
|
||||||
dataType: 'json',
|
|
||||||
url: '/localization'
|
|
||||||
}).request;
|
|
||||||
}
|
|
||||||
|
|
||||||
let translations = {};
|
|
||||||
|
|
||||||
getTranslations().then((data) => {
|
|
||||||
translations = data.strings;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function translate(key, tokens) {
|
|
||||||
const translation = translations[key] || key;
|
|
||||||
|
|
||||||
if (tokens) {
|
|
||||||
return translation.replace(
|
|
||||||
/\{([a-z0-9]+?)\}/gi,
|
|
||||||
(match, tokenMatch) => String(tokens[tokenMatch]) ?? match
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return translation;
|
|
||||||
}
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
|
||||||
|
function getTranslations() {
|
||||||
|
return createAjaxRequest({
|
||||||
|
global: false,
|
||||||
|
dataType: 'json',
|
||||||
|
url: '/localization',
|
||||||
|
}).request;
|
||||||
|
}
|
||||||
|
|
||||||
|
let translations: Record<string, string> = {};
|
||||||
|
|
||||||
|
export async function fetchTranslations(): Promise<boolean> {
|
||||||
|
return new Promise(async (resolve) => {
|
||||||
|
try {
|
||||||
|
const data = await getTranslations();
|
||||||
|
translations = data.strings;
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function translate(
|
||||||
|
key: string,
|
||||||
|
tokens?: Record<string, string | number | boolean>
|
||||||
|
) {
|
||||||
|
const translation = translations[key] || key;
|
||||||
|
|
||||||
|
if (tokens) {
|
||||||
|
return translation.replace(
|
||||||
|
/\{([a-z0-9]+?)\}/gi,
|
||||||
|
(match, tokenMatch) => String(tokens[tokenMatch]) ?? match
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return translation;
|
||||||
|
}
|
|
@ -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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
@ -196,16 +197,16 @@ class CutoffUnmet extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && error &&
|
!isFetching && error &&
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
Error fetching cutoff unmet
|
Error fetching cutoff unmet
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !error && !items.length &&
|
isPopulated && !error && !items.length &&
|
||||||
<div>
|
<Alert kind={kinds.INFO}>
|
||||||
No cutoff unmet items
|
No cutoff unmet items
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
@ -209,16 +210,16 @@ class Missing extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && error &&
|
!isFetching && error &&
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
Error fetching missing items
|
Error fetching missing items
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !error && !items.length &&
|
isPopulated && !error && !items.length &&
|
||||||
<div>
|
<Alert kind={kinds.INFO}>
|
||||||
No missing items
|
No missing items
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
import React from 'react';
|
||||||
|
import { render } from 'react-dom';
|
||||||
|
import createAppStore from 'Store/createAppStore';
|
||||||
|
import App from './App/App';
|
||||||
|
|
||||||
|
import 'Diag/ConsoleApi';
|
||||||
|
|
||||||
|
export async function bootstrap() {
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
const store = createAppStore(history);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<App store={store} history={history} />,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
}
|
|
@ -48,7 +48,15 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
<link rel="stylesheet" type="text/css" href="/Content/Fonts/fonts.css">
|
||||||
<!-- webpack bundles head -->
|
|
||||||
|
<script>
|
||||||
|
window.Sonarr = {
|
||||||
|
urlBase: '__URL_BASE__'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<% for (key in htmlWebpackPlugin.files.js) { %><script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[key] %>" data-no-hash></script><% } %>
|
||||||
|
<% for (key in htmlWebpackPlugin.files.css) { %><link href="<%= htmlWebpackPlugin.files.css[key] %>" rel="stylesheet"></link><% } %>
|
||||||
|
|
||||||
<title>Sonarr</title>
|
<title>Sonarr</title>
|
||||||
|
|
||||||
|
@ -77,7 +85,4 @@
|
||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<div id="root" class="root"></div>
|
<div id="root" class="root"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script src="/initialize.js" data-no-hash></script>
|
|
||||||
<!-- webpack bundles body -->
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { createBrowserHistory } from 'history';
|
|
||||||
import React from 'react';
|
|
||||||
import { render } from 'react-dom';
|
|
||||||
import createAppStore from 'Store/createAppStore';
|
|
||||||
import App from './App/App';
|
|
||||||
|
|
||||||
import './preload';
|
|
||||||
import './polyfills';
|
|
||||||
import 'Diag/ConsoleApi';
|
|
||||||
import 'Styles/globals.css';
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const history = createBrowserHistory();
|
|
||||||
const store = createAppStore(history);
|
|
||||||
|
|
||||||
render(
|
|
||||||
<App
|
|
||||||
store={store}
|
|
||||||
history={history}
|
|
||||||
/>,
|
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import './polyfills';
|
||||||
|
import 'Styles/globals.css';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const initializeUrl = `${
|
||||||
|
window.Sonarr.urlBase
|
||||||
|
}/initialize.json?t=${Date.now()}`;
|
||||||
|
const response = await fetch(initializeUrl);
|
||||||
|
|
||||||
|
window.Sonarr = await response.json();
|
||||||
|
|
||||||
|
/* eslint-disable no-undef, @typescript-eslint/ban-ts-comment */
|
||||||
|
// @ts-ignore 2304
|
||||||
|
__webpack_public_path__ = `${window.Sonarr.urlBase}/`;
|
||||||
|
/* eslint-enable no-undef, @typescript-eslint/ban-ts-comment */
|
||||||
|
|
||||||
|
const { bootstrap } = await import('./bootstrap');
|
||||||
|
|
||||||
|
await bootstrap();
|
|
@ -1,2 +0,0 @@
|
||||||
/* eslint no-undef: 0 */
|
|
||||||
__webpack_public_path__ = `${window.Sonarr.urlBase}/`;
|
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "esnext",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": false,
|
"checkJs": false,
|
||||||
"baseUrl": "src",
|
"baseUrl": "src",
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"module": "commonjs",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"author": "Team Sonarr",
|
"author": "Team Sonarr",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"readmeFilename": "readme.md",
|
"readmeFilename": "readme.md",
|
||||||
"main": "index.js",
|
"main": "index.ts",
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"defaults"
|
"defaults"
|
||||||
],
|
],
|
||||||
|
@ -101,6 +101,7 @@
|
||||||
"@types/react-router-dom": "5.3.3",
|
"@types/react-router-dom": "5.3.3",
|
||||||
"@types/react-text-truncate": "0.14.1",
|
"@types/react-text-truncate": "0.14.1",
|
||||||
"@types/react-window": "1.8.5",
|
"@types/react-window": "1.8.5",
|
||||||
|
"@types/redux-actions": "2.6.2",
|
||||||
"@types/webpack-livereload-plugin": "^2.3.3",
|
"@types/webpack-livereload-plugin": "^2.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||||
"@typescript-eslint/parser": "5.59.5",
|
"@typescript-eslint/parser": "5.59.5",
|
||||||
|
|
|
@ -121,6 +121,11 @@ namespace NzbDrone.Common.Serializer
|
||||||
return JsonConvert.SerializeObject(obj, SerializerSettings);
|
return JsonConvert.SerializeObject(obj, SerializerSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string ToJson(this object obj, Formatting formatting)
|
||||||
|
{
|
||||||
|
return JsonConvert.SerializeObject(obj, formatting, SerializerSettings);
|
||||||
|
}
|
||||||
|
|
||||||
public static void Serialize<TModel>(TModel model, TextWriter outputStream)
|
public static void Serialize<TModel>(TModel model, TextWriter outputStream)
|
||||||
{
|
{
|
||||||
var jsonTextWriter = new JsonTextWriter(outputStream);
|
var jsonTextWriter = new JsonTextWriter(outputStream);
|
||||||
|
|
|
@ -117,7 +117,7 @@ namespace NzbDrone.Console
|
||||||
{
|
{
|
||||||
System.Threading.Thread.Sleep(1000);
|
System.Threading.Thread.Sleep(1000);
|
||||||
|
|
||||||
if (System.Console.KeyAvailable)
|
if (!System.Console.IsInputRedirected && System.Console.KeyAvailable)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
using System.Linq;
|
||||||
|
using Dapper;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Datastore.Migration;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Datastore.Migration
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class import_exclusion_typeFixture : MigrationTest<import_exclusion_type>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_alter_tvdbid_column()
|
||||||
|
{
|
||||||
|
var db = WithDapperMigrationTestDb(c =>
|
||||||
|
{
|
||||||
|
c.Insert.IntoTable("ImportListExclusions").Row(new
|
||||||
|
{
|
||||||
|
TvdbId = "1",
|
||||||
|
Title = "Some Series"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should be able to insert as int after migration
|
||||||
|
db.Execute("INSERT INTO ImportListExclusions (TvdbId, Title) VALUES (2, 'Some Other Series')");
|
||||||
|
|
||||||
|
var exclusions = db.Query<ImportListExclusions192>("SELECT * FROM ImportListExclusions");
|
||||||
|
|
||||||
|
exclusions.Should().HaveCount(2);
|
||||||
|
exclusions.First().TvdbId.Should().Be(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListExclusions192
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int TvdbId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.Download
|
||||||
.Returns(_blockedProviders);
|
.Returns(_blockedProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mock<IDownloadClient> WithUsenetClient(int priority = 0)
|
private Mock<IDownloadClient> WithUsenetClient(int priority = 0, HashSet<int> tags = null)
|
||||||
{
|
{
|
||||||
var mock = new Mock<IDownloadClient>(MockBehavior.Default);
|
var mock = new Mock<IDownloadClient>(MockBehavior.Default);
|
||||||
mock.SetupGet(s => s.Definition)
|
mock.SetupGet(s => s.Definition)
|
||||||
|
@ -42,6 +42,7 @@ namespace NzbDrone.Core.Test.Download
|
||||||
.CreateNew()
|
.CreateNew()
|
||||||
.With(v => v.Id = _nextId++)
|
.With(v => v.Id = _nextId++)
|
||||||
.With(v => v.Priority = priority)
|
.With(v => v.Priority = priority)
|
||||||
|
.With(v => v.Tags = tags ?? new HashSet<int>())
|
||||||
.Build());
|
.Build());
|
||||||
|
|
||||||
_downloadClients.Add(mock.Object);
|
_downloadClients.Add(mock.Object);
|
||||||
|
@ -51,7 +52,7 @@ namespace NzbDrone.Core.Test.Download
|
||||||
return mock;
|
return mock;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mock<IDownloadClient> WithTorrentClient(int priority = 0)
|
private Mock<IDownloadClient> WithTorrentClient(int priority = 0, HashSet<int> tags = null)
|
||||||
{
|
{
|
||||||
var mock = new Mock<IDownloadClient>(MockBehavior.Default);
|
var mock = new Mock<IDownloadClient>(MockBehavior.Default);
|
||||||
mock.SetupGet(s => s.Definition)
|
mock.SetupGet(s => s.Definition)
|
||||||
|
@ -59,6 +60,7 @@ namespace NzbDrone.Core.Test.Download
|
||||||
.CreateNew()
|
.CreateNew()
|
||||||
.With(v => v.Id = _nextId++)
|
.With(v => v.Id = _nextId++)
|
||||||
.With(v => v.Priority = priority)
|
.With(v => v.Priority = priority)
|
||||||
|
.With(v => v.Tags = tags ?? new HashSet<int>())
|
||||||
.Build());
|
.Build());
|
||||||
|
|
||||||
_downloadClients.Add(mock.Object);
|
_downloadClients.Add(mock.Object);
|
||||||
|
@ -148,6 +150,69 @@ namespace NzbDrone.Core.Test.Download
|
||||||
client4.Definition.Id.Should().Be(2);
|
client4.Definition.Id.Should().Be(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_roundrobin_over_clients_with_matching_tags()
|
||||||
|
{
|
||||||
|
var seriesTags = new HashSet<int> { 1 };
|
||||||
|
var clientTags = new HashSet<int> { 1 };
|
||||||
|
|
||||||
|
WithTorrentClient();
|
||||||
|
WithTorrentClient(0, clientTags);
|
||||||
|
WithTorrentClient();
|
||||||
|
WithTorrentClient(0, clientTags);
|
||||||
|
|
||||||
|
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
|
||||||
|
client1.Definition.Id.Should().Be(2);
|
||||||
|
client2.Definition.Id.Should().Be(4);
|
||||||
|
client3.Definition.Id.Should().Be(2);
|
||||||
|
client4.Definition.Id.Should().Be(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_roundrobin_over_non_tagged_when_no_matching_tags()
|
||||||
|
{
|
||||||
|
var seriesTags = new HashSet<int> { 2 };
|
||||||
|
var clientTags = new HashSet<int> { 1 };
|
||||||
|
|
||||||
|
WithTorrentClient();
|
||||||
|
WithTorrentClient(0, clientTags);
|
||||||
|
WithTorrentClient();
|
||||||
|
WithTorrentClient(0, clientTags);
|
||||||
|
|
||||||
|
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
|
||||||
|
client1.Definition.Id.Should().Be(1);
|
||||||
|
client2.Definition.Id.Should().Be(3);
|
||||||
|
client3.Definition.Id.Should().Be(1);
|
||||||
|
client4.Definition.Id.Should().Be(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_fail_to_choose_when_clients_have_tags_but_no_match()
|
||||||
|
{
|
||||||
|
var seriesTags = new HashSet<int> { 2 };
|
||||||
|
var clientTags = new HashSet<int> { 1 };
|
||||||
|
|
||||||
|
WithTorrentClient(0, clientTags);
|
||||||
|
WithTorrentClient(0, clientTags);
|
||||||
|
WithTorrentClient(0, clientTags);
|
||||||
|
WithTorrentClient(0, clientTags);
|
||||||
|
|
||||||
|
var client1 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags);
|
||||||
|
|
||||||
|
Subject.GetDownloadClient(DownloadProtocol.Torrent, 0, false, seriesTags).Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_skip_blocked_torrent_client()
|
public void should_skip_blocked_torrent_client()
|
||||||
{
|
{
|
||||||
|
@ -162,7 +227,6 @@ namespace NzbDrone.Core.Test.Download
|
||||||
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
var client2 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||||
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
var client3 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||||
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
var client4 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
||||||
var client5 = Subject.GetDownloadClient(DownloadProtocol.Torrent);
|
|
||||||
|
|
||||||
client1.Definition.Id.Should().Be(2);
|
client1.Definition.Id.Should().Be(2);
|
||||||
client2.Definition.Id.Should().Be(4);
|
client2.Definition.Id.Should().Be(4);
|
||||||
|
|
|
@ -32,8 +32,8 @@ namespace NzbDrone.Core.Test.Download
|
||||||
.Returns(_downloadClients);
|
.Returns(_downloadClients);
|
||||||
|
|
||||||
Mocker.GetMock<IProvideDownloadClient>()
|
Mocker.GetMock<IProvideDownloadClient>()
|
||||||
.Setup(v => v.GetDownloadClient(It.IsAny<DownloadProtocol>(), It.IsAny<int>(), It.IsAny<bool>()))
|
.Setup(v => v.GetDownloadClient(It.IsAny<DownloadProtocol>(), It.IsAny<int>(), It.IsAny<bool>(), It.IsAny<HashSet<int>>()))
|
||||||
.Returns<DownloadProtocol, int, bool>((v, i, f) => _downloadClients.FirstOrDefault(d => d.Protocol == v));
|
.Returns<DownloadProtocol, int, bool, HashSet<int>>((v, i, f, t) => _downloadClients.FirstOrDefault(d => d.Protocol == v));
|
||||||
|
|
||||||
var episodes = Builder<Episode>.CreateListOfSize(2)
|
var episodes = Builder<Episode>.CreateListOfSize(2)
|
||||||
.TheFirst(1).With(s => s.Id = 12)
|
.TheFirst(1).With(s => s.Id = 12)
|
||||||
|
|
|
@ -120,6 +120,8 @@ namespace NzbDrone.Core.Test.ParserTests
|
||||||
[TestCase("[HatSubs] Anime Title 1004 [E63F2984].mkv", "Anime Title", 1004, 0, 0)]
|
[TestCase("[HatSubs] Anime Title 1004 [E63F2984].mkv", "Anime Title", 1004, 0, 0)]
|
||||||
[TestCase("Anime Title 100 S3 - 01 (1080p) [5A493522]", "Anime Title 100 S3", 1, 0, 0)]
|
[TestCase("Anime Title 100 S3 - 01 (1080p) [5A493522]", "Anime Title 100 S3", 1, 0, 0)]
|
||||||
[TestCase("[SubsPlease] Anime Title 100 S3 - 01 (1080p) [5A493522]", "Anime Title 100 S3", 1, 0, 0)]
|
[TestCase("[SubsPlease] Anime Title 100 S3 - 01 (1080p) [5A493522]", "Anime Title 100 S3", 1, 0, 0)]
|
||||||
|
[TestCase("[CameEsp] Another Anime 100 - Another 100 Anime - 01 [720p][ESP-ENG][mkv]", "Another Anime 100 - Another 100 Anime", 1, 0, 0)]
|
||||||
|
[TestCase("[SubsPlease] Another Anime 100 - Another 100 Anime - 01 (1080p) [4E6B4518].mkv", "Another Anime 100 - Another 100 Anime", 1, 0, 0)]
|
||||||
|
|
||||||
// [TestCase("", "", 0, 0, 0)]
|
// [TestCase("", "", 0, 0, 0)]
|
||||||
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
|
public void should_parse_absolute_numbers(string postTitle, string title, int absoluteEpisodeNumber, int seasonNumber, int episodeNumber)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue