Merge branch 'Sonarr:develop' into putio-download

This commit is contained in:
Michael Feinbier 2023-11-29 21:47:46 +01:00 committed by GitHub
commit b8007391b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
378 changed files with 7797 additions and 2425 deletions

View File

@ -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

View File

@ -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>
); );
} }

View File

@ -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');

View File

@ -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,

View File

@ -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
}; };
} }

View File

@ -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');
} }

View File

@ -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}
/>
);
}

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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}
/>
);
}

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>
); );
} }

View File

@ -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')}

View File

@ -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 {

View File

@ -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;

View File

@ -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>
); );

View File

@ -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>
); );

View File

@ -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>
{ {

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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';

View File

@ -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]}

View File

@ -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>

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 {

View File

@ -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;

View File

@ -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;
} }

View File

@ -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,

View File

@ -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' });
}, },
}, },
]; ];

View File

@ -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>
} }

View File

@ -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}

View File

@ -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>

View File

@ -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

View File

@ -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
} }

View File

@ -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';

View File

@ -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,

View File

@ -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);

View File

@ -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')
} }

View File

@ -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}

View File

@ -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>

View File

@ -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) {

View File

@ -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,

View File

@ -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,

View File

@ -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
}; };

View File

@ -3,3 +3,7 @@
margin-right: auto; margin-right: auto;
} }
.labelIcon {
margin-left: 8px;
}

View File

@ -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;

View File

@ -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>

View File

@ -38,6 +38,7 @@ function createMapStateToProps() {
const seriesSettings = _.pick(series, [ const seriesSettings = _.pick(series, [
'monitored', 'monitored',
'monitorNewItems',
'seasonFolder', 'seasonFolder',
'qualityProfileId', 'qualityProfileId',
'seriesType', 'seriesType',

View File

@ -63,7 +63,7 @@ const rows = [
{ {
name: 'qualityProfileId', name: 'qualityProfileId',
showProp: 'showQualityProfile', showProp: 'showQualityProfile',
valueProp: 'qualityProfileId', valueProp: 'qualityProfile',
}, },
{ {
name: 'previousAiring', name: 'previousAiring',

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
} }

View File

@ -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`);
} }
} }

View File

@ -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}

View File

@ -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}
/> />

View File

@ -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}>

View File

@ -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}>

View File

@ -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>

View File

@ -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}
/> />

View File

@ -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')}

View File

@ -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>

View File

@ -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}
/> />

View File

@ -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}

View File

@ -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' }
]; ];

View File

@ -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}
/> />

View File

@ -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>

View File

@ -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
} }
> >

View File

@ -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}
/> />

View File

@ -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>

View File

@ -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

View File

@ -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) => {

View File

@ -52,8 +52,6 @@ export const defaultState = {
selectedFilterKey: 'monitored', selectedFilterKey: 'monitored',
customFilters: [],
filters: [ filters: [
{ {
key: 'all', key: 'all',

View File

@ -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'
]; ];
// //

View File

@ -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
}
] ]
}; };

View File

@ -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: {

View File

@ -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
), ),

View File

@ -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
} }
]; ];

View File

@ -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}

View File

@ -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;

View File

@ -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;

View File

@ -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');
} }
} }
]; ];

View File

@ -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;
}

View File

@ -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')}

View File

@ -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')}

View File

@ -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;
}

View File

@ -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