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
[![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.
## Getting Started

View File

@ -36,6 +36,7 @@ class Blocklist extends Component {
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
isConfirmClearModalOpen: false,
items: props.items
};
}
@ -90,6 +91,19 @@ class Blocklist extends Component {
this.setState({ isConfirmRemoveModalOpen: false });
};
onClearBlocklistPress = () => {
this.setState({ isConfirmClearModalOpen: true });
};
onClearBlocklistConfirmed = () => {
this.props.onClearBlocklistPress();
this.setState({ isConfirmClearModalOpen: false });
};
onConfirmClearModalClose = () => {
this.setState({ isConfirmClearModalOpen: false });
};
//
// Render
@ -103,7 +117,6 @@ class Blocklist extends Component {
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
@ -111,7 +124,8 @@ class Blocklist extends Component {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
isConfirmRemoveModalOpen,
isConfirmClearModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
@ -131,8 +145,9 @@ class Blocklist extends Component {
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isDisabled={!items.length}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
onPress={this.onClearBlocklistPress}
/>
</PageToolbarSection>
@ -215,6 +230,16 @@ class Blocklist extends Component {
onConfirm={this.onRemoveSelectedConfirmed}
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>
);
}

View File

@ -231,7 +231,7 @@ function HistoryDetails(props) {
reasonMessage = translate('DeletedReasonManual');
break;
case 'MissingFromDisk':
reasonMessage = translate('DeletedReasonMissingFromDisk');
reasonMessage = translate('DeletedReasonEpisodeMissingFromDisk');
break;
case 'Upgrade':
reasonMessage = translate('DeletedReasonUpgrade');

View File

@ -15,6 +15,7 @@ import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
@ -52,6 +53,7 @@ class History extends Component {
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
isEpisodesFetching,
isEpisodesPopulated,
@ -92,7 +94,8 @@ class History extends Component {
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
@ -163,8 +166,9 @@ History.propTypes = {
error: PropTypes.object,
items: 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,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isEpisodesFetching: 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 { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
import * as historyActions from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
@ -15,11 +16,13 @@ function createMapStateToProps() {
return createSelector(
(state) => state.history,
(state) => state.episodes,
(history, episodes) => {
createCustomFiltersSelector('history'),
(history, episodes, customFilters) => {
return {
isEpisodesFetching: episodes.isFetching,
isEpisodesPopulated: episodes.isPopulated,
episodesError: episodes.error,
customFilters,
...history
};
}

View File

@ -39,19 +39,19 @@ function getIconKind(eventType) {
function getTooltip(eventType, data) {
switch (eventType) {
case 'grabbed':
return translate('GrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
return translate('EpisodeGrabbedTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
case 'seriesFolderImported':
return translate('SeriesFolderImportedTooltip');
case 'downloadFolderImported':
return translate('EpisodeImportedTooltip');
case 'downloadFailed':
return translate('DownloadFailedTooltip');
return translate('DownloadFailedEpisodeTooltip');
case 'episodeFileDeleted':
return translate('EpisodeFileDeletedTooltip');
case 'episodeFileRenamed':
return translate('EpisodeFileRenamedTooltip');
case 'downloadIgnored':
return translate('DownloadIgnoredTooltip');
return translate('DownloadIgnoredEpisodeTooltip');
default:
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 Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@ -21,6 +22,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import QueueFilterModal from './QueueFilterModal';
import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
@ -151,11 +153,16 @@ class Queue extends Component {
isEpisodesPopulated,
episodesError,
columns,
selectedFilterKey,
filters,
customFilters,
count,
totalRecords,
isGrabbing,
isRemoving,
isRefreshMonitoredDownloadsExecuting,
onRefreshPress,
onFilterSelect,
...otherProps
} = this.props;
@ -218,6 +225,15 @@ class Queue extends Component {
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={QueueFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
@ -239,7 +255,11 @@ class Queue extends Component {
{
isAllPopulated && !hasError && !items.length ?
<Alert kind={kinds.INFO}>
{translate('QueueIsEmpty')}
{
selectedFilterKey !== 'all' && count > 0 ?
translate('QueueFilterHasNoItems') :
translate('QueueIsEmpty')
}
</Alert> :
null
}
@ -323,13 +343,22 @@ Queue.propTypes = {
isEpisodesPopulated: PropTypes.bool.isRequired,
episodesError: PropTypes.object,
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,
isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired,
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
onRefreshPress: 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;

View File

@ -7,6 +7,7 @@ import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
import * as queueActions from 'Store/Actions/queueActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
@ -18,12 +19,16 @@ function createMapStateToProps() {
(state) => state.episodes,
(state) => state.queue.options,
(state) => state.queue.paged,
(state) => state.queue.status.item,
createCustomFiltersSelector('queue'),
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => {
(episodes, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
return {
count: options.includeUnknownSeriesItems ? status.totalCount : status.count,
isEpisodesFetching: episodes.isFetching,
isEpisodesPopulated: episodes.isPopulated,
episodesError: episodes.error,
customFilters,
isRefreshMonitoredDownloadsExecuting,
...options,
...queue
@ -122,6 +127,10 @@ class QueueConnector extends Component {
this.props.setQueueSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setQueueFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => {
this.props.setQueueTableOption(payload);
@ -156,6 +165,7 @@ class QueueConnector extends Component {
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onTableOptionChange={this.onTableOptionChange}
onRefreshPress={this.onRefreshPress}
onGrabSelectedPress={this.onGrabSelectedPress}
@ -178,6 +188,7 @@ QueueConnector.propTypes = {
gotoQueueLastPage: PropTypes.func.isRequired,
gotoQueuePage: PropTypes.func.isRequired,
setQueueSort: PropTypes.func.isRequired,
setQueueFilter: PropTypes.func.isRequired,
setQueueTableOption: PropTypes.func.isRequired,
clearQueue: PropTypes.func.isRequired,
grabQueueItems: PropTypes.func.isRequired,

View File

@ -81,4 +81,9 @@ QueueDetails.propTypes = {
progressBar: PropTypes.node.isRequired
};
QueueDetails.defaultProps = {
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading'
};
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
import { tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css';
@ -41,8 +40,8 @@ QueueStatusCell.propTypes = {
};
QueueStatusCell.defaultProps = {
trackedDownloadStatus: translate('Ok'),
trackedDownloadState: translate('Downloading')
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading'
};
export default QueueStatusCell;

View File

@ -120,7 +120,7 @@ class RemoveQueueItemModal extends Component {
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistReleaseHelpText')}
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>

View File

@ -123,7 +123,7 @@ class RemoveQueueItemsModal extends Component {
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistReleaseHelpText')}
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>

View File

@ -1,6 +1,9 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
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 formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
@ -25,11 +28,13 @@ function TimeleftCell(props) {
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell
className={styles.timeleft}
title={translate('DelayingDownloadUntil', { date, time })}
>
-
<TableRowCell className={styles.timeleft}>
<Tooltip
anchor={<Icon name={icons.INFO} />}
tooltip={translate('DelayingDownloadUntil', { date, time })}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
</TableRowCell>
);
}
@ -39,11 +44,13 @@ function TimeleftCell(props) {
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell
className={styles.timeleft}
title={translate('RetryingDownloadOn', { date, time })}
>
-
<TableRowCell className={styles.timeleft}>
<Tooltip
anchor={<Icon name={icons.INFO} />}
tooltip={translate('RetryingDownloadOn', { date, time })}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
</TableRowCell>
);
}

View File

@ -79,17 +79,17 @@ class ImportSeriesSelectFolder extends Component {
!error && isPopulated &&
<div>
<div className={styles.header}>
{translate('LibraryImportHeader')}
{translate('LibraryImportSeriesHeader')}
</div>
<div className={styles.tips}>
{translate('LibraryImportTips')}
<ul>
<li className={styles.tip}>
<InlineMarkdown data={translate('LibraryImportTipsQualityInFilename')} />
<InlineMarkdown data={translate('LibraryImportTipsQualityInEpisodeFilename')} />
</li>
<li className={styles.tip}>
<InlineMarkdown data={translate('LibraryImportTipsUseRootFolder', { goodFolderExample, badFolderExample })} />
<InlineMarkdown data={translate('LibraryImportTipsSeriesUseRootFolder', { goodFolderExample, badFolderExample })} />
</li>
<li className={styles.tip}>
{translate('LibraryImportTipsDontUseDownloadsFolder')}

View File

@ -5,12 +5,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ImportSeriesSelectFolder from './ImportSeriesSelectFolder';
function createMapStateToProps() {
return createSelector(
(state) => state.rootFolders,
createRootFoldersSelector(),
createSystemStatusSelector(),
(rootFolders, systemStatus) => {
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')}
/>
<DescriptionListItem
title={translate('MonitorRecentEpisodes')}
data={translate('MonitorRecentEpisodesDescription')}
/>
<DescriptionListItem
title={translate('MonitorPilotEpisode')}
data={translate('MonitorPilotEpisodeDescription')}
/>
<DescriptionListItem
title={translate('MonitorFirstSeason')}
data={translate('MonitorFirstSeasonDescription')}
/>
<DescriptionListItem
title={translate('MonitorLatestSeason')}
data={translate('MonitorLatestSeasonDescription')}
title={translate('MonitorLastSeason')}
data={translate('MonitorLastSeasonDescription')}
/>
<DescriptionListItem
title={translate('MonitorSpecials')}
data={translate('MonitorSpecialsDescription')}
title={translate('MonitorSpecialEpisodes')}
data={translate('MonitorSpecialEpisodesDescription')}
/>
<DescriptionListItem
title={translate('UnmonitorSpecials')}
data={translate('UnmonitorSpecialsDescription')}
title={translate('UnmonitorSpecialEpisodes')}
data={translate('UnmonitorSpecialsEpisodesDescription')}
/>
<DescriptionListItem
title={translate('MonitorNone')}
data={translate('MonitorNoneDescription')}
title={translate('MonitorNoEpisodes')}
data={translate('MonitorNoEpisodesDescription')}
/>
</DescriptionList>
);

View File

@ -8,17 +8,17 @@ function SeriesTypePopoverContent() {
<DescriptionList>
<DescriptionListItem
title={translate('Anime')}
data={translate('AnimeTypeDescription')}
data={translate('AnimeEpisodeTypeDescription')}
/>
<DescriptionListItem
title={translate('Daily')}
data={translate('DailyTypeDescription')}
data={translate('DailyEpisodeTypeDescription')}
/>
<DescriptionListItem
title={translate('Standard')}
data={translate('StandardTypeDescription')}
data={translate('StandardEpisodeTypeDescription')}
/>
</DescriptionList>
);

View File

@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AppUpdated', { appName: 'Sonarr' })}
{translate('AppUpdated')}
</ModalHeader>
<ModalBody>
<div>
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Sonarr', version })} blockClassName={styles.version} />
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
</div>
{

View File

@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
{translate('ConnectionLostToBackend', { appName: 'Sonarr' })}
{translate('ConnectionLostToBackend')}
</div>
<div className={styles.automatic}>
{translate('ConnectionLostReconnect', { appName: 'Sonarr' })}
{translate('ConnectionLostReconnect')}
</div>
</ModalBody>
<ModalFooter>

View File

@ -1,4 +1,5 @@
import SortDirection from 'Helpers/Props/SortDirection';
import { FilterBuilderProp } from './AppState';
export interface Error {
responseJSON: {
@ -20,6 +21,10 @@ export interface PagedAppSectionState {
pageSize: number;
}
export interface AppSectionFilterState<T> {
filterBuilderProps: FilterBuilderProp<T>[];
}
export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;

View File

@ -3,6 +3,7 @@ import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import EpisodeFilesAppState from './EpisodeFilesAppState';
import EpisodesAppState from './EpisodesAppState';
import HistoryAppState from './HistoryAppState';
import ParseAppState from './ParseAppState';
import QueueAppState from './QueueAppState';
import RootFolderAppState from './RootFolderAppState';
@ -48,6 +49,7 @@ interface AppState {
commands: CommandAppState;
episodeFiles: EpisodeFilesAppState;
episodesSelection: EpisodesAppState;
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
parse: ParseAppState;
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 { FilterBuilderProp } from './AppState';
interface CalendarAppState extends AppSectionState<Episode> {
filterBuilderProps: FilterBuilderProp<Episode>[];
}
interface CalendarAppState
extends AppSectionState<Episode>,
AppSectionFilterState<Episode> {}
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 Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import AppSectionState, { AppSectionItemState, 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;
}
import Queue from 'typings/Queue';
import AppSectionState, {
AppSectionFilterState,
AppSectionItemState,
Error,
} from './AppSectionState';
export interface QueueDetailsAppState extends AppSectionState<Queue> {
params: unknown;
}
export interface QueuePagedAppState extends AppSectionState<Queue> {
export interface QueuePagedAppState
extends AppSectionState<Queue>,
AppSectionFilterState<Queue> {
isGrabbing: boolean;
grabError: Error;
isRemoving: boolean;

View File

@ -23,13 +23,11 @@ function createFilterBuilderPropsSelector() {
);
}
interface SeriesIndexFilterModalProps {
interface CalendarFilterModalProps {
isOpen: boolean;
}
export default function CalendarFilterModal(
props: SeriesIndexFilterModalProps
) {
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
const sectionItems = useSelector(createCalendarSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'calendar';

View File

@ -25,7 +25,7 @@ function Legend(props) {
name="Finale"
icon={icons.INFO}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
tooltip={translate('CalendarLegendFinaleTooltip')}
tooltip={translate('CalendarLegendSeriesFinaleTooltip')}
/>
);
}
@ -58,7 +58,7 @@ function Legend(props) {
<div>
<LegendItem
status="unaired"
tooltip={translate('CalendarLegendUnairedTooltip')}
tooltip={translate('CalendarLegendEpisodeUnairedTooltip')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
@ -66,7 +66,7 @@ function Legend(props) {
<LegendItem
status="unmonitored"
tooltip={translate('CalendarLegendUnmonitoredTooltip')}
tooltip={translate('CalendarLegendEpisodeUnmonitoredTooltip')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
@ -77,7 +77,7 @@ function Legend(props) {
<LegendItem
status="onAir"
name="On Air"
tooltip={translate('CalendarLegendOnAirTooltip')}
tooltip={translate('CalendarLegendEpisodeOnAirTooltip')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
@ -85,7 +85,7 @@ function Legend(props) {
<LegendItem
status="missing"
tooltip={translate('CalendarLegendMissingTooltip')}
tooltip={translate('CalendarLegendEpisodeMissingTooltip')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
@ -95,7 +95,7 @@ function Legend(props) {
<div>
<LegendItem
status="downloading"
tooltip={translate('CalendarLegendDownloadingTooltip')}
tooltip={translate('CalendarLegendEpisodeDownloadingTooltip')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
@ -103,7 +103,7 @@ function Legend(props) {
<LegendItem
status="downloaded"
tooltip={translate('CalendarLegendDownloadedTooltip')}
tooltip={translate('CalendarLegendEpisodeDownloadedTooltip')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
@ -116,7 +116,7 @@ function Legend(props) {
icon={icons.INFO}
kind={kinds.INFO}
darken={true}
tooltip={translate('CalendarLegendPremiereTooltip')}
tooltip={translate('CalendarLegendSeriesPremiereTooltip')}
/>
{iconsToShow[0]}

View File

@ -116,7 +116,7 @@ class CalendarLinkModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('CalendarFeed', { appName: 'Sonarr' })}
{translate('CalendarFeed')}
</ModalHeader>
<ModalBody>
@ -128,7 +128,7 @@ class CalendarLinkModalContent extends Component {
type={inputTypes.CHECK}
name="unmonitored"
value={unmonitored}
helpText={translate('ICalIncludeUnmonitoredHelpText')}
helpText={translate('ICalIncludeUnmonitoredEpisodesHelpText')}
onChange={this.onInputChange}
/>
</FormGroup>
@ -164,7 +164,7 @@ class CalendarLinkModalContent extends Component {
type={inputTypes.TAG}
name="tags"
value={tags}
helpText={translate('ICalTagsHelpText')}
helpText={translate('ICalTagsSeriesHelpText')}
onChange={this.onInputChange}
/>
</FormGroup>

View File

@ -1,9 +1,7 @@
.description {
line-height: $lineHeight;
}
.description {
margin-left: 0;
line-height: $lineHeight;
overflow-wrap: break-word;
}
@media (min-width: 768px) {

View File

@ -6,10 +6,13 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue';
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
@ -57,9 +60,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.DATE:
return DateFilterBuilderRowValue;
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
return HistoryEventTypeFilterBuilderRowValue;
case filterBuilderValueTypes.INDEXER:
return IndexerFilterBuilderRowValueConnector;
case filterBuilderValueTypes.LANGUAGE:
return LanguageFilterBuilderRowValue;
case filterBuilderValueTypes.PROTOCOL:
return ProtocolFilterBuilderRowValue;
@ -69,6 +78,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.SERIES:
return SeriesFilterBuilderRowValue;
case filterBuilderValueTypes.SERIES_STATUS:
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 KeyValueListInput from './KeyValueListInput';
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
import MonitorNewItemsSelectInput from './MonitorNewItemsSelectInput';
import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput';
@ -49,6 +50,9 @@ function getComponent(type) {
case inputTypes.MONITOR_EPISODES_SELECT:
return MonitorEpisodesSelectInput;
case inputTypes.MONITOR_NEW_ITEMS_SELECT:
return MonitorNewItemsSelectInput;
case inputTypes.NUMBER:
return NumberInput;

View File

@ -2,8 +2,10 @@
display: flex;
justify-content: flex-end;
margin-right: $formLabelRightMarginWidth;
padding-top: 8px;
min-height: 35px;
text-align: end;
font-weight: bold;
line-height: 35px;
}
.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;
case 'rootFolder':
return inputTypes.ROOT_FOLDER_SELECT;
case 'qualityProfile':
return inputTypes.QUALITY_PROFILE_SELECT;
default:
return inputTypes.TEXT;
}

View File

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRootFolder } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import translate from 'Utilities/String/translate';
import RootFolderSelectInput from './RootFolderSelectInput';
@ -10,7 +11,7 @@ const ADD_NEW_KEY = 'addNew';
function createMapStateToProps() {
return createSelector(
(state) => state.rootFolders,
createRootFoldersSelector(),
(state, { value }) => value,
(state, { includeMissingValue }) => includeMissingValue,
(state, { includeNoChange }) => includeNoChange,

View File

@ -23,21 +23,21 @@ const seriesTypeOptions: ISeriesTypeOption[] = [
key: seriesTypes.STANDARD,
value: 'Standard',
get format() {
return translate('StandardTypeFormat', { format: 'S01E05' });
return translate('StandardEpisodeTypeFormat', { format: 'S01E05' });
},
},
{
key: seriesTypes.DAILY,
value: 'Daily / Date',
get format() {
return translate('DailyTypeFormat', { format: '2020-05-25' });
return translate('DailyEpisodeTypeFormat', { format: '2020-05-25' });
},
},
{
key: seriesTypes.ANIME,
value: 'Anime / Absolute',
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 Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ModalContent.css';
function ModalContent(props) {
@ -28,6 +29,7 @@ function ModalContent(props) {
<Icon
name={icons.CLOSE}
size={18}
title={translate('Close')}
/>
</Link>
}

View File

@ -81,6 +81,7 @@ class PageHeader extends Component {
aria-label={translate('Donate')}
to="https://sonarr.tv/donate.html"
size={14}
title={translate('Donate')}
/>
<PageHeaderActionsMenuConnector
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}

View File

@ -24,6 +24,7 @@ function PageHeaderActionsMenu(props) {
<MenuButton className={styles.menuButton} aria-label="Menu Button">
<Icon
name={icons.INTERACTIVE}
title={translate('Menu')}
/>
</MenuButton>

View File

@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EpisodeDetailsModal from './EpisodeDetailsModal';
import styles from './EpisodeSearchCell.css';
@ -50,11 +51,13 @@ class EpisodeSearchCell extends Component {
name={icons.SEARCH}
isSpinning={isSearching}
onPress={onSearchPress}
title={translate('AutomaticSearch')}
/>
<IconButton
name={icons.INTERACTIVE}
onPress={this.onManualSearchPress}
title={translate('InteractiveSearch')}
/>
<EpisodeDetailsModal

View File

@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
authenticationMethod,
authenticationRequired,
username,
password
password,
passwordConfirmation
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
@ -63,7 +64,7 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{translate('AuthenticationRequiredWarning', { appName: 'Sonarr' })}
{translate('AuthenticationRequiredWarning')}
</Alert>
{
@ -76,7 +77,7 @@ function AuthenticationRequiredModalContent(props) {
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Sonarr' })}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
helpLink="https://wiki.servarr.com/sonarr/faq#forced-authentication"
onChange={onInputChange}
@ -120,6 +121,18 @@ function AuthenticationRequiredModalContent(props) {
{...password}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
{...passwordConfirmation}
/>
</FormGroup>
</div> :
null
}

View File

@ -2,10 +2,13 @@ export const BOOL = 'bool';
export const BYTES = 'bytes';
export const DATE = 'date';
export const DEFAULT = 'default';
export const HISTORY_EVENT_TYPE = 'historyEventType';
export const INDEXER = 'indexer';
export const LANGUAGE = 'language';
export const PROTOCOL = 'protocol';
export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile';
export const SERIES = 'series';
export const SERIES_STATUS = 'seriesStatus';
export const SERIES_TYPES = 'seriesType';
export const TAG = 'tag';

View File

@ -4,6 +4,7 @@ export const CHECK = 'check';
export const DEVICE = 'device';
export const KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_EPISODES_SELECT = 'monitorEpisodesSelect';
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
export const FLOAT = 'float';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
@ -31,6 +32,7 @@ export const all = [
DEVICE,
KEY_VALUE_LIST,
MONITOR_EPISODES_SELECT,
MONITOR_NEW_ITEMS_SELECT,
FLOAT,
NUMBER,
OAUTH,

View File

@ -69,8 +69,6 @@ interface SelectEpisodeModalContentProps {
seasonNumber?: number;
selectedDetails?: string;
isAnime: boolean;
sortKey?: string;
sortDirection?: string;
modalTitle: string;
onEpisodesSelect(selectedEpisodes: SelectedEpisode[]): unknown;
onModalClose(): unknown;
@ -86,8 +84,6 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
seasonNumber,
selectedDetails,
isAnime,
sortKey,
sortDirection,
modalTitle,
onEpisodesSelect,
onModalClose,
@ -97,9 +93,8 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const { isFetching, isPopulated, items, error } = useSelector(
episodesSelector()
);
const { isFetching, isPopulated, items, error, sortKey, sortDirection } =
useSelector(episodesSelector());
const dispatch = useDispatch();
const filterEpisodeNumber = parseInt(filter);

View File

@ -139,7 +139,7 @@ function InteractiveSearch(props) {
{
errorMessage ?
<Fragment>
{translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
{translate('InteractiveSearchResultsSeriesFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
</Fragment> :
translate('EpisodeSearchResultsLoadError')
}

View File

@ -309,7 +309,9 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
isOpen={isConfirmGrabModalOpen}
kind={kinds.WARNING}
title={translate('GrabRelease')}
message={translate('GrabReleaseMessageText', { title })}
message={translate('GrabReleaseUnknownSeriesOrEpisodeMessageText', {
title,
})}
confirmLabel={translate('Grab')}
onConfirm={onGrabConfirm}
onCancel={onGrabCancel}

View File

@ -89,7 +89,7 @@ class DeleteSeriesModalContent extends Component {
type={inputTypes.CHECK}
name="addImportListExclusion"
value={addImportListExclusion}
helpText={translate('AddListExclusionHelpText')}
helpText={translate('AddListExclusionSeriesHelpText')}
onChange={onDeleteOptionChange}
/>
</FormGroup>

View File

@ -156,6 +156,12 @@
.headerContent {
padding: 15px;
}
.title {
font-weight: 300;
font-size: 30px;
line-height: 30px;
}
}
@media only screen and (max-width: $breakpointLarge) {

View File

@ -45,11 +45,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) {
const fanartImage = _.find(images, { coverType: 'fanart' });
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
return _.find(images, { coverType: 'fanart' })?.url;
}
function getExpandedState(newState) {
@ -194,7 +190,7 @@ class SeriesDetails extends Component {
genres,
tags,
year,
previousAiring,
lastAired,
isSaving,
isRefreshing,
isSearching,
@ -231,7 +227,7 @@ class SeriesDetails extends Component {
} = this.state;
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');
@ -715,6 +711,7 @@ SeriesDetails.propTypes = {
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
year: PropTypes.number.isRequired,
lastAired: PropTypes.string,
previousAiring: PropTypes.string,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,

View File

@ -210,12 +210,15 @@ class SeriesDetailsSeason extends Component {
seasonNumber,
items,
columns,
sortKey,
sortDirection,
statistics,
isSaving,
isExpanded,
isSearching,
seriesMonitored,
isSmallScreen,
onSortPress,
onTableOptionChange,
onMonitorSeasonPress,
onSearchPress
@ -447,6 +450,9 @@ class SeriesDetailsSeason extends Component {
items.length ?
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
onTableOptionChange={onTableOptionChange}
>
<TableBody>
@ -530,6 +536,8 @@ SeriesDetailsSeason.propTypes = {
seasonNumber: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string.isRequired,
sortDirection: PropTypes.oneOf(sortDirections.all),
statistics: PropTypes.object.isRequired,
isSaving: PropTypes.bool,
isExpanded: PropTypes.bool,
@ -537,6 +545,7 @@ SeriesDetailsSeason.propTypes = {
seriesMonitored: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onMonitorSeasonPress: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired,
onMonitorEpisodePress: PropTypes.func.isRequired,

View File

@ -4,8 +4,9 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
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 createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
@ -15,7 +16,7 @@ import SeriesDetailsSeason from './SeriesDetailsSeason';
function createMapStateToProps() {
return createSelector(
(state, { seasonNumber }) => seasonNumber,
(state) => state.episodes,
createClientSideCollectionSelector('episodes'),
createSeriesSelector(),
createCommandsSelector(),
createDimensionsSelector(),
@ -27,11 +28,12 @@ function createMapStateToProps() {
}));
const episodesInSeason = episodes.items.filter((episode) => episode.seasonNumber === seasonNumber);
const sortedEpisodes = episodesInSeason.sort((a, b) => b.episodeNumber - a.episodeNumber);
return {
items: sortedEpisodes,
items: episodesInSeason,
columns: episodes.columns,
sortKey: episodes.sortKey,
sortDirection: episodes.sortDirection,
isSearching,
seriesMonitored: series.monitored,
path: series.path,
@ -45,6 +47,7 @@ const mapDispatchToProps = {
toggleSeasonMonitored,
toggleEpisodesMonitored,
setEpisodesTableOption,
setEpisodesSort,
executeCommand
};
@ -90,6 +93,13 @@ class SeriesDetailsSeasonConnector extends Component {
});
};
onSortPress = (sortKey, sortDirection) => {
this.props.setEpisodesSort({
sortKey,
sortDirection
});
};
//
// Render
@ -98,6 +108,7 @@ class SeriesDetailsSeasonConnector extends Component {
<SeriesDetailsSeason
{...this.props}
onTableOptionChange={this.onTableOptionChange}
onSortPress={this.onSortPress}
onMonitorSeasonPress={this.onMonitorSeasonPress}
onSearchPress={this.onSearchPress}
onMonitorEpisodePress={this.onMonitorEpisodePress}
@ -112,6 +123,7 @@ SeriesDetailsSeasonConnector.propTypes = {
toggleSeasonMonitored: PropTypes.func.isRequired,
toggleEpisodesMonitored: PropTypes.func.isRequired,
setEpisodesTableOption: PropTypes.func.isRequired,
setEpisodesSort: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};

View File

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

View File

@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'deleteButton': string;
'labelIcon': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -1,16 +1,19 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal';
import translate from 'Utilities/String/translate';
import styles from './EditSeriesModalContent.css';
@ -73,6 +76,7 @@ class EditSeriesModalContent extends Component {
const {
monitored,
monitorNewItems,
seasonFolder,
qualityProfileId,
seriesType,
@ -94,12 +98,37 @@ class EditSeriesModalContent extends Component {
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredHelpText')}
helpText={translate('MonitoredEpisodesHelpText')}
{...monitored}
onChange={onInputChange}
/>
</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>
<FormLabel>{translate('UseSeasonFolder')}</FormLabel>

View File

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

View File

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

View File

@ -101,7 +101,7 @@ function SeriesIndexPosterOptionsModalContent(
type={inputTypes.CHECK}
name="showTitle"
value={showTitle}
helpText={translate('ShowTitleHelpText')}
helpText={translate('ShowSeriesTitleHelpText')}
onChange={onPosterOptionChange}
/>
</FormGroup>

View File

@ -98,7 +98,7 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
type={inputTypes.CHECK}
name="addImportListExclusion"
value={addImportListExclusion}
helpText={translate('AddListExclusionHelpText')}
helpText={translate('AddListExclusionSeriesHelpText')}
onChange={onDeleteOptionChange}
/>
</FormGroup>

View File

@ -14,6 +14,7 @@ import styles from './EditSeriesModalContent.css';
interface SavePayload {
monitored?: boolean;
monitorNewItems?: string;
qualityProfileId?: number;
seriesType?: string;
seasonFolder?: boolean;
@ -77,6 +78,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
const { seriesIds, onSavePress, onModalClose } = props;
const [monitored, setMonitored] = useState(NO_CHANGE);
const [monitorNewItems, setMonitorNewItems] = useState(NO_CHANGE);
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
NO_CHANGE
);
@ -95,6 +97,11 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
payload.monitored = monitored === 'monitored';
}
if (monitorNewItems !== NO_CHANGE) {
hasChanges = true;
payload.monitorNewItems = monitorNewItems;
}
if (qualityProfileId !== NO_CHANGE) {
hasChanges = true;
payload.qualityProfileId = qualityProfileId as number;
@ -124,6 +131,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
},
[
monitored,
monitorNewItems,
qualityProfileId,
seriesType,
seasonFolder,
@ -139,6 +147,9 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
case 'monitored':
setMonitored(value);
break;
case 'monitorNewItems':
setMonitorNewItems(value);
break;
case 'qualityProfileId':
setQualityProfileId(value);
break;
@ -199,6 +210,19 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
/>
</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>
<FormLabel>{translate('QualityProfile')}</FormLabel>

View File

@ -15,7 +15,10 @@ function createSeriesQueueDetailsSelector(
(queueItems) => {
return queueItems.reduce(
(acc: SeriesQueueDetails, item) => {
if (item.seriesId !== seriesId) {
if (
item.trackedDownloadState === 'imported' ||
item.seriesId !== seriesId
) {
return acc;
}

View File

@ -7,12 +7,10 @@ function findImage(images, coverType) {
}
function getUrl(image, coverType, size) {
if (image) {
// Remove protocol
let url = image.url.replace(/^https?:/, '');
url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
const imageUrl = image?.url;
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}
kind={kinds.DANGER}
title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', [name])}
message={translate('DeleteCustomFormatMessageText', { customFormatName: name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat}

View File

@ -147,7 +147,7 @@ class EditDownloadClientModalContent extends Component {
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('DownloadClientTagHelpText')}
helpText={translate('DownloadClientSeriesTagHelpText')}
{...tags}
onChange={onInputChange}
/>

View File

@ -61,7 +61,7 @@ function DownloadClientOptions(props) {
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('RedownloadFailed')}</FormLabel>
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
@ -71,6 +71,26 @@ function DownloadClientOptions(props) {
{...settings.autoRedownloadFailed}
/>
</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>
<Alert kind={kinds.INFO}>

View File

@ -54,7 +54,7 @@ class RemotePathMappings extends Component {
>
<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>
<div className={styles.remotePathMappingsHeader}>

View File

@ -124,6 +124,7 @@ class SecuritySettings extends Component {
authenticationRequired,
username,
password,
passwordConfirmation,
apiKey,
certificateValidation
} = settings;
@ -139,8 +140,8 @@ class SecuritySettings extends Component {
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Sonarr' })}
helpTextWarning={translate('AuthenticationRequiredWarning', { appName: 'Sonarr' })}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={translate('AuthenticationRequiredWarning')}
onChange={onInputChange}
{...authenticationMethod}
/>
@ -193,6 +194,21 @@ class SecuritySettings extends Component {
null
}
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
{...passwordConfirmation}
/>
</FormGroup> :
null
}
<FormGroup>
<FormLabel>{translate('ApiKey')}</FormLabel>

View File

@ -83,7 +83,7 @@ function UpdateSettings(props) {
type={inputTypes.CHECK}
name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Sonarr' }) : undefined}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined}
onChange={onInputChange}
{...updateAutomatically}
/>

View File

@ -56,7 +56,7 @@ class AddImportListModalContent extends Component {
<Alert kind={kinds.INFO}>
<div>
{translate('SupportedLists')}
{translate('SupportedListsSeries')}
</div>
<div>
{translate('SupportedListsMoreInfo')}

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent';
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
@ -46,9 +47,11 @@ function EditImportListModalContent(props) {
implementationName,
name,
enableAutomaticAdd,
searchForMissingEpisodes,
minRefreshInterval,
shouldMonitor,
rootFolderPath,
monitorNewItems,
qualityProfileId,
seriesType,
seasonFolder,
@ -107,12 +110,24 @@ function EditImportListModalContent(props) {
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticAdd"
helpText={translate('EnableAutomaticAddHelpText')}
helpText={translate('EnableAutomaticAddSeriesHelpText')}
{...enableAutomaticAdd}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ImportListSearchForMissingEpisodes')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="searchForMissingEpisodes"
helpText={translate('ImportListSearchForMissingEpisodesHelpText')}
{...searchForMissingEpisodes}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('Monitor')}
@ -138,6 +153,31 @@ function EditImportListModalContent(props) {
/>
</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>
<FormLabel>{translate('RootFolder')}</FormLabel>

View File

@ -200,7 +200,7 @@ function EditIndexerModalContent(props) {
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagHelpText')}
helpText={translate('IndexerTagSeriesHelpText')}
{...tags}
onChange={onInputChange}
/>

View File

@ -180,7 +180,7 @@ class MediaManagement extends Component {
<FormInputGroup
type={inputTypes.CHECK}
name="deleteEmptyFolders"
helpText={translate('DeleteEmptyFoldersHelpText')}
helpText={translate('DeleteEmptySeriesFoldersHelpText')}
onChange={onInputChange}
{...settings.deleteEmptyFolders}
/>
@ -257,7 +257,7 @@ class MediaManagement extends Component {
<FormInputGroup
type={inputTypes.CHECK}
name="copyUsingHardlinks"
helpText={translate('CopyUsingHardlinksHelpText')}
helpText={translate('CopyUsingHardlinksSeriesHelpText')}
helpTextWarning={translate('CopyUsingHardlinksHelpTextWarning')}
onChange={onInputChange}
{...settings.copyUsingHardlinks}
@ -305,7 +305,7 @@ class MediaManagement extends Component {
<FormInputGroup
type={inputTypes.CHECK}
name="importExtraFiles"
helpText={translate('ImportExtraFilesHelpText')}
helpText={translate('ImportExtraFilesEpisodeHelpText')}
onChange={onInputChange}
{...settings.importExtraFiles}
/>
@ -399,7 +399,7 @@ class MediaManagement extends Component {
<FormInputGroup
type={inputTypes.SELECT}
name="rescanAfterRefresh"
helpText={translate('RescanAfterRefreshHelpText')}
helpText={translate('RescanAfterRefreshSeriesHelpText')}
helpTextWarning={translate('RescanAfterRefreshHelpTextWarning')}
values={rescanAfterRefreshOptions}
onChange={onInputChange}

View File

@ -82,13 +82,16 @@ const fileNameTokens = [
const seriesTokens = [
{ token: '{Series Title}', 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 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 CleanTitleTheYear}', example: 'Series Title\'s!, The 2010' },
{ token: '{Series TitleTheWithoutYear}', example: 'Series Title\'s!, The' },
{ token: '{Series TitleYear}', example: 'The Series Title\'s! (2010)' },
{ token: '{Series TitleWithoutYear}', example: 'Series Title\'s!' },
{ token: '{Series CleanTitleTheWithoutYear}', example: 'Series Title\'s!, The' },
{ token: '{Series TitleFirstCharacter}', example: 'S' },
{ token: '{Series Year}', example: '2010' }
];

View File

@ -99,7 +99,7 @@ function EditNotificationModalContent(props) {
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('NotificationsTagsHelpText')}
helpText={translate('NotificationsTagsSeriesHelpText')}
{...tags}
onChange={onInputChange}
/>

View File

@ -186,7 +186,7 @@ function EditDelayProfileModalContent(props) {
{
id === 1 ?
<Alert>
{translate('DefaultDelayProfile')}
{translate('DefaultDelayProfileSeries')}
</Alert> :
<FormGroup>
@ -196,7 +196,7 @@ function EditDelayProfileModalContent(props) {
type={inputTypes.TAG}
name="tags"
{...tags}
helpText={translate('DelayProfileTagsHelpText')}
helpText={translate('DelayProfileSeriesTagsHelpText')}
onChange={onInputChange}
/>
</FormGroup>

View File

@ -203,7 +203,7 @@ class EditQualityProfileModalContent extends Component {
name="cutoff"
{...cutoff}
values={qualities}
helpText={translate('UpgradeUntilHelpText')}
helpText={translate('UpgradeUntilEpisodeHelpText')}
onChange={onCutoffChange}
/>
</FormGroup>
@ -237,7 +237,7 @@ class EditQualityProfileModalContent extends Component {
type={inputTypes.NUMBER}
name="cutoffFormatScore"
{...cutoffFormatScore}
helpText={translate('UpgradeUntilCustomFormatScoreHelpText')}
helpText={translate('UpgradeUntilCustomFormatScoreEpisodeHelpText')}
onChange={onInputChange}
/>
</FormGroup>
@ -281,7 +281,7 @@ class EditQualityProfileModalContent extends Component {
className={styles.deleteButtonContainer}
title={
isInUse ?
translate('QualityProfileInUse') :
translate('QualityProfileInUseSeriesListCollection') :
undefined
}
>

View File

@ -126,7 +126,7 @@ function EditReleaseProfileModalContent(props) {
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('ReleaseProfileTagHelpText')}
helpText={translate('ReleaseProfileTagSeriesHelpText')}
{...tags}
onChange={onInputChange}
/>

View File

@ -60,7 +60,7 @@ class QualityDefinitions extends Component {
<div className={styles.sizeLimitHelpTextContainer}>
<div className={styles.sizeLimitHelpText}>
{translate('QualityLimitsHelpText')}
{translate('QualityLimitsSeriesRuntimeHelpText')}
</div>
</div>
</PageSectionContent>

View File

@ -110,7 +110,7 @@ function Settings() {
</Link>
<div className={styles.summary}>
{translate('MetadataSettingsSummary')}
{translate('MetadataSettingsSeriesSummary')}
</div>
<Link
@ -121,7 +121,7 @@ function Settings() {
</Link>
<div className={styles.summary}>
{translate('MetadataSourceSettingsSummary')}
{translate('MetadataSourceSettingsSeriesSummary')}
</div>
<Link

View File

@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState';
import { set, updateServerSideCollection } from '../baseActions';
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
const [baseSection] = section.split('.');
return function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
const {
selectedFilterKey,
filters,
customFilters
filters
} = sectionState;
const customFilters = getState().customFilters.items.filter((customFilter) => {
return customFilter.type === section || customFilter.type === baseSection;
});
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
selectedFilters.forEach((filter) => {
@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
const promise = createAjaxRequest({
url,
data
data,
traditional: true
}).request;
promise.done((response) => {

View File

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

View File

@ -40,32 +40,38 @@ export const defaultState = {
{
name: 'episodeNumber',
label: '#',
isVisible: true
isVisible: true,
isSortable: true
},
{
name: 'title',
label: () => translate('Title'),
isVisible: true
isVisible: true,
isSortable: true
},
{
name: 'path',
label: () => translate('Path'),
isVisible: false
isVisible: false,
isSortable: true
},
{
name: 'relativePath',
label: () => translate('RelativePath'),
isVisible: false
isVisible: false,
isSortable: true
},
{
name: 'airDateUtc',
label: () => translate('AirDate'),
isVisible: true
isVisible: true,
isSortable: true
},
{
name: 'runtime',
label: () => translate('Runtime'),
isVisible: false
isVisible: false,
isSortable: true
},
{
name: 'languages',
@ -100,7 +106,8 @@ export const defaultState = {
{
name: 'size',
label: () => translate('Size'),
isVisible: false
isVisible: false,
isSortable: true
},
{
name: 'releaseGroup',
@ -119,7 +126,8 @@ export const defaultState = {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isVisible: false
isVisible: false,
isSortable: true
},
{
name: 'status',
@ -136,7 +144,9 @@ export const defaultState = {
};
export const persistState = [
'episodes.columns'
'episodes.columns',
'episodes.sortDirection',
'episodes.sortKey'
];
//

View File

@ -1,7 +1,7 @@
import React from 'react';
import { createAction } from 'redux-actions';
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 createAjaxRequest from 'Utilities/createAjaxRequest';
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,
items: [],
originalItems: [],
sortKey: 'quality',
sortDirection: sortDirections.DESCENDING,
sortKey: 'relativePath',
sortDirection: sortDirections.ASCENDING,
recentFolders: [],
importMode: 'chooseImportMode',
sortPredicates: {

View File

@ -3,7 +3,7 @@ import React from 'react';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
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 createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
@ -144,7 +144,7 @@ export const defaultState = {
name: 'size',
label: () => translate('Size'),
isSortable: true,
isVisibile: false
isVisible: false
},
{
name: 'outputPath',
@ -170,6 +170,43 @@ export const defaultState = {
isVisible: true,
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.sortKey',
'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_QUEUE_PAGE = 'queue/gotoQueuePage';
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_OPTION = 'queue/setQueueOption';
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 gotoQueuePage = createThunk(GOTO_QUEUE_PAGE);
export const setQueueSort = createThunk(SET_QUEUE_SORT);
export const setQueueFilter = createThunk(SET_QUEUE_FILTER);
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
export const setQueueOption = createAction(SET_QUEUE_OPTION);
export const clearQueue = createAction(CLEAR_QUEUE);
@ -279,7 +319,8 @@ export const actionHandlers = handleThunks({
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE,
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE,
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE,
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT,
[serverSideCollectionHandlers.FILTER]: SET_QUEUE_FILTER
},
fetchDataAugmenter
),

View File

@ -168,9 +168,10 @@ export const filterPredicates = {
},
hasMissingSeason: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const { seasons = [] } = item;
return seasons.some((season) => {
const hasMissingSeason = seasons.some((season) => {
const {
seasonNumber,
statistics = {}
@ -189,6 +190,8 @@ export const filterPredicates = {
episodeFileCount === 0
);
});
return predicate(hasMissingSeason, filterValue);
}
};
@ -347,7 +350,13 @@ export const filterBuilderProps = [
{
name: '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) {
switch (source) {
case 'IndexerStatusCheck':
case 'IndexerLongTermStatusCheck':
return (
<SpinnerIconButton
name={icons.TEST}

View File

@ -3,6 +3,7 @@ function getNewSeries(series, payload) {
const {
rootFolderPath,
monitor,
monitorNewItems,
qualityProfileId,
seriesType,
seasonFolder,
@ -19,6 +20,7 @@ function getNewSeries(series, payload) {
series.addOptions = addOptions;
series.monitored = true;
series.monitorNewItems = monitorNewItems;
series.qualityProfileId = qualityProfileId;
series.rootFolderPath = rootFolderPath;
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');
}
},
{
key: 'recent',
get value() {
return translate('MonitorRecentEpisodes');
}
},
{
key: 'pilot',
get value() {
@ -38,27 +44,27 @@ const monitorOptions = [
}
},
{
key: 'latestSeason',
key: 'lastSeason',
get value() {
return translate('MonitorLatestSeason');
return translate('MonitorLastSeason');
}
},
{
key: 'monitorSpecials',
get value() {
return translate('MonitorSpecials');
return translate('MonitorSpecialEpisodes');
}
},
{
key: 'unmonitorSpecials',
get value() {
return translate('UnmonitorSpecials');
return translate('UnmonitorSpecialEpisodes');
}
},
{
key: 'none',
get value() {
return translate('MonitorNone');
return translate('MonitorNoEpisodes');
}
}
];

View File

@ -25,15 +25,13 @@ export async function fetchTranslations(): Promise<boolean> {
export default function translate(
key: string,
tokens?: Record<string, string | number | boolean>
tokens: Record<string, string | number | boolean> = {}
) {
const translation = translations[key] || key;
if (tokens) {
tokens.appName = 'Sonarr';
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
}
return translation;
}

View File

@ -247,11 +247,11 @@ class CutoffUnmet extends Component {
<ConfirmModal
isOpen={isConfirmSearchAllCutoffUnmetModalOpen}
kind={kinds.DANGER}
title={translate('SearchForCutoffUnmet')}
title={translate('SearchForCutoffUnmetEpisodes')}
message={
<div>
<div>
{translate('SearchForCutoffUnmetConfirmationCount', { totalRecords })}
{translate('SearchForCutoffUnmetEpisodesConfirmationCount', { totalRecords })}
</div>
<div>
{translate('MassSearchCancelWarning')}

View File

@ -260,11 +260,11 @@ class Missing extends Component {
<ConfirmModal
isOpen={isConfirmSearchAllMissingModalOpen}
kind={kinds.DANGER}
title={translate('SearchForAllMissing')}
title={translate('SearchForAllMissingEpisodes')}
message={
<div>
<div>
{translate('SearchForAllMissingConfirmationCount', { totalRecords })}
{translate('SearchForAllMissingEpisodesConfirmationCount', { totalRecords })}
</div>
<div>
{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