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
456 changed files with 6359 additions and 9443 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)
+3 -4
View File
@@ -9,15 +9,15 @@ 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.2' 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.2' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04' linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11' macImage: 'macOS-11'
@@ -1242,7 +1242,6 @@ stages:
- stage: Report_Out - stage: Report_Out
dependsOn: dependsOn:
- Analyze - Analyze
- Installer
- Unit_Test - Unit_Test
- Integration - Integration
- Automation - Automation
+1 -1
View File
@@ -254,7 +254,7 @@ InstallInno()
ProgressStart "Installing portable Inno Setup" ProgressStart "Installing portable Inno Setup"
rm -rf _inno rm -rf _inno
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe" curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
mkdir _inno mkdir _inno
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno ./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
rm innosetup.exe rm innosetup.exe
-2
View File
@@ -2,8 +2,6 @@ const loose = true;
module.exports = { module.exports = {
plugins: [ plugins: [
'@babel/plugin-transform-logical-assignment-operators',
// Stage 1 // Stage 1
'@babel/plugin-proposal-export-default-from', '@babel/plugin-proposal-export-default-from',
['@babel/plugin-transform-optional-chaining', { loose }], ['@babel/plugin-transform-optional-chaining', { loose }],
+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>
); );
} }
@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css'; import styles from './HistoryEventTypeCell.css';
function getIconName(eventType, data) { function getIconName(eventType) {
switch (eventType) { switch (eventType) {
case 'grabbed': case 'grabbed':
return icons.DOWNLOADING; return icons.DOWNLOADING;
@@ -17,7 +17,7 @@ function getIconName(eventType, data) {
case 'downloadFailed': case 'downloadFailed':
return icons.DOWNLOADING; return icons.DOWNLOADING;
case 'movieFileDeleted': case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE; return icons.DELETE;
case 'movieFileRenamed': case 'movieFileRenamed':
return icons.ORGANIZE; return icons.ORGANIZE;
case 'downloadIgnored': case 'downloadIgnored':
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
case 'downloadFailed': case 'downloadFailed':
return translate('MovieDownloadFailedTooltip'); return translate('MovieDownloadFailedTooltip');
case 'movieFileDeleted': case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip'); return translate('MovieFileDeletedTooltip');
case 'movieFileRenamed': case 'movieFileRenamed':
return translate('MovieFileRenamedTooltip'); return translate('MovieFileRenamedTooltip');
case 'downloadIgnored': case 'downloadIgnored':
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
} }
function HistoryEventTypeCell({ eventType, data }) { function HistoryEventTypeCell({ eventType, data }) {
const iconName = getIconName(eventType, data); const iconName = getIconName(eventType);
const iconKind = getIconKind(eventType); const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data); const tooltip = getTooltip(eventType, data);
@@ -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;
+1 -12
View File
@@ -4,7 +4,7 @@ import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar'; import ProgressBar from 'Components/ProgressBar';
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 TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
@@ -97,7 +97,6 @@ class QueueRow extends Component {
outputPath, outputPath,
downloadClient, downloadClient,
estimatedCompletionTime, estimatedCompletionTime,
added,
timeleft, timeleft,
size, size,
sizeleft, sizeleft,
@@ -316,15 +315,6 @@ class QueueRow extends Component {
); );
} }
if (name === 'added') {
return (
<RelativeDateCellConnector
key={name}
date={added}
/>
);
}
if (name === 'actions') { if (name === 'actions') {
return ( return (
<TableRowCell <TableRowCell
@@ -403,7 +393,6 @@ QueueRow.propTypes = {
outputPath: PropTypes.string, outputPath: PropTypes.string,
downloadClient: PropTypes.string, downloadClient: PropTypes.string,
estimatedCompletionTime: PropTypes.string, estimatedCompletionTime: PropTypes.string,
added: PropTypes.string,
timeleft: PropTypes.string, timeleft: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
year: PropTypes.number, year: PropTypes.number,
@@ -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>
-9
View File
@@ -44,16 +44,7 @@ export interface CustomFilter {
filers: PropertyFilter[]; filers: PropertyFilter[];
} }
export interface AppSectionState {
dimensions: {
isSmallScreen: boolean;
width: number;
height: number;
};
}
interface AppState { interface AppState {
app: AppSectionState;
calendar: CalendarAppState; calendar: CalendarAppState;
commands: CommandAppState; commands: CommandAppState;
history: HistoryAppState; history: HistoryAppState;
+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
@@ -147,7 +147,7 @@ class AgendaEvent extends Component {
className={styles.statusIcon} className={styles.statusIcon}
name={icons.MOVIE_FILE} name={icons.MOVIE_FILE}
kind={kinds.WARNING} kind={kinds.WARNING}
title={translate('QualityCutoffNotMet')} title={translate('QualityCutoffHasNotBeenMet')}
/> />
} }
</div> </div>
+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();
@@ -48,10 +48,6 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.statusContainer { .statusContainer {
display: flex; display: flex;
align-items: center; align-items: center;
&:global(.fullColor) {
filter: var(--calendarFullColorFilter)
}
} }
.statusIcon { .statusIcon {
+3 -11
View File
@@ -76,18 +76,12 @@ class CalendarEvent extends Component {
{title} {title}
</div> </div>
<div <div className={styles.statusContainer}>
className={classNames(
styles.statusContainer,
fullColorEvents && 'fullColor'
)}
>
{ {
queueItem ? queueItem ?
<span className={styles.statusIcon}> <span className={styles.statusIcon}>
<CalendarEventQueueDetails <CalendarEventQueueDetails
{...queueItem} {...queueItem}
fullColorEvents={fullColorEvents}
/> />
</span> : </span> :
null null
@@ -104,14 +98,12 @@ class CalendarEvent extends Component {
} }
{ {
showCutoffUnmetIcon && showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet ?
!!movieFile &&
movieFile.qualityCutoffNotMet ?
<Icon <Icon
className={styles.statusIcon} className={styles.statusIcon}
name={icons.MOVIE_FILE} name={icons.MOVIE_FILE}
kind={kinds.WARNING} kind={kinds.WARNING}
title={translate('QualityCutoffNotMet')} title={translate('QualityCutoffHasNotBeenMet')}
/> : /> :
null null
} }
+3 -4
View File
@@ -20,11 +20,10 @@ function Legend(props) {
if (showCutoffUnmetIcon) { if (showCutoffUnmetIcon) {
iconsToShow.push( iconsToShow.push(
<LegendIconItem <LegendIconItem
name={translate('CutoffNotMet')} name={translate('CutoffUnmet')}
icon={icons.MOVIE_FILE} icon={icons.MOVIE_FILE}
kind={kinds.WARNING} kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
fullColorEvents={fullColorEvents} tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
tooltip={translate('QualityCutoffNotMet')}
/> />
); );
} }
@@ -4,8 +4,4 @@
.icon { .icon {
margin-right: 5px; margin-right: 5px;
&:global(.fullColorEvents) {
filter: var(--calendarFullColorFilter)
}
} }
@@ -1,4 +1,3 @@
import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
@@ -7,9 +6,9 @@ import styles from './LegendIconItem.css';
function LegendIconItem(props) { function LegendIconItem(props) {
const { const {
name, name,
fullColorEvents,
icon, icon,
kind, kind,
darken,
tooltip tooltip
} = props; } = props;
@@ -19,11 +18,9 @@ function LegendIconItem(props) {
title={tooltip} title={tooltip}
> >
<Icon <Icon
className={classNames( className={styles.icon}
styles.icon,
fullColorEvents && 'fullColorEvents'
)}
name={icon} name={icon}
darken={darken}
kind={kind} kind={kind}
/> />
@@ -34,10 +31,14 @@ function LegendIconItem(props) {
LegendIconItem.propTypes = { LegendIconItem.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
icon: PropTypes.object.isRequired, icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired, kind: PropTypes.string.isRequired,
darken: PropTypes.bool.isRequired,
tooltip: PropTypes.string.isRequired tooltip: PropTypes.string.isRequired
}; };
LegendIconItem.defaultProps = {
darken: false
};
export default LegendIconItem; export default LegendIconItem;
+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,
@@ -74,7 +74,11 @@ CollectionMovieLabel.propTypes = {
CollectionMovieLabel.defaultProps = { CollectionMovieLabel.defaultProps = {
isSaving: false, isSaving: false,
statistics: {} statistics: {
episodeFileCount: 0,
totalEpisodeCount: 0,
percentOfEpisodes: 0
}
}; };
export default CollectionMovieLabel; export default CollectionMovieLabel;
@@ -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');
}, },
@@ -30,24 +30,22 @@ function CustomFiltersModalContent(props) {
<ModalBody> <ModalBody>
{ {
customFilters customFilters.map((customFilter) => {
.sort((a, b) => a.label.localeCompare(b.label)) return (
.map((customFilter) => { <CustomFilter
return ( key={customFilter.id}
<CustomFilter id={customFilter.id}
key={customFilter.id} label={customFilter.label}
id={customFilter.id} filters={customFilter.filters}
label={customFilter.label} selectedFilterKey={selectedFilterKey}
filters={customFilter.filters} isDeleting={isDeleting}
selectedFilterKey={selectedFilterKey} deleteError={deleteError}
isDeleting={isDeleting} dispatchSetFilter={dispatchSetFilter}
deleteError={deleteError} dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
dispatchSetFilter={dispatchSetFilter} onEditPress={onEditCustomFilter}
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter} />
onEditPress={onEditCustomFilter} );
/> })
);
})
} }
<div className={styles.addButtonContainer}> <div className={styles.addButtonContainer}>
@@ -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,
+8
View File
@@ -12,10 +12,18 @@
.info { .info {
color: var(--infoColor); color: var(--infoColor);
&:global(.darken) {
color: color(var(--infoColor) shade(30%));
}
} }
.pink { .pink {
color: var(--pink); color: var(--pink);
&:global(.darken) {
color: color(var(--pink) shade(30%));
}
} }
.success { .success {
+5 -1
View File
@@ -18,6 +18,7 @@ class Icon extends PureComponent {
kind, kind,
size, size,
title, title,
darken,
isSpinning, isSpinning,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -26,7 +27,8 @@ class Icon extends PureComponent {
<FontAwesomeIcon <FontAwesomeIcon
className={classNames( className={classNames(
className, className,
styles[kind] styles[kind],
darken && 'darken'
)} )}
icon={name} icon={name}
spin={isSpinning} spin={isSpinning}
@@ -59,6 +61,7 @@ Icon.propTypes = {
kind: PropTypes.string.isRequired, kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
darken: PropTypes.bool.isRequired,
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired fixedWidth: PropTypes.bool.isRequired
}; };
@@ -66,6 +69,7 @@ Icon.propTypes = {
Icon.defaultProps = { Icon.defaultProps = {
kind: kinds.DEFAULT, kind: kinds.DEFAULT,
size: 14, size: 14,
darken: false,
isSpinning: false, isSpinning: false,
fixedWidth: false fixedWidth: false
}; };
@@ -40,26 +40,18 @@ class FilterMenuContent extends Component {
} }
{ {
customFilters.length > 0 ? customFilters.map((filter) => {
<MenuItemSeparator /> : return (
null <FilterMenuItem
} key={filter.id}
filterKey={filter.id}
{ selectedFilterKey={selectedFilterKey}
customFilters onPress={onFilterSelect}
.sort((a, b) => a.label.localeCompare(b.label)) >
.map((filter) => { {filter.label}
return ( </FilterMenuItem>
<FilterMenuItem );
key={filter.id} })
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
</FilterMenuItem>
);
})
} }
{ {
+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
} }
-2
View File
@@ -59,7 +59,6 @@ import {
faEye as fasEye, faEye as fasEye,
faFastBackward as fasFastBackward, faFastBackward as fasFastBackward,
faFastForward as fasFastForward, faFastForward as fasFastForward,
faFileCircleQuestion as fasFileCircleQuestion,
faFileExport as fasFileExport, faFileExport as fasFileExport,
faFileInvoice as farFileInvoice, faFileInvoice as farFileInvoice,
faFilm as fasFilm, faFilm as fasFilm,
@@ -160,7 +159,6 @@ export const EXPORT = fasFileExport;
export const EXTERNAL_LINK = fasExternalLinkAlt; export const EXTERNAL_LINK = fasExternalLinkAlt;
export const FATAL = fasTimesCircle; export const FATAL = fasTimesCircle;
export const FILE = farFile; export const FILE = farFile;
export const FILE_MISSING = fasFileCircleQuestion;
export const FILM = fasFilm; export const FILM = fasFilm;
export const FILTER = fasFilter; export const FILTER = fasFilter;
export const FLAG = fasFlag; export const FLAG = fasFlag;
+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;
@@ -50,16 +50,12 @@ class DeleteMovieModalContent extends Component {
title, title,
path, path,
hasFile, hasFile,
statistics,
deleteOptions, deleteOptions,
sizeOnDisk,
onModalClose, onModalClose,
onDeleteOptionChange onDeleteOptionChange
} = this.props; } = this.props;
const {
sizeOnDisk = 0
} = statistics;
const deleteFiles = this.state.deleteFiles; const deleteFiles = this.state.deleteFiles;
const addImportExclusion = deleteOptions.addImportExclusion; const addImportExclusion = deleteOptions.addImportExclusion;
@@ -155,16 +151,12 @@ class DeleteMovieModalContent extends Component {
DeleteMovieModalContent.propTypes = { DeleteMovieModalContent.propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
hasFile: PropTypes.bool.isRequired, hasFile: PropTypes.bool.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
deleteOptions: PropTypes.object.isRequired, deleteOptions: PropTypes.object.isRequired,
onDeleteOptionChange: PropTypes.func.isRequired, onDeleteOptionChange: PropTypes.func.isRequired,
onDeletePress: PropTypes.func.isRequired, onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
DeleteMovieModalContent.defaultProps = {
statistics: {}
};
export default DeleteMovieModalContent; export default DeleteMovieModalContent;
@@ -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,58 +63,162 @@ 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();
}
}
//
// Control
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 {
items,
itemComponent
} = this.props;
const {
posterWidth,
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
render() { render() {
const { const {
items, items
itemComponent,
isSmallScreen
} = this.props; } = this.props;
const { const {
posterWidth, width,
posterHeight, 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={isSmallScreen ? 1 : 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 {
+148 -89
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
@@ -238,7 +261,7 @@ class MovieDetails extends Component {
certification, certification,
ratings, ratings,
path, path,
statistics, sizeOnDisk,
qualityProfileId, qualityProfileId,
monitored, monitored,
studio, studio,
@@ -263,23 +286,18 @@ class MovieDetails extends Component {
onMonitorTogglePress, onMonitorTogglePress,
onRefreshPress, onRefreshPress,
onSearchPress, onSearchPress,
queueItem, queueItems,
movieRuntimeFormat movieRuntimeFormat
} = this.props; } = this.props;
const {
sizeOnDisk = 0
} = statistics;
const { const {
isOrganizeModalOpen, isOrganizeModalOpen,
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);
@@ -306,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
@@ -324,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
@@ -540,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>
@@ -650,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
@@ -692,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}
@@ -708,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>
); );
@@ -738,7 +797,7 @@ MovieDetails.propTypes = {
certification: PropTypes.string, certification: PropTypes.string,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired, sizeOnDisk: PropTypes.number.isRequired,
qualityProfileId: PropTypes.number.isRequired, qualityProfileId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
@@ -771,15 +830,15 @@ 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
}; };
MovieDetails.defaultProps = { MovieDetails.defaultProps = {
genres: [], genres: [],
statistics: {},
tags: [], tags: [],
isSaving: false isSaving: false,
sizeOnDisk: 0
}; };
export default MovieDetails; export default MovieDetails;
@@ -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);
+6 -10
View File
@@ -17,13 +17,13 @@ function createUnoptimizedSelector() {
createClientSideCollectionSelector('movies', 'movieIndex'), createClientSideCollectionSelector('movies', 'movieIndex'),
(movies: MoviesAppState) => { (movies: MoviesAppState) => {
return movies.items.map((m) => { return movies.items.map((m) => {
const { monitored, status, hasFile, statistics } = m; const { monitored, status, hasFile, sizeOnDisk } = m;
return { return {
monitored, monitored,
status, status,
hasFile, hasFile,
statistics, sizeOnDisk,
}; };
}); });
} }
@@ -44,20 +44,16 @@ export default function MovieIndexFooter() {
let monitored = 0; let monitored = 0;
let totalFileSize = 0; let totalFileSize = 0;
movies.forEach((m) => { movies.forEach((s) => {
const { statistics = { sizeOnDisk: 0 } } = m; if (s.hasFile) {
const { sizeOnDisk = 0 } = statistics;
if (m.hasFile) {
movieFiles += 1; movieFiles += 1;
} }
if (m.monitored) { if (s.monitored) {
monitored++; monitored++;
} }
totalFileSize += sizeOnDisk; totalFileSize += s.sizeOnDisk;
}); });
return ( return (
@@ -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}
/>
</>
); );
} }
@@ -13,7 +13,6 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar'; import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect'; import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
@@ -67,19 +66,17 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
status, status,
path, path,
overview, overview,
statistics = {} as Statistics,
images, images,
hasFile, hasFile,
isAvailable, isAvailable,
tmdbId, tmdbId,
imdbId, imdbId,
studio, studio,
sizeOnDisk,
added, added,
youTubeTrailerId, youTubeTrailerId,
} = movie; } = movie;
const { sizeOnDisk = 0 } = statistics;
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false); const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false); const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
@@ -16,7 +16,6 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar'; import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect'; import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
import { Statistics } from 'Movie/Movie';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@@ -76,14 +75,12 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
path, path,
movieFile, movieFile,
ratings, ratings,
statistics = {} as Statistics, sizeOnDisk,
certification, certification,
originalTitle, originalTitle,
originalLanguage, originalLanguage,
} = movie; } = movie;
const { sizeOnDisk = 0 } = statistics;
const dispatch = useDispatch(); const dispatch = useDispatch();
const [hasPosterError, setHasPosterError] = useState(false); const [hasPosterError, setHasPosterError] = useState(false);
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false); const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
@@ -244,15 +244,11 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
if (isSmallScreen) { if (isSmallScreen) {
const padding = bodyPaddingSmallScreen - 5; const padding = bodyPaddingSmallScreen - 5;
const width = window.innerWidth - padding * 2;
const height = window.innerHeight;
if (width !== size.width || height !== size.height) { setSize({
setSize({ width: window.innerWidth - padding * 2,
width, height: window.innerHeight,
height, });
});
}
return; return;
} }
@@ -260,18 +256,13 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
if (current) { if (current) {
const width = current.clientWidth; const width = current.clientWidth;
const padding = bodyPadding - 5; const padding = bodyPadding - 5;
const finalWidth = width - padding * 2;
if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) {
return;
}
setSize({ setSize({
width: finalWidth, width: width - padding * 2,
height: window.innerHeight, height: window.innerHeight,
}); });
} }
}, [isSmallScreen, size, scrollerRef, bounds]); }, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => { useEffect(() => {
const currentScrollerRef = scrollerRef.current as HTMLElement; const currentScrollerRef = scrollerRef.current as HTMLElement;
@@ -38,7 +38,6 @@
flex: 1 0 125px; flex: 1 0 125px;
} }
.releaseGroups,
.inCinemas, .inCinemas,
.physicalRelease, .physicalRelease,
.digitalRelease, .digitalRelease,
-1
View File
@@ -20,7 +20,6 @@ interface CssExports {
'physicalRelease': string; 'physicalRelease': string;
'popularity': string; 'popularity': string;
'qualityProfileId': string; 'qualityProfileId': string;
'releaseGroups': string;
'rottenTomatoesRating': string; 'rottenTomatoesRating': string;
'runtime': string; 'runtime': string;
'sizeOnDisk': string; 'sizeOnDisk': string;
@@ -19,7 +19,6 @@ import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector'; import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector';
import { Statistics } from 'Movie/Movie';
import MoviePopularityIndex from 'Movie/MoviePopularityIndex'; import MoviePopularityIndex from 'Movie/MoviePopularityIndex';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
@@ -27,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';
@@ -61,7 +60,6 @@ function MovieIndexRow(props: MovieIndexRowProps) {
originalLanguage, originalLanguage,
originalTitle, originalTitle,
added, added,
statistics = {} as Statistics,
year, year,
inCinemas, inCinemas,
digitalRelease, digitalRelease,
@@ -69,6 +67,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
runtime, runtime,
minimumAvailability, minimumAvailability,
path, path,
sizeOnDisk,
genres = [], genres = [],
ratings, ratings,
popularity, popularity,
@@ -83,8 +82,6 @@ function MovieIndexRow(props: MovieIndexRowProps) {
isSaving = false, isSaving = false,
} = movie; } = movie;
const { sizeOnDisk = 0, releaseGroups = [] } = statistics;
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false); const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false); const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
@@ -289,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>
); );
} }
@@ -383,20 +380,6 @@ function MovieIndexRow(props: MovieIndexRowProps) {
); );
} }
if (name === 'releaseGroups') {
const joinedReleaseGroups = releaseGroups.join(', ');
const truncatedReleaseGroups =
releaseGroups.length > 3
? `${releaseGroups.slice(0, 3).join(', ')}...`
: joinedReleaseGroups;
return (
<VirtualTableRowCell key={name} className={styles[name]}>
<span title={joinedReleaseGroups}>{truncatedReleaseGroups}</span>
</VirtualTableRowCell>
);
}
if (name === 'tags') { if (name === 'tags') {
return ( return (
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>

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