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
356 changed files with 5464 additions and 6875 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)
+2 -2
View File
@@ -9,13 +9,13 @@ 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.3.0' 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)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.417' dotnetVersion: '6.0.413'
nodeVersion: '16.X' nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
+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
} }
@@ -85,13 +85,8 @@
margin-top: 20px; margin-top: 20px;
} }
.studio,
.genres {
margin-left: 5px;
}
.links { .links {
margin-left: 5px; margin-left: 8px;
pointer-events: all; pointer-events: all;
} }
@@ -5,7 +5,6 @@ interface CssExports {
'certification': string; 'certification': string;
'content': string; 'content': string;
'exclusionIcon': string; 'exclusionIcon': string;
'genres': string;
'icons': string; 'icons': string;
'links': string; 'links': string;
'overlay': string; 'overlay': string;
@@ -15,7 +14,6 @@ interface CssExports {
'runtime': string; 'runtime': string;
'searchResult': string; 'searchResult': string;
'statusContainer': string; 'statusContainer': string;
'studio': string;
'title': string; 'title': string;
'titleContainer': string; 'titleContainer': string;
'titleRow': string; 'titleRow': string;
@@ -1,7 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import Label from 'Components/Label'; import Label from 'Components/Label';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
@@ -62,13 +61,11 @@ class AddNewMovieSearchResult extends Component {
titleSlug, titleSlug,
year, year,
studio, studio,
genres,
status, status,
overview, overview,
ratings, ratings,
folder, folder,
images, images,
existingMovieId,
isExistingMovie, isExistingMovie,
isExclusionMovie, isExclusionMovie,
isSmallScreen, isSmallScreen,
@@ -77,8 +74,8 @@ class AddNewMovieSearchResult extends Component {
monitored, monitored,
hasFile, hasFile,
isAvailable, isAvailable,
movieFile, queueStatus,
queueItem, queueState,
runtime, runtime,
movieRuntimeFormat, movieRuntimeFormat,
certification certification
@@ -123,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}
/> />
} }
@@ -200,46 +197,13 @@ class AddNewMovieSearchResult extends Component {
/> />
</Label> </Label>
{
ratings.imdb ?
<Label size={sizes.LARGE}>
<ImdbRating
ratings={ratings}
iconSize={13}
/>
</Label> :
null
}
{ {
!!studio && !!studio &&
<Label size={sizes.LARGE}> <Label size={sizes.LARGE}>
<Icon {studio}
name={icons.STUDIO}
size={13}
/>
<span className={styles.studio}>
{studio}
</span>
</Label> </Label>
} }
{
genres.length > 0 ?
<Label size={sizes.LARGE}>
<Icon
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.slice(0, 3).join(', ')}
</span>
</Label> :
null
}
<Tooltip <Tooltip
anchor={ anchor={
<Label <Label
@@ -251,15 +215,15 @@ class AddNewMovieSearchResult extends Component {
/> />
<span className={styles.links}> <span className={styles.links}>
{translate('Links')} Links
</span> </span>
</Label> </Label>
} }
tooltip={ tooltip={
<MovieDetailsLinks <MovieDetailsLinks
tmdbId={tmdbId} tmdbId={tmdbId}
imdbId={imdbId}
youTubeTrailerId={youTubeTrailerId} youTubeTrailerId={youTubeTrailerId}
imdbId={imdbId}
/> />
} }
canFlip={true} canFlip={true}
@@ -273,7 +237,6 @@ class AddNewMovieSearchResult extends Component {
hasMovieFiles={hasFile} hasMovieFiles={hasFile}
monitored={monitored} monitored={monitored}
isAvailable={isAvailable} isAvailable={isAvailable}
queueItem={queueItem}
id={id} id={id}
useLabel={true} useLabel={true}
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
@@ -310,30 +273,25 @@ AddNewMovieSearchResult.propTypes = {
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
year: PropTypes.number.isRequired, year: PropTypes.number.isRequired,
studio: PropTypes.string, studio: PropTypes.string,
genres: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
overview: PropTypes.string, overview: PropTypes.string,
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,
id: PropTypes.number, id: PropTypes.number,
queueItems: PropTypes.arrayOf(PropTypes.object),
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,
queueItem: 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
}; };
AddNewMovieSearchResult.defaultProps = {
genres: []
};
export default AddNewMovieSearchResult; export default AddNewMovieSearchResult;
@@ -12,17 +12,15 @@ function createMapStateToProps() {
createDimensionsSelector(), createDimensionsSelector(),
(state) => state.queue.details.items, (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, queueItems, internalId, movieRuntimeFormat) => { const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0);
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
return { return {
existingMovieId: internalId,
isExistingMovie, isExistingMovie,
isExclusionMovie, isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
queueItem, queueStatus: firstQueueItem ? firstQueueItem.status : null,
movieRuntimeFormat queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
}; };
} }
); );
@@ -32,7 +32,7 @@
.contentContainer { .contentContainer {
z-index: $popperZIndex; z-index: $popperZIndex;
margin-top: 4px; margin-top: 4px;
/* 400px container width with 8px padding on each side */ /* 400px container witdh with 8px padding on each side */
width: 384px; width: 384px;
} }
@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
className={styles.addErrorAlert} className={styles.addErrorAlert}
kind={kinds.DANGER} kind={kinds.DANGER}
> >
{translate('AddRootFolderError')} {translate('UnableToAddRootFolder')}
<ul> <ul>
{ {
@@ -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 -1
View File
@@ -55,7 +55,7 @@ class CalendarConnector extends Component {
gotoCalendarToday gotoCalendarToday
} = this.props; } = this.props;
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']); registerPagePopulator(this.repopulate);
if (useCurrentPage) { if (useCurrentPage) {
fetchCalendar(); fetchCalendar();
+1 -12
View File
@@ -6,7 +6,6 @@ import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition'; import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions'; import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import scrollPositions from 'Store/scrollPositions'; import scrollPositions from 'Store/scrollPositions';
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector'; import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
@@ -39,12 +38,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchRootFolders() { dispatchFetchRootFolders() {
dispatch(fetchRootFolders()); dispatch(fetchRootFolders());
}, },
dispatchFetchQueueDetails() {
dispatch(fetchQueueDetails());
},
dispatchClearQueueDetails() {
dispatch(clearQueueDetails());
},
onUpdateSelectedPress(payload) { onUpdateSelectedPress(payload) {
dispatch(saveMovieCollections(payload)); dispatch(saveMovieCollections(payload));
}, },
@@ -70,12 +63,10 @@ class CollectionConnector extends Component {
componentDidMount() { componentDidMount() {
registerPagePopulator(this.repopulate); registerPagePopulator(this.repopulate);
this.props.dispatchFetchRootFolders(); this.props.dispatchFetchRootFolders();
this.props.dispatchFetchQueueDetails();
} }
componentWillUnmount() { componentWillUnmount() {
unregisterPagePopulator(this.repopulate); unregisterPagePopulator(this.repopulate);
this.props.dispatchClearQueueDetails();
} }
// //
@@ -108,9 +99,7 @@ CollectionConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
onUpdateSelectedPress: PropTypes.func.isRequired, onUpdateSelectedPress: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired, dispatchFetchRootFolders: PropTypes.func.isRequired
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchClearQueueDetails: PropTypes.func.isRequired
}; };
export default withScrollPosition( export default withScrollPosition(
@@ -70,7 +70,6 @@ class CollectionMovie extends Component {
hasFile, hasFile,
folder, folder,
isAvailable, isAvailable,
movieFile,
isExistingMovie, isExistingMovie,
posterWidth, posterWidth,
posterHeight, posterHeight,
@@ -132,8 +131,6 @@ class CollectionMovie extends Component {
id ? id ?
<div className={styles.overlayStatus}> <div className={styles.overlayStatus}>
<MovieIndexProgressBar <MovieIndexProgressBar
movieId={id}
movieFile={movieFile}
monitored={monitored} monitored={monitored}
hasFile={hasFile} hasFile={hasFile}
status={status} status={status}
@@ -183,7 +180,6 @@ CollectionMovie.propTypes = {
hasFile: PropTypes.bool, hasFile: PropTypes.bool,
folder: PropTypes.string, folder: PropTypes.string,
isAvailable: PropTypes.bool, isAvailable: PropTypes.bool,
movieFile: PropTypes.object,
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,
@@ -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;
+1 -3
View File
@@ -167,7 +167,7 @@ class SignalRConnector extends Component {
const resource = body.resource; const resource = body.resource;
const status = resource.status; const status = resource.status;
// Both successful and failed commands need to be // Both sucessful and failed commands need to be
// completed, otherwise they spin until they timeout. // completed, otherwise they spin until they timeout.
if (status === 'completed' || status === 'failed') { if (status === 'completed' || status === 'failed') {
@@ -187,8 +187,6 @@ class SignalRConnector extends Component {
repopulatePage('movieFileUpdated'); repopulatePage('movieFileUpdated');
} else if (body.action === 'deleted') { } else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id }); this.props.dispatchRemoveItem({ section, id: body.resource.id });
repopulatePage('movieFileDeleted');
} }
}; };
@@ -15,5 +15,5 @@
"start_url": "../../../../", "start_url": "../../../../",
"theme_color": "#3a3f51", "theme_color": "#3a3f51",
"background_color": "#3a3f51", "background_color": "#3a3f51",
"display": "minimal-ui" "display": "standalone"
} }
-120
View File
@@ -1,120 +0,0 @@
import createAjaxRequest from 'Utilities/createAjaxRequest';
// This file contains some helpers for power users in a browser console
let hasWarned = false;
function checkActivationWarning() {
if (!hasWarned) {
console.log('Activated RadarrApi console helpers.');
console.warn('Be warned: There will be no further confirmation checks.');
hasWarned = true;
}
}
function attachAsyncActions(promise) {
promise.filter = function() {
const args = arguments;
const res = this.then((d) => d.filter(...args));
attachAsyncActions(res);
return res;
};
promise.map = function() {
const args = arguments;
const res = this.then((d) => d.map(...args));
attachAsyncActions(res);
return res;
};
promise.all = function() {
const res = this.then((d) => Promise.all(d));
attachAsyncActions(res);
return res;
};
promise.forEach = function(action) {
const res = this.then((d) => Promise.all(d.map(action)));
attachAsyncActions(res);
return res;
};
}
class ResourceApi {
constructor(api, url) {
this.api = api;
this.url = url;
}
single(id) {
return this.api.fetch(`${this.url}/${id}`);
}
all() {
return this.api.fetch(this.url);
}
filter(pred) {
return this.all().filter(pred);
}
update(resource) {
return this.api.fetch(`${this.url}/${resource.id}`, { method: 'PUT', data: resource });
}
delete(resource) {
if (typeof resource === 'object' && resource !== null && resource.id) {
resource = resource.id;
}
if (!resource || !Number.isInteger(resource)) {
throw Error('Invalid resource', resource);
}
return this.api.fetch(`${this.url}/${resource}`, { method: 'DELETE' });
}
fetch(url, options) {
return this.api.fetch(`${this.url}${url}`, options);
}
}
class ConsoleApi {
constructor() {
this.movie = new ResourceApi(this, '/movie');
}
resource(url) {
return new ResourceApi(this, url);
}
fetch(url, options) {
checkActivationWarning();
options = options || {};
const req = {
url,
method: options.method || 'GET'
};
if (options.data) {
req.dataType = 'json';
req.data = JSON.stringify(options.data);
}
const promise = createAjaxRequest(req).request;
promise.fail((xhr) => {
console.error(`Failed to fetch ${url}`, xhr);
});
attachAsyncActions(promise);
return promise;
}
}
window.RadarrApi = new ConsoleApi();
export default ConsoleApi;
@@ -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;
} }
@@ -68,19 +68,6 @@ $hoverScale: 1.05;
color: var(--white); color: var(--white);
} }
.existing {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 0;
height: 0;
border-width: 25px 25px 0 0;
border-style: solid;
border-color: #37bc9b transparent transparent;
color: var(--white);
}
.controls { .controls {
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;
@@ -7,7 +7,6 @@ interface CssExports {
'controls': string; 'controls': string;
'editorSelect': string; 'editorSelect': string;
'excluded': string; 'excluded': string;
'existing': string;
'externalLinks': string; 'externalLinks': string;
'link': string; 'link': string;
'overlayTitle': string; 'overlayTitle': string;
@@ -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
@@ -159,14 +158,6 @@ class DiscoverMoviePoster extends Component {
/> />
} }
{
isExisting &&
<div
className={styles.existing}
title={translate('Existing')}
/>
}
<Link <Link
className={styles.link} className={styles.link}
style={elementStyle} style={elementStyle}
@@ -194,7 +185,7 @@ class DiscoverMoviePoster extends Component {
{ {
showTitle && showTitle &&
<div className={styles.title} title={title}> <div className={styles.title}>
{title} {title}
</div> </div>
} }
@@ -203,7 +194,6 @@ class DiscoverMoviePoster extends Component {
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}
movieRuntimeFormat={movieRuntimeFormat}
{...otherProps} {...otherProps}
/> />
@@ -246,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,29 +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;
}
.quality {
white-space: nowrap;
} }
.languages { .languages {
@@ -31,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;
@@ -39,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;
} }
@@ -70,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}>
@@ -266,7 +313,11 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.quality}> <TableRowCell className={styles.quality}>
<MovieQuality quality={quality} showRevision={true} /> <MovieQuality quality={quality} />
</TableRowCell>
<TableRowCell className={styles.customFormat}>
<MovieFormats formats={customFormats} />
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
@@ -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;
@@ -5,29 +5,6 @@ import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters'; import MovieCreditPosters from '../MovieCreditPosters';
import MovieCrewPoster from './MovieCrewPoster'; import MovieCrewPoster from './MovieCrewPoster';
function crewSort(a, b) {
const jobOrder = ['Director', 'Writer', 'Producer', 'Executive Producer', 'Director of Photography'];
const indexA = jobOrder.indexOf(a.job);
const indexB = jobOrder.indexOf(b.job);
if (indexA === -1 && indexB === -1) {
return 0;
} else if (indexA === -1) {
return 1;
} else if (indexB === -1) {
return -1;
}
if (indexA < indexB) {
return -1;
} else if (indexA > indexB) {
return 1;
}
return 0;
}
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.movieCredits.items, (state) => state.movieCredits.items,
@@ -40,10 +17,8 @@ function createMapStateToProps() {
return acc; return acc;
}, []); }, []);
const sortedCrew = crew.sort(crewSort);
return { return {
items: _.uniqBy(sortedCrew, '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,16 +2,6 @@
flex: 1 0 auto; flex: 1 0 auto;
} }
.movie {
padding: 10px;
}
.container { .container {
padding: 10px; padding: 10px;
} }
.sliderContainer {
--swiper-navigation-color: var(--white);
display: block;
}
@@ -3,8 +3,6 @@
interface CssExports { interface CssExports {
'container': string; 'container': string;
'grid': string; 'grid': string;
'movie': string;
'sliderContainer': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -1,19 +1,34 @@
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);
const additionalColumnCount = {
small: 3,
medium: 2,
large: 1
};
function calculateColumnWidth(width, posterSize, isSmallScreen) {
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
const columns = Math.floor(width / maxiumColumnWidth);
const remainder = width % maxiumColumnWidth;
if (remainder === 0 && posterSize === 'large') {
return maxiumColumnWidth;
}
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
}
function calculateRowHeight(posterHeight, isSmallScreen) { function calculateRowHeight(posterHeight, isSmallScreen) {
const titleHeight = 19; const titleHeight = 19;
const characterHeight = 19; const characterHeight = 19;
@@ -28,6 +43,10 @@ function calculateRowHeight(posterHeight, isSmallScreen) {
return heights.reduce((acc, height) => acc + height, 0); return heights.reduce((acc, height) => acc + height, 0);
} }
function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth);
}
class MovieCreditPosters extends Component { class MovieCreditPosters extends Component {
// //
@@ -44,12 +63,61 @@ class MovieCreditPosters extends Component {
posterHeight: 238, posterHeight: 238,
rowHeight: calculateRowHeight(238, props.isSmallScreen) rowHeight: calculateRowHeight(238, props.isSmallScreen)
}; };
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();
}
} }
// //
// Render // Control
render() { setGridRef = (ref) => {
this._grid = ref;
};
calculateGrid = (width = this.state.width, isSmallScreen) => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
const columnWidth = calculateColumnWidth(width, 'small', isSmallScreen);
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
const posterWidth = columnWidth - padding;
const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, isSmallScreen);
this.setState({
width,
columnWidth,
columnCount,
posterWidth,
posterHeight,
rowHeight
});
};
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
const { const {
items, items,
itemComponent itemComponent
@@ -58,43 +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} >
navigation={true} <WindowScroller
loop={false} scrollElement={undefined}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
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 {
+144 -81
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
@@ -320,17 +334,11 @@ class MovieDetails extends Component {
/> />
<PageToolbarButton <PageToolbarButton
label={translate('ManageFiles')} label={translate('ManualImport')}
iconName={icons.MOVIE_FILE} iconName={icons.INTERACTIVE}
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}
@@ -704,19 +774,12 @@ class MovieDetails extends Component {
<InteractiveImportModal <InteractiveImportModal
isOpen={isInteractiveImportModalOpen} isOpen={isInteractiveImportModalOpen}
movieId={id} movieId={id}
modalTitle={translate('ManageFiles')}
folder={path} folder={path}
allowMovieChange={false} allowMovieChange={false}
showFilterExistingFiles={true} showFilterExistingFiles={true}
showImportMode={false} showImportMode={false}
onModalClose={this.onInteractiveImportModalClose} onModalClose={this.onInteractiveImportModalClose}
/> />
<MovieInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
movieId={id}
onModalClose={this.onInteractiveSearchModalClose}
/>
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>
); );
@@ -767,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';
@@ -33,11 +34,14 @@ const selectMovieFiles = createSelector(
const hasMovieFiles = !!items.length; const hasMovieFiles = !!items.length;
const sizeOnDisk = items.map((item) => item.size).reduce((prev, curr) => prev + curr, 0);
return { return {
isMovieFilesFetching: isFetching, isMovieFilesFetching: isFetching,
isMovieFilesPopulated: isPopulated, isMovieFilesPopulated: isPopulated,
movieFilesError: error, movieFilesError: error,
hasMovieFiles hasMovieFiles,
sizeOnDisk
}; };
} }
); );
@@ -101,7 +105,8 @@ function createMapStateToProps() {
isMovieFilesFetching, isMovieFilesFetching,
isMovieFilesPopulated, isMovieFilesPopulated,
movieFilesError, movieFilesError,
hasMovieFiles hasMovieFiles,
sizeOnDisk
} = movieFiles; } = movieFiles;
const { const {
@@ -140,8 +145,6 @@ function createMapStateToProps() {
return acc; return acc;
}, []); }, []);
const queueItem = queueItems.find((item) => item.movieId === movie.id);
return { return {
...movie, ...movie,
alternateTitles, alternateTitles,
@@ -157,11 +160,12 @@ function createMapStateToProps() {
movieCreditsError, movieCreditsError,
extraFilesError, extraFilesError,
hasMovieFiles, hasMovieFiles,
sizeOnDisk,
previousMovie, previousMovie,
nextMovie, nextMovie,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
isSidebarVisible, isSidebarVisible,
queueItem, queueItems,
movieRuntimeFormat movieRuntimeFormat
}; };
} }
@@ -176,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 }));
}, },
@@ -271,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 });
@@ -281,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();
@@ -337,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>
); );
} }
@@ -6,43 +6,31 @@ import MovieTitlesTableContent from './MovieTitlesTableContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { movieId }) => movieId,
(state) => state.movies, (state) => state.movies,
(movieId, movies) => { (movies) => {
const { return movies;
isFetching,
isPopulated,
error,
items
} = movies;
const alternateTitles = items.find((m) => m.id === movieId)?.alternateTitles;
return {
isFetching,
isPopulated,
error,
alternateTitles
};
} }
); );
} }
const mapDispatchToProps = {
// fetchMovies
};
class MovieTitlesTableContentConnector extends Component { class MovieTitlesTableContentConnector extends Component {
// //
// Render // Render
render() { render() {
const { const movie = this.props.items.filter((obj) => {
alternateTitles, return obj.id === this.props.movieId;
...otherProps });
} = this.props;
return ( return (
<MovieTitlesTableContent <MovieTitlesTableContent
{...otherProps} {...this.props}
items={alternateTitles} items={movie[0].alternateTitles}
/> />
); );
} }
@@ -50,11 +38,7 @@ class MovieTitlesTableContentConnector extends Component {
MovieTitlesTableContentConnector.propTypes = { MovieTitlesTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired, movieId: PropTypes.number.isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired items: PropTypes.arrayOf(PropTypes.object).isRequired
}; };
MovieTitlesTableContentConnector.defaultProps = { export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
alternateTitles: []
};
export default connect(createMapStateToProps)(MovieTitlesTableContentConnector);
@@ -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,141 +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',
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;
+13 -4
View File
@@ -7,7 +7,8 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
@@ -102,11 +103,20 @@ class MovieHistoryRow extends Component {
</TableRowCell> </TableRowCell>
<TableRowCell> <TableRowCell>
<MovieFormats formats={customFormats} /> <MovieFormats
formats={customFormats}
/>
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.customFormatScore}> <TableRowCell className={styles.customFormatScore}>
{formatCustomFormatScore(customFormatScore, customFormats.length)} <Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.TOP}
/>
</TableRowCell> </TableRowCell>
<RelativeDateCellConnector <RelativeDateCellConnector
@@ -124,7 +134,6 @@ class MovieHistoryRow extends Component {
<IconButton <IconButton
title={translate('MarkAsFailed')} title={translate('MarkAsFailed')}
name={icons.REMOVE} name={icons.REMOVE}
size={14}
onPress={this.onMarkAsFailedPress} onPress={this.onMarkAsFailedPress}
/> />
} }
@@ -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);
@@ -1,12 +1,11 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext'; import { useSelect } from 'App/SelectContext';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState'; import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState'; import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
import { MOVIE_SEARCH } from 'Commands/commandNames'; import { MOVIE_SEARCH } from 'Commands/commandNames';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import { icons, kinds } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector'; import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
@@ -22,12 +21,11 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH)); const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
const { const {
items, items,
totalItems,
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState = }: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex')); useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
const { isSelectMode, selectedFilterKey } = props; const { isSelectMode, selectedFilterKey } = props;
const [selectState] = useSelect(); const [selectState] = useSelect();
const { selectedState } = selectState; const { selectedState } = selectState;
@@ -52,8 +50,6 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
: translate('SearchAll'); : translate('SearchAll');
const onPress = useCallback(() => { const onPress = useCallback(() => {
setIsConfirmModalOpen(false);
dispatch( dispatch(
executeCommand({ executeCommand({
name: MOVIE_SEARCH, name: MOVIE_SEARCH,
@@ -62,36 +58,14 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
); );
}, [dispatch, moviesToSearch]); }, [dispatch, moviesToSearch]);
const onConfirmPress = useCallback(() => {
setIsConfirmModalOpen(true);
}, [setIsConfirmModalOpen]);
const onConfirmModalClose = useCallback(() => {
setIsConfirmModalOpen(false);
}, [setIsConfirmModalOpen]);
return ( return (
<> <PageToolbarButton
<PageToolbarButton label={isSelectMode ? searchSelectLabel : searchIndexLabel}
label={isSelectMode ? searchSelectLabel : searchIndexLabel} isSpinning={isSearching}
isSpinning={isSearching} isDisabled={!totalItems}
isDisabled={!items.length} iconName={icons.SEARCH}
iconName={icons.SEARCH} onPress={onPress}
onPress={moviesToSearch.length > 5 ? onConfirmPress : onPress} />
/>
<ConfirmModal
isOpen={isConfirmModalOpen}
kind={kinds.DANGER}
title={isSelectMode ? searchSelectLabel : searchIndexLabel}
message={translate('SearchMoviesConfirmationMessageText', {
count: moviesToSearch.length,
})}
confirmLabel={isSelectMode ? searchSelectLabel : searchIndexLabel}
onConfirm={onPress}
onCancel={onConfirmModalClose}
/>
</>
); );
} }
@@ -26,7 +26,7 @@ import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
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 firstCharToUpper from 'Utilities/String/firstCharToUpper'; import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import MovieIndexProgressBar from '../ProgressBar/MovieIndexProgressBar'; import MovieIndexProgressBar from '../ProgressBar/MovieIndexProgressBar';
import MovieStatusCell from './MovieStatusCell'; import MovieStatusCell from './MovieStatusCell';
@@ -286,7 +286,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
if (name === 'minimumAvailability') { if (name === 'minimumAvailability') {
return ( return (
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>
{translate(firstCharToUpper(minimumAvailability))} {titleCase(minimumAvailability)}
</VirtualTableRowCell> </VirtualTableRowCell>
); );
} }
@@ -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;
} }
+10 -46
View File
@@ -3,7 +3,6 @@ import React from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) { function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
const revision = quality.revision; const revision = quality.revision;
@@ -29,36 +28,6 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
return title; return title;
} }
function revisionLabel(className, quality, showRevision) {
if (!showRevision) {
return;
}
if (quality.revision.isRepack) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Repack')}
>
R
</Label>
);
}
if (quality.revision.version && quality.revision.version > 1) {
return (
<Label
className={className}
kind={kinds.PRIMARY}
title={translate('Proper')}
>
P
</Label>
);
}
}
function MovieQuality(props) { function MovieQuality(props) {
const { const {
className, className,
@@ -66,8 +35,7 @@ function MovieQuality(props) {
quality, quality,
size, size,
isMonitored, isMonitored,
isCutoffNotMet, isCutoffNotMet
showRevision
} = props; } = props;
let kind = kinds.DEFAULT; let kind = kinds.DEFAULT;
@@ -82,15 +50,13 @@ function MovieQuality(props) {
} }
return ( return (
<span> <Label
<Label className={className}
className={className} kind={kind}
kind={kind} title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)} >
> {quality.quality.name}
{quality.quality.name} </Label>
</Label>{revisionLabel(className, quality, showRevision)}
</span>
); );
} }
@@ -100,14 +66,12 @@ MovieQuality.propTypes = {
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
size: PropTypes.number, size: PropTypes.number,
isMonitored: PropTypes.bool, isMonitored: PropTypes.bool,
isCutoffNotMet: PropTypes.bool, isCutoffNotMet: PropTypes.bool
showRevision: PropTypes.bool
}; };
MovieQuality.defaultProps = { MovieQuality.defaultProps = {
title: '', title: '',
isMonitored: true, isMonitored: true
showRevision: false
}; };
export default MovieQuality; export default MovieQuality;
@@ -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);
+82
View File
@@ -0,0 +1,82 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './RootFolderRow.css';
function RootFolderRow(props) {
const {
id,
path,
accessible,
freeSpace,
unmappedFolders,
onDeletePress
} = props;
const isUnavailable = !accessible;
return (
<TableRow>
<TableRowCell>
{
isUnavailable ?
<div className={styles.unavailablePath}>
{path}
<Label
className={styles.unavailableLabel}
kind={kinds.DANGER}
>
{translate('Unavailable')}
</Label>
</div> :
<Link
className={styles.link}
to={`/add/import/${id}`}
>
{path}
</Link>
}
</TableRowCell>
<TableRowCell className={styles.freeSpace}>
{(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
</TableRowCell>
<TableRowCell className={styles.unmappedFolders}>
{isUnavailable ? '-' : unmappedFolders.length}
</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RemoveRootFolder')}
name={icons.REMOVE}
onPress={onDeletePress}
/>
</TableRowCell>
</TableRow>
);
}
RootFolderRow.propTypes = {
id: PropTypes.number.isRequired,
path: PropTypes.string.isRequired,
accessible: PropTypes.bool.isRequired,
freeSpace: PropTypes.number,
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired
};
RootFolderRow.defaultProps = {
unmappedFolders: []
};
export default RootFolderRow;
+92
View File
@@ -0,0 +1,92 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import RootFolderRowConnector from './RootFolderRowConnector';
const rootFolderColumns = [
{
name: 'path',
get label() {
return translate('Path');
},
isVisible: true
},
{
name: 'freeSpace',
get label() {
return translate('FreeSpace');
},
isVisible: true
},
{
name: 'unmappedFolders',
get label() {
return translate('UnmappedFolders');
},
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
function RootFolders(props) {
const {
isFetching,
isPopulated,
error,
items
} = props;
if (isFetching && !isPopulated) {
return (
<LoadingIndicator />
);
}
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
</Alert>
);
}
return (
<Table
columns={rootFolderColumns}
>
<TableBody>
{
items.map((rootFolder) => {
return (
<RootFolderRowConnector
key={rootFolder.id}
id={rootFolder.id}
path={rootFolder.path}
accessible={rootFolder.accessible}
freeSpace={rootFolder.freeSpace}
unmappedFolders={rootFolder.unmappedFolders}
/>
);
})
}
</TableBody>
</Table>
);
}
RootFolders.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default RootFolders;
+1 -1
View File
@@ -49,7 +49,7 @@ function RootFolders() {
if (!isFetching && !!error) { if (!isFetching && !!error) {
return ( return (
<Alert kind={kinds.DANGER}>{translate('RootFoldersLoadError')}</Alert> <Alert kind={kinds.DANGER}>{translate('UnableToLoadRootFolders')}</Alert>
); );
} }
@@ -152,7 +152,7 @@ class CustomFormat extends Component {
isOpen={this.state.isDeleteCustomFormatModalOpen} isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DeleteCustomFormat')} title={translate('DeleteCustomFormat')}
message={translate('DeleteCustomFormatMessageText', { customFormatName: name })} message={translate('DeleteCustomFormatMessageText', { name })}
confirmLabel={translate('Delete')} confirmLabel={translate('Delete')}
isSpinning={isDeleting} isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat} onConfirm={this.onConfirmDeleteCustomFormat}
@@ -14,11 +14,9 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteDownloadClients, bulkDeleteDownloadClients,
bulkEditDownloadClients, bulkEditDownloadClients,
setManageDownloadClientsSort,
} from 'Store/Actions/settingsActions'; } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
@@ -82,8 +80,6 @@ const COLUMNS = [
interface ManageDownloadClientsModalContentProps { interface ManageDownloadClientsModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageDownloadClientsModalContent( function ManageDownloadClientsModalContent(
@@ -98,8 +94,6 @@ function ManageDownloadClientsModalContent(
isSaving, isSaving,
error, error,
items, items,
sortKey,
sortDirection,
}: DownloadClientAppState = useSelector( }: DownloadClientAppState = useSelector(
createClientSideCollectionSelector('settings.downloadClients') createClientSideCollectionSelector('settings.downloadClients')
); );
@@ -120,13 +114,6 @@ function ManageDownloadClientsModalContent(
const selectedCount = selectedIds.length; const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageDownloadClientsSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => { const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]); }, [setIsDeleteModalOpen]);
@@ -232,9 +219,6 @@ function ManageDownloadClientsModalContent(
allSelected={allSelected} allSelected={allSelected}
allUnselected={allUnselected} allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange} onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
> >
<TableBody> <TableBody>
{items.map((item) => { {items.map((item) => {
@@ -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,10 +12,8 @@ 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;
minimumAvailability?: string;
rootFolderPath?: string; rootFolderPath?: string;
} }
@@ -27,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() {
@@ -54,23 +52,16 @@ 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
); );
const [minimumAvailability, setMinimumAvailability] = useState(NO_CHANGE);
const [rootFolderPath, setRootFolderPath] = useState(NO_CHANGE); const [rootFolderPath, setRootFolderPath] = useState(NO_CHANGE);
const save = useCallback(() => { const save = useCallback(() => {
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';
@@ -81,11 +72,6 @@ function ManageImportListsEditModalContent(
payload.qualityProfileId = qualityProfileId as number; payload.qualityProfileId = qualityProfileId as number;
} }
if (minimumAvailability !== NO_CHANGE) {
hasChanges = true;
payload.minimumAvailability = minimumAvailability as string;
}
if (rootFolderPath !== NO_CHANGE) { if (rootFolderPath !== NO_CHANGE) {
hasChanges = true; hasChanges = true;
payload.rootFolderPath = rootFolderPath; payload.rootFolderPath = rootFolderPath;
@@ -96,31 +82,17 @@ function ManageImportListsEditModalContent(
} }
onModalClose(); onModalClose();
}, [ }, [enableAuto, qualityProfileId, rootFolderPath, onSavePress, onModalClose]);
enabled,
enableAuto,
qualityProfileId,
minimumAvailability,
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;
case 'qualityProfileId': case 'qualityProfileId':
setQualityProfileId(value); setQualityProfileId(value);
break; break;
case 'minimumAvailability':
setMinimumAvailability(value);
break;
case 'rootFolderPath': case 'rootFolderPath':
setRootFolderPath(value); setRootFolderPath(value);
break; break;
@@ -138,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>
@@ -157,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>
@@ -175,19 +135,6 @@ function ManageImportListsEditModalContent(
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
value={minimumAvailability}
includeNoChange={true}
includeNoChangeDisabled={false}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel> <FormLabel>{translate('RootFolder')}</FormLabel>
@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import ManageImportListsModalContent from './ManageImportListsModalContent'; import ManageImportListsModalContent from './ManageImportListsModalContent';
interface ManageImportListsModalProps { interface ManageImportListsModalProps {
@@ -12,7 +11,7 @@ function ManageImportListsModal(props: ManageImportListsModalProps) {
const { isOpen, onModalClose } = props; const { isOpen, onModalClose } = props;
return ( return (
<Modal isOpen={isOpen} size={sizes.EXTRA_LARGE} onModalClose={onModalClose}> <Modal isOpen={isOpen} onModalClose={onModalClose}>
<ManageImportListsModalContent onModalClose={onModalClose} /> <ManageImportListsModalContent onModalClose={onModalClose} />
</Modal> </Modal>
); );
@@ -52,24 +52,12 @@ const COLUMNS = [
isSortable: true, isSortable: true,
isVisible: true, isVisible: true,
}, },
{
name: 'minimumAvailability',
label: () => translate('MinimumAvailability'),
isSortable: true,
isVisible: true,
},
{ {
name: 'rootFolderPath', name: 'rootFolderPath',
label: () => translate('RootFolder'), label: () => translate('RootFolder'),
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,12 +1,10 @@
.name, .name,
.tags, .tags,
.enabled,
.enableAuto, .enableAuto,
.minimumAvailability,
.qualityProfileId, .qualityProfileId,
.rootFolderPath, .rootFolderPath,
.implementation { .implementation {
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,9 +2,7 @@
// 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;
'minimumAvailability': string;
'name': string; 'name': string;
'qualityProfileId': string; 'qualityProfileId': string;
'rootFolderPath': string; 'rootFolderPath': string;
@@ -7,8 +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 firstCharToUpper from 'Utilities/String/firstCharToUpper';
import translate from 'Utilities/String/translate';
import styles from './ManageImportListsModalRow.css'; import styles from './ManageImportListsModalRow.css';
interface ManageImportListsModalRowProps { interface ManageImportListsModalRowProps {
@@ -16,10 +14,8 @@ interface ManageImportListsModalRowProps {
name: string; name: string;
rootFolderPath: string; rootFolderPath: string;
qualityProfileId: number; qualityProfileId: number;
minimumAvailability: string;
implementation: string; implementation: string;
tags: number[]; tags: number[];
enabled: boolean;
enableAuto: boolean; enableAuto: boolean;
columns: Column[]; columns: Column[];
isSelected?: boolean; isSelected?: boolean;
@@ -32,10 +28,8 @@ function ManageImportListsModalRow(props: ManageImportListsModalRowProps) {
isSelected, isSelected,
name, name,
rootFolderPath, rootFolderPath,
minimumAvailability,
qualityProfileId, qualityProfileId,
implementation, implementation,
enabled,
enableAuto, enableAuto,
tags, tags,
onSelectedChange, onSelectedChange,
@@ -69,23 +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 className={styles.minimumAvailability}>
{translate(firstCharToUpper(minimumAvailability))}
</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}>
@@ -14,11 +14,9 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import useSelectState from 'Helpers/Hooks/useSelectState'; import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import { import {
bulkDeleteIndexers, bulkDeleteIndexers,
bulkEditIndexers, bulkEditIndexers,
setManageIndexersSort,
} from 'Store/Actions/settingsActions'; } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
@@ -82,8 +80,6 @@ const COLUMNS = [
interface ManageIndexersModalContentProps { interface ManageIndexersModalContentProps {
onModalClose(): void; onModalClose(): void;
sortKey?: string;
sortDirection?: SortDirection;
} }
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
@@ -96,8 +92,6 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
isSaving, isSaving,
error, error,
items, items,
sortKey,
sortDirection,
}: IndexerAppState = useSelector( }: IndexerAppState = useSelector(
createClientSideCollectionSelector('settings.indexers') createClientSideCollectionSelector('settings.indexers')
); );
@@ -118,13 +112,6 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
const selectedCount = selectedIds.length; const selectedCount = selectedIds.length;
const onSortPress = useCallback(
(value: string) => {
dispatch(setManageIndexersSort({ sortKey: value }));
},
[dispatch]
);
const onDeletePress = useCallback(() => { const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]); }, [setIsDeleteModalOpen]);
@@ -227,9 +214,6 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
allSelected={allSelected} allSelected={allSelected}
allUnselected={allUnselected} allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange} onSelectAllChange={onSelectAllChange}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
> >
<TableBody> <TableBody>
{items.map((item) => { {items.map((item) => {
@@ -0,0 +1,72 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import { icons, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './AddRootFolder.css';
class AddRootFolder extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddNewRootFolderModalOpen: false
};
}
//
// Lifecycle
onAddNewRootFolderPress = () => {
this.setState({ isAddNewRootFolderModalOpen: true });
};
onNewRootFolderSelect = ({ value }) => {
this.props.onNewRootFolderSelect(value);
};
onAddRootFolderModalClose = () => {
this.setState({ isAddNewRootFolderModalOpen: false });
};
//
// Render
render() {
return (
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{translate('AddRootFolder')}
</Button>
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={this.onNewRootFolderSelect}
onModalClose={this.onAddRootFolderModalClose}
/>
</div>
);
}
}
AddRootFolder.propTypes = {
onNewRootFolderSelect: PropTypes.func.isRequired
};
export default AddRootFolder;
@@ -1,18 +1,14 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch } from 'react-redux';
import Alert from 'Components/Alert';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import { icons, kinds, sizes } from 'Helpers/Props'; import { icons, kinds, sizes } from 'Helpers/Props';
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 styles from './AddRootFolder.css'; import styles from './AddRootFolder.css';
function AddRootFolder() { function AddRootFolder() {
const { isSaving, saveError } = useSelector(createRootFoldersSelector());
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] = const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
@@ -34,42 +30,24 @@ function AddRootFolder() {
}, [setIsAddNewRootFolderModalOpen]); }, [setIsAddNewRootFolderModalOpen]);
return ( return (
<> <div className={styles.addRootFolderButtonContainer}>
{!isSaving && saveError ? ( <Button
<Alert kind={kinds.DANGER}> kind={kinds.PRIMARY}
{translate('AddRootFolderError')} size={sizes.LARGE}
onPress={onAddNewRootFolderPress}
>
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
{translate('AddRootFolder')}
</Button>
<ul> <FileBrowserModal
{Array.isArray(saveError.responseJSON) ? ( isOpen={isAddNewRootFolderModalOpen}
saveError.responseJSON.map((e, index) => { name="rootFolderPath"
return <li key={index}>{e.errorMessage}</li>; value=""
}) onChange={onNewRootFolderSelect}
) : ( onModalClose={onAddRootFolderModalClose}
<li>{JSON.stringify(saveError.responseJSON)}</li> />
)} </div>
</ul>
</Alert>
) : null}
<div className={styles.addRootFolderButtonContainer}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={onAddNewRootFolderPress}
>
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
{translate('AddRootFolder')}
</Button>
<FileBrowserModal
isOpen={isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={onNewRootFolderSelect}
onModalClose={onAddRootFolderModalClose}
/>
</div>
</>
); );
} }
@@ -1,5 +1,4 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { sortDirections } from 'Helpers/Props';
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler'; import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler'; import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler'; import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
@@ -8,7 +7,6 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler'; import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler'; import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler'; import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer'; import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks'; import { createThunk } from 'Store/thunks';
@@ -33,9 +31,9 @@ export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadCl
export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient'; export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient'; export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients'; export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients'; export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort'; export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
// //
// Action Creators // Action Creators
@@ -50,9 +48,9 @@ export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT);
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT); export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT); export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS); export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS); export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT); export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => { export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
return { return {
@@ -92,9 +90,7 @@ export default {
isTesting: false, isTesting: false,
isTestingAll: false, isTestingAll: false,
items: [], items: [],
pendingChanges: {}, pendingChanges: {}
sortKey: 'name',
sortDirection: sortDirections.DESCENDING
}, },
// //
@@ -128,10 +124,7 @@ export default {
return selectedSchema; return selectedSchema;
}); });
}, }
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
} }
}; };

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