1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Qstick 200be6451a fixup! Remove db calls from list threads 2023-10-23 17:12:00 +03:00
Qstick b279984bd7 fixup! Remove db calls from list threads 2023-10-23 17:12:00 +03:00
Qstick 3f6f4fc65f Remove db calls from list threads 2023-10-23 17:12:00 +03:00
202 changed files with 3844 additions and 4414 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop) [![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget) [![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation/docker) [![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation#docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg) ![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers) [![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors) [![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)
+1 -1
View File
@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.2.3' majorVersion: '5.1.2'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
+3 -28
View File
@@ -36,7 +36,6 @@ class Blocklist extends Component {
lastToggled: null, lastToggled: null,
selectedState: {}, selectedState: {},
isConfirmRemoveModalOpen: false, isConfirmRemoveModalOpen: false,
isConfirmClearModalOpen: false,
items: props.items items: props.items
}; };
} }
@@ -91,19 +90,6 @@ class Blocklist extends Component {
this.setState({ isConfirmRemoveModalOpen: false }); this.setState({ isConfirmRemoveModalOpen: false });
}; };
onClearBlocklistPress = () => {
this.setState({ isConfirmClearModalOpen: true });
};
onClearBlocklistConfirmed = () => {
this.props.onClearBlocklistPress();
this.setState({ isConfirmClearModalOpen: false });
};
onConfirmClearModalClose = () => {
this.setState({ isConfirmClearModalOpen: false });
};
// //
// Render // Render
@@ -117,6 +103,7 @@ class Blocklist extends Component {
totalRecords, totalRecords,
isRemoving, isRemoving,
isClearingBlocklistExecuting, isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -124,8 +111,7 @@ class Blocklist extends Component {
allSelected, allSelected,
allUnselected, allUnselected,
selectedState, selectedState,
isConfirmRemoveModalOpen, isConfirmRemoveModalOpen
isConfirmClearModalOpen
} = this.state; } = this.state;
const selectedIds = this.getSelectedIds(); const selectedIds = this.getSelectedIds();
@@ -145,9 +131,8 @@ class Blocklist extends Component {
<PageToolbarButton <PageToolbarButton
label={translate('Clear')} label={translate('Clear')}
iconName={icons.CLEAR} iconName={icons.CLEAR}
isDisabled={!items.length}
isSpinning={isClearingBlocklistExecuting} isSpinning={isClearingBlocklistExecuting}
onPress={this.onClearBlocklistPress} onPress={onClearBlocklistPress}
/> />
</PageToolbarSection> </PageToolbarSection>
@@ -230,16 +215,6 @@ class Blocklist extends Component {
onConfirm={this.onRemoveSelectedConfirmed} onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose} onCancel={this.onConfirmRemoveModalClose}
/> />
<ConfirmModal
isOpen={isConfirmClearModalOpen}
kind={kinds.DANGER}
title={translate('ClearBlocklist')}
message={translate('ClearBlocklistMessageText')}
confirmLabel={translate('Clear')}
onConfirm={this.onClearBlocklistConfirmed}
onCancel={this.onConfirmClearModalClose}
/>
</PageContent> </PageContent>
); );
} }
@@ -81,9 +81,4 @@ QueueDetails.propTypes = {
progressBar: PropTypes.node.isRequired progressBar: PropTypes.node.isRequired
}; };
QueueDetails.defaultProps = {
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading'
};
export default QueueDetails; export default QueueDetails;
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { tooltipPositions } from 'Helpers/Props'; import { tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import QueueStatus from './QueueStatus'; import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css'; import styles from './QueueStatusCell.css';
@@ -40,8 +41,8 @@ QueueStatusCell.propTypes = {
}; };
QueueStatusCell.defaultProps = { QueueStatusCell.defaultProps = {
trackedDownloadStatus: 'ok', trackedDownloadStatus: translate('Ok'),
trackedDownloadState: 'downloading' trackedDownloadState: translate('Downloading')
}; };
export default QueueStatusCell; export default QueueStatusCell;
+10 -17
View File
@@ -1,9 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatTime from 'Utilities/Date/formatTime'; import formatTime from 'Utilities/Date/formatTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
@@ -28,13 +25,11 @@ function TimeleftCell(props) {
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return ( return (
<TableRowCell className={styles.timeleft}> <TableRowCell
<Tooltip className={styles.timeleft}
anchor={<Icon name={icons.INFO} />} title={translate('DelayingDownloadUntil', { date, time })}
tooltip={translate('DelayingDownloadUntil', { date, time })} >
kind={kinds.INVERSE} -
position={tooltipPositions.TOP}
/>
</TableRowCell> </TableRowCell>
); );
} }
@@ -44,13 +39,11 @@ function TimeleftCell(props) {
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return ( return (
<TableRowCell className={styles.timeleft}> <TableRowCell
<Tooltip className={styles.timeleft}
anchor={<Icon name={icons.INFO} />} title={translate('RetryingDownloadOn', { date, time })}
tooltip={translate('RetryingDownloadOn', { date, time })} >
kind={kinds.INVERSE} -
position={tooltipPositions.TOP}
/>
</TableRowCell> </TableRowCell>
); );
} }
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
@@ -131,12 +130,7 @@ class AddNewMovie extends Component {
<div className={styles.helpText}> <div className={styles.helpText}>
{translate('FailedLoadingSearchResults')} {translate('FailedLoadingSearchResults')}
</div> </div>
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert> <div>{getErrorMessage(error)}</div>
<div>
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
{translate('WhySearchesCouldBeFailing')}
</Link>
</div>
</div> : null </div> : null
} }
@@ -66,7 +66,6 @@ class AddNewMovieSearchResult extends Component {
ratings, ratings,
folder, folder,
images, images,
existingMovieId,
isExistingMovie, isExistingMovie,
isExclusionMovie, isExclusionMovie,
isSmallScreen, isSmallScreen,
@@ -75,7 +74,8 @@ class AddNewMovieSearchResult extends Component {
monitored, monitored,
hasFile, hasFile,
isAvailable, isAvailable,
movieFile, queueStatus,
queueState,
runtime, runtime,
movieRuntimeFormat, movieRuntimeFormat,
certification certification
@@ -120,13 +120,13 @@ class AddNewMovieSearchResult extends Component {
{ {
isExistingMovie && isExistingMovie &&
<MovieIndexProgressBar <MovieIndexProgressBar
movieId={existingMovieId}
movieFile={movieFile}
monitored={monitored} monitored={monitored}
hasFile={hasFile} hasFile={hasFile}
status={status} status={status}
width={posterWidth} width={posterWidth}
detailedProgressBar={true} detailedProgressBar={true}
queueStatus={queueStatus}
queueState={queueState}
isAvailable={isAvailable} isAvailable={isAvailable}
/> />
} }
@@ -278,7 +278,6 @@ AddNewMovieSearchResult.propTypes = {
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired, folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
existingMovieId: PropTypes.number,
isExistingMovie: PropTypes.bool.isRequired, isExistingMovie: PropTypes.bool.isRequired,
isExclusionMovie: PropTypes.bool.isRequired, isExclusionMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
@@ -287,8 +286,9 @@ AddNewMovieSearchResult.propTypes = {
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired, hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired, isAvailable: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
colorImpairedMode: PropTypes.bool, colorImpairedMode: PropTypes.bool,
queueStatus: PropTypes.string,
queueState: PropTypes.string,
runtime: PropTypes.number.isRequired, runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired, movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string certification: PropTypes.string
@@ -10,15 +10,17 @@ function createMapStateToProps() {
createExistingMovieSelector(), createExistingMovieSelector(),
createExclusionMovieSelector(), createExclusionMovieSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(state) => state.queue.details.items,
(state, { internalId }) => internalId, (state, { internalId }) => internalId,
(state) => state.settings.ui.item.movieRuntimeFormat, (isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId) => {
(isExistingMovie, isExclusionMovie, dimensions, internalId, movieRuntimeFormat) => { const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0);
return { return {
existingMovieId: internalId,
isExistingMovie, isExistingMovie,
isExclusionMovie, isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
movieRuntimeFormat queueStatus: firstQueueItem ? firstQueueItem.status : null,
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
}; };
} }
); );
@@ -5,13 +5,12 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ImportMovieSelectFolder from './ImportMovieSelectFolder'; import ImportMovieSelectFolder from './ImportMovieSelectFolder';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createRootFoldersSelector(), (state) => state.rootFolders,
createSystemStatusSelector(), createSystemStatusSelector(),
(rootFolders, systemStatus) => { (rootFolders, systemStatus) => {
return { return {
+2 -2
View File
@@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{translate('AppUpdated')} {translate('AppUpdated', { appName: 'Radarr' })}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} /> <InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Radarr', version })} blockClassName={styles.version} />
</div> </div>
{ {
+2 -2
View File
@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody> <ModalBody>
<div> <div>
{translate('ConnectionLostToBackend')} {translate('ConnectionLostToBackend', { appName: 'Radarr' })}
</div> </div>
<div className={styles.automatic}> <div className={styles.automatic}>
{translate('ConnectionLostReconnect')} {translate('ConnectionLostReconnect', { appName: 'Radarr' })}
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
+31 -1
View File
@@ -1,10 +1,40 @@
import Queue from 'typings/Queue'; import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import AppSectionState, { import AppSectionState, {
AppSectionFilterState, AppSectionFilterState,
AppSectionItemState, AppSectionItemState,
Error, Error,
} from './AppSectionState'; } 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;
movieHasFile: boolean;
movieId?: number;
}
export interface QueueDetailsAppState extends AppSectionState<Queue> { export interface QueueDetailsAppState extends AppSectionState<Queue> {
params: unknown; params: unknown;
} }
@@ -1,7 +1,9 @@
.description {
line-height: $lineHeight;
}
.description { .description {
margin-left: 0; margin-left: 0;
line-height: $lineHeight;
overflow-wrap: break-word;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
@@ -23,19 +23,19 @@ const EVENT_TYPE_OPTIONS = [
}, },
}, },
{ {
id: 6, id: 5,
get name() { get name() {
return translate('Deleted'); return translate('Deleted');
}, },
}, },
{ {
id: 8, id: 6,
get name() { get name() {
return translate('Renamed'); return translate('Renamed');
}, },
}, },
{ {
id: 9, id: 7,
get name() { get name() {
return translate('Ignored'); return translate('Ignored');
}, },
@@ -3,7 +3,6 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addRootFolder } from 'Store/Actions/rootFolderActions'; import { addRootFolder } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import RootFolderSelectInput from './RootFolderSelectInput'; import RootFolderSelectInput from './RootFolderSelectInput';
@@ -11,7 +10,7 @@ const ADD_NEW_KEY = 'addNew';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createRootFoldersSelector(), (state) => state.rootFolders,
(state, { value }) => value, (state, { value }) => value,
(state, { includeMissingValue }) => includeMissingValue, (state, { includeMissingValue }) => includeMissingValue,
(state, { includeNoChange }) => includeNoChange, (state, { includeNoChange }) => includeNoChange,
+1 -8
View File
@@ -63,12 +63,6 @@
width: 1280px; width: 1280px;
} }
.extraExtraLarge {
composes: modal;
width: 1600px;
}
@media only screen and (max-width: $breakpointExtraLarge) { @media only screen and (max-width: $breakpointExtraLarge) {
.modal.extraLarge { .modal.extraLarge {
width: 90%; width: 90%;
@@ -96,8 +90,7 @@
.modal.small, .modal.small,
.modal.medium, .modal.medium,
.modal.large, .modal.large,
.modal.extraLarge, .modal.extraLarge {
.modal.extraExtraLarge {
max-height: 100%; max-height: 100%;
width: 100%; width: 100%;
height: 100% !important; height: 100% !important;
-1
View File
@@ -1,7 +1,6 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'extraExtraLarge': string;
'extraLarge': string; 'extraLarge': string;
'large': string; 'large': string;
'medium': string; 'medium': string;
@@ -78,8 +78,7 @@ function createMapDispatchToProps(dispatch, props) {
onImportListSyncPress() { onImportListSyncPress() {
dispatch(executeCommand({ dispatch(executeCommand({
name: commandNames.IMPORT_LIST_SYNC, name: commandNames.IMPORT_LIST_SYNC
commandFinished: this.dispatchFetchListMovies
})); }));
} }
}; };
@@ -50,7 +50,7 @@ $hoverScale: 1.05;
.title { .title {
@add-mixin truncate; @add-mixin truncate;
background-color: var(--movieBackgroundColor); background-color: #fafbfc;
text-align: center; text-align: center;
font-size: $smallFontSize; font-size: $smallFontSize;
} }
@@ -92,7 +92,6 @@ class DiscoverMoviePoster extends Component {
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
timeFormat, timeFormat,
movieRuntimeFormat,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -111,7 +110,7 @@ class DiscoverMoviePoster extends Component {
return ( return (
<div className={styles.content}> <div className={styles.content}>
<div className={styles.posterContainer} title={title}> <div className={styles.posterContainer}>
{ {
<div className={styles.editorSelect}> <div className={styles.editorSelect}>
<CheckInput <CheckInput
@@ -186,7 +185,7 @@ class DiscoverMoviePoster extends Component {
{ {
showTitle && showTitle &&
<div className={styles.title} title={title}> <div className={styles.title}>
{title} {title}
</div> </div>
} }
@@ -195,7 +194,6 @@ class DiscoverMoviePoster extends Component {
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}
movieRuntimeFormat={movieRuntimeFormat}
{...otherProps} {...otherProps}
/> />
@@ -238,7 +236,6 @@ DiscoverMoviePoster.propTypes = {
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
isExisting: PropTypes.bool.isRequired, isExisting: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool.isRequired, isExcluded: PropTypes.bool.isRequired,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
@@ -5,11 +5,9 @@ import DiscoverMoviePoster from './DiscoverMoviePoster';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.ui.item.movieRuntimeFormat,
createDimensionsSelector(), createDimensionsSelector(),
(movieRuntimeFormat, dimensions) => { ( dimensions) => {
return { return {
movieRuntimeFormat,
isSmallScreen: dimensions.isSmallScreen isSmallScreen: dimensions.isSmallScreen
}; };
} }
@@ -1,5 +1,5 @@
.info { .info {
background-color: var(--movieBackgroundColor); background-color: #fafbfc;
text-align: center; text-align: center;
font-size: $smallFontSize; font-size: $smallFontSize;
} }
@@ -1,12 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import { icons } from 'Helpers/Props';
import { getMovieStatusDetails } from 'Movie/MovieStatus'; import { getMovieStatusDetails } from 'Movie/MovieStatus';
import formatRuntime from 'Utilities/Date/formatRuntime'; import formatRuntime from 'Utilities/Date/formatRuntime';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate';
import styles from './DiscoverMoviePosterInfo.css'; import styles from './DiscoverMoviePosterInfo.css';
function DiscoverMoviePosterInfo(props) { function DiscoverMoviePosterInfo(props) {
@@ -22,13 +19,12 @@ function DiscoverMoviePosterInfo(props) {
sortKey, sortKey,
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
timeFormat, timeFormat
movieRuntimeFormat
} = props; } = props;
if (sortKey === 'status' && status) { if (sortKey === 'status' && status) {
return ( return (
<div className={styles.info} title={translate('Status')}> <div className={styles.info}>
{getMovieStatusDetails(status).title} {getMovieStatusDetails(status).title}
</div> </div>
); );
@@ -36,7 +32,7 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'studio' && studio) { if (sortKey === 'studio' && studio) {
return ( return (
<div className={styles.info} title={translate('Studio')}> <div className={styles.info}>
{studio} {studio}
</div> </div>
); );
@@ -54,8 +50,8 @@ function DiscoverMoviePosterInfo(props) {
); );
return ( return (
<div className={styles.info} title={translate('InCinemas')}> <div className={styles.info}>
<Icon name={icons.IN_CINEMAS} /> {inCinemasDate} {`In Cinemas ${inCinemasDate}`}
</div> </div>
); );
} }
@@ -72,8 +68,8 @@ function DiscoverMoviePosterInfo(props) {
); );
return ( return (
<div className={styles.info} title={translate('DigitalRelease')}> <div className={styles.info}>
<Icon name={icons.MOVIE_FILE} /> {digitalReleaseDate} {`Digital ${digitalReleaseDate}`}
</div> </div>
); );
} }
@@ -90,15 +86,15 @@ function DiscoverMoviePosterInfo(props) {
); );
return ( return (
<div className={styles.info} title={translate('PhysicalRelease')}> <div className={styles.info}>
<Icon name={icons.DISC} /> {physicalReleaseDate} {`Released ${physicalReleaseDate}`}
</div> </div>
); );
} }
if (sortKey === 'certification' && certification) { if (sortKey === 'certification' && certification) {
return ( return (
<div className={styles.info} title={translate('Certification')}> <div className={styles.info}>
{certification} {certification}
</div> </div>
); );
@@ -106,8 +102,8 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'runtime' && runtime) { if (sortKey === 'runtime' && runtime) {
return ( return (
<div className={styles.info} title={translate('Runtime')}> <div className={styles.info}>
{formatRuntime(runtime, movieRuntimeFormat)} {formatRuntime(runtime)}
</div> </div>
); );
} }
@@ -115,7 +111,9 @@ function DiscoverMoviePosterInfo(props) {
if (sortKey === 'ratings' && ratings) { if (sortKey === 'ratings' && ratings) {
return ( return (
<div className={styles.info}> <div className={styles.info}>
<TmdbRating ratings={ratings} /> <TmdbRating
ratings={ratings}
/>
</div> </div>
); );
} }
@@ -135,8 +133,7 @@ DiscoverMoviePosterInfo.propTypes = {
sortKey: PropTypes.string.isRequired, sortKey: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired
movieRuntimeFormat: PropTypes.string.isRequired
}; };
export default DiscoverMoviePosterInfo; export default DiscoverMoviePosterInfo;
@@ -76,7 +76,6 @@ class DiscoverMovieRow extends Component {
ratings, ratings,
popularity, popularity,
certification, certification,
movieRuntimeFormat,
collection, collection,
columns, columns,
isExisting, isExisting,
@@ -231,7 +230,7 @@ class DiscoverMovieRow extends Component {
key={name} key={name}
className={styles[name]} className={styles[name]}
> >
{formatRuntime(runtime, movieRuntimeFormat)} {formatRuntime(runtime)}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@@ -398,7 +397,6 @@ DiscoverMovieRow.propTypes = {
popularity: PropTypes.number.isRequired, popularity: PropTypes.number.isRequired,
certification: PropTypes.string, certification: PropTypes.string,
collection: PropTypes.object, collection: PropTypes.object,
movieRuntimeFormat: PropTypes.string.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isExisting: PropTypes.bool.isRequired, isExisting: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool.isRequired, isExcluded: PropTypes.bool.isRequired,
@@ -5,11 +5,9 @@ import DiscoverMovieRow from './DiscoverMovieRow';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.ui.item.movieRuntimeFormat,
createDimensionsSelector(), createDimensionsSelector(),
(movieRuntimeFormat, dimensions) => { (dimensions) => {
return { return {
movieRuntimeFormat,
isSmallScreen: dimensions.isSmallScreen isSmallScreen: dimensions.isSmallScreen
}; };
} }
@@ -34,8 +34,7 @@ function AuthenticationRequiredModalContent(props) {
authenticationMethod, authenticationMethod,
authenticationRequired, authenticationRequired,
username, username,
password, password
passwordConfirmation
} = settings; } = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
@@ -64,7 +63,7 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert} className={styles.authRequiredAlert}
kind={kinds.WARNING} kind={kinds.WARNING}
> >
{translate('AuthenticationRequiredWarning')} {translate('AuthenticationRequiredWarning', { appName: 'Radarr' })}
</Alert> </Alert>
{ {
@@ -77,7 +76,7 @@ function AuthenticationRequiredModalContent(props) {
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="authenticationMethod" name="authenticationMethod"
values={authenticationMethodOptions} values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')} helpText={translate('AuthenticationMethodHelpText', { appName: 'Radarr' })}
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined} helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication" helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
onChange={onInputChange} onChange={onInputChange}
@@ -121,18 +120,6 @@ function AuthenticationRequiredModalContent(props) {
{...password} {...password}
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
{...passwordConfirmation}
/>
</FormGroup>
</div> : </div> :
null null
} }
+1 -2
View File
@@ -3,6 +3,5 @@ export const SMALL = 'small';
export const MEDIUM = 'medium'; export const MEDIUM = 'medium';
export const LARGE = 'large'; export const LARGE = 'large';
export const EXTRA_LARGE = 'extraLarge'; export const EXTRA_LARGE = 'extraLarge';
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE]; export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
@@ -3,16 +3,13 @@ import React, { Fragment } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { align, icons, kinds, sortDirections } from 'Helpers/Props'; import { icons, kinds, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector'; import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearch.css'; import styles from './InteractiveSearchContent.css';
const columns = [ const columns = [
{ {
@@ -27,6 +24,23 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{ {
name: 'title', name: 'title',
label: () => translate('Title'), label: () => translate('Title'),
@@ -70,6 +84,12 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'customFormat',
label: () => translate('Formats'),
isSortable: true,
isVisible: true
},
{ {
name: 'customFormatScore', name: 'customFormatScore',
label: React.createElement(Icon, { label: React.createElement(Icon, {
@@ -87,27 +107,10 @@ const columns = [
}), }),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
},
{
name: 'releaseWeight',
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
title: () => translate('Rejections')
}),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
} }
]; ];
function InteractiveSearch(props) { function InteractiveSearchContent(props) {
const { const {
searchPayload, searchPayload,
isFetching, isFetching,
@@ -115,36 +118,18 @@ function InteractiveSearch(props) {
error, error,
totalReleasesCount, totalReleasesCount,
items, items,
selectedFilterKey,
filters,
customFilters,
sortKey, sortKey,
sortDirection, sortDirection,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
onSortPress, onSortPress,
onFilterSelect,
onGrabPress onGrabPress
} = props; } = props;
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
const type = 'movies';
return ( return (
<div> <div>
<div className={styles.filterMenuContainer}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
filterModalConnectorComponentProps={{ type }}
onFilterSelect={onFilterSelect}
/>
</div>
{ {
isFetching ? <LoadingIndicator /> : null isFetching ? <LoadingIndicator /> : null
} }
@@ -218,23 +203,19 @@ function InteractiveSearch(props) {
); );
} }
InteractiveSearch.propTypes = { InteractiveSearchContent.propTypes = {
searchPayload: PropTypes.object.isRequired, searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
totalReleasesCount: PropTypes.number.isRequired, totalReleasesCount: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: 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,
sortKey: PropTypes.string, sortKey: PropTypes.string,
sortDirection: PropTypes.string, sortDirection: PropTypes.string,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired onGrabPress: PropTypes.func.isRequired
}; };
export default InteractiveSearch; export default InteractiveSearchContent;
@@ -2,11 +2,10 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import * as releaseActions from 'Store/Actions/releaseActions'; import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import InteractiveSearch from './InteractiveSearch'; import InteractiveSearchContent from './InteractiveSearchContent';
function createMapStateToProps(appState) { function createMapStateToProps(appState) {
return createSelector( return createSelector(
@@ -30,12 +29,8 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(releaseActions.fetchReleases(payload)); dispatch(releaseActions.fetchReleases(payload));
}, },
dispatchFetchMovieHistory({ movieId }) { dispatchClearReleases(payload) {
dispatch(fetchMovieHistory({ movieId })); dispatch(releaseActions.clearReleases(payload));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
}, },
onSortPress(sortKey, sortDirection) { onSortPress(sortKey, sortDirection) {
@@ -43,7 +38,8 @@ function createMapDispatchToProps(dispatch, props) {
}, },
onFilterSelect(selectedFilterKey) { onFilterSelect(selectedFilterKey) {
dispatch(releaseActions.setReleasesFilter({ selectedFilterKey })); const action = releaseActions.setReleasesFilter;
dispatch(action({ selectedFilterKey }));
}, },
onGrabPress(payload) { onGrabPress(payload) {
@@ -52,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) {
}; };
} }
class InteractiveSearchConnector extends Component { class InteractiveSearchContentConnector extends Component {
// //
// Lifecycle // Lifecycle
@@ -61,8 +57,7 @@ class InteractiveSearchConnector extends Component {
const { const {
searchPayload, searchPayload,
isPopulated, isPopulated,
dispatchFetchReleases, dispatchFetchReleases
dispatchFetchMovieHistory
} = this.props; } = this.props;
// If search results are not yet isPopulated fetch them, // If search results are not yet isPopulated fetch them,
@@ -70,12 +65,6 @@ class InteractiveSearchConnector extends Component {
if (!isPopulated) { if (!isPopulated) {
dispatchFetchReleases(searchPayload); dispatchFetchReleases(searchPayload);
} }
dispatchFetchMovieHistory(searchPayload);
}
componentWillUnmount() {
this.props.dispatchClearMovieHistory();
} }
// //
@@ -84,26 +73,24 @@ class InteractiveSearchConnector extends Component {
render() { render() {
const { const {
dispatchFetchReleases, dispatchFetchReleases,
dispatchFetchMovieHistory, dispatchClearReleases,
dispatchClearMovieHistory,
...otherProps ...otherProps
} = this.props; } = this.props;
return ( return (
<InteractiveSearch <InteractiveSearchContent
{...otherProps} {...otherProps}
/> />
); );
} }
} }
InteractiveSearchConnector.propTypes = { InteractiveSearchContentConnector.propTypes = {
searchPayload: PropTypes.object.isRequired, searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired, dispatchFetchReleases: PropTypes.func.isRequired,
dispatchFetchMovieHistory: PropTypes.func.isRequired, dispatchClearReleases: PropTypes.func.isRequired
dispatchClearMovieHistory: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector); export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);
@@ -4,7 +4,7 @@ import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton'; import PageMenuButton from 'Components/Menu/PageMenuButton';
import { align } from 'Helpers/Props'; import { align } from 'Helpers/Props';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import styles from './InteractiveSearch.css'; import styles from './InteractiveSearchContent.css';
function InteractiveSearchFilterMenu(props) { function InteractiveSearchFilterMenu(props) {
const { const {
@@ -1,25 +1,23 @@
.protocol { .cell {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
}
.protocol {
composes: cell;
width: 80px; width: 80px;
} }
.titleContent {
display: flex;
align-items: center;
justify-content: space-between;
word-break: break-all;
}
.indexer { .indexer {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell;
width: 85px; width: 85px;
} }
.quality, .quality,
.customFormat,
.languages { .languages {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell;
} }
.languages { .languages {
@@ -27,7 +25,7 @@
} }
.customFormatScore { .customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell;
width: 55px; width: 55px;
font-weight: bold; font-weight: bold;
@@ -35,28 +33,31 @@
} }
.rejected, .rejected,
.indexerFlags, .indexerFlags {
.download { composes: cell;
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px; width: 50px;
} }
.age, .age,
.size { .size {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell;
white-space: nowrap; white-space: nowrap;
} }
.peers { .peers {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell;
width: 75px; width: 75px;
} }
.titleContent {
overflow-wrap: break-word;
}
.history { .history {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell;
width: 75px; width: 75px;
} }
@@ -66,7 +67,7 @@
} }
.download { .download {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell;
width: 80px; width: 80px;
} }
@@ -3,6 +3,8 @@
interface CssExports { interface CssExports {
'age': string; 'age': string;
'blocklist': string; 'blocklist': string;
'cell': string;
'customFormat': string;
'customFormatScore': string; 'customFormatScore': string;
'download': string; 'download': string;
'downloadIcon': string; 'downloadIcon': string;
@@ -133,9 +133,9 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
longDateFormat, longDateFormat,
timeFormat, timeFormat,
grabError, grabError,
historyGrabbedData = {} as MovieHistory, historyGrabbedData,
historyFailedData = {} as MovieHistory, historyFailedData,
blocklistData = {} as MovieBlocklist, blocklistData,
searchPayload, searchPayload,
onGrabPress, onGrabPress,
} = props; } = props;
@@ -199,6 +199,53 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
{formatAge(age, ageHours, ageMinutes)} {formatAge(age, ageHours, ageMinutes)}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.RIGHT}
/>
) : null}
</TableRowCell>
<TableRowCell> <TableRowCell>
<div className={styles.titleContent}> <div className={styles.titleContent}>
<Link to={infoUrl} title={title}> <Link to={infoUrl} title={title}>
@@ -269,6 +316,10 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
<MovieQuality quality={quality} /> <MovieQuality quality={quality} />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats formats={customFormats} />
</TableRowCell>
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
<Tooltip <Tooltip
anchor={formatCustomFormatScore( anchor={formatCustomFormatScore(
@@ -297,53 +348,6 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
) : null} ) : null}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.rejected}>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell className={styles.download}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isSpinning={isGrabbing}
onPress={onGrabPressWrapper}
/>
<Link
className={styles.manualDownloadContent}
title={translate('OverrideAndAddToDownloadQueue')}
onPress={onOverridePress}
>
<div className={styles.manualDownloadContent}>
<Icon
className={styles.interactiveIcon}
name={icons.INTERACTIVE}
size={12}
/>
<Icon
className={styles.downloadIcon}
name={icons.CIRCLE_DOWN}
size={10}
/>
</div>
</Link>
</TableRowCell>
<ConfirmModal <ConfirmModal
isOpen={isConfirmGrabModalOpen} isOpen={isConfirmGrabModalOpen}
kind={kinds.WARNING} kind={kinds.WARNING}
@@ -0,0 +1,16 @@
import React from 'react';
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
function InteractiveSearchTable(props) {
return (
<InteractiveSearchContentConnector
searchPayload={props}
/>
);
}
InteractiveSearchTable.propTypes = {
};
export default InteractiveSearchTable;
@@ -1,8 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot'; import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector'; import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css'; import styles from '../MovieCreditPoster.css';
class MovieCastPoster extends Component { class MovieCastPoster extends Component {
@@ -57,7 +60,7 @@ class MovieCastPoster extends Component {
images, images,
posterWidth, posterWidth,
posterHeight, posterHeight,
importList importListId
} = this.props; } = this.props;
const { const {
@@ -66,31 +69,36 @@ class MovieCastPoster extends Component {
const elementStyle = { const elementStyle = {
width: `${posterWidth}px`, width: `${posterWidth}px`,
height: `${posterHeight}px`, height: `${posterHeight}px`
borderRadius: '5px'
}; };
const contentStyle = { const contentStyle = {
width: `${posterWidth}px` width: `${posterWidth}px`
}; };
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return ( return (
<div <div
className={styles.content} className={styles.content}
style={contentStyle} style={contentStyle}
> >
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
<div className={styles.controls}> <Label className={styles.controls}>
<MonitorToggleButton {
className={styles.action} importListId > 0 ?
monitored={monitored} <IconButton
size={20} className={styles.action}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress} name={icons.EDIT}
/> title={translate('EditPerson')}
</div> onPress={this.onEditImportListPress}
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title={translate('FollowPerson')}
onPress={this.onAddImportListPress}
/>
}
</Label>
<div <div
style={elementStyle} style={elementStyle}
@@ -140,8 +148,12 @@ MovieCastPoster.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired, posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired, posterHeight: PropTypes.number.isRequired,
importList: PropTypes.object, importListId: PropTypes.number.isRequired,
onImportListSelect: PropTypes.func.isRequired onImportListSelect: PropTypes.func.isRequired
}; };
MovieCastPoster.defaultProps = {
importListId: 0
};
export default MovieCastPoster; export default MovieCastPoster;
@@ -1,8 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import MonitorToggleButton from 'Components/MonitorToggleButton'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import { icons } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot'; import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector'; import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css'; import styles from '../MovieCreditPoster.css';
class MovieCrewPoster extends Component { class MovieCrewPoster extends Component {
@@ -57,7 +60,7 @@ class MovieCrewPoster extends Component {
images, images,
posterWidth, posterWidth,
posterHeight, posterHeight,
importList importListId
} = this.props; } = this.props;
const { const {
@@ -66,31 +69,36 @@ class MovieCrewPoster extends Component {
const elementStyle = { const elementStyle = {
width: `${posterWidth}px`, width: `${posterWidth}px`,
height: `${posterHeight}px`, height: `${posterHeight}px`
borderRadius: '5px'
}; };
const contentStyle = { const contentStyle = {
width: `${posterWidth}px` width: `${posterWidth}px`
}; };
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return ( return (
<div <div
className={styles.content} className={styles.content}
style={contentStyle} style={contentStyle}
> >
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
<div className={styles.controls}> <Label className={styles.controls}>
<MonitorToggleButton {
className={styles.action} importListId > 0 ?
monitored={monitored} <IconButton
size={20} className={styles.action}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress} name={icons.EDIT}
/> title={translate('EditPerson')}
</div> onPress={this.onEditImportListPress}
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title={translate('FollowPerson')}
onPress={this.onAddImportListPress}
/>
}
</Label>
<div <div
style={elementStyle} style={elementStyle}
@@ -140,8 +148,12 @@ MovieCrewPoster.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired, posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired, posterHeight: PropTypes.number.isRequired,
importList: PropTypes.object, importListId: PropTypes.number.isRequired,
onImportListSelect: PropTypes.func.isRequired onImportListSelect: PropTypes.func.isRequired
}; };
MovieCrewPoster.defaultProps = {
importListId: 0
};
export default MovieCrewPoster; export default MovieCrewPoster;
@@ -18,7 +18,7 @@ function createMapStateToProps() {
}, []); }, []);
return { return {
items: _.uniqBy(crew, 'personName') items: crew
}; };
} }
); );
@@ -1,13 +1,17 @@
$hoverScale: 1.05; $hoverScale: 1.05;
.content { .content {
border-radius: '5px';
transition: all 200ms ease-in; transition: all 200ms ease-in;
&:hover { &:hover {
z-index: 2; z-index: 2;
box-shadow: 0 0 12px var(--black); box-shadow: 0 0 12px var(--black);
transition: all 200ms ease-in; transition: all 200ms ease-in;
.controls {
opacity: 0.9;
transition: opacity 200ms linear 150ms;
}
} }
} }
@@ -46,18 +50,22 @@ $hoverScale: 1.05;
.controls { .controls {
position: absolute; position: absolute;
top: 10px; bottom: 10px;
left: 10px;
z-index: 3; z-index: 3;
border-radius: 4px;
background-color: #707070;
color: var(--white);
font-size: $smallFontSize;
opacity: 0;
transition: opacity 0;
} }
.action { .action {
composes: toggleButton from '~Components/MonitorToggleButton.css'; composes: button from '~Components/Link/IconButton.css';
width: 25px;
color: var(--white);
&:hover { &:hover {
color: var(--iconButtonHoverLightColor); color: var(--radarrYellow);
} }
} }
@@ -1,19 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions'; import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector'; import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createMovieCreditListSelector();
createMovieCreditListSelector(),
(importList) => {
return {
importList
};
}
);
} }
const mapDispatchToProps = { const mapDispatchToProps = {
@@ -28,7 +20,7 @@ class MovieCreditPosterConnector extends Component {
// Listeners // Listeners
onImportListSelect = () => { onImportListSelect = () => {
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', implementationName: 'TMDb Person', presetName: undefined }); this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', presetName: undefined });
this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() }); this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` }); this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` });
}; };
@@ -2,10 +2,6 @@
flex: 1 0 auto; flex: 1 0 auto;
} }
.movie {
padding: 10px;
}
.container { .container {
padding: 10px; padding: 10px;
} }
@@ -3,7 +3,6 @@
interface CssExports { interface CssExports {
'container': string; 'container': string;
'grid': string; 'grid': string;
'movie': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -1,15 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Navigation } from 'swiper'; import { Grid, WindowScroller } from 'react-virtualized';
import { Swiper, SwiperSlide } from 'swiper/react'; import Measure from 'Components/Measure';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import MovieCreditPosterConnector from './MovieCreditPosterConnector'; import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCreditPosters.css'; import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions // Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding); const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen); const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
@@ -68,11 +65,39 @@ class MovieCreditPosters extends Component {
}; };
this._isInitialized = false; this._isInitialized = false;
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
const {
items
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
if (this._grid &&
(prevState.width !== width ||
prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
} }
// //
// Control // Control
setGridRef = (ref) => {
this._grid = ref;
};
calculateGrid = (width = this.state.width, isSmallScreen) => { calculateGrid = (width = this.state.width, isSmallScreen) => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
@@ -92,10 +117,7 @@ class MovieCreditPosters extends Component {
}); });
}; };
// cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
// Render
render() {
const { const {
items, items,
itemComponent itemComponent
@@ -104,44 +126,99 @@ class MovieCreditPosters extends Component {
const { const {
posterWidth, posterWidth,
posterHeight, posterHeight,
columnCount
} = this.state;
const movieIdx = rowIndex * columnCount + columnIndex;
const movie = items[movieIdx];
if (!movie) {
return null;
}
return (
<div
className={styles.container}
key={key}
style={style}
>
<MovieCreditPosterConnector
key={movie.order}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={movie.personTmdbId}
personName={movie.personName}
job={movie.job}
character={movie.character}
images={movie.images}
/>
</div>
);
};
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
};
//
// Render
render() {
const {
items
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight rowHeight
} = this.state; } = this.state;
return ( const rowCount = Math.ceil(items.length / columnCount);
<div className={styles.sliderContainer}> return (
<Swiper <Measure
slidesPerView='auto' whitelist={['width']}
spaceBetween={10} onMeasure={this.onMeasure}
slidesPerGroup={3} >
loop={false} <WindowScroller
loopFillGroupWithBlank={true} scrollElement={undefined}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.params.navigation.prevEl = this._swiperPrevRef;
swiper.params.navigation.nextEl = this._swiperNextRef;
swiper.navigation.init();
swiper.navigation.update();
}}
> >
{items.map((credit) => ( {({ height, registerChild, onChildScroll, scrollTop }) => {
<SwiperSlide key={credit.id} style={{ width: posterWidth, height: rowHeight }}> if (!height) {
<MovieCreditPosterConnector return <div />;
key={credit.id} }
component={itemComponent}
posterWidth={posterWidth} return (
posterHeight={posterHeight} <div ref={registerChild}>
tmdbId={credit.personTmdbId} <Grid
personName={credit.personName} ref={this.setGridRef}
job={credit.job} className={styles.grid}
character={credit.character} autoHeight={true}
images={credit.images} height={height}
/> columnCount={columnCount}
</SwiperSlide> columnWidth={columnWidth}
))} rowCount={rowCount}
</Swiper> rowHeight={rowHeight}
</div> width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
); );
} }
} }
@@ -0,0 +1,3 @@
.alternateTitle {
white-space: nowrap;
}
@@ -0,0 +1,28 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './MovieAlternateTitles.css';
function MovieAlternateTitles({ alternateTitles }) {
return (
<ul>
{
alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
return (
<li
key={alternateTitle}
className={styles.alternateTitle}
>
{alternateTitle}
</li>
);
})
}
</ul>
);
}
MovieAlternateTitles.propTypes = {
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default MovieAlternateTitles;
+3 -4
View File
@@ -5,7 +5,7 @@
.header { .header {
position: relative; position: relative;
width: 100%; width: 100%;
height: 425px; height: 375px;
} }
.errorMessage { .errorMessage {
@@ -39,11 +39,10 @@
} }
.poster { .poster {
z-index: 2;
flex-shrink: 0; flex-shrink: 0;
margin-right: 35px; margin-right: 35px;
width: 250px; width: 217px;
height: 368px; height: 319px;
} }
.info { .info {
+142 -78
View File
@@ -1,9 +1,9 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import TextTruncate from 'react-text-truncate'; import TextTruncate from 'react-text-truncate';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating'; import ImdbRating from 'Components/ImdbRating';
import InfoLabel from 'Components/InfoLabel'; import InfoLabel from 'Components/InfoLabel';
@@ -23,11 +23,12 @@ import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieHistoryModal from 'Movie/History/MovieHistoryModal'; import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable'; import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable'; import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -37,6 +38,8 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
import formatRuntime from 'Utilities/Date/formatRuntime'; import formatRuntime from 'Utilities/Date/formatRuntime';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector'; import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector'; import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector'; import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
@@ -54,6 +57,14 @@ function getFanartUrl(images) {
return _.find(images, { coverType: 'fanart' })?.url; return _.find(images, { coverType: 'fanart' })?.url;
} }
function getExpandedState(newState) {
return {
allExpanded: newState.allSelected,
allCollapsed: newState.allUnselected,
expandedState: newState.selectedState
};
}
class MovieDetails extends Component { class MovieDetails extends Component {
// //
@@ -67,8 +78,10 @@ class MovieDetails extends Component {
isEditMovieModalOpen: false, isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false, isDeleteMovieModalOpen: false,
isInteractiveImportModalOpen: false, isInteractiveImportModalOpen: false,
isInteractiveSearchModalOpen: false, allExpanded: false,
isMovieHistoryModalOpen: false, allCollapsed: false,
expandedState: {},
selectedTabIndex: 0,
overviewHeight: 0, overviewHeight: 0,
titleWidth: 0 titleWidth: 0
}; };
@@ -101,6 +114,10 @@ class MovieDetails extends Component {
this.setState({ isOrganizeModalOpen: false }); this.setState({ isOrganizeModalOpen: false });
}; };
onManageEpisodesPress = () => {
this.setState({ isManageEpisodesOpen: true });
};
onInteractiveImportPress = () => { onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true }); this.setState({ isInteractiveImportModalOpen: true });
}; };
@@ -117,14 +134,6 @@ class MovieDetails extends Component {
this.setState({ isEditMovieModalOpen: false }); this.setState({ isEditMovieModalOpen: false });
}; };
onInteractiveSearchPress = () => {
this.setState({ isInteractiveSearchModalOpen: true });
};
onInteractiveSearchModalClose = () => {
this.setState({ isInteractiveSearchModalOpen: false });
};
onDeleteMoviePress = () => { onDeleteMoviePress = () => {
this.setState({ this.setState({
isEditMovieModalOpen: false, isEditMovieModalOpen: false,
@@ -136,12 +145,27 @@ class MovieDetails extends Component {
this.setState({ isDeleteMovieModalOpen: false }); this.setState({ isDeleteMovieModalOpen: false });
}; };
onMovieHistoryPress = () => { onExpandAllPress = () => {
this.setState({ isMovieHistoryModalOpen: true }); const {
allExpanded,
expandedState
} = this.state;
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
}; };
onMovieHistoryModalClose = () => { onExpandPress = (seasonNumber, isExpanded) => {
this.setState({ isMovieHistoryModalOpen: false }); this.setState((state) => {
const convertedState = {
allSelected: state.allExpanded,
allUnselected: state.allCollapsed,
selectedState: state.expandedState
};
const newState = toggleSelected(convertedState, [], seasonNumber, isExpanded, false);
return getExpandedState(newState);
});
}; };
onMeasure = ({ height }) => { onMeasure = ({ height }) => {
@@ -180,12 +204,7 @@ class MovieDetails extends Component {
if ( if (
touchStart < 50 || touchStart < 50 ||
this.props.isSidebarVisible || this.props.isSidebarVisible ||
this.state.isOrganizeModalOpen || this.state.isEventModalOpen
this.state.isEditMovieModalOpen ||
this.state.isDeleteMovieModalOpen ||
this.state.isInteractiveImportModalOpen ||
this.state.isInteractiveSearchModalOpen ||
this.state.isMovieHistoryModalOpen
) { ) {
return; return;
} }
@@ -220,6 +239,10 @@ class MovieDetails extends Component {
} }
}; };
onTabSelect = (index, lastIndex) => {
this.setState({ selectedTabIndex: index });
};
// //
// Render // Render
@@ -263,7 +286,7 @@ class MovieDetails extends Component {
onMonitorTogglePress, onMonitorTogglePress,
onRefreshPress, onRefreshPress,
onSearchPress, onSearchPress,
queueItem, queueItems,
movieRuntimeFormat movieRuntimeFormat
} = this.props; } = this.props;
@@ -272,10 +295,9 @@ class MovieDetails extends Component {
isEditMovieModalOpen, isEditMovieModalOpen,
isDeleteMovieModalOpen, isDeleteMovieModalOpen,
isInteractiveImportModalOpen, isInteractiveImportModalOpen,
isInteractiveSearchModalOpen,
isMovieHistoryModalOpen,
overviewHeight, overviewHeight,
titleWidth titleWidth,
selectedTabIndex
} = this.state; } = this.state;
const fanartUrl = getFanartUrl(images); const fanartUrl = getFanartUrl(images);
@@ -302,14 +324,6 @@ class MovieDetails extends Component {
onPress={onSearchPress} onPress={onSearchPress}
/> />
<PageToolbarButton
label={translate('InteractiveSearch')}
iconName={icons.INTERACTIVE}
isSpinning={isSearching}
title={undefined}
onPress={this.onInteractiveSearchPress}
/>
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
@@ -325,12 +339,6 @@ class MovieDetails extends Component {
onPress={this.onInteractiveImportPress} onPress={this.onInteractiveImportPress}
/> />
<PageToolbarButton
label={translate('History')}
iconName={icons.HISTORY}
onPress={this.onMovieHistoryPress}
/>
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
@@ -536,7 +544,7 @@ class MovieDetails extends Component {
hasMovieFiles={hasMovieFiles} hasMovieFiles={hasMovieFiles}
monitored={monitored} monitored={monitored}
isAvailable={isAvailable} isAvailable={isAvailable}
queueItem={queueItem} queueItem={(queueItems.length > 0) ? queueItems[0] : null}
/> />
</span> </span>
</InfoLabel> </InfoLabel>
@@ -646,33 +654,101 @@ class MovieDetails extends Component {
null null
} }
<FieldSet legend={translate('Files')}> <Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
<MovieFileEditorTable <TabList
movieId={id} className={styles.tabList}
/> >
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('History')}
</Tab>
<ExtraFileTable <Tab
movieId={id} className={styles.tab}
/> selectedClassName={styles.selectedTab}
</FieldSet> >
{translate('Search')}
</Tab>
<FieldSet legend={translate('Cast')}> <Tab
<MovieCastPostersConnector className={styles.tab}
isSmallScreen={isSmallScreen} selectedClassName={styles.selectedTab}
/> >
</FieldSet> {translate('Files')}
</Tab>
<FieldSet legend={translate('Crew')}> <Tab
<MovieCrewPostersConnector className={styles.tab}
isSmallScreen={isSmallScreen} selectedClassName={styles.selectedTab}
/> >
</FieldSet> {translate('Titles')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Cast')}
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Crew')}
</Tab>
{
selectedTabIndex === 1 &&
<div className={styles.filterIcon}>
<InteractiveSearchFilterMenuConnector />
</div>
}
</TabList>
<TabPanel>
<MovieHistoryTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<InteractiveSearchTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieFileEditorTable
movieId={id}
/>
<ExtraFileTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieTitlesTable
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
<TabPanel>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
</Tabs>
<FieldSet legend={translate('Titles')}>
<MovieTitlesTable
movieId={id}
/>
</FieldSet>
</div> </div>
<OrganizePreviewModalConnector <OrganizePreviewModalConnector
@@ -688,12 +764,6 @@ class MovieDetails extends Component {
onDeleteMoviePress={this.onDeleteMoviePress} onDeleteMoviePress={this.onDeleteMoviePress}
/> />
<MovieHistoryModal
isOpen={isMovieHistoryModalOpen}
movieId={id}
onModalClose={this.onMovieHistoryModalClose}
/>
<DeleteMovieModal <DeleteMovieModal
isOpen={isDeleteMovieModalOpen} isOpen={isDeleteMovieModalOpen}
movieId={id} movieId={id}
@@ -710,12 +780,6 @@ class MovieDetails extends Component {
showImportMode={false} showImportMode={false}
onModalClose={this.onInteractiveImportModalClose} onModalClose={this.onInteractiveImportModalClose}
/> />
<MovieInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
movieId={id}
onModalClose={this.onInteractiveSearchModalClose}
/>
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );
@@ -766,7 +830,7 @@ MovieDetails.propTypes = {
onRefreshPress: PropTypes.func.isRequired, onRefreshPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired,
onGoToMovie: PropTypes.func.isRequired, onGoToMovie: PropTypes.func.isRequired,
queueItem: PropTypes.object, queueItems: PropTypes.arrayOf(PropTypes.object),
movieRuntimeFormat: PropTypes.string.isRequired movieRuntimeFormat: PropTypes.string.isRequired
}; };
@@ -11,6 +11,7 @@ import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions'; import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions'; import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions'; import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions'; import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import { fetchImportListSchema } from 'Store/Actions/settingsActions'; import { fetchImportListSchema } from 'Store/Actions/settingsActions';
@@ -144,8 +145,6 @@ function createMapStateToProps() {
return acc; return acc;
}, []); }, []);
const queueItem = queueItems.find((item) => item.movieId === movie.id);
return { return {
...movie, ...movie,
alternateTitles, alternateTitles,
@@ -166,7 +165,7 @@ function createMapStateToProps() {
nextMovie, nextMovie,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
isSidebarVisible, isSidebarVisible,
queueItem, queueItems,
movieRuntimeFormat movieRuntimeFormat
}; };
} }
@@ -181,6 +180,12 @@ function createMapDispatchToProps(dispatch, props) {
dispatchClearMovieFiles() { dispatchClearMovieFiles() {
dispatch(clearMovieFiles()); dispatch(clearMovieFiles());
}, },
dispatchFetchMovieHistory({ movieId }) {
dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
},
dispatchFetchMovieCredits({ movieId }) { dispatchFetchMovieCredits({ movieId }) {
dispatch(fetchMovieCredits({ movieId })); dispatch(fetchMovieCredits({ movieId }));
}, },
@@ -276,6 +281,7 @@ class MovieDetailsConnector extends Component {
this.props.dispatchFetchMovieFiles({ movieId }); this.props.dispatchFetchMovieFiles({ movieId });
this.props.dispatchFetchMovieBlocklist({ movieId }); this.props.dispatchFetchMovieBlocklist({ movieId });
this.props.dispatchFetchMovieHistory({ movieId });
this.props.dispatchFetchExtraFiles({ movieId }); this.props.dispatchFetchExtraFiles({ movieId });
this.props.dispatchFetchMovieCredits({ movieId }); this.props.dispatchFetchMovieCredits({ movieId });
this.props.dispatchFetchQueueDetails({ movieId }); this.props.dispatchFetchQueueDetails({ movieId });
@@ -286,6 +292,7 @@ class MovieDetailsConnector extends Component {
this.props.dispatchCancelFetchReleases(); this.props.dispatchCancelFetchReleases();
this.props.dispatchClearMovieBlocklist(); this.props.dispatchClearMovieBlocklist();
this.props.dispatchClearMovieFiles(); this.props.dispatchClearMovieFiles();
this.props.dispatchClearMovieHistory();
this.props.dispatchClearExtraFiles(); this.props.dispatchClearExtraFiles();
this.props.dispatchClearMovieCredits(); this.props.dispatchClearMovieCredits();
this.props.dispatchClearQueueDetails(); this.props.dispatchClearQueueDetails();
@@ -342,6 +349,8 @@ MovieDetailsConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
dispatchFetchMovieFiles: PropTypes.func.isRequired, dispatchFetchMovieFiles: PropTypes.func.isRequired,
dispatchClearMovieFiles: PropTypes.func.isRequired, dispatchClearMovieFiles: PropTypes.func.isRequired,
dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired,
dispatchFetchExtraFiles: PropTypes.func.isRequired, dispatchFetchExtraFiles: PropTypes.func.isRequired,
dispatchClearExtraFiles: PropTypes.func.isRequired, dispatchClearExtraFiles: PropTypes.func.isRequired,
dispatchFetchMovieCredits: PropTypes.func.isRequired, dispatchFetchMovieCredits: PropTypes.func.isRequired,
@@ -8,6 +8,7 @@ import translate from 'Utilities/String/translate';
import styles from './MovieStatusLabel.css'; import styles from './MovieStatusLabel.css';
function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) { function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) {
if (queueItem) { if (queueItem) {
const queueStatus = queueItem.status; const queueStatus = queueItem.status;
const queueState = queueItem.trackedDownloadStatus; const queueState = queueItem.trackedDownloadStatus;
@@ -115,4 +116,8 @@ MovieStatusLabel.propTypes = {
colorImpairedMode: PropTypes.bool colorImpairedMode: PropTypes.bool
}; };
MovieStatusLabel.defaultProps = {
title: ''
};
export default MovieStatusLabel; export default MovieStatusLabel;
@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector'; import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
import styles from './MovieTitlesTable.css';
function MovieTitlesTable(props) { function MovieTitlesTable(props) {
const { const {
@@ -8,11 +7,9 @@ function MovieTitlesTable(props) {
} = props; } = props;
return ( return (
<div className={styles.container}> <MovieTitlesTableContentConnector
<MovieTitlesTableContentConnector {...otherProps}
{...otherProps} />
/>
</div>
); );
} }
@@ -1,33 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieHistoryModalContentConnector from './MovieHistoryModalContentConnector';
function MovieHistoryModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
size={sizes.EXTRA_LARGE}
>
<MovieHistoryModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieHistoryModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModal;
@@ -1,142 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: () => translate('CustomFormatScore')
}),
isSortable: true,
isVisible: true
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true
},
{
name: 'actions',
label: () => translate('Actions'),
isVisible: true
}
];
class MovieHistoryModalContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress,
onModalClose
} = this.props;
const hasItems = !!items.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('History')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
}
{
isPopulated && !hasItems && !error &&
<div>{translate('NoHistory')}</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
MovieHistoryModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieHistoryModalContent;
@@ -1,4 +1,5 @@
.container { .container {
margin-top: 20px;
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
border-radius: 4px; border-radius: 4px;
background-color: var(--inputBackgroundColor); background-color: var(--inputBackgroundColor);
@@ -0,0 +1,22 @@
import React from 'react';
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
import styles from './MovieHistoryTable.css';
function MovieHistoryTable(props) {
const {
...otherProps
} = props;
return (
<div className={styles.container}>
<MovieHistoryTableContentConnector
{...otherProps}
/>
</div>
);
}
MovieHistoryTable.propTypes = {
};
export default MovieHistoryTable;
@@ -0,0 +1,128 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
import styles from './MovieHistoryTableContent.css';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'sourceTitle',
label: () => translate('SourceTitle'),
isVisible: true
},
{
name: 'languages',
label: () => translate('Languages'),
isVisible: true
},
{
name: 'quality',
label: () => translate('Quality'),
isVisible: true
},
{
name: 'customFormats',
label: () => translate('CustomFormats'),
isSortable: false,
isVisible: true
},
{
name: 'customFormatScore',
label: React.createElement(Icon, {
name: icons.SCORE,
title: 'Custom format score'
}),
isSortable: true,
isVisible: true
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true
},
{
name: 'actions',
label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
isVisible: true
}
];
class MovieHistoryTableContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress
} = this.props;
const hasItems = !!items.length;
return (
<div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>
{translate('UnableToLoadHistory')}
</div>
}
{
isPopulated && !hasItems && !error &&
<div className={styles.blankpad}>
{translate('NoHistory')}
</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<MovieHistoryRowConnector
key={item.id}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</div>
);
}
}
MovieHistoryTableContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
export default MovieHistoryTableContent;
@@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { clearMovieHistory, fetchMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions'; import { movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryModalContent from './MovieHistoryModalContent'; import MovieHistoryTableContent from './MovieHistoryTableContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
@@ -15,29 +15,10 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchMovieHistory,
clearMovieHistory,
movieHistoryMarkAsFailed movieHistoryMarkAsFailed
}; };
class MovieHistoryModalContentConnector extends Component { class MovieHistoryTableContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
movieId
} = this.props;
this.props.fetchMovieHistory({
movieId
});
}
componentWillUnmount() {
this.props.clearMovieHistory();
}
// //
// Listeners // Listeners
@@ -58,7 +39,7 @@ class MovieHistoryModalContentConnector extends Component {
render() { render() {
return ( return (
<MovieHistoryModalContent <MovieHistoryTableContent
{...this.props} {...this.props}
onMarkAsFailedPress={this.onMarkAsFailedPress} onMarkAsFailedPress={this.onMarkAsFailedPress}
/> />
@@ -66,11 +47,9 @@ class MovieHistoryModalContentConnector extends Component {
} }
} }
MovieHistoryModalContentConnector.propTypes = { MovieHistoryTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired, movieId: PropTypes.number.isRequired,
fetchMovieHistory: PropTypes.func.isRequired,
clearMovieHistory: PropTypes.func.isRequired,
movieHistoryMarkAsFailed: PropTypes.func.isRequired movieHistoryMarkAsFailed: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector); export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryTableContentConnector);
@@ -11,10 +11,7 @@ function createMovieQueueDetailsSelector(movieId: number) {
(queueItems) => { (queueItems) => {
return queueItems.reduce( return queueItems.reduce(
(acc: MovieQueueDetails, item) => { (acc: MovieQueueDetails, item) => {
if ( if (item.movieId !== movieId) {
item.trackedDownloadState === 'imported' ||
item.movieId !== movieId
) {
return acc; return acc;
} }
@@ -1,35 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import MovieInteractiveSearchModalContent from './MovieInteractiveSearchModalContent';
function MovieInteractiveSearchModal(props) {
const {
isOpen,
movieId,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
size={sizes.EXTRA_EXTRA_LARGE}
>
<MovieInteractiveSearchModalContent
movieId={movieId}
onModalClose={onModalClose}
/>
</Modal>
);
}
MovieInteractiveSearchModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
movieId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieInteractiveSearchModal;
@@ -1,59 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import MovieInteractiveSearchModal from './MovieInteractiveSearchModal';
function createMapDispatchToProps(dispatch, props) {
return {
dispatchCancelFetchReleases() {
dispatch(cancelFetchReleases());
},
dispatchClearReleases() {
dispatch(clearReleases());
},
onModalClose() {
dispatch(cancelFetchReleases());
dispatch(clearReleases());
props.onModalClose();
}
};
}
class MovieInteractiveSearchModalConnector extends Component {
//
// Lifecycle
componentWillUnmount() {
this.props.dispatchCancelFetchReleases();
this.props.dispatchClearReleases();
}
//
// Render
render() {
const {
dispatchCancelFetchReleases,
dispatchClearReleases,
...otherProps
} = this.props;
return (
<MovieInteractiveSearchModal
{...otherProps}
/>
);
}
}
MovieInteractiveSearchModalConnector.propTypes = {
...MovieInteractiveSearchModal.propTypes,
dispatchCancelFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(MovieInteractiveSearchModalConnector);
@@ -1,44 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { scrollDirections } from 'Helpers/Props';
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
import translate from 'Utilities/String/translate';
function MovieInteractiveSearchModalContent(props) {
const {
movieId,
onModalClose
} = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('InteractiveSearchModalHeader')}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
<InteractiveSearchConnector
searchPayload={{ movieId }}
/>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
MovieInteractiveSearchModalContent.propTypes = {
movieId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default MovieInteractiveSearchModalContent;
@@ -1,4 +1,5 @@
.container { .container {
margin-top: 20px;
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
border-radius: 4px; border-radius: 4px;
background-color: var(--inputBackgroundColor); background-color: var(--inputBackgroundColor);
@@ -124,7 +124,6 @@ class SecuritySettings extends Component {
authenticationRequired, authenticationRequired,
username, username,
password, password,
passwordConfirmation,
apiKey, apiKey,
certificateValidation certificateValidation
} = settings; } = settings;
@@ -140,8 +139,8 @@ class SecuritySettings extends Component {
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="authenticationMethod" name="authenticationMethod"
values={authenticationMethodOptions} values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')} helpText={translate('AuthenticationMethodHelpText', { appName: 'Radarr' })}
helpTextWarning={translate('AuthenticationRequiredWarning')} helpTextWarning={translate('AuthenticationRequiredWarning', { appName: 'Radarr' })}
onChange={onInputChange} onChange={onInputChange}
{...authenticationMethod} {...authenticationMethod}
/> />
@@ -194,21 +193,6 @@ class SecuritySettings extends Component {
null null
} }
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="passwordConfirmation"
onChange={onInputChange}
{...passwordConfirmation}
/>
</FormGroup> :
null
}
<FormGroup> <FormGroup>
<FormLabel>{translate('ApiKey')}</FormLabel> <FormLabel>{translate('ApiKey')}</FormLabel>
@@ -83,7 +83,7 @@ function UpdateSettings(props) {
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="updateAutomatically" name="updateAutomatically"
helpText={translate('UpdateAutomaticallyHelpText')} helpText={translate('UpdateAutomaticallyHelpText')}
helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker') : undefined} helpTextWarning={updateMechanism.value === 'docker' ? translate('AutomaticUpdatesDisabledDocker', { appName: 'Radarr' }) : undefined}
onChange={onInputChange} onChange={onInputChange}
{...updateAutomatically} {...updateAutomatically}
/> />
@@ -74,15 +74,9 @@ class ImportList extends Component {
<div className={styles.enabled}> <div className={styles.enabled}>
{ {
enabled ? enabled &&
<Label kind={kinds.SUCCESS}> <Label kind={kinds.SUCCESS}>
{translate('Enabled')} {translate('Enabled')}
</Label> :
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label> </Label>
} }
@@ -92,6 +86,16 @@ class ImportList extends Component {
{translate('Auto')} {translate('Auto')}
</Label> </Label>
} }
{
!enabled && !enableAuto &&
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
</div> </div>
<div className={styles.enabled}> <div className={styles.enabled}>
@@ -12,7 +12,6 @@ import translate from 'Utilities/String/translate';
import styles from './ManageImportListsEditModalContent.css'; import styles from './ManageImportListsEditModalContent.css';
interface SavePayload { interface SavePayload {
enabled?: boolean;
enableAuto?: boolean; enableAuto?: boolean;
qualityProfileId?: number; qualityProfileId?: number;
rootFolderPath?: string; rootFolderPath?: string;
@@ -26,7 +25,7 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const enableOptions = [ const autoAddOptions = [
{ {
key: NO_CHANGE, key: NO_CHANGE,
get value() { get value() {
@@ -53,7 +52,6 @@ function ManageImportListsEditModalContent(
) { ) {
const { importListIds, onSavePress, onModalClose } = props; const { importListIds, onSavePress, onModalClose } = props;
const [enabled, setEnabled] = useState(NO_CHANGE);
const [enableAuto, setEnableAuto] = useState(NO_CHANGE); const [enableAuto, setEnableAuto] = useState(NO_CHANGE);
const [qualityProfileId, setQualityProfileId] = useState<string | number>( const [qualityProfileId, setQualityProfileId] = useState<string | number>(
NO_CHANGE NO_CHANGE
@@ -64,11 +62,6 @@ function ManageImportListsEditModalContent(
let hasChanges = false; let hasChanges = false;
const payload: SavePayload = {}; const payload: SavePayload = {};
if (enabled !== NO_CHANGE) {
hasChanges = true;
payload.enabled = enabled === 'enabled';
}
if (enableAuto !== NO_CHANGE) { if (enableAuto !== NO_CHANGE) {
hasChanges = true; hasChanges = true;
payload.enableAuto = enableAuto === 'enabled'; payload.enableAuto = enableAuto === 'enabled';
@@ -89,21 +82,11 @@ function ManageImportListsEditModalContent(
} }
onModalClose(); onModalClose();
}, [ }, [enableAuto, qualityProfileId, rootFolderPath, onSavePress, onModalClose]);
enabled,
enableAuto,
qualityProfileId,
rootFolderPath,
onSavePress,
onModalClose,
]);
const onInputChange = useCallback( const onInputChange = useCallback(
({ name, value }: { name: string; value: string }) => { ({ name, value }: { name: string; value: string }) => {
switch (name) { switch (name) {
case 'enabled':
setEnabled(value);
break;
case 'enableAuto': case 'enableAuto':
setEnableAuto(value); setEnableAuto(value);
break; break;
@@ -127,18 +110,6 @@ function ManageImportListsEditModalContent(
<ModalHeader>{translate('EditSelectedImportLists')}</ModalHeader> <ModalHeader>{translate('EditSelectedImportLists')}</ModalHeader>
<ModalBody> <ModalBody>
<FormGroup>
<FormLabel>{translate('Enabled')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="enabled"
value={enabled}
values={enableOptions}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('AutomaticAdd')}</FormLabel> <FormLabel>{translate('AutomaticAdd')}</FormLabel>
@@ -146,7 +117,7 @@ function ManageImportListsEditModalContent(
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="enableAuto" name="enableAuto"
value={enableAuto} value={enableAuto}
values={enableOptions} values={autoAddOptions}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>
@@ -58,12 +58,6 @@ const COLUMNS = [
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{
name: 'enabled',
label: () => translate('Enabled'),
isSortable: true,
isVisible: true,
},
{ {
name: 'enableAuto', name: 'enableAuto',
label: () => translate('AutomaticAdd'), label: () => translate('AutomaticAdd'),
@@ -1,6 +1,5 @@
.name, .name,
.tags, .tags,
.enabled,
.enableAuto, .enableAuto,
.qualityProfileId, .qualityProfileId,
.rootFolderPath, .rootFolderPath,
@@ -8,4 +7,4 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all; word-break: break-all;
} }
@@ -2,7 +2,6 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'enableAuto': string; 'enableAuto': string;
'enabled': string;
'implementation': string; 'implementation': string;
'name': string; 'name': string;
'qualityProfileId': string; 'qualityProfileId': string;
@@ -7,7 +7,6 @@ import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector'; import TagListConnector from 'Components/TagListConnector';
import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector'; import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import styles from './ManageImportListsModalRow.css'; import styles from './ManageImportListsModalRow.css';
interface ManageImportListsModalRowProps { interface ManageImportListsModalRowProps {
@@ -17,7 +16,6 @@ interface ManageImportListsModalRowProps {
qualityProfileId: number; qualityProfileId: number;
implementation: string; implementation: string;
tags: number[]; tags: number[];
enabled: boolean;
enableAuto: boolean; enableAuto: boolean;
columns: Column[]; columns: Column[];
isSelected?: boolean; isSelected?: boolean;
@@ -32,7 +30,6 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
rootFolderPath, rootFolderPath,
qualityProfileId, qualityProfileId,
implementation, implementation,
enabled,
enableAuto, enableAuto,
tags, tags,
onSelectedChange, onSelectedChange,
@@ -66,19 +63,15 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.qualityProfileId}> <TableRowCell className={styles.qualityProfileId}>
{qualityProfile?.name ?? translate('None')} {qualityProfile?.name ?? 'None'}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.rootFolderPath}> <TableRowCell className={styles.rootFolderPath}>
{rootFolderPath} {rootFolderPath}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.enabled}>
{enabled ? translate('Yes') : translate('No')}
</TableRowCell>
<TableRowCell className={styles.enableAuto}> <TableRowCell className={styles.enableAuto}>
{enableAuto ? translate('Yes') : translate('No')} {enableAuto ? 'Yes' : 'No'}
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.tags}> <TableRowCell className={styles.tags}>
@@ -122,7 +122,7 @@ export default {
return selectProviderSchema(state, section, payload, (selectedSchema) => { return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.name = payload.presetName ?? payload.implementationName; selectedSchema.name = payload.presetName ?? payload.implementationName;
selectedSchema.implementationName = payload.implementationName; selectedSchema.implementationName = payload.implementationName;
selectedSchema.minRefreshInterval = selectedSchema.minRefreshInterval ?? payload.minRefreshInterval; selectedSchema.minRefreshInterval = payload.minRefreshInterval;
selectedSchema.minimumAvailability = 'released'; selectedSchema.minimumAvailability = 'released';
selectedSchema.rootFolderPath = ''; selectedSchema.rootFolderPath = '';
@@ -1,5 +1,4 @@
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment/moment';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
@@ -10,7 +9,7 @@ import getNewMovie from 'Utilities/Movie/getNewMovie';
import getSectionState from 'Utilities/State/getSectionState'; import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState'; import updateSectionState from 'Utilities/State/updateSectionState';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import { removeItem, set, update, updateItem } from './baseActions'; import { removeItem, set, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import createClearReducer from './Creators/Reducers/createClearReducer'; import createClearReducer from './Creators/Reducers/createClearReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
@@ -220,42 +219,6 @@ export const defaultState = {
const { ratings = {} } = item; const { ratings = {} } = item;
return ratings.tmdb? ratings.tmdb.value : 0; return ratings.tmdb? ratings.tmdb.value : 0;
},
inCinemas: function(item, direction) {
if (item.inCinemas) {
return moment(item.inCinemas).unix();
}
if (direction === sortDirections.DESCENDING) {
return -1 * Number.MAX_VALUE;
}
return Number.MAX_VALUE;
},
physicalRelease: function(item, direction) {
if (item.physicalRelease) {
return moment(item.physicalRelease).unix();
}
if (direction === sortDirections.DESCENDING) {
return -1 * Number.MAX_VALUE;
}
return Number.MAX_VALUE;
},
digitalRelease: function(item, direction) {
if (item.digitalRelease) {
return moment(item.digitalRelease).unix();
}
if (direction === sortDirections.DESCENDING) {
return -1 * Number.MAX_VALUE;
}
return Number.MAX_VALUE;
} }
}, },
@@ -544,11 +507,11 @@ export const actionHandlers = handleThunks({
}).request; }).request;
promise.done((data) => { promise.done((data) => {
// set an ID so the selectors and updaters done blow up. // set an Id so the selectors and updaters done blow up.
data = data.map((movie) => ({ ...movie, id: movie.tmdbId })); data = data.map((movie) => ({ ...movie, id: movie.tmdbId }));
dispatch(batchActions([ dispatch(batchActions([
update({ section, data }), ...data.map((movie) => updateItem({ section, ...movie })),
set({ set({
section, section,
+1 -1
View File
@@ -91,7 +91,7 @@ export const defaultState = {
columnLabel: () => translate('CustomFormatScore'), columnLabel: () => translate('CustomFormatScore'),
label: React.createElement(Icon, { label: React.createElement(Icon, {
name: icons.SCORE, name: icons.SCORE,
title: () => translate('CustomFormatScore') title: 'Custom format score'
}), }),
isVisible: false isVisible: false
}, },
@@ -28,8 +28,8 @@ export const defaultState = {
isReprocessing: false, isReprocessing: false,
error: null, error: null,
items: [], items: [],
sortKey: 'relativePath', sortKey: 'quality',
sortDirection: sortDirections.ASCENDING, sortDirection: sortDirections.DESCENDING,
recentFolders: [], recentFolders: [],
importMode: 'chooseImportMode', importMode: 'chooseImportMode',
sortPredicates: { sortPredicates: {
@@ -0,0 +1,33 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
function createMovieCollectionListSelector() {
return createSelector(
(state, { tmdbId }) => tmdbId,
(state) => state.settings.importLists.items,
(tmdbId, importLists) => {
const importListIds = _.reduce(importLists, (acc, list) => {
if (list.implementation === 'TMDbCollectionImport') {
const collectionIdField = list.fields.find((field) => {
return field.name === 'collectionId';
});
if (collectionIdField && parseInt(collectionIdField.value) === tmdbId) {
acc.push(list);
return acc;
}
}
return acc;
}, []);
if (importListIds.length === 0) {
return undefined;
}
return importListIds[0];
}
);
}
export default createMovieCollectionListSelector;
@@ -21,11 +21,15 @@ function createMovieCreditListSelector() {
return acc; return acc;
}, []); }, []);
if (importListIds.length === 0) { let importListId = 0;
return undefined;
if (importListIds.length > 0) {
importListId = importListIds[0].id;
} }
return importListIds[0]; return {
importListId
};
} }
); );
} }
+1 -1
View File
@@ -70,7 +70,7 @@ module.exports = {
// Toolbar // Toolbar
toolbarColor: '#e1e2e3', toolbarColor: '#e1e2e3',
toolbarBackgroundColor: '#262626', toolbarBackgroundColor: '#262626',
toolbarMenuItemBackgroundColor: '#303030', toolbarMenuItemBackgroundColor: '#606060',
toolbarMenuItemHoverBackgroundColor: '#515151', toolbarMenuItemHoverBackgroundColor: '#515151',
toolbarLabelColor: '#e1e2e3', toolbarLabelColor: '#e1e2e3',
+1 -4
View File
@@ -116,7 +116,6 @@ class BackupRow extends Component {
<TableRowCell className={styles.actions}> <TableRowCell className={styles.actions}>
<IconButton <IconButton
title={translate('RestoreBackup')}
name={icons.RESTORE} name={icons.RESTORE}
onPress={this.onRestorePress} onPress={this.onRestorePress}
/> />
@@ -139,9 +138,7 @@ class BackupRow extends Component {
isOpen={isConfirmDeleteModalOpen} isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteBackup')} title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', { message={translate('DeleteBackupMessageText', { name })}
name
})}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress} onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose} onCancel={this.onConfirmDeleteModalClose}
+1 -1
View File
@@ -109,7 +109,7 @@ class Backups extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<Alert kind={kinds.DANGER}> <Alert kind={kinds.DANGER}>
{translate('BackupsLoadError')} {translate('UnableToLoadBackups')}
</Alert> </Alert>
} }
@@ -146,9 +146,7 @@ class RestoreBackupModalContent extends Component {
<ModalBody> <ModalBody>
{ {
!!id && translate('WouldYouLikeToRestoreBackup', { !!id && translate('WouldYouLikeToRestoreBackup', { name })
name
})
} }
{ {
+4 -6
View File
@@ -1,8 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
@@ -67,7 +67,7 @@ class LogFiles extends Component {
/> />
<PageToolbarButton <PageToolbarButton
label={translate('Clear')} label={translate('Delete')}
iconName={icons.CLEAR} iconName={icons.CLEAR}
isSpinning={deleteFilesExecuting} isSpinning={deleteFilesExecuting}
onPress={onDeleteFilesPress} onPress={onDeleteFilesPress}
@@ -77,15 +77,13 @@ class LogFiles extends Component {
<PageContentBody> <PageContentBody>
<Alert> <Alert>
<div> <div>
{translate('LogFilesLocation', { Log files are located in: {location}
location
})}
</div> </div>
{ {
currentLogView === 'Log Files' && currentLogView === 'Log Files' &&
<div> <div>
<InlineMarkdown data={translate('TheLogLevelDefault')} /> {translate('TheLogLevelDefault')} <Link to="/settings/general">{translate('GeneralSettings')}</Link>
</div> </div>
} }
</Alert> </Alert>
+2 -3
View File
@@ -4,7 +4,6 @@ import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton'; import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent'; import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem'; import MenuItem from 'Components/Menu/MenuItem';
import translate from 'Utilities/String/translate';
class LogsNavMenu extends Component { class LogsNavMenu extends Component {
@@ -51,13 +50,13 @@ class LogsNavMenu extends Component {
<MenuItem <MenuItem
to={'/system/logs/files'} to={'/system/logs/files'}
> >
{translate('LogFiles')} Log Files
</MenuItem> </MenuItem>
<MenuItem <MenuItem
to={'/system/logs/files/update'} to={'/system/logs/files/update'}
> >
{translate('UpdaterLogFiles')} Updater Log Files
</MenuItem> </MenuItem>
</MenuContent> </MenuContent>
</Menu> </Menu>
+1 -8
View File
@@ -45,14 +45,7 @@ class About extends Component {
packageVersion && packageVersion &&
<DescriptionListItem <DescriptionListItem
title={translate('PackageVersion')} title={translate('PackageVersion')}
data={(packageAuthor ? data={(packageAuthor ? <span> {packageVersion} {' by '} <InlineMarkdown data={packageAuthor} /> </span> : packageVersion)}
<InlineMarkdown data={translate('PackageVersionInfo', {
packageVersion,
packageAuthor
})}
/> :
packageVersion
)}
/> />
} }
+1 -1
View File
@@ -153,7 +153,7 @@ class Health extends Component {
{ {
!healthIssues && !healthIssues &&
<div className={styles.healthOk}> <div className={styles.healthOk}>
{translate('NoIssuesWithYourConfiguration')} {translate('HealthNoIssues')}
</div> </div>
} }
@@ -26,7 +26,7 @@ class MoreInfo extends Component {
{translate('Wiki')} {translate('Wiki')}
</DescriptionListItemTitle> </DescriptionListItemTitle>
<DescriptionListItemDescription> <DescriptionListItemDescription>
<Link to="https://wiki.servarr.com/radarr">wiki.servarr.com/radarr</Link> <Link to="https://wiki.servarr.com/radarr">{translate('Wiki')}</Link>
</DescriptionListItemDescription> </DescriptionListItemDescription>
<DescriptionListItemTitle> <DescriptionListItemTitle>
+4 -4
View File
@@ -44,7 +44,7 @@ class Updates extends Component {
const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true });
const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; const noUpdateToInstall = hasUpdates && !hasUpdateToInstall;
const externalUpdaterPrefix = translate('UpdateRadarrDirectlyLoadError'); const externalUpdaterPrefix = translate('UnableToUpdateRadarrDirectly');
const externalUpdaterMessages = { const externalUpdaterMessages = {
external: translate('ExternalUpdater'), external: translate('ExternalUpdater'),
apt: translate('AptUpdater'), apt: translate('AptUpdater'),
@@ -176,7 +176,7 @@ class Updates extends Component {
kind={kinds.INVERSE} kind={kinds.INVERSE}
title={formatDateTime(update.installedOn, longDateFormat, timeFormat)} title={formatDateTime(update.installedOn, longDateFormat, timeFormat)}
> >
{translate('PreviouslyInstalled')} Previously Installed
</Label> : </Label> :
null null
} }
@@ -213,14 +213,14 @@ class Updates extends Component {
{ {
!!updatesError && !!updatesError &&
<div> <div>
{translate('FailedToFetchUpdates')} Failed to fetch updates
</div> </div>
} }
{ {
!!generalSettingsError && !!generalSettingsError &&
<div> <div>
{translate('FailedToUpdateSettings')} Failed to update settings
</div> </div>
} }
</PageContentBody> </PageContentBody>
+11 -9
View File
@@ -25,18 +25,20 @@ export async function fetchTranslations(): Promise<boolean> {
export default function translate( export default function translate(
key: string, key: string,
tokens: Record<string, string | number | boolean> = {} tokens?: Record<string, string | number | boolean>
) { ) {
const translation = translations[key] || key; const translation = translations[key] || key;
tokens.appName = 'Radarr'; if (tokens) {
// Fallback to the old behaviour for translations not yet updated to use named tokens
Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
});
// Fallback to the old behaviour for translations not yet updated to use named tokens return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
Object.values(tokens).forEach((value, index) => { String(tokens[tokenMatch] ?? match)
tokens[index] = value; );
}); }
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) => return translation;
String(tokens[tokenMatch] ?? match)
);
} }
-1
View File
@@ -12,7 +12,6 @@ export interface Field {
interface ImportList extends ModelBase { interface ImportList extends ModelBase {
enable: boolean; enable: boolean;
enabled: boolean;
enableAuto: boolean; enableAuto: boolean;
qualityProfileId: number; qualityProfileId: number;
rootFolderPath: string; rootFolderPath: string;
-43
View File
@@ -1,43 +0,0 @@
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;
movieHasFile: boolean;
movieId?: number;
}
export default Queue;
@@ -86,13 +86,5 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_remoteMovie.Release.Title = title; _remoteMovie.Release.Title = title;
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
} }
[TestCase("Series Title EP50 USLT NTSC DVDRemux DD2.0")]
[TestCase("Series.Title.S01.NTSC.DVDRip.DD2.0.x264-PLAiD")]
public void should_return_true_if_dvdrip(string title)
{
_remoteMovie.Release.Title = title;
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
}
} }
} }
@@ -7,8 +7,6 @@ using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.ImportLists; using NzbDrone.Core.ImportLists;
using NzbDrone.Core.ImportLists.ImportListMovies; using NzbDrone.Core.ImportLists.ImportListMovies;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.ImportListTests namespace NzbDrone.Core.Test.ImportListTests
@@ -36,10 +34,6 @@ namespace NzbDrone.Core.Test.ImportListTests
_listMovies = Builder<ImportListMovie>.CreateListOfSize(5) _listMovies = Builder<ImportListMovie>.CreateListOfSize(5)
.Build().ToList(); .Build().ToList();
Mocker.GetMock<ISearchForNewMovie>()
.Setup(v => v.MapMovieToTmdbMovie(It.IsAny<MovieMetadata>()))
.Returns<MovieMetadata>(m => new MovieMetadata { TmdbId = m.TmdbId });
} }
private void GivenList(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult) private void GivenList(int id, bool enabled, bool enabledAuto, ImportListFetchResult fetchResult)
@@ -135,9 +129,6 @@ namespace NzbDrone.Core.Test.ImportListTests
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeFalse(); listResult.AnyFailure.Should().BeFalse();
Mocker.GetMock<IImportListMovieService>()
.Verify(v => v.SyncMoviesForList(It.IsAny<List<ImportListMovie>>(), listId), Times.Once());
} }
[Test] [Test]
@@ -149,9 +140,6 @@ namespace NzbDrone.Core.Test.ImportListTests
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeTrue(); listResult.AnyFailure.Should().BeTrue();
Mocker.GetMock<IImportListMovieService>()
.Verify(v => v.SyncMoviesForList(It.IsAny<List<ImportListMovie>>(), listId), Times.Never());
} }
[Test] [Test]
@@ -166,9 +154,6 @@ namespace NzbDrone.Core.Test.ImportListTests
var listResult = Subject.Fetch(); var listResult = Subject.Fetch();
listResult.AnyFailure.Should().BeTrue(); listResult.AnyFailure.Should().BeTrue();
Mocker.GetMock<IImportListMovieService>()
.Verify(v => v.SyncMoviesForList(It.IsAny<List<ImportListMovie>>(), passedListId), Times.Once());
} }
[Test] [Test]
@@ -7,6 +7,7 @@ using NzbDrone.Core.Configuration;
using NzbDrone.Core.ImportLists; using NzbDrone.Core.ImportLists;
using NzbDrone.Core.ImportLists.ImportExclusions; using NzbDrone.Core.ImportLists.ImportExclusions;
using NzbDrone.Core.ImportLists.ImportListMovies; using NzbDrone.Core.ImportLists.ImportListMovies;
using NzbDrone.Core.MetadataSource;
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@@ -59,8 +60,7 @@ namespace NzbDrone.Core.Test.ImportList
_importListFetch = new ImportListFetchResult _importListFetch = new ImportListFetchResult
{ {
Movies = _list1Movies, Movies = _list1Movies,
AnyFailure = false, AnyFailure = false
SyncedLists = 1
}; };
_commandAll = new ImportListSyncCommand _commandAll = new ImportListSyncCommand
@@ -84,6 +84,10 @@ namespace NzbDrone.Core.Test.ImportList
.Setup(v => v.MovieExists(It.IsAny<Movie>())) .Setup(v => v.MovieExists(It.IsAny<Movie>()))
.Returns(false); .Returns(false);
Mocker.GetMock<IMovieService>()
.Setup(v => v.MovieExists(It.IsAny<Movie>()))
.Returns(false);
Mocker.GetMock<IMovieService>() Mocker.GetMock<IMovieService>()
.Setup(v => v.AllMovieTmdbIds()) .Setup(v => v.AllMovieTmdbIds())
.Returns(new List<int>()); .Returns(new List<int>());
@@ -91,6 +95,10 @@ namespace NzbDrone.Core.Test.ImportList
Mocker.GetMock<IFetchAndParseImportList>() Mocker.GetMock<IFetchAndParseImportList>()
.Setup(v => v.Fetch()) .Setup(v => v.Fetch())
.Returns(_importListFetch); .Returns(_importListFetch);
Mocker.GetMock<ISearchForNewMovie>()
.Setup(v => v.MapMovieToTmdbMovie(It.IsAny<MovieMetadata>()))
.Returns<MovieMetadata>(m => new MovieMetadata { TmdbId = m.TmdbId });
} }
private void GivenListFailure() private void GivenListFailure()
@@ -100,7 +108,8 @@ namespace NzbDrone.Core.Test.ImportList
private void GivenNoListSync() private void GivenNoListSync()
{ {
_importListFetch.SyncedLists = 0; _importListFetch.SyncedLists = new List<int>();
_importListFetch.SyncedWithoutFailure = new List<int>();
} }
private void GivenCleanLevel(string cleanLevel) private void GivenCleanLevel(string cleanLevel)
@@ -114,6 +123,9 @@ namespace NzbDrone.Core.Test.ImportList
{ {
var importListDefinition = new ImportListDefinition { Id = id, EnableAuto = enabledAuto }; var importListDefinition = new ImportListDefinition { Id = id, EnableAuto = enabledAuto };
_importListFetch.SyncedLists.Add(id);
_importListFetch.SyncedWithoutFailure.Add(id);
Mocker.GetMock<IImportListFactory>() Mocker.GetMock<IImportListFactory>()
.Setup(v => v.Get(id)) .Setup(v => v.Get(id))
.Returns(importListDefinition); .Returns(importListDefinition);
@@ -68,16 +68,5 @@ namespace NzbDrone.Core.Test.IndexerTests
VerifyNoUpdate(); VerifyNoUpdate();
} }
[Test]
public void should_not_record_failure_for_unknown_provider()
{
Subject.RecordFailure(0);
Mocker.GetMock<IIndexerStatusRepository>()
.Verify(v => v.FindByProviderId(1), Times.Never);
VerifyNoUpdate();
}
} }
} }
@@ -24,28 +24,35 @@ namespace NzbDrone.Core.Test.Localization
[Test] [Test]
public void should_get_string_in_dictionary_if_lang_exists_and_string_exists() public void should_get_string_in_dictionary_if_lang_exists_and_string_exists()
{ {
var localizedString = Subject.GetLocalizedString("UILanguage"); var localizedString = Subject.GetLocalizedString("UiLanguage");
localizedString.Should().Be("UI Language"); localizedString.Should().Be("UI Language");
} }
[Test] [Test]
public void should_get_string_in_french() public void should_get_string_in_default_language_dictionary_if_no_lang_country_code_exists_and_string_exists()
{ {
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.French); var localizedString = Subject.GetLocalizedString("UiLanguage", "fr_fr");
var localizedString = Subject.GetLocalizedString("UILanguage");
localizedString.Should().Be("Langue de l'IU"); localizedString.Should().Be("Langue de l'IU");
ExceptionVerification.ExpectedErrors(0); ExceptionVerification.ExpectedErrors(1);
} }
[Test] [Test]
public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists() public void should_get_string_in_default_dictionary_if_no_lang_exists_and_string_exists()
{ {
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns(0); var localizedString = Subject.GetLocalizedString("UiLanguage", "an");
var localizedString = Subject.GetLocalizedString("UILanguage");
localizedString.Should().Be("UI Language");
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_get_string_in_default_dictionary_if_lang_empty_and_string_exists()
{
var localizedString = Subject.GetLocalizedString("UiLanguage", "");
localizedString.Should().Be("UI Language"); localizedString.Should().Be("UI Language");
} }
@@ -53,7 +60,7 @@ namespace NzbDrone.Core.Test.Localization
[Test] [Test]
public void should_return_argument_if_string_doesnt_exists() public void should_return_argument_if_string_doesnt_exists()
{ {
var localizedString = Subject.GetLocalizedString("badString"); var localizedString = Subject.GetLocalizedString("badString", "en");
localizedString.Should().Be("badString"); localizedString.Should().Be("badString");
} }
@@ -37,12 +37,6 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
[TestCase("The Mist", "M", "The Mist")] [TestCase("The Mist", "M", "The Mist")]
[TestCase("A", "A", "A")] [TestCase("A", "A", "A")]
[TestCase("30 Rock", "3", "30 Rock")] [TestCase("30 Rock", "3", "30 Rock")]
[TestCase("The '80s Greatest", "8", "The '80s Greatest")]
[TestCase("좀비버스", "좀", "좀비버스")]
[TestCase("¡Mucha Lucha!", "M", "¡Mucha Lucha!")]
[TestCase(".hack", "H", "hack")]
[TestCase("Ütopya", "U", "Ütopya")]
[TestCase("Æon Flux", "A", "Æon Flux")]
public void should_get_expected_folder_name_back(string title, string parent, string child) public void should_get_expected_folder_name_back(string title, string parent, string child)
{ {
_movie.Title = title; _movie.Title = title;
@@ -219,7 +219,6 @@ namespace NzbDrone.Core.Test.ParserTests
} }
[TestCase("Movie.Title.1994.Vietnamese.1080p.XviD-LOL")] [TestCase("Movie.Title.1994.Vietnamese.1080p.XviD-LOL")]
[TestCase("Movie.Title.1994.VIE.1080p.XviD-LOL")]
public void should_parse_language_vietnamese(string postTitle) public void should_parse_language_vietnamese(string postTitle)
{ {
var result = Parser.Parser.ParseMovieTitle(postTitle, true); var result = Parser.Parser.ParseMovieTitle(postTitle, true);

Some files were not shown because too many files have changed in this diff Show More