Merge branch 'Sonarr:develop' into putio-download
This commit is contained in:
commit
b8007391b8
|
@ -1,5 +1,10 @@
|
|||
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
||||
|
||||
[![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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||
|
||||
function createHistorySelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface HistoryFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||
const sectionItems = useSelector(createHistorySelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'history';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setHistoryFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import 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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -81,4 +81,9 @@ QueueDetails.propTypes = {
|
|||
progressBar: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
QueueDetails.defaultProps = {
|
||||
trackedDownloadStatus: 'ok',
|
||||
trackedDownloadState: 'downloading'
|
||||
};
|
||||
|
||||
export default QueueDetails;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setQueueFilter } from 'Store/Actions/queueActions';
|
||||
|
||||
function createQueueSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.paged.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.paged.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface QueueFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||
const sectionItems = useSelector(createQueueSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'queue';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setQueueFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import 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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function SeriesMonitorNewItemsOptionsPopoverContent() {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('MonitorAllSeasons')}
|
||||
data={translate('MonitorAllSeasonsDescription')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('MonitorNoNewSeasons')}
|
||||
data={translate('MonitorNoNewSeasonsDescription')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesMonitorNewItemsOptionsPopoverContent;
|
|
@ -26,29 +26,39 @@ function SeriesMonitoringOptionsPopoverContent() {
|
|||
data={translate('MonitorExistingEpisodesDescription')}
|
||||
/>
|
||||
|
||||
<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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import History from 'typings/History';
|
||||
|
||||
interface HistoryAppState
|
||||
extends AppSectionState<History>,
|
||||
AppSectionFilterState<History> {}
|
||||
|
||||
export default HistoryAppState;
|
|
@ -1,43 +1,17 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import 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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
.description {
|
||||
line-height: $lineHeight;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-left: 0;
|
||||
line-height: $lineHeight;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { FilterBuilderProp } from 'App/State/AppState';
|
||||
|
||||
interface FilterBuilderRowOnChangeProps {
|
||||
name: string;
|
||||
value: unknown[];
|
||||
}
|
||||
|
||||
interface FilterBuilderRowValueProps {
|
||||
filterType?: string;
|
||||
filterValue: string | number | object | string[] | number[] | object[];
|
||||
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||
sectionItem: unknown[];
|
||||
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||
}
|
||||
|
||||
export default FilterBuilderRowValueProps;
|
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
const EVENT_TYPE_OPTIONS = [
|
||||
{
|
||||
id: 1,
|
||||
get name() {
|
||||
return translate('Grabbed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
get name() {
|
||||
return translate('Imported');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
get name() {
|
||||
return translate('Failed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
get name() {
|
||||
return translate('Deleted');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
get name() {
|
||||
return translate('Renamed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
get name() {
|
||||
return translate('Ignored');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function HistoryEventTypeFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||
}
|
||||
|
||||
export default HistoryEventTypeFilterBuilderRowValue;
|
|
@ -0,0 +1,13 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
const { items } = useSelector(createLanguagesSelector());
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={items} />;
|
||||
}
|
||||
|
||||
export default LanguageFilterBuilderRowValue;
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Series from 'Series/Series';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
function SeriesFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
const allSeries: Series[] = useSelector(createAllSeriesSelector());
|
||||
|
||||
const tagList = allSeries
|
||||
.map((series) => ({ id: series.id, name: series.title }))
|
||||
.sort(sortByName);
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default SeriesFilterBuilderRowValue;
|
|
@ -14,6 +14,7 @@ import FormInputHelpText from './FormInputHelpText';
|
|||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import 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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function MonitorNewItemsSelectInput(props) {
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const values = [...monitorNewItemsOptions];
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
values={values}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
MonitorNewItemsSelectInput.propTypes = {
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
includeMixed: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
MonitorNewItemsSelectInput.defaultProps = {
|
||||
includeNoChange: false,
|
||||
includeMixed: false
|
||||
};
|
||||
|
||||
export default MonitorNewItemsSelectInput;
|
|
@ -37,6 +37,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||
return inputTypes.OAUTH;
|
||||
case 'rootFolder':
|
||||
return inputTypes.ROOT_FOLDER_SELECT;
|
||||
case 'qualityProfile':
|
||||
return inputTypes.QUALITY_PROFILE_SELECT;
|
||||
default:
|
||||
return inputTypes.TEXT;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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' });
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -24,6 +24,7 @@ function PageHeaderActionsMenu(props) {
|
|||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('Menu')}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -89,7 +89,7 @@ class DeleteSeriesModalContent extends Component {
|
|||
type={inputTypes.CHECK}
|
||||
name="addImportListExclusion"
|
||||
value={addImportListExclusion}
|
||||
helpText={translate('AddListExclusionHelpText')}
|
||||
helpText={translate('AddListExclusionSeriesHelpText')}
|
||||
onChange={onDeleteOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -156,6 +156,12 @@
|
|||
.headerContent {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -3,3 +3,7 @@
|
|||
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'deleteButton': string;
|
||||
'labelIcon': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ function createMapStateToProps() {
|
|||
|
||||
const seriesSettings = _.pick(series, [
|
||||
'monitored',
|
||||
'monitorNewItems',
|
||||
'seasonFolder',
|
||||
'qualityProfileId',
|
||||
'seriesType',
|
||||
|
|
|
@ -63,7 +63,7 @@ const rows = [
|
|||
{
|
||||
name: 'qualityProfileId',
|
||||
showProp: 'showQualityProfile',
|
||||
valueProp: 'qualityProfileId',
|
||||
valueProp: 'qualityProfile',
|
||||
},
|
||||
{
|
||||
name: 'previousAiring',
|
||||
|
|
|
@ -101,7 +101,7 @@ function SeriesIndexPosterOptionsModalContent(
|
|||
type={inputTypes.CHECK}
|
||||
name="showTitle"
|
||||
value={showTitle}
|
||||
helpText={translate('ShowTitleHelpText')}
|
||||
helpText={translate('ShowSeriesTitleHelpText')}
|
||||
onChange={onPosterOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -98,7 +98,7 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
|
|||
type={inputTypes.CHECK}
|
||||
name="addImportListExclusion"
|
||||
value={addImportListExclusion}
|
||||
helpText={translate('AddListExclusionHelpText')}
|
||||
helpText={translate('AddListExclusionSeriesHelpText')}
|
||||
onChange={onDeleteOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -147,7 +147,7 @@ class EditDownloadClientModalContent extends Component {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('DownloadClientTagHelpText')}
|
||||
helpText={translate('DownloadClientSeriesTagHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -56,7 +56,7 @@ class AddImportListModalContent extends Component {
|
|||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('SupportedLists')}
|
||||
{translate('SupportedListsSeries')}
|
||||
</div>
|
||||
<div>
|
||||
{translate('SupportedListsMoreInfo')}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -200,7 +200,7 @@ function EditIndexerModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('IndexerTagHelpText')}
|
||||
helpText={translate('IndexerTagSeriesHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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' }
|
||||
];
|
||||
|
|
|
@ -99,7 +99,7 @@ function EditNotificationModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('NotificationsTagsHelpText')}
|
||||
helpText={translate('NotificationsTagsSeriesHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
>
|
||||
|
|
|
@ -126,7 +126,7 @@ function EditReleaseProfileModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('ReleaseProfileTagHelpText')}
|
||||
helpText={translate('ReleaseProfileTagSeriesHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
|
|
@ -60,7 +60,7 @@ class QualityDefinitions extends Component {
|
|||
|
||||
<div className={styles.sizeLimitHelpTextContainer}>
|
||||
<div className={styles.sizeLimitHelpText}>
|
||||
{translate('QualityLimitsHelpText')}
|
||||
{translate('QualityLimitsSeriesRuntimeHelpText')}
|
||||
</div>
|
||||
</div>
|
||||
</PageSectionContent>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -52,8 +52,6 @@ export const defaultState = {
|
|||
|
||||
selectedFilterKey: 'monitored',
|
||||
|
||||
customFilters: [],
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
|
|
|
@ -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'
|
||||
];
|
||||
|
||||
//
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
||||
};
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ function getInternalLink(source) {
|
|||
function getTestLink(source, props) {
|
||||
switch (source) {
|
||||
case 'IndexerStatusCheck':
|
||||
case 'IndexerLongTermStatusCheck':
|
||||
return (
|
||||
<SpinnerIconButton
|
||||
name={icons.TEST}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const monitorNewItemsOptions = [
|
||||
{
|
||||
key: 'all',
|
||||
get value() {
|
||||
return translate('MonitorAllSeasons');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'none',
|
||||
get value() {
|
||||
return translate('MonitorNoNewSeasons');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default monitorNewItemsOptions;
|
|
@ -25,6 +25,12 @@ const monitorOptions = [
|
|||
return translate('MonitorExistingEpisodes');
|
||||
}
|
||||
},
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -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) {
|
||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||
String(tokens[tokenMatch] ?? match)
|
||||
);
|
||||
}
|
||||
tokens.appName = 'Sonarr';
|
||||
|
||||
return translation;
|
||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||
String(tokens[tokenMatch] ?? match)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from './CustomFormat';
|
||||
|
||||
export type HistoryEventType =
|
||||
| 'grabbed'
|
||||
| 'seriesFolderImported'
|
||||
| 'downloadFolderImported'
|
||||
| 'downloadFailed'
|
||||
| 'episodeFileDeleted'
|
||||
| 'episodeFileRenamed'
|
||||
| 'downloadIgnored';
|
||||
|
||||
export default interface History {
|
||||
episodeId: number;
|
||||
seriesId: number;
|
||||
sourceTitle: string;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
qualityCutoffNotMet: boolean;
|
||||
date: string;
|
||||
downloadId: string;
|
||||
eventType: HistoryEventType;
|
||||
data: unknown;
|
||||
id: number;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
|
||||
export type QueueTrackedDownloadStatus = 'ok' | 'warning' | 'error';
|
||||
|
||||
export type QueueTrackedDownloadState =
|
||||
| 'downloading'
|
||||
| 'importPending'
|
||||
| 'importing'
|
||||
| 'imported'
|
||||
| 'failedPending'
|
||||
| 'failed'
|
||||
| 'ignored';
|
||||
|
||||
export interface StatusMessage {
|
||||
title: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
interface Queue extends ModelBase {
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
size: number;
|
||||
title: string;
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: QueueTrackedDownloadStatus;
|
||||
trackedDownloadState: QueueTrackedDownloadState;
|
||||
statusMessages: StatusMessage[];
|
||||
errorMessage: string;
|
||||
downloadId: string;
|
||||
protocol: string;
|
||||
downloadClient: string;
|
||||
outputPath: string;
|
||||
episodeHasFile: boolean;
|
||||
seriesId?: number;
|
||||
episodeId?: number;
|
||||
seasonNumber?: number;
|
||||
}
|
||||
|
||||
export default Queue;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue