Merge branch 'Sonarr:develop' into putio-download
This commit is contained in:
commit
b8007391b8
|
@ -1,5 +1,10 @@
|
||||||
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
||||||
|
|
||||||
|
[![Translated](https://translate.servarr.com/widgets/servarr/-/sonarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/)
|
||||||
|
[![Backers on Open Collective](https://opencollective.com/Sonarr/backers/badge.svg)](#backers)
|
||||||
|
[![Sponsors on Open Collective](https://opencollective.com/Sonarr/sponsors/badge.svg)](#sponsors)
|
||||||
|
[![Mega Sponsors on Open Collective](https://opencollective.com/Sonarr/megasponsors/badge.svg)](#mega-sponsors)
|
||||||
|
|
||||||
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
|
@ -36,6 +36,7 @@ class Blocklist extends Component {
|
||||||
lastToggled: null,
|
lastToggled: null,
|
||||||
selectedState: {},
|
selectedState: {},
|
||||||
isConfirmRemoveModalOpen: false,
|
isConfirmRemoveModalOpen: false,
|
||||||
|
isConfirmClearModalOpen: false,
|
||||||
items: props.items
|
items: props.items
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -90,6 +91,19 @@ class Blocklist extends Component {
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onClearBlocklistPress = () => {
|
||||||
|
this.setState({ isConfirmClearModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onClearBlocklistConfirmed = () => {
|
||||||
|
this.props.onClearBlocklistPress();
|
||||||
|
this.setState({ isConfirmClearModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmClearModalClose = () => {
|
||||||
|
this.setState({ isConfirmClearModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -103,7 +117,6 @@ class Blocklist extends Component {
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
isClearingBlocklistExecuting,
|
isClearingBlocklistExecuting,
|
||||||
onClearBlocklistPress,
|
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -111,7 +124,8 @@ class Blocklist extends Component {
|
||||||
allSelected,
|
allSelected,
|
||||||
allUnselected,
|
allUnselected,
|
||||||
selectedState,
|
selectedState,
|
||||||
isConfirmRemoveModalOpen
|
isConfirmRemoveModalOpen,
|
||||||
|
isConfirmClearModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const selectedIds = this.getSelectedIds();
|
const selectedIds = this.getSelectedIds();
|
||||||
|
@ -131,8 +145,9 @@ class Blocklist extends Component {
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('Clear')}
|
label={translate('Clear')}
|
||||||
iconName={icons.CLEAR}
|
iconName={icons.CLEAR}
|
||||||
|
isDisabled={!items.length}
|
||||||
isSpinning={isClearingBlocklistExecuting}
|
isSpinning={isClearingBlocklistExecuting}
|
||||||
onPress={onClearBlocklistPress}
|
onPress={this.onClearBlocklistPress}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
@ -215,6 +230,16 @@ class Blocklist extends Component {
|
||||||
onConfirm={this.onRemoveSelectedConfirmed}
|
onConfirm={this.onRemoveSelectedConfirmed}
|
||||||
onCancel={this.onConfirmRemoveModalClose}
|
onCancel={this.onConfirmRemoveModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmClearModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('ClearBlocklist')}
|
||||||
|
message={translate('ClearBlocklistMessageText')}
|
||||||
|
confirmLabel={translate('Clear')}
|
||||||
|
onConfirm={this.onClearBlocklistConfirmed}
|
||||||
|
onCancel={this.onConfirmClearModalClose}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,7 +231,7 @@ function HistoryDetails(props) {
|
||||||
reasonMessage = translate('DeletedReasonManual');
|
reasonMessage = translate('DeletedReasonManual');
|
||||||
break;
|
break;
|
||||||
case 'MissingFromDisk':
|
case 'MissingFromDisk':
|
||||||
reasonMessage = translate('DeletedReasonMissingFromDisk');
|
reasonMessage = translate('DeletedReasonEpisodeMissingFromDisk');
|
||||||
break;
|
break;
|
||||||
case 'Upgrade':
|
case 'Upgrade':
|
||||||
reasonMessage = translate('DeletedReasonUpgrade');
|
reasonMessage = translate('DeletedReasonUpgrade');
|
||||||
|
|
|
@ -15,6 +15,7 @@ import TablePager from 'Components/Table/TablePager';
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import HistoryFilterModal from './HistoryFilterModal';
|
||||||
import HistoryRowConnector from './HistoryRowConnector';
|
import HistoryRowConnector from './HistoryRowConnector';
|
||||||
|
|
||||||
class History extends Component {
|
class History extends Component {
|
||||||
|
@ -52,6 +53,7 @@ class History extends Component {
|
||||||
columns,
|
columns,
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
|
customFilters,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isEpisodesFetching,
|
isEpisodesFetching,
|
||||||
isEpisodesPopulated,
|
isEpisodesPopulated,
|
||||||
|
@ -92,7 +94,8 @@ class History extends Component {
|
||||||
alignMenu={align.RIGHT}
|
alignMenu={align.RIGHT}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={[]}
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={HistoryFilterModal}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
@ -163,8 +166,9 @@ History.propTypes = {
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isEpisodesFetching: PropTypes.bool.isRequired,
|
isEpisodesFetching: PropTypes.bool.isRequired,
|
||||||
isEpisodesPopulated: PropTypes.bool.isRequired,
|
isEpisodesPopulated: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||||
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||||
import * as historyActions from 'Store/Actions/historyActions';
|
import * as historyActions from 'Store/Actions/historyActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
@ -15,11 +16,13 @@ function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.history,
|
(state) => state.history,
|
||||||
(state) => state.episodes,
|
(state) => state.episodes,
|
||||||
(history, episodes) => {
|
createCustomFiltersSelector('history'),
|
||||||
|
(history, episodes, customFilters) => {
|
||||||
return {
|
return {
|
||||||
isEpisodesFetching: episodes.isFetching,
|
isEpisodesFetching: episodes.isFetching,
|
||||||
isEpisodesPopulated: episodes.isPopulated,
|
isEpisodesPopulated: episodes.isPopulated,
|
||||||
episodesError: episodes.error,
|
episodesError: episodes.error,
|
||||||
|
customFilters,
|
||||||
...history
|
...history
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,19 +39,19 @@ function getIconKind(eventType) {
|
||||||
function getTooltip(eventType, data) {
|
function getTooltip(eventType, data) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return translate('GrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
|
return translate('EpisodeGrabbedTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
|
||||||
case 'seriesFolderImported':
|
case 'seriesFolderImported':
|
||||||
return translate('SeriesFolderImportedTooltip');
|
return translate('SeriesFolderImportedTooltip');
|
||||||
case 'downloadFolderImported':
|
case 'downloadFolderImported':
|
||||||
return translate('EpisodeImportedTooltip');
|
return translate('EpisodeImportedTooltip');
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return translate('DownloadFailedTooltip');
|
return translate('DownloadFailedEpisodeTooltip');
|
||||||
case 'episodeFileDeleted':
|
case 'episodeFileDeleted':
|
||||||
return translate('EpisodeFileDeletedTooltip');
|
return translate('EpisodeFileDeletedTooltip');
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return translate('EpisodeFileRenamedTooltip');
|
return translate('EpisodeFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
return translate('DownloadIgnoredTooltip');
|
return translate('DownloadIgnoredEpisodeTooltip');
|
||||||
default:
|
default:
|
||||||
return translate('UnknownEventTooltip');
|
return translate('UnknownEventTooltip');
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||||
|
|
||||||
|
function createHistorySelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.history.items,
|
||||||
|
(queueItems) => {
|
||||||
|
return queueItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.history.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createHistorySelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'history';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setHistoryFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
@ -21,6 +22,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import QueueFilterModal from './QueueFilterModal';
|
||||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
import QueueRowConnector from './QueueRowConnector';
|
||||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||||
|
@ -151,11 +153,16 @@ class Queue extends Component {
|
||||||
isEpisodesPopulated,
|
isEpisodesPopulated,
|
||||||
episodesError,
|
episodesError,
|
||||||
columns,
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
count,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isGrabbing,
|
isGrabbing,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
onRefreshPress,
|
onRefreshPress,
|
||||||
|
onFilterSelect,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -218,6 +225,15 @@ class Queue extends Component {
|
||||||
iconName={icons.TABLE}
|
iconName={icons.TABLE}
|
||||||
/>
|
/>
|
||||||
</TableOptionsModalWrapper>
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={QueueFilterModal}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
|
@ -239,7 +255,11 @@ class Queue extends Component {
|
||||||
{
|
{
|
||||||
isAllPopulated && !hasError && !items.length ?
|
isAllPopulated && !hasError && !items.length ?
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{translate('QueueIsEmpty')}
|
{
|
||||||
|
selectedFilterKey !== 'all' && count > 0 ?
|
||||||
|
translate('QueueFilterHasNoItems') :
|
||||||
|
translate('QueueIsEmpty')
|
||||||
|
}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -323,13 +343,22 @@ Queue.propTypes = {
|
||||||
isEpisodesPopulated: PropTypes.bool.isRequired,
|
isEpisodesPopulated: PropTypes.bool.isRequired,
|
||||||
episodesError: PropTypes.object,
|
episodesError: PropTypes.object,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
count: PropTypes.number.isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
onRefreshPress: PropTypes.func.isRequired,
|
||||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
onRemoveSelectedPress: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
Queue.defaultProps = {
|
||||||
|
count: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Queue;
|
export default Queue;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||||
import * as queueActions from 'Store/Actions/queueActions';
|
import * as queueActions from 'Store/Actions/queueActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
@ -18,12 +19,16 @@ function createMapStateToProps() {
|
||||||
(state) => state.episodes,
|
(state) => state.episodes,
|
||||||
(state) => state.queue.options,
|
(state) => state.queue.options,
|
||||||
(state) => state.queue.paged,
|
(state) => state.queue.paged,
|
||||||
|
(state) => state.queue.status.item,
|
||||||
|
createCustomFiltersSelector('queue'),
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||||
(episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
(episodes, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
||||||
return {
|
return {
|
||||||
|
count: options.includeUnknownSeriesItems ? status.totalCount : status.count,
|
||||||
isEpisodesFetching: episodes.isFetching,
|
isEpisodesFetching: episodes.isFetching,
|
||||||
isEpisodesPopulated: episodes.isPopulated,
|
isEpisodesPopulated: episodes.isPopulated,
|
||||||
episodesError: episodes.error,
|
episodesError: episodes.error,
|
||||||
|
customFilters,
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
...options,
|
...options,
|
||||||
...queue
|
...queue
|
||||||
|
@ -122,6 +127,10 @@ class QueueConnector extends Component {
|
||||||
this.props.setQueueSort({ sortKey });
|
this.props.setQueueSort({ sortKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setQueueFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
onTableOptionChange = (payload) => {
|
||||||
this.props.setQueueTableOption(payload);
|
this.props.setQueueTableOption(payload);
|
||||||
|
|
||||||
|
@ -156,6 +165,7 @@ class QueueConnector extends Component {
|
||||||
onLastPagePress={this.onLastPagePress}
|
onLastPagePress={this.onLastPagePress}
|
||||||
onPageSelect={this.onPageSelect}
|
onPageSelect={this.onPageSelect}
|
||||||
onSortPress={this.onSortPress}
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
onRefreshPress={this.onRefreshPress}
|
onRefreshPress={this.onRefreshPress}
|
||||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||||
|
@ -178,6 +188,7 @@ QueueConnector.propTypes = {
|
||||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||||
gotoQueuePage: PropTypes.func.isRequired,
|
gotoQueuePage: PropTypes.func.isRequired,
|
||||||
setQueueSort: PropTypes.func.isRequired,
|
setQueueSort: PropTypes.func.isRequired,
|
||||||
|
setQueueFilter: PropTypes.func.isRequired,
|
||||||
setQueueTableOption: PropTypes.func.isRequired,
|
setQueueTableOption: PropTypes.func.isRequired,
|
||||||
clearQueue: PropTypes.func.isRequired,
|
clearQueue: PropTypes.func.isRequired,
|
||||||
grabQueueItems: PropTypes.func.isRequired,
|
grabQueueItems: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -81,4 +81,9 @@ QueueDetails.propTypes = {
|
||||||
progressBar: PropTypes.node.isRequired
|
progressBar: PropTypes.node.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QueueDetails.defaultProps = {
|
||||||
|
trackedDownloadStatus: 'ok',
|
||||||
|
trackedDownloadState: 'downloading'
|
||||||
|
};
|
||||||
|
|
||||||
export default QueueDetails;
|
export default QueueDetails;
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setQueueFilter } from 'Store/Actions/queueActions';
|
||||||
|
|
||||||
|
function createQueueSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.paged.items,
|
||||||
|
(queueItems) => {
|
||||||
|
return queueItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.paged.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createQueueSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'queue';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setQueueFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { tooltipPositions } from 'Helpers/Props';
|
import { tooltipPositions } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QueueStatus from './QueueStatus';
|
import QueueStatus from './QueueStatus';
|
||||||
import styles from './QueueStatusCell.css';
|
import styles from './QueueStatusCell.css';
|
||||||
|
|
||||||
|
@ -41,8 +40,8 @@ QueueStatusCell.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
QueueStatusCell.defaultProps = {
|
QueueStatusCell.defaultProps = {
|
||||||
trackedDownloadStatus: translate('Ok'),
|
trackedDownloadStatus: 'ok',
|
||||||
trackedDownloadState: translate('Downloading')
|
trackedDownloadState: 'downloading'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QueueStatusCell;
|
export default QueueStatusCell;
|
||||||
|
|
|
@ -120,7 +120,7 @@ class RemoveQueueItemModal extends Component {
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="blocklist"
|
name="blocklist"
|
||||||
value={blocklist}
|
value={blocklist}
|
||||||
helpText={translate('BlocklistReleaseHelpText')}
|
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
||||||
onChange={this.onBlocklistChange}
|
onChange={this.onBlocklistChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -123,7 +123,7 @@ class RemoveQueueItemsModal extends Component {
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="blocklist"
|
name="blocklist"
|
||||||
value={blocklist}
|
value={blocklist}
|
||||||
helpText={translate('BlocklistReleaseHelpText')}
|
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
||||||
onChange={this.onBlocklistChange}
|
onChange={this.onBlocklistChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import formatTime from 'Utilities/Date/formatTime';
|
import formatTime from 'Utilities/Date/formatTime';
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
@ -25,11 +28,13 @@ function TimeleftCell(props) {
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell className={styles.timeleft}>
|
||||||
className={styles.timeleft}
|
<Tooltip
|
||||||
title={translate('DelayingDownloadUntil', { date, time })}
|
anchor={<Icon name={icons.INFO} />}
|
||||||
>
|
tooltip={translate('DelayingDownloadUntil', { date, time })}
|
||||||
-
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -39,11 +44,13 @@ function TimeleftCell(props) {
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell className={styles.timeleft}>
|
||||||
className={styles.timeleft}
|
<Tooltip
|
||||||
title={translate('RetryingDownloadOn', { date, time })}
|
anchor={<Icon name={icons.INFO} />}
|
||||||
>
|
tooltip={translate('RetryingDownloadOn', { date, time })}
|
||||||
-
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,17 +79,17 @@ class ImportSeriesSelectFolder extends Component {
|
||||||
!error && isPopulated &&
|
!error && isPopulated &&
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
{translate('LibraryImportHeader')}
|
{translate('LibraryImportSeriesHeader')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.tips}>
|
<div className={styles.tips}>
|
||||||
{translate('LibraryImportTips')}
|
{translate('LibraryImportTips')}
|
||||||
<ul>
|
<ul>
|
||||||
<li className={styles.tip}>
|
<li className={styles.tip}>
|
||||||
<InlineMarkdown data={translate('LibraryImportTipsQualityInFilename')} />
|
<InlineMarkdown data={translate('LibraryImportTipsQualityInEpisodeFilename')} />
|
||||||
</li>
|
</li>
|
||||||
<li className={styles.tip}>
|
<li className={styles.tip}>
|
||||||
<InlineMarkdown data={translate('LibraryImportTipsUseRootFolder', { goodFolderExample, badFolderExample })} />
|
<InlineMarkdown data={translate('LibraryImportTipsSeriesUseRootFolder', { goodFolderExample, badFolderExample })} />
|
||||||
</li>
|
</li>
|
||||||
<li className={styles.tip}>
|
<li className={styles.tip}>
|
||||||
{translate('LibraryImportTipsDontUseDownloadsFolder')}
|
{translate('LibraryImportTipsDontUseDownloadsFolder')}
|
||||||
|
|
|
@ -5,12 +5,13 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { addRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ImportSeriesSelectFolder from './ImportSeriesSelectFolder';
|
import ImportSeriesSelectFolder from './ImportSeriesSelectFolder';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.rootFolders,
|
createRootFoldersSelector(),
|
||||||
createSystemStatusSelector(),
|
createSystemStatusSelector(),
|
||||||
(rootFolders, systemStatus) => {
|
(rootFolders, systemStatus) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function SeriesMonitorNewItemsOptionsPopoverContent() {
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('MonitorAllSeasons')}
|
||||||
|
data={translate('MonitorAllSeasonsDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('MonitorNoNewSeasons')}
|
||||||
|
data={translate('MonitorNoNewSeasonsDescription')}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SeriesMonitorNewItemsOptionsPopoverContent;
|
|
@ -26,29 +26,39 @@ function SeriesMonitoringOptionsPopoverContent() {
|
||||||
data={translate('MonitorExistingEpisodesDescription')}
|
data={translate('MonitorExistingEpisodesDescription')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('MonitorRecentEpisodes')}
|
||||||
|
data={translate('MonitorRecentEpisodesDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('MonitorPilotEpisode')}
|
||||||
|
data={translate('MonitorPilotEpisodeDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('MonitorFirstSeason')}
|
title={translate('MonitorFirstSeason')}
|
||||||
data={translate('MonitorFirstSeasonDescription')}
|
data={translate('MonitorFirstSeasonDescription')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('MonitorLatestSeason')}
|
title={translate('MonitorLastSeason')}
|
||||||
data={translate('MonitorLatestSeasonDescription')}
|
data={translate('MonitorLastSeasonDescription')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('MonitorSpecials')}
|
title={translate('MonitorSpecialEpisodes')}
|
||||||
data={translate('MonitorSpecialsDescription')}
|
data={translate('MonitorSpecialEpisodesDescription')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('UnmonitorSpecials')}
|
title={translate('UnmonitorSpecialEpisodes')}
|
||||||
data={translate('UnmonitorSpecialsDescription')}
|
data={translate('UnmonitorSpecialsEpisodesDescription')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('MonitorNone')}
|
title={translate('MonitorNoEpisodes')}
|
||||||
data={translate('MonitorNoneDescription')}
|
data={translate('MonitorNoEpisodesDescription')}
|
||||||
/>
|
/>
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,17 +8,17 @@ function SeriesTypePopoverContent() {
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('Anime')}
|
title={translate('Anime')}
|
||||||
data={translate('AnimeTypeDescription')}
|
data={translate('AnimeEpisodeTypeDescription')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('Daily')}
|
title={translate('Daily')}
|
||||||
data={translate('DailyTypeDescription')}
|
data={translate('DailyEpisodeTypeDescription')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('Standard')}
|
title={translate('Standard')}
|
||||||
data={translate('StandardTypeDescription')}
|
data={translate('StandardEpisodeTypeDescription')}
|
||||||
/>
|
/>
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|
|
@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('AppUpdated', { appName: 'Sonarr' })}
|
{translate('AppUpdated')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Sonarr', version })} blockClassName={styles.version} />
|
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
{translate('ConnectionLostToBackend', { appName: 'Sonarr' })}
|
{translate('ConnectionLostToBackend')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.automatic}>
|
<div className={styles.automatic}>
|
||||||
{translate('ConnectionLostReconnect', { appName: 'Sonarr' })}
|
{translate('ConnectionLostReconnect')}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import { FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
responseJSON: {
|
||||||
|
@ -20,6 +21,10 @@ export interface PagedAppSectionState {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionFilterState<T> {
|
||||||
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppSectionSchemaState<T> {
|
export interface AppSectionSchemaState<T> {
|
||||||
isSchemaFetching: boolean;
|
isSchemaFetching: boolean;
|
||||||
isSchemaPopulated: boolean;
|
isSchemaPopulated: boolean;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||||
import EpisodesAppState from './EpisodesAppState';
|
import EpisodesAppState from './EpisodesAppState';
|
||||||
|
import HistoryAppState from './HistoryAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
import RootFolderAppState from './RootFolderAppState';
|
import RootFolderAppState from './RootFolderAppState';
|
||||||
|
@ -48,6 +49,7 @@ interface AppState {
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
episodeFiles: EpisodeFilesAppState;
|
episodeFiles: EpisodeFilesAppState;
|
||||||
episodesSelection: EpisodesAppState;
|
episodesSelection: EpisodesAppState;
|
||||||
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
parse: ParseAppState;
|
parse: ParseAppState;
|
||||||
queue: QueueAppState;
|
queue: QueueAppState;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
import Episode from 'Episode/Episode';
|
import Episode from 'Episode/Episode';
|
||||||
import { FilterBuilderProp } from './AppState';
|
|
||||||
|
|
||||||
interface CalendarAppState extends AppSectionState<Episode> {
|
interface CalendarAppState
|
||||||
filterBuilderProps: FilterBuilderProp<Episode>[];
|
extends AppSectionState<Episode>,
|
||||||
}
|
AppSectionFilterState<Episode> {}
|
||||||
|
|
||||||
export default CalendarAppState;
|
export default CalendarAppState;
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import History from 'typings/History';
|
||||||
|
|
||||||
|
interface HistoryAppState
|
||||||
|
extends AppSectionState<History>,
|
||||||
|
AppSectionFilterState<History> {}
|
||||||
|
|
||||||
|
export default HistoryAppState;
|
|
@ -1,43 +1,17 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import Queue from 'typings/Queue';
|
||||||
import Language from 'Language/Language';
|
import AppSectionState, {
|
||||||
import { QualityModel } from 'Quality/Quality';
|
AppSectionFilterState,
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
AppSectionItemState,
|
||||||
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
|
Error,
|
||||||
|
} from './AppSectionState';
|
||||||
export interface StatusMessage {
|
|
||||||
title: string;
|
|
||||||
messages: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Queue extends ModelBase {
|
|
||||||
languages: Language[];
|
|
||||||
quality: QualityModel;
|
|
||||||
customFormats: CustomFormat[];
|
|
||||||
size: number;
|
|
||||||
title: string;
|
|
||||||
sizeleft: number;
|
|
||||||
timeleft: string;
|
|
||||||
estimatedCompletionTime: string;
|
|
||||||
status: string;
|
|
||||||
trackedDownloadStatus: string;
|
|
||||||
trackedDownloadState: string;
|
|
||||||
statusMessages: StatusMessage[];
|
|
||||||
errorMessage: string;
|
|
||||||
downloadId: string;
|
|
||||||
protocol: string;
|
|
||||||
downloadClient: string;
|
|
||||||
outputPath: string;
|
|
||||||
episodeHasFile: boolean;
|
|
||||||
seriesId?: number;
|
|
||||||
episodeId?: number;
|
|
||||||
seasonNumber?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||||
params: unknown;
|
params: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueuePagedAppState extends AppSectionState<Queue> {
|
export interface QueuePagedAppState
|
||||||
|
extends AppSectionState<Queue>,
|
||||||
|
AppSectionFilterState<Queue> {
|
||||||
isGrabbing: boolean;
|
isGrabbing: boolean;
|
||||||
grabError: Error;
|
grabError: Error;
|
||||||
isRemoving: boolean;
|
isRemoving: boolean;
|
||||||
|
|
|
@ -23,13 +23,11 @@ function createFilterBuilderPropsSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SeriesIndexFilterModalProps {
|
interface CalendarFilterModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CalendarFilterModal(
|
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
||||||
props: SeriesIndexFilterModalProps
|
|
||||||
) {
|
|
||||||
const sectionItems = useSelector(createCalendarSelector());
|
const sectionItems = useSelector(createCalendarSelector());
|
||||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
const customFilterType = 'calendar';
|
const customFilterType = 'calendar';
|
||||||
|
|
|
@ -25,7 +25,7 @@ function Legend(props) {
|
||||||
name="Finale"
|
name="Finale"
|
||||||
icon={icons.INFO}
|
icon={icons.INFO}
|
||||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||||
tooltip={translate('CalendarLegendFinaleTooltip')}
|
tooltip={translate('CalendarLegendSeriesFinaleTooltip')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ function Legend(props) {
|
||||||
<div>
|
<div>
|
||||||
<LegendItem
|
<LegendItem
|
||||||
status="unaired"
|
status="unaired"
|
||||||
tooltip={translate('CalendarLegendUnairedTooltip')}
|
tooltip={translate('CalendarLegendEpisodeUnairedTooltip')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
|
@ -66,7 +66,7 @@ function Legend(props) {
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
status="unmonitored"
|
status="unmonitored"
|
||||||
tooltip={translate('CalendarLegendUnmonitoredTooltip')}
|
tooltip={translate('CalendarLegendEpisodeUnmonitoredTooltip')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
|
@ -77,7 +77,7 @@ function Legend(props) {
|
||||||
<LegendItem
|
<LegendItem
|
||||||
status="onAir"
|
status="onAir"
|
||||||
name="On Air"
|
name="On Air"
|
||||||
tooltip={translate('CalendarLegendOnAirTooltip')}
|
tooltip={translate('CalendarLegendEpisodeOnAirTooltip')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
|
@ -85,7 +85,7 @@ function Legend(props) {
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
status="missing"
|
status="missing"
|
||||||
tooltip={translate('CalendarLegendMissingTooltip')}
|
tooltip={translate('CalendarLegendEpisodeMissingTooltip')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
|
@ -95,7 +95,7 @@ function Legend(props) {
|
||||||
<div>
|
<div>
|
||||||
<LegendItem
|
<LegendItem
|
||||||
status="downloading"
|
status="downloading"
|
||||||
tooltip={translate('CalendarLegendDownloadingTooltip')}
|
tooltip={translate('CalendarLegendEpisodeDownloadingTooltip')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
|
@ -103,7 +103,7 @@ function Legend(props) {
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
status="downloaded"
|
status="downloaded"
|
||||||
tooltip={translate('CalendarLegendDownloadedTooltip')}
|
tooltip={translate('CalendarLegendEpisodeDownloadedTooltip')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
|
@ -116,7 +116,7 @@ function Legend(props) {
|
||||||
icon={icons.INFO}
|
icon={icons.INFO}
|
||||||
kind={kinds.INFO}
|
kind={kinds.INFO}
|
||||||
darken={true}
|
darken={true}
|
||||||
tooltip={translate('CalendarLegendPremiereTooltip')}
|
tooltip={translate('CalendarLegendSeriesPremiereTooltip')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{iconsToShow[0]}
|
{iconsToShow[0]}
|
||||||
|
|
|
@ -116,7 +116,7 @@ class CalendarLinkModalContent extends Component {
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('CalendarFeed', { appName: 'Sonarr' })}
|
{translate('CalendarFeed')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
@ -128,7 +128,7 @@ class CalendarLinkModalContent extends Component {
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="unmonitored"
|
name="unmonitored"
|
||||||
value={unmonitored}
|
value={unmonitored}
|
||||||
helpText={translate('ICalIncludeUnmonitoredHelpText')}
|
helpText={translate('ICalIncludeUnmonitoredEpisodesHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
value={tags}
|
value={tags}
|
||||||
helpText={translate('ICalTagsHelpText')}
|
helpText={translate('ICalTagsSeriesHelpText')}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
.description {
|
|
||||||
line-height: $lineHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
line-height: $lineHeight;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
|
|
@ -6,10 +6,13 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
|
||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
|
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||||
|
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
||||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||||
|
import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue';
|
||||||
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
|
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
|
||||||
import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue';
|
import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue';
|
||||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||||
|
@ -57,9 +60,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
case filterBuilderValueTypes.DATE:
|
case filterBuilderValueTypes.DATE:
|
||||||
return DateFilterBuilderRowValue;
|
return DateFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||||
|
return HistoryEventTypeFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.INDEXER:
|
case filterBuilderValueTypes.INDEXER:
|
||||||
return IndexerFilterBuilderRowValueConnector;
|
return IndexerFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.LANGUAGE:
|
||||||
|
return LanguageFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.PROTOCOL:
|
case filterBuilderValueTypes.PROTOCOL:
|
||||||
return ProtocolFilterBuilderRowValue;
|
return ProtocolFilterBuilderRowValue;
|
||||||
|
|
||||||
|
@ -69,6 +78,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||||
return QualityProfileFilterBuilderRowValueConnector;
|
return QualityProfileFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.SERIES:
|
||||||
|
return SeriesFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.SERIES_STATUS:
|
case filterBuilderValueTypes.SERIES_STATUS:
|
||||||
return SeriesStatusFilterBuilderRowValue;
|
return SeriesStatusFilterBuilderRowValue;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { FilterBuilderProp } from 'App/State/AppState';
|
||||||
|
|
||||||
|
interface FilterBuilderRowOnChangeProps {
|
||||||
|
name: string;
|
||||||
|
value: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterBuilderRowValueProps {
|
||||||
|
filterType?: string;
|
||||||
|
filterValue: string | number | object | string[] | number[] | object[];
|
||||||
|
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||||
|
sectionItem: unknown[];
|
||||||
|
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterBuilderRowValueProps;
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
const EVENT_TYPE_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
get name() {
|
||||||
|
return translate('Grabbed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
get name() {
|
||||||
|
return translate('Imported');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
get name() {
|
||||||
|
return translate('Failed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
get name() {
|
||||||
|
return translate('Deleted');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
get name() {
|
||||||
|
return translate('Renamed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
get name() {
|
||||||
|
return translate('Ignored');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function HistoryEventTypeFilterBuilderRowValue(
|
||||||
|
props: FilterBuilderRowValueProps
|
||||||
|
) {
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryEventTypeFilterBuilderRowValue;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
const { items } = useSelector(createLanguagesSelector());
|
||||||
|
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={items} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LanguageFilterBuilderRowValue;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import Series from 'Series/Series';
|
||||||
|
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||||
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
function SeriesFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
const allSeries: Series[] = useSelector(createAllSeriesSelector());
|
||||||
|
|
||||||
|
const tagList = allSeries
|
||||||
|
.map((series) => ({ id: series.id, name: series.title }))
|
||||||
|
.sort(sortByName);
|
||||||
|
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SeriesFilterBuilderRowValue;
|
|
@ -14,6 +14,7 @@ import FormInputHelpText from './FormInputHelpText';
|
||||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
|
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
|
||||||
|
import MonitorNewItemsSelectInput from './MonitorNewItemsSelectInput';
|
||||||
import NumberInput from './NumberInput';
|
import NumberInput from './NumberInput';
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
import OAuthInputConnector from './OAuthInputConnector';
|
||||||
import PasswordInput from './PasswordInput';
|
import PasswordInput from './PasswordInput';
|
||||||
|
@ -49,6 +50,9 @@ function getComponent(type) {
|
||||||
case inputTypes.MONITOR_EPISODES_SELECT:
|
case inputTypes.MONITOR_EPISODES_SELECT:
|
||||||
return MonitorEpisodesSelectInput;
|
return MonitorEpisodesSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.MONITOR_NEW_ITEMS_SELECT:
|
||||||
|
return MonitorNewItemsSelectInput;
|
||||||
|
|
||||||
case inputTypes.NUMBER:
|
case inputTypes.NUMBER:
|
||||||
return NumberInput;
|
return NumberInput;
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-right: $formLabelRightMarginWidth;
|
margin-right: $formLabelRightMarginWidth;
|
||||||
|
padding-top: 8px;
|
||||||
|
min-height: 35px;
|
||||||
|
text-align: end;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 35px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
|
||||||
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
|
function MonitorNewItemsSelectInput(props) {
|
||||||
|
const {
|
||||||
|
includeNoChange,
|
||||||
|
includeMixed,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const values = [...monitorNewItemsOptions];
|
||||||
|
|
||||||
|
if (includeNoChange) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'noChange',
|
||||||
|
value: 'No Change',
|
||||||
|
disabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeMixed) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'mixed',
|
||||||
|
value: '(Mixed)',
|
||||||
|
disabled: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectInput
|
||||||
|
values={values}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorNewItemsSelectInput.propTypes = {
|
||||||
|
includeNoChange: PropTypes.bool.isRequired,
|
||||||
|
includeMixed: PropTypes.bool.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
MonitorNewItemsSelectInput.defaultProps = {
|
||||||
|
includeNoChange: false,
|
||||||
|
includeMixed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MonitorNewItemsSelectInput;
|
|
@ -37,6 +37,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||||
return inputTypes.OAUTH;
|
return inputTypes.OAUTH;
|
||||||
case 'rootFolder':
|
case 'rootFolder':
|
||||||
return inputTypes.ROOT_FOLDER_SELECT;
|
return inputTypes.ROOT_FOLDER_SELECT;
|
||||||
|
case 'qualityProfile':
|
||||||
|
return inputTypes.QUALITY_PROFILE_SELECT;
|
||||||
default:
|
default:
|
||||||
return inputTypes.TEXT;
|
return inputTypes.TEXT;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||||
|
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import RootFolderSelectInput from './RootFolderSelectInput';
|
import RootFolderSelectInput from './RootFolderSelectInput';
|
||||||
|
|
||||||
|
@ -10,7 +11,7 @@ const ADD_NEW_KEY = 'addNew';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.rootFolders,
|
createRootFoldersSelector(),
|
||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
(state, { includeMissingValue }) => includeMissingValue,
|
(state, { includeMissingValue }) => includeMissingValue,
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
|
|
|
@ -23,21 +23,21 @@ const seriesTypeOptions: ISeriesTypeOption[] = [
|
||||||
key: seriesTypes.STANDARD,
|
key: seriesTypes.STANDARD,
|
||||||
value: 'Standard',
|
value: 'Standard',
|
||||||
get format() {
|
get format() {
|
||||||
return translate('StandardTypeFormat', { format: 'S01E05' });
|
return translate('StandardEpisodeTypeFormat', { format: 'S01E05' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: seriesTypes.DAILY,
|
key: seriesTypes.DAILY,
|
||||||
value: 'Daily / Date',
|
value: 'Daily / Date',
|
||||||
get format() {
|
get format() {
|
||||||
return translate('DailyTypeFormat', { format: '2020-05-25' });
|
return translate('DailyEpisodeTypeFormat', { format: '2020-05-25' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: seriesTypes.ANIME,
|
key: seriesTypes.ANIME,
|
||||||
value: 'Anime / Absolute',
|
value: 'Anime / Absolute',
|
||||||
get format() {
|
get format() {
|
||||||
return translate('AnimeTypeFormat', { format: '005' });
|
return translate('AnimeEpisodeTypeFormat', { format: '005' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ModalContent.css';
|
import styles from './ModalContent.css';
|
||||||
|
|
||||||
function ModalContent(props) {
|
function ModalContent(props) {
|
||||||
|
@ -28,6 +29,7 @@ function ModalContent(props) {
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.CLOSE}
|
name={icons.CLOSE}
|
||||||
size={18}
|
size={18}
|
||||||
|
title={translate('Close')}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,7 @@ class PageHeader extends Component {
|
||||||
aria-label={translate('Donate')}
|
aria-label={translate('Donate')}
|
||||||
to="https://sonarr.tv/donate.html"
|
to="https://sonarr.tv/donate.html"
|
||||||
size={14}
|
size={14}
|
||||||
|
title={translate('Donate')}
|
||||||
/>
|
/>
|
||||||
<PageHeaderActionsMenuConnector
|
<PageHeaderActionsMenuConnector
|
||||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||||
|
|
|
@ -24,6 +24,7 @@ function PageHeaderActionsMenu(props) {
|
||||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.INTERACTIVE}
|
name={icons.INTERACTIVE}
|
||||||
|
title={translate('Menu')}
|
||||||
/>
|
/>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import EpisodeDetailsModal from './EpisodeDetailsModal';
|
import EpisodeDetailsModal from './EpisodeDetailsModal';
|
||||||
import styles from './EpisodeSearchCell.css';
|
import styles from './EpisodeSearchCell.css';
|
||||||
|
|
||||||
|
@ -50,11 +51,13 @@ class EpisodeSearchCell extends Component {
|
||||||
name={icons.SEARCH}
|
name={icons.SEARCH}
|
||||||
isSpinning={isSearching}
|
isSpinning={isSearching}
|
||||||
onPress={onSearchPress}
|
onPress={onSearchPress}
|
||||||
|
title={translate('AutomaticSearch')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
name={icons.INTERACTIVE}
|
name={icons.INTERACTIVE}
|
||||||
onPress={this.onManualSearchPress}
|
onPress={this.onManualSearchPress}
|
||||||
|
title={translate('InteractiveSearch')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EpisodeDetailsModal
|
<EpisodeDetailsModal
|
||||||
|
|
|
@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
|
||||||
authenticationMethod,
|
authenticationMethod,
|
||||||
authenticationRequired,
|
authenticationRequired,
|
||||||
username,
|
username,
|
||||||
password
|
password,
|
||||||
|
passwordConfirmation
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||||
|
@ -63,7 +64,7 @@ function AuthenticationRequiredModalContent(props) {
|
||||||
className={styles.authRequiredAlert}
|
className={styles.authRequiredAlert}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
{translate('AuthenticationRequiredWarning', { appName: 'Sonarr' })}
|
{translate('AuthenticationRequiredWarning')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -76,7 +77,7 @@ function AuthenticationRequiredModalContent(props) {
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
name="authenticationMethod"
|
name="authenticationMethod"
|
||||||
values={authenticationMethodOptions}
|
values={authenticationMethodOptions}
|
||||||
helpText={translate('AuthenticationMethodHelpText', { appName: 'Sonarr' })}
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
||||||
helpLink="https://wiki.servarr.com/sonarr/faq#forced-authentication"
|
helpLink="https://wiki.servarr.com/sonarr/faq#forced-authentication"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
@ -120,6 +121,18 @@ function AuthenticationRequiredModalContent(props) {
|
||||||
{...password}
|
{...password}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.PASSWORD}
|
||||||
|
name="passwordConfirmation"
|
||||||
|
onChange={onInputChange}
|
||||||
|
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
|
||||||
|
{...passwordConfirmation}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</div> :
|
</div> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,13 @@ export const BOOL = 'bool';
|
||||||
export const BYTES = 'bytes';
|
export const BYTES = 'bytes';
|
||||||
export const DATE = 'date';
|
export const DATE = 'date';
|
||||||
export const DEFAULT = 'default';
|
export const DEFAULT = 'default';
|
||||||
|
export const HISTORY_EVENT_TYPE = 'historyEventType';
|
||||||
export const INDEXER = 'indexer';
|
export const INDEXER = 'indexer';
|
||||||
|
export const LANGUAGE = 'language';
|
||||||
export const PROTOCOL = 'protocol';
|
export const PROTOCOL = 'protocol';
|
||||||
export const QUALITY = 'quality';
|
export const QUALITY = 'quality';
|
||||||
export const QUALITY_PROFILE = 'qualityProfile';
|
export const QUALITY_PROFILE = 'qualityProfile';
|
||||||
|
export const SERIES = 'series';
|
||||||
export const SERIES_STATUS = 'seriesStatus';
|
export const SERIES_STATUS = 'seriesStatus';
|
||||||
export const SERIES_TYPES = 'seriesType';
|
export const SERIES_TYPES = 'seriesType';
|
||||||
export const TAG = 'tag';
|
export const TAG = 'tag';
|
||||||
|
|
|
@ -4,6 +4,7 @@ export const CHECK = 'check';
|
||||||
export const DEVICE = 'device';
|
export const DEVICE = 'device';
|
||||||
export const KEY_VALUE_LIST = 'keyValueList';
|
export const KEY_VALUE_LIST = 'keyValueList';
|
||||||
export const MONITOR_EPISODES_SELECT = 'monitorEpisodesSelect';
|
export const MONITOR_EPISODES_SELECT = 'monitorEpisodesSelect';
|
||||||
|
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
|
||||||
export const FLOAT = 'float';
|
export const FLOAT = 'float';
|
||||||
export const NUMBER = 'number';
|
export const NUMBER = 'number';
|
||||||
export const OAUTH = 'oauth';
|
export const OAUTH = 'oauth';
|
||||||
|
@ -31,6 +32,7 @@ export const all = [
|
||||||
DEVICE,
|
DEVICE,
|
||||||
KEY_VALUE_LIST,
|
KEY_VALUE_LIST,
|
||||||
MONITOR_EPISODES_SELECT,
|
MONITOR_EPISODES_SELECT,
|
||||||
|
MONITOR_NEW_ITEMS_SELECT,
|
||||||
FLOAT,
|
FLOAT,
|
||||||
NUMBER,
|
NUMBER,
|
||||||
OAUTH,
|
OAUTH,
|
||||||
|
|
|
@ -69,8 +69,6 @@ interface SelectEpisodeModalContentProps {
|
||||||
seasonNumber?: number;
|
seasonNumber?: number;
|
||||||
selectedDetails?: string;
|
selectedDetails?: string;
|
||||||
isAnime: boolean;
|
isAnime: boolean;
|
||||||
sortKey?: string;
|
|
||||||
sortDirection?: string;
|
|
||||||
modalTitle: string;
|
modalTitle: string;
|
||||||
onEpisodesSelect(selectedEpisodes: SelectedEpisode[]): unknown;
|
onEpisodesSelect(selectedEpisodes: SelectedEpisode[]): unknown;
|
||||||
onModalClose(): unknown;
|
onModalClose(): unknown;
|
||||||
|
@ -86,8 +84,6 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
||||||
seasonNumber,
|
seasonNumber,
|
||||||
selectedDetails,
|
selectedDetails,
|
||||||
isAnime,
|
isAnime,
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
modalTitle,
|
modalTitle,
|
||||||
onEpisodesSelect,
|
onEpisodesSelect,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
|
@ -97,9 +93,8 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
||||||
const [selectState, setSelectState] = useSelectState();
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
|
||||||
const { allSelected, allUnselected, selectedState } = selectState;
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
const { isFetching, isPopulated, items, error } = useSelector(
|
const { isFetching, isPopulated, items, error, sortKey, sortDirection } =
|
||||||
episodesSelector()
|
useSelector(episodesSelector());
|
||||||
);
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const filterEpisodeNumber = parseInt(filter);
|
const filterEpisodeNumber = parseInt(filter);
|
||||||
|
|
|
@ -139,7 +139,7 @@ function InteractiveSearch(props) {
|
||||||
{
|
{
|
||||||
errorMessage ?
|
errorMessage ?
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
|
{translate('InteractiveSearchResultsSeriesFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
|
||||||
</Fragment> :
|
</Fragment> :
|
||||||
translate('EpisodeSearchResultsLoadError')
|
translate('EpisodeSearchResultsLoadError')
|
||||||
}
|
}
|
||||||
|
|
|
@ -309,7 +309,9 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||||
isOpen={isConfirmGrabModalOpen}
|
isOpen={isConfirmGrabModalOpen}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
title={translate('GrabRelease')}
|
title={translate('GrabRelease')}
|
||||||
message={translate('GrabReleaseMessageText', { title })}
|
message={translate('GrabReleaseUnknownSeriesOrEpisodeMessageText', {
|
||||||
|
title,
|
||||||
|
})}
|
||||||
confirmLabel={translate('Grab')}
|
confirmLabel={translate('Grab')}
|
||||||
onConfirm={onGrabConfirm}
|
onConfirm={onGrabConfirm}
|
||||||
onCancel={onGrabCancel}
|
onCancel={onGrabCancel}
|
||||||
|
|
|
@ -89,7 +89,7 @@ class DeleteSeriesModalContent extends Component {
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="addImportListExclusion"
|
name="addImportListExclusion"
|
||||||
value={addImportListExclusion}
|
value={addImportListExclusion}
|
||||||
helpText={translate('AddListExclusionHelpText')}
|
helpText={translate('AddListExclusionSeriesHelpText')}
|
||||||
onChange={onDeleteOptionChange}
|
onChange={onDeleteOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -156,6 +156,12 @@
|
||||||
.headerContent {
|
.headerContent {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointLarge) {
|
@media only screen and (max-width: $breakpointLarge) {
|
||||||
|
|
|
@ -45,11 +45,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||||
const lineHeight = parseFloat(fonts.lineHeight);
|
const lineHeight = parseFloat(fonts.lineHeight);
|
||||||
|
|
||||||
function getFanartUrl(images) {
|
function getFanartUrl(images) {
|
||||||
const fanartImage = _.find(images, { coverType: 'fanart' });
|
return _.find(images, { coverType: 'fanart' })?.url;
|
||||||
if (fanartImage) {
|
|
||||||
// Remove protocol
|
|
||||||
return fanartImage.url.replace(/^https?:/, '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExpandedState(newState) {
|
function getExpandedState(newState) {
|
||||||
|
@ -194,7 +190,7 @@ class SeriesDetails extends Component {
|
||||||
genres,
|
genres,
|
||||||
tags,
|
tags,
|
||||||
year,
|
year,
|
||||||
previousAiring,
|
lastAired,
|
||||||
isSaving,
|
isSaving,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
isSearching,
|
isSearching,
|
||||||
|
@ -231,7 +227,7 @@ class SeriesDetails extends Component {
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const statusDetails = getSeriesStatusDetails(status);
|
const statusDetails = getSeriesStatusDetails(status);
|
||||||
const runningYears = statusDetails.title === translate('Ended') ? `${year}-${getDateYear(previousAiring)}` : `${year}-`;
|
const runningYears = statusDetails.title === translate('Ended') ? `${year}-${getDateYear(lastAired)}` : `${year}-`;
|
||||||
|
|
||||||
let episodeFilesCountMessage = translate('SeriesDetailsNoEpisodeFiles');
|
let episodeFilesCountMessage = translate('SeriesDetailsNoEpisodeFiles');
|
||||||
|
|
||||||
|
@ -715,6 +711,7 @@ SeriesDetails.propTypes = {
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
|
lastAired: PropTypes.string,
|
||||||
previousAiring: PropTypes.string,
|
previousAiring: PropTypes.string,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
|
|
|
@ -210,12 +210,15 @@ class SeriesDetailsSeason extends Component {
|
||||||
seasonNumber,
|
seasonNumber,
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
statistics,
|
statistics,
|
||||||
isSaving,
|
isSaving,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
isSearching,
|
isSearching,
|
||||||
seriesMonitored,
|
seriesMonitored,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
|
onSortPress,
|
||||||
onTableOptionChange,
|
onTableOptionChange,
|
||||||
onMonitorSeasonPress,
|
onMonitorSeasonPress,
|
||||||
onSearchPress
|
onSearchPress
|
||||||
|
@ -447,6 +450,9 @@ class SeriesDetailsSeason extends Component {
|
||||||
items.length ?
|
items.length ?
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
onTableOptionChange={onTableOptionChange}
|
onTableOptionChange={onTableOptionChange}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
@ -530,6 +536,8 @@ SeriesDetailsSeason.propTypes = {
|
||||||
seasonNumber: PropTypes.number.isRequired,
|
seasonNumber: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string.isRequired,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
statistics: PropTypes.object.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
isSaving: PropTypes.bool,
|
isSaving: PropTypes.bool,
|
||||||
isExpanded: PropTypes.bool,
|
isExpanded: PropTypes.bool,
|
||||||
|
@ -537,6 +545,7 @@ SeriesDetailsSeason.propTypes = {
|
||||||
seriesMonitored: PropTypes.bool.isRequired,
|
seriesMonitored: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
onTableOptionChange: PropTypes.func.isRequired,
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
|
onSortPress: PropTypes.func.isRequired,
|
||||||
onMonitorSeasonPress: PropTypes.func.isRequired,
|
onMonitorSeasonPress: PropTypes.func.isRequired,
|
||||||
onExpandPress: PropTypes.func.isRequired,
|
onExpandPress: PropTypes.func.isRequired,
|
||||||
onMonitorEpisodePress: PropTypes.func.isRequired,
|
onMonitorEpisodePress: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -4,8 +4,9 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { setEpisodesTableOption, toggleEpisodesMonitored } from 'Store/Actions/episodeActions';
|
import { setEpisodesSort, setEpisodesTableOption, toggleEpisodesMonitored } from 'Store/Actions/episodeActions';
|
||||||
import { toggleSeasonMonitored } from 'Store/Actions/seriesActions';
|
import { toggleSeasonMonitored } from 'Store/Actions/seriesActions';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||||
|
@ -15,7 +16,7 @@ import SeriesDetailsSeason from './SeriesDetailsSeason';
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { seasonNumber }) => seasonNumber,
|
(state, { seasonNumber }) => seasonNumber,
|
||||||
(state) => state.episodes,
|
createClientSideCollectionSelector('episodes'),
|
||||||
createSeriesSelector(),
|
createSeriesSelector(),
|
||||||
createCommandsSelector(),
|
createCommandsSelector(),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
|
@ -27,11 +28,12 @@ function createMapStateToProps() {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const episodesInSeason = episodes.items.filter((episode) => episode.seasonNumber === seasonNumber);
|
const episodesInSeason = episodes.items.filter((episode) => episode.seasonNumber === seasonNumber);
|
||||||
const sortedEpisodes = episodesInSeason.sort((a, b) => b.episodeNumber - a.episodeNumber);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: sortedEpisodes,
|
items: episodesInSeason,
|
||||||
columns: episodes.columns,
|
columns: episodes.columns,
|
||||||
|
sortKey: episodes.sortKey,
|
||||||
|
sortDirection: episodes.sortDirection,
|
||||||
isSearching,
|
isSearching,
|
||||||
seriesMonitored: series.monitored,
|
seriesMonitored: series.monitored,
|
||||||
path: series.path,
|
path: series.path,
|
||||||
|
@ -45,6 +47,7 @@ const mapDispatchToProps = {
|
||||||
toggleSeasonMonitored,
|
toggleSeasonMonitored,
|
||||||
toggleEpisodesMonitored,
|
toggleEpisodesMonitored,
|
||||||
setEpisodesTableOption,
|
setEpisodesTableOption,
|
||||||
|
setEpisodesSort,
|
||||||
executeCommand
|
executeCommand
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,6 +93,13 @@ class SeriesDetailsSeasonConnector extends Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onSortPress = (sortKey, sortDirection) => {
|
||||||
|
this.props.setEpisodesSort({
|
||||||
|
sortKey,
|
||||||
|
sortDirection
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -98,6 +108,7 @@ class SeriesDetailsSeasonConnector extends Component {
|
||||||
<SeriesDetailsSeason
|
<SeriesDetailsSeason
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
onMonitorSeasonPress={this.onMonitorSeasonPress}
|
onMonitorSeasonPress={this.onMonitorSeasonPress}
|
||||||
onSearchPress={this.onSearchPress}
|
onSearchPress={this.onSearchPress}
|
||||||
onMonitorEpisodePress={this.onMonitorEpisodePress}
|
onMonitorEpisodePress={this.onMonitorEpisodePress}
|
||||||
|
@ -112,6 +123,7 @@ SeriesDetailsSeasonConnector.propTypes = {
|
||||||
toggleSeasonMonitored: PropTypes.func.isRequired,
|
toggleSeasonMonitored: PropTypes.func.isRequired,
|
||||||
toggleEpisodesMonitored: PropTypes.func.isRequired,
|
toggleEpisodesMonitored: PropTypes.func.isRequired,
|
||||||
setEpisodesTableOption: PropTypes.func.isRequired,
|
setEpisodesTableOption: PropTypes.func.isRequired,
|
||||||
|
setEpisodesSort: PropTypes.func.isRequired,
|
||||||
executeCommand: PropTypes.func.isRequired
|
executeCommand: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,7 @@
|
||||||
|
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.labelIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'deleteButton': string;
|
'deleteButton': string;
|
||||||
|
'labelIcon': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
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 ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal';
|
import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditSeriesModalContent.css';
|
import styles from './EditSeriesModalContent.css';
|
||||||
|
@ -73,6 +76,7 @@ class EditSeriesModalContent extends Component {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
monitored,
|
monitored,
|
||||||
|
monitorNewItems,
|
||||||
seasonFolder,
|
seasonFolder,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
seriesType,
|
seriesType,
|
||||||
|
@ -94,12 +98,37 @@ class EditSeriesModalContent extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="monitored"
|
name="monitored"
|
||||||
helpText={translate('MonitoredHelpText')}
|
helpText={translate('MonitoredEpisodesHelpText')}
|
||||||
{...monitored}
|
{...monitored}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('MonitorNewSeasons')}
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MonitorNewSeasons')}
|
||||||
|
body={<SeriesMonitorNewItemsOptionsPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
|
||||||
|
name="monitorNewItems"
|
||||||
|
helpText={translate('MonitorNewSeasonsHelpText')}
|
||||||
|
{...monitorNewItems}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('UseSeasonFolder')}</FormLabel>
|
<FormLabel>{translate('UseSeasonFolder')}</FormLabel>
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ function createMapStateToProps() {
|
||||||
|
|
||||||
const seriesSettings = _.pick(series, [
|
const seriesSettings = _.pick(series, [
|
||||||
'monitored',
|
'monitored',
|
||||||
|
'monitorNewItems',
|
||||||
'seasonFolder',
|
'seasonFolder',
|
||||||
'qualityProfileId',
|
'qualityProfileId',
|
||||||
'seriesType',
|
'seriesType',
|
||||||
|
|
|
@ -63,7 +63,7 @@ const rows = [
|
||||||
{
|
{
|
||||||
name: 'qualityProfileId',
|
name: 'qualityProfileId',
|
||||||
showProp: 'showQualityProfile',
|
showProp: 'showQualityProfile',
|
||||||
valueProp: 'qualityProfileId',
|
valueProp: 'qualityProfile',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'previousAiring',
|
name: 'previousAiring',
|
||||||
|
|
|
@ -101,7 +101,7 @@ function SeriesIndexPosterOptionsModalContent(
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="showTitle"
|
name="showTitle"
|
||||||
value={showTitle}
|
value={showTitle}
|
||||||
helpText={translate('ShowTitleHelpText')}
|
helpText={translate('ShowSeriesTitleHelpText')}
|
||||||
onChange={onPosterOptionChange}
|
onChange={onPosterOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -98,7 +98,7 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="addImportListExclusion"
|
name="addImportListExclusion"
|
||||||
value={addImportListExclusion}
|
value={addImportListExclusion}
|
||||||
helpText={translate('AddListExclusionHelpText')}
|
helpText={translate('AddListExclusionSeriesHelpText')}
|
||||||
onChange={onDeleteOptionChange}
|
onChange={onDeleteOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import styles from './EditSeriesModalContent.css';
|
||||||
|
|
||||||
interface SavePayload {
|
interface SavePayload {
|
||||||
monitored?: boolean;
|
monitored?: boolean;
|
||||||
|
monitorNewItems?: string;
|
||||||
qualityProfileId?: number;
|
qualityProfileId?: number;
|
||||||
seriesType?: string;
|
seriesType?: string;
|
||||||
seasonFolder?: boolean;
|
seasonFolder?: boolean;
|
||||||
|
@ -77,6 +78,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
|
||||||
const { seriesIds, onSavePress, onModalClose } = props;
|
const { seriesIds, onSavePress, onModalClose } = props;
|
||||||
|
|
||||||
const [monitored, setMonitored] = useState(NO_CHANGE);
|
const [monitored, setMonitored] = useState(NO_CHANGE);
|
||||||
|
const [monitorNewItems, setMonitorNewItems] = useState(NO_CHANGE);
|
||||||
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
|
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
|
||||||
NO_CHANGE
|
NO_CHANGE
|
||||||
);
|
);
|
||||||
|
@ -95,6 +97,11 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
|
||||||
payload.monitored = monitored === 'monitored';
|
payload.monitored = monitored === 'monitored';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (monitorNewItems !== NO_CHANGE) {
|
||||||
|
hasChanges = true;
|
||||||
|
payload.monitorNewItems = monitorNewItems;
|
||||||
|
}
|
||||||
|
|
||||||
if (qualityProfileId !== NO_CHANGE) {
|
if (qualityProfileId !== NO_CHANGE) {
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
payload.qualityProfileId = qualityProfileId as number;
|
payload.qualityProfileId = qualityProfileId as number;
|
||||||
|
@ -124,6 +131,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
monitored,
|
monitored,
|
||||||
|
monitorNewItems,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
seriesType,
|
seriesType,
|
||||||
seasonFolder,
|
seasonFolder,
|
||||||
|
@ -139,6 +147,9 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
|
||||||
case 'monitored':
|
case 'monitored':
|
||||||
setMonitored(value);
|
setMonitored(value);
|
||||||
break;
|
break;
|
||||||
|
case 'monitorNewItems':
|
||||||
|
setMonitorNewItems(value);
|
||||||
|
break;
|
||||||
case 'qualityProfileId':
|
case 'qualityProfileId':
|
||||||
setQualityProfileId(value);
|
setQualityProfileId(value);
|
||||||
break;
|
break;
|
||||||
|
@ -199,6 +210,19 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('MonitorNewItems')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
|
||||||
|
name="monitorNewItems"
|
||||||
|
value={monitorNewItems}
|
||||||
|
includeNoChange={true}
|
||||||
|
includeNoChangeDisabled={false}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,10 @@ function createSeriesQueueDetailsSelector(
|
||||||
(queueItems) => {
|
(queueItems) => {
|
||||||
return queueItems.reduce(
|
return queueItems.reduce(
|
||||||
(acc: SeriesQueueDetails, item) => {
|
(acc: SeriesQueueDetails, item) => {
|
||||||
if (item.seriesId !== seriesId) {
|
if (
|
||||||
|
item.trackedDownloadState === 'imported' ||
|
||||||
|
item.seriesId !== seriesId
|
||||||
|
) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,10 @@ function findImage(images, coverType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUrl(image, coverType, size) {
|
function getUrl(image, coverType, size) {
|
||||||
if (image) {
|
const imageUrl = image?.url;
|
||||||
// Remove protocol
|
|
||||||
let url = image.url.replace(/^https?:/, '');
|
|
||||||
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
|
|
||||||
|
|
||||||
return url;
|
if (imageUrl) {
|
||||||
|
return imageUrl.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -152,7 +152,7 @@ class CustomFormat extends Component {
|
||||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('DeleteCustomFormat')}
|
title={translate('DeleteCustomFormat')}
|
||||||
message={translate('DeleteCustomFormatMessageText', [name])}
|
message={translate('DeleteCustomFormatMessageText', { customFormatName: name })}
|
||||||
confirmLabel={translate('Delete')}
|
confirmLabel={translate('Delete')}
|
||||||
isSpinning={isDeleting}
|
isSpinning={isDeleting}
|
||||||
onConfirm={this.onConfirmDeleteCustomFormat}
|
onConfirm={this.onConfirmDeleteCustomFormat}
|
||||||
|
|
|
@ -147,7 +147,7 @@ class EditDownloadClientModalContent extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('DownloadClientTagHelpText')}
|
helpText={translate('DownloadClientSeriesTagHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -61,7 +61,7 @@ function DownloadClientOptions(props) {
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
size={sizes.MEDIUM}
|
size={sizes.MEDIUM}
|
||||||
>
|
>
|
||||||
<FormLabel>{translate('RedownloadFailed')}</FormLabel>
|
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
|
@ -71,6 +71,26 @@ function DownloadClientOptions(props) {
|
||||||
{...settings.autoRedownloadFailed}
|
{...settings.autoRedownloadFailed}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
settings.autoRedownloadFailed.value ?
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
>
|
||||||
|
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="autoRedownloadFailedFromInteractiveSearch"
|
||||||
|
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...settings.autoRedownloadFailedFromInteractiveSearch}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
|
|
|
@ -54,7 +54,7 @@ class RemotePathMappings extends Component {
|
||||||
>
|
>
|
||||||
|
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
<InlineMarkdown data={translate('RemotePathMappingsInfo', { app: 'Sonarr', wikiLink: 'https://wiki.servarr.com/sonarr/settings#remote-path-mappings' })} />
|
<InlineMarkdown data={translate('RemotePathMappingsInfo', { wikiLink: 'https://wiki.servarr.com/sonarr/settings#remote-path-mappings' })} />
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<div className={styles.remotePathMappingsHeader}>
|
<div className={styles.remotePathMappingsHeader}>
|
||||||
|
|
|
@ -124,6 +124,7 @@ class SecuritySettings extends Component {
|
||||||
authenticationRequired,
|
authenticationRequired,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
|
passwordConfirmation,
|
||||||
apiKey,
|
apiKey,
|
||||||
certificateValidation
|
certificateValidation
|
||||||
} = settings;
|
} = settings;
|
||||||
|
@ -139,8 +140,8 @@ class SecuritySettings extends Component {
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
name="authenticationMethod"
|
name="authenticationMethod"
|
||||||
values={authenticationMethodOptions}
|
values={authenticationMethodOptions}
|
||||||
helpText={translate('AuthenticationMethodHelpText', { appName: 'Sonarr' })}
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
helpTextWarning={translate('AuthenticationRequiredWarning', { appName: 'Sonarr' })}
|
helpTextWarning={translate('AuthenticationRequiredWarning')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...authenticationMethod}
|
{...authenticationMethod}
|
||||||
/>
|
/>
|
||||||
|
@ -193,6 +194,21 @@ class SecuritySettings extends Component {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
authenticationEnabled ?
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.PASSWORD}
|
||||||
|
name="passwordConfirmation"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...passwordConfirmation}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('ApiKey')}</FormLabel>
|
<FormLabel>{translate('ApiKey')}</FormLabel>
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ function UpdateSettings(props) {
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="updateAutomatically"
|
name="updateAutomatically"
|
||||||
helpText={translate('UpdateAutomaticallyHelpText')}
|
helpText={translate('UpdateAutomaticallyHelpText')}
|
||||||
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Sonarr' }) : undefined}
|
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...updateAutomatically}
|
{...updateAutomatically}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -56,7 +56,7 @@ class AddImportListModalContent extends Component {
|
||||||
|
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
<div>
|
<div>
|
||||||
{translate('SupportedLists')}
|
{translate('SupportedListsSeries')}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{translate('SupportedListsMoreInfo')}
|
{translate('SupportedListsMoreInfo')}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||||
|
import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent';
|
||||||
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
|
@ -46,9 +47,11 @@ function EditImportListModalContent(props) {
|
||||||
implementationName,
|
implementationName,
|
||||||
name,
|
name,
|
||||||
enableAutomaticAdd,
|
enableAutomaticAdd,
|
||||||
|
searchForMissingEpisodes,
|
||||||
minRefreshInterval,
|
minRefreshInterval,
|
||||||
shouldMonitor,
|
shouldMonitor,
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
|
monitorNewItems,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
seriesType,
|
seriesType,
|
||||||
seasonFolder,
|
seasonFolder,
|
||||||
|
@ -107,12 +110,24 @@ function EditImportListModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="enableAutomaticAdd"
|
name="enableAutomaticAdd"
|
||||||
helpText={translate('EnableAutomaticAddHelpText')}
|
helpText={translate('EnableAutomaticAddSeriesHelpText')}
|
||||||
{...enableAutomaticAdd}
|
{...enableAutomaticAdd}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ImportListSearchForMissingEpisodes')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="searchForMissingEpisodes"
|
||||||
|
helpText={translate('ImportListSearchForMissingEpisodesHelpText')}
|
||||||
|
{...searchForMissingEpisodes}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{translate('Monitor')}
|
{translate('Monitor')}
|
||||||
|
@ -138,6 +153,31 @@ function EditImportListModalContent(props) {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('MonitorNewSeasons')}
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MonitorNewSeasons')}
|
||||||
|
body={<SeriesMonitorNewItemsOptionsPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
|
||||||
|
name="monitorNewItems"
|
||||||
|
helpText={translate('MonitorNewSeasonsHelpText')}
|
||||||
|
{...monitorNewItems}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||||
|
|
||||||
|
|
|
@ -200,7 +200,7 @@ function EditIndexerModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('IndexerTagHelpText')}
|
helpText={translate('IndexerTagSeriesHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -180,7 +180,7 @@ class MediaManagement extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="deleteEmptyFolders"
|
name="deleteEmptyFolders"
|
||||||
helpText={translate('DeleteEmptyFoldersHelpText')}
|
helpText={translate('DeleteEmptySeriesFoldersHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.deleteEmptyFolders}
|
{...settings.deleteEmptyFolders}
|
||||||
/>
|
/>
|
||||||
|
@ -257,7 +257,7 @@ class MediaManagement extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="copyUsingHardlinks"
|
name="copyUsingHardlinks"
|
||||||
helpText={translate('CopyUsingHardlinksHelpText')}
|
helpText={translate('CopyUsingHardlinksSeriesHelpText')}
|
||||||
helpTextWarning={translate('CopyUsingHardlinksHelpTextWarning')}
|
helpTextWarning={translate('CopyUsingHardlinksHelpTextWarning')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.copyUsingHardlinks}
|
{...settings.copyUsingHardlinks}
|
||||||
|
@ -305,7 +305,7 @@ class MediaManagement extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="importExtraFiles"
|
name="importExtraFiles"
|
||||||
helpText={translate('ImportExtraFilesHelpText')}
|
helpText={translate('ImportExtraFilesEpisodeHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.importExtraFiles}
|
{...settings.importExtraFiles}
|
||||||
/>
|
/>
|
||||||
|
@ -399,7 +399,7 @@ class MediaManagement extends Component {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
name="rescanAfterRefresh"
|
name="rescanAfterRefresh"
|
||||||
helpText={translate('RescanAfterRefreshHelpText')}
|
helpText={translate('RescanAfterRefreshSeriesHelpText')}
|
||||||
helpTextWarning={translate('RescanAfterRefreshHelpTextWarning')}
|
helpTextWarning={translate('RescanAfterRefreshHelpTextWarning')}
|
||||||
values={rescanAfterRefreshOptions}
|
values={rescanAfterRefreshOptions}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
|
|
@ -82,13 +82,16 @@ const fileNameTokens = [
|
||||||
const seriesTokens = [
|
const seriesTokens = [
|
||||||
{ token: '{Series Title}', example: 'The Series Title\'s!' },
|
{ token: '{Series Title}', example: 'The Series Title\'s!' },
|
||||||
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!' },
|
{ token: '{Series CleanTitle}', example: 'The Series Title\'s!' },
|
||||||
{ token: '{Series CleanTitleYear}', example: 'The Series Titles! 2010' },
|
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)' },
|
||||||
|
{ token: '{Series CleanTitleYear}', example: 'The Series Title\'s! 2010' },
|
||||||
|
{ token: '{Series TitleWithoutYear}', example: 'The Series Title\'s!' },
|
||||||
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!' },
|
{ token: '{Series CleanTitleWithoutYear}', example: 'The Series Title\'s!' },
|
||||||
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The' },
|
{ token: '{Series TitleThe}', example: 'Series Title\'s!, The' },
|
||||||
|
{ token: '{Series CleanTitleThe}', example: 'Series Title\'s!, The' },
|
||||||
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)' },
|
{ token: '{Series TitleTheYear}', example: 'Series Title\'s!, The (2010)' },
|
||||||
|
{ token: '{Series CleanTitleTheYear}', example: 'Series Title\'s!, The 2010' },
|
||||||
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The' },
|
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The' },
|
||||||
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)' },
|
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The' },
|
||||||
{ token: '{Series TitleWithoutYear}', example: 'Series Title\'s!' },
|
|
||||||
{ token: '{Series TitleFirstCharacter}', example: 'S' },
|
{ token: '{Series TitleFirstCharacter}', example: 'S' },
|
||||||
{ token: '{Series Year}', example: '2010' }
|
{ token: '{Series Year}', example: '2010' }
|
||||||
];
|
];
|
||||||
|
|
|
@ -99,7 +99,7 @@ function EditNotificationModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('NotificationsTagsHelpText')}
|
helpText={translate('NotificationsTagsSeriesHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) {
|
||||||
{
|
{
|
||||||
id === 1 ?
|
id === 1 ?
|
||||||
<Alert>
|
<Alert>
|
||||||
{translate('DefaultDelayProfile')}
|
{translate('DefaultDelayProfileSeries')}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) {
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
{...tags}
|
{...tags}
|
||||||
helpText={translate('DelayProfileTagsHelpText')}
|
helpText={translate('DelayProfileSeriesTagsHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -203,7 +203,7 @@ class EditQualityProfileModalContent extends Component {
|
||||||
name="cutoff"
|
name="cutoff"
|
||||||
{...cutoff}
|
{...cutoff}
|
||||||
values={qualities}
|
values={qualities}
|
||||||
helpText={translate('UpgradeUntilHelpText')}
|
helpText={translate('UpgradeUntilEpisodeHelpText')}
|
||||||
onChange={onCutoffChange}
|
onChange={onCutoffChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
@ -237,7 +237,7 @@ class EditQualityProfileModalContent extends Component {
|
||||||
type={inputTypes.NUMBER}
|
type={inputTypes.NUMBER}
|
||||||
name="cutoffFormatScore"
|
name="cutoffFormatScore"
|
||||||
{...cutoffFormatScore}
|
{...cutoffFormatScore}
|
||||||
helpText={translate('UpgradeUntilCustomFormatScoreHelpText')}
|
helpText={translate('UpgradeUntilCustomFormatScoreEpisodeHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
@ -281,7 +281,7 @@ class EditQualityProfileModalContent extends Component {
|
||||||
className={styles.deleteButtonContainer}
|
className={styles.deleteButtonContainer}
|
||||||
title={
|
title={
|
||||||
isInUse ?
|
isInUse ?
|
||||||
translate('QualityProfileInUse') :
|
translate('QualityProfileInUseSeriesListCollection') :
|
||||||
undefined
|
undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -126,7 +126,7 @@ function EditReleaseProfileModalContent(props) {
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.TAG}
|
type={inputTypes.TAG}
|
||||||
name="tags"
|
name="tags"
|
||||||
helpText={translate('ReleaseProfileTagHelpText')}
|
helpText={translate('ReleaseProfileTagSeriesHelpText')}
|
||||||
{...tags}
|
{...tags}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -60,7 +60,7 @@ class QualityDefinitions extends Component {
|
||||||
|
|
||||||
<div className={styles.sizeLimitHelpTextContainer}>
|
<div className={styles.sizeLimitHelpTextContainer}>
|
||||||
<div className={styles.sizeLimitHelpText}>
|
<div className={styles.sizeLimitHelpText}>
|
||||||
{translate('QualityLimitsHelpText')}
|
{translate('QualityLimitsSeriesRuntimeHelpText')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageSectionContent>
|
</PageSectionContent>
|
||||||
|
|
|
@ -110,7 +110,7 @@ function Settings() {
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className={styles.summary}>
|
<div className={styles.summary}>
|
||||||
{translate('MetadataSettingsSummary')}
|
{translate('MetadataSettingsSeriesSummary')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
|
@ -121,7 +121,7 @@ function Settings() {
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className={styles.summary}>
|
<div className={styles.summary}>
|
||||||
{translate('MetadataSourceSettingsSummary')}
|
{translate('MetadataSourceSettingsSeriesSummary')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState';
|
||||||
import { set, updateServerSideCollection } from '../baseActions';
|
import { set, updateServerSideCollection } from '../baseActions';
|
||||||
|
|
||||||
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
|
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
|
||||||
|
const [baseSection] = section.split('.');
|
||||||
|
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
dispatch(set({ section, isFetching: true }));
|
dispatch(set({ section, isFetching: true }));
|
||||||
|
|
||||||
|
@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters
|
||||||
customFilters
|
|
||||||
} = sectionState;
|
} = sectionState;
|
||||||
|
|
||||||
|
const customFilters = getState().customFilters.items.filter((customFilter) => {
|
||||||
|
return customFilter.type === section || customFilter.type === baseSection;
|
||||||
|
});
|
||||||
|
|
||||||
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
||||||
|
|
||||||
selectedFilters.forEach((filter) => {
|
selectedFilters.forEach((filter) => {
|
||||||
|
@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url,
|
url,
|
||||||
data
|
data,
|
||||||
|
traditional: true
|
||||||
}).request;
|
}).request;
|
||||||
|
|
||||||
promise.done((response) => {
|
promise.done((response) => {
|
||||||
|
|
|
@ -52,8 +52,6 @@ export const defaultState = {
|
||||||
|
|
||||||
selectedFilterKey: 'monitored',
|
selectedFilterKey: 'monitored',
|
||||||
|
|
||||||
customFilters: [],
|
|
||||||
|
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'all',
|
key: 'all',
|
||||||
|
|
|
@ -40,32 +40,38 @@ export const defaultState = {
|
||||||
{
|
{
|
||||||
name: 'episodeNumber',
|
name: 'episodeNumber',
|
||||||
label: '#',
|
label: '#',
|
||||||
isVisible: true
|
isVisible: true,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
label: () => translate('Title'),
|
label: () => translate('Title'),
|
||||||
isVisible: true
|
isVisible: true,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'path',
|
name: 'path',
|
||||||
label: () => translate('Path'),
|
label: () => translate('Path'),
|
||||||
isVisible: false
|
isVisible: false,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'relativePath',
|
name: 'relativePath',
|
||||||
label: () => translate('RelativePath'),
|
label: () => translate('RelativePath'),
|
||||||
isVisible: false
|
isVisible: false,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'airDateUtc',
|
name: 'airDateUtc',
|
||||||
label: () => translate('AirDate'),
|
label: () => translate('AirDate'),
|
||||||
isVisible: true
|
isVisible: true,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'runtime',
|
name: 'runtime',
|
||||||
label: () => translate('Runtime'),
|
label: () => translate('Runtime'),
|
||||||
isVisible: false
|
isVisible: false,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'languages',
|
name: 'languages',
|
||||||
|
@ -100,7 +106,8 @@ export const defaultState = {
|
||||||
{
|
{
|
||||||
name: 'size',
|
name: 'size',
|
||||||
label: () => translate('Size'),
|
label: () => translate('Size'),
|
||||||
isVisible: false
|
isVisible: false,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'releaseGroup',
|
name: 'releaseGroup',
|
||||||
|
@ -119,7 +126,8 @@ export const defaultState = {
|
||||||
name: icons.SCORE,
|
name: icons.SCORE,
|
||||||
title: () => translate('CustomFormatScore')
|
title: () => translate('CustomFormatScore')
|
||||||
}),
|
}),
|
||||||
isVisible: false
|
isVisible: false,
|
||||||
|
isSortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
|
@ -136,7 +144,9 @@ export const defaultState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
'episodes.columns'
|
'episodes.columns',
|
||||||
|
'episodes.sortDirection',
|
||||||
|
'episodes.sortKey'
|
||||||
];
|
];
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import { filterTypes, icons, sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, 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';
|
||||||
|
@ -185,6 +185,33 @@ export const defaultState = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
filterBuilderProps: [
|
||||||
|
{
|
||||||
|
name: 'eventType',
|
||||||
|
label: () => translate('EventType'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'seriesIds',
|
||||||
|
label: () => translate('Series'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.SERIES
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quality',
|
||||||
|
label: () => translate('Quality'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.QUALITY
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'languages',
|
||||||
|
label: () => translate('Languages'),
|
||||||
|
type: filterBuilderTypes.CONTAINS,
|
||||||
|
valueType: filterBuilderValueTypes.LANGUAGE
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,8 +28,8 @@ export const defaultState = {
|
||||||
error: null,
|
error: null,
|
||||||
items: [],
|
items: [],
|
||||||
originalItems: [],
|
originalItems: [],
|
||||||
sortKey: 'quality',
|
sortKey: 'relativePath',
|
||||||
sortDirection: sortDirections.DESCENDING,
|
sortDirection: sortDirections.ASCENDING,
|
||||||
recentFolders: [],
|
recentFolders: [],
|
||||||
importMode: 'chooseImportMode',
|
importMode: 'chooseImportMode',
|
||||||
sortPredicates: {
|
sortPredicates: {
|
||||||
|
|
|
@ -3,7 +3,7 @@ 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 Icon from 'Components/Icon';
|
||||||
import { icons, sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, 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';
|
||||||
|
@ -144,7 +144,7 @@ export const defaultState = {
|
||||||
name: 'size',
|
name: 'size',
|
||||||
label: () => translate('Size'),
|
label: () => translate('Size'),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisibile: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'outputPath',
|
name: 'outputPath',
|
||||||
|
@ -170,6 +170,43 @@ export const defaultState = {
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isModifiable: false
|
isModifiable: false
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
selectedFilterKey: 'all',
|
||||||
|
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
label: 'All',
|
||||||
|
filters: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
filterBuilderProps: [
|
||||||
|
{
|
||||||
|
name: 'seriesIds',
|
||||||
|
label: () => translate('Series'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.SERIES
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quality',
|
||||||
|
label: () => translate('Quality'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.QUALITY
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'languages',
|
||||||
|
label: () => translate('Languages'),
|
||||||
|
type: filterBuilderTypes.CONTAINS,
|
||||||
|
valueType: filterBuilderValueTypes.LANGUAGE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'protocol',
|
||||||
|
label: () => translate('Protocol'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.PROTOCOL
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -179,7 +216,8 @@ export const persistState = [
|
||||||
'queue.paged.pageSize',
|
'queue.paged.pageSize',
|
||||||
'queue.paged.sortKey',
|
'queue.paged.sortKey',
|
||||||
'queue.paged.sortDirection',
|
'queue.paged.sortDirection',
|
||||||
'queue.paged.columns'
|
'queue.paged.columns',
|
||||||
|
'queue.paged.selectedFilterKey'
|
||||||
];
|
];
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -204,6 +242,7 @@ export const GOTO_NEXT_QUEUE_PAGE = 'queue/gotoQueueNextPage';
|
||||||
export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage';
|
export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage';
|
||||||
export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage';
|
export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage';
|
||||||
export const SET_QUEUE_SORT = 'queue/setQueueSort';
|
export const SET_QUEUE_SORT = 'queue/setQueueSort';
|
||||||
|
export const SET_QUEUE_FILTER = 'queue/setQueueFilter';
|
||||||
export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption';
|
export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption';
|
||||||
export const SET_QUEUE_OPTION = 'queue/setQueueOption';
|
export const SET_QUEUE_OPTION = 'queue/setQueueOption';
|
||||||
export const CLEAR_QUEUE = 'queue/clearQueue';
|
export const CLEAR_QUEUE = 'queue/clearQueue';
|
||||||
|
@ -228,6 +267,7 @@ export const gotoQueueNextPage = createThunk(GOTO_NEXT_QUEUE_PAGE);
|
||||||
export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE);
|
export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE);
|
||||||
export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE);
|
export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE);
|
||||||
export const setQueueSort = createThunk(SET_QUEUE_SORT);
|
export const setQueueSort = createThunk(SET_QUEUE_SORT);
|
||||||
|
export const setQueueFilter = createThunk(SET_QUEUE_FILTER);
|
||||||
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
|
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
|
||||||
export const setQueueOption = createAction(SET_QUEUE_OPTION);
|
export const setQueueOption = createAction(SET_QUEUE_OPTION);
|
||||||
export const clearQueue = createAction(CLEAR_QUEUE);
|
export const clearQueue = createAction(CLEAR_QUEUE);
|
||||||
|
@ -279,7 +319,8 @@ export const actionHandlers = handleThunks({
|
||||||
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE,
|
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE,
|
||||||
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE,
|
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE,
|
||||||
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE,
|
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE,
|
||||||
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT
|
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT,
|
||||||
|
[serverSideCollectionHandlers.FILTER]: SET_QUEUE_FILTER
|
||||||
},
|
},
|
||||||
fetchDataAugmenter
|
fetchDataAugmenter
|
||||||
),
|
),
|
||||||
|
|
|
@ -168,9 +168,10 @@ export const filterPredicates = {
|
||||||
},
|
},
|
||||||
|
|
||||||
hasMissingSeason: function(item, filterValue, type) {
|
hasMissingSeason: function(item, filterValue, type) {
|
||||||
|
const predicate = filterTypePredicates[type];
|
||||||
const { seasons = [] } = item;
|
const { seasons = [] } = item;
|
||||||
|
|
||||||
return seasons.some((season) => {
|
const hasMissingSeason = seasons.some((season) => {
|
||||||
const {
|
const {
|
||||||
seasonNumber,
|
seasonNumber,
|
||||||
statistics = {}
|
statistics = {}
|
||||||
|
@ -189,6 +190,8 @@ export const filterPredicates = {
|
||||||
episodeFileCount === 0
|
episodeFileCount === 0
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return predicate(hasMissingSeason, filterValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -347,7 +350,13 @@ export const filterBuilderProps = [
|
||||||
{
|
{
|
||||||
name: 'hasMissingSeason',
|
name: 'hasMissingSeason',
|
||||||
label: () => translate('HasMissingSeason'),
|
label: () => translate('HasMissingSeason'),
|
||||||
type: filterBuilderTypes.EXACT
|
type: filterBuilderTypes.EXACT,
|
||||||
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'year',
|
||||||
|
label: () => translate('Year'),
|
||||||
|
type: filterBuilderTypes.NUMBER
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ function getInternalLink(source) {
|
||||||
function getTestLink(source, props) {
|
function getTestLink(source, props) {
|
||||||
switch (source) {
|
switch (source) {
|
||||||
case 'IndexerStatusCheck':
|
case 'IndexerStatusCheck':
|
||||||
|
case 'IndexerLongTermStatusCheck':
|
||||||
return (
|
return (
|
||||||
<SpinnerIconButton
|
<SpinnerIconButton
|
||||||
name={icons.TEST}
|
name={icons.TEST}
|
||||||
|
|
|
@ -3,6 +3,7 @@ function getNewSeries(series, payload) {
|
||||||
const {
|
const {
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
monitor,
|
monitor,
|
||||||
|
monitorNewItems,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
seriesType,
|
seriesType,
|
||||||
seasonFolder,
|
seasonFolder,
|
||||||
|
@ -19,6 +20,7 @@ function getNewSeries(series, payload) {
|
||||||
|
|
||||||
series.addOptions = addOptions;
|
series.addOptions = addOptions;
|
||||||
series.monitored = true;
|
series.monitored = true;
|
||||||
|
series.monitorNewItems = monitorNewItems;
|
||||||
series.qualityProfileId = qualityProfileId;
|
series.qualityProfileId = qualityProfileId;
|
||||||
series.rootFolderPath = rootFolderPath;
|
series.rootFolderPath = rootFolderPath;
|
||||||
series.seriesType = seriesType;
|
series.seriesType = seriesType;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
const monitorNewItemsOptions = [
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
get value() {
|
||||||
|
return translate('MonitorAllSeasons');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'none',
|
||||||
|
get value() {
|
||||||
|
return translate('MonitorNoNewSeasons');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default monitorNewItemsOptions;
|
|
@ -25,6 +25,12 @@ const monitorOptions = [
|
||||||
return translate('MonitorExistingEpisodes');
|
return translate('MonitorExistingEpisodes');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'recent',
|
||||||
|
get value() {
|
||||||
|
return translate('MonitorRecentEpisodes');
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'pilot',
|
key: 'pilot',
|
||||||
get value() {
|
get value() {
|
||||||
|
@ -38,27 +44,27 @@ const monitorOptions = [
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'latestSeason',
|
key: 'lastSeason',
|
||||||
get value() {
|
get value() {
|
||||||
return translate('MonitorLatestSeason');
|
return translate('MonitorLastSeason');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'monitorSpecials',
|
key: 'monitorSpecials',
|
||||||
get value() {
|
get value() {
|
||||||
return translate('MonitorSpecials');
|
return translate('MonitorSpecialEpisodes');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'unmonitorSpecials',
|
key: 'unmonitorSpecials',
|
||||||
get value() {
|
get value() {
|
||||||
return translate('UnmonitorSpecials');
|
return translate('UnmonitorSpecialEpisodes');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'none',
|
key: 'none',
|
||||||
get value() {
|
get value() {
|
||||||
return translate('MonitorNone');
|
return translate('MonitorNoEpisodes');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -25,15 +25,13 @@ export async function fetchTranslations(): Promise<boolean> {
|
||||||
|
|
||||||
export default function translate(
|
export default function translate(
|
||||||
key: string,
|
key: string,
|
||||||
tokens?: Record<string, string | number | boolean>
|
tokens: Record<string, string | number | boolean> = {}
|
||||||
) {
|
) {
|
||||||
const translation = translations[key] || key;
|
const translation = translations[key] || key;
|
||||||
|
|
||||||
if (tokens) {
|
tokens.appName = 'Sonarr';
|
||||||
|
|
||||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||||
String(tokens[tokenMatch] ?? match)
|
String(tokens[tokenMatch] ?? match)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return translation;
|
|
||||||
}
|
|
||||||
|
|
|
@ -247,11 +247,11 @@ class CutoffUnmet extends Component {
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isConfirmSearchAllCutoffUnmetModalOpen}
|
isOpen={isConfirmSearchAllCutoffUnmetModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('SearchForCutoffUnmet')}
|
title={translate('SearchForCutoffUnmetEpisodes')}
|
||||||
message={
|
message={
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{translate('SearchForCutoffUnmetConfirmationCount', { totalRecords })}
|
{translate('SearchForCutoffUnmetEpisodesConfirmationCount', { totalRecords })}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{translate('MassSearchCancelWarning')}
|
{translate('MassSearchCancelWarning')}
|
||||||
|
|
|
@ -260,11 +260,11 @@ class Missing extends Component {
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={isConfirmSearchAllMissingModalOpen}
|
isOpen={isConfirmSearchAllMissingModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('SearchForAllMissing')}
|
title={translate('SearchForAllMissingEpisodes')}
|
||||||
message={
|
message={
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{translate('SearchForAllMissingConfirmationCount', { totalRecords })}
|
{translate('SearchForAllMissingEpisodesConfirmationCount', { totalRecords })}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{translate('MassSearchCancelWarning')}
|
{translate('MassSearchCancelWarning')}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import CustomFormat from './CustomFormat';
|
||||||
|
|
||||||
|
export type HistoryEventType =
|
||||||
|
| 'grabbed'
|
||||||
|
| 'seriesFolderImported'
|
||||||
|
| 'downloadFolderImported'
|
||||||
|
| 'downloadFailed'
|
||||||
|
| 'episodeFileDeleted'
|
||||||
|
| 'episodeFileRenamed'
|
||||||
|
| 'downloadIgnored';
|
||||||
|
|
||||||
|
export default interface History {
|
||||||
|
episodeId: number;
|
||||||
|
seriesId: number;
|
||||||
|
sourceTitle: string;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
|
qualityCutoffNotMet: boolean;
|
||||||
|
date: string;
|
||||||
|
downloadId: string;
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
data: unknown;
|
||||||
|
id: number;
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
|
||||||
|
export type QueueTrackedDownloadStatus = 'ok' | 'warning' | 'error';
|
||||||
|
|
||||||
|
export type QueueTrackedDownloadState =
|
||||||
|
| 'downloading'
|
||||||
|
| 'importPending'
|
||||||
|
| 'importing'
|
||||||
|
| 'imported'
|
||||||
|
| 'failedPending'
|
||||||
|
| 'failed'
|
||||||
|
| 'ignored';
|
||||||
|
|
||||||
|
export interface StatusMessage {
|
||||||
|
title: string;
|
||||||
|
messages: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Queue extends ModelBase {
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats: CustomFormat[];
|
||||||
|
size: number;
|
||||||
|
title: string;
|
||||||
|
sizeleft: number;
|
||||||
|
timeleft: string;
|
||||||
|
estimatedCompletionTime: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState: QueueTrackedDownloadState;
|
||||||
|
statusMessages: StatusMessage[];
|
||||||
|
errorMessage: string;
|
||||||
|
downloadId: string;
|
||||||
|
protocol: string;
|
||||||
|
downloadClient: string;
|
||||||
|
outputPath: string;
|
||||||
|
episodeHasFile: boolean;
|
||||||
|
seriesId?: number;
|
||||||
|
episodeId?: number;
|
||||||
|
seasonNumber?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Queue;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue