mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b43c2e345 | |||
| dc599b6531 | |||
| 1421179654 | |||
| 41dcf32e24 | |||
| 7a813a44b6 | |||
| 54a5059080 | |||
| adaf7444d3 | |||
| 49d11e59b3 | |||
| a7eb4a4a04 | |||
| 66a6a663ba | |||
| f735e31835 | |||
| b8f1286abb | |||
| 9df45199d0 | |||
| a692c35b03 | |||
| ddcad270c3 | |||
| b06f1d7c12 | |||
| 480bb50b85 | |||
| dbc94dbe4e | |||
| b89271fc01 | |||
| 66fcde7325 | |||
| 463741da1f | |||
| 3388fae1a5 | |||
| 72b2cfe8be | |||
| d5dd5e08ca | |||
| fabd40cbae | |||
| 3ca327f611 | |||
| c804140896 | |||
| bb43d0c796 | |||
| 5757fa797f | |||
| 2fc32189d8 | |||
| 5975be3690 | |||
| 6095819005 | |||
| 7528882adf | |||
| c1f1307345 | |||
| 348060351a | |||
| ca31cdd33a | |||
| 36e278aa82 | |||
| 927e84654f | |||
| 96e60906c5 | |||
| 7a55b563c0 | |||
| b4bbb71a9b | |||
| 0361299a73 | |||
| e11339fb83 | |||
| fbdd3129f5 |
@@ -1,7 +1,7 @@
|
|||||||
# Radarr
|
# Radarr
|
||||||
|
|
||||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
||||||
[](https://wiki.servarr.com/radarr/installation/docker)
|
[](https://wiki.servarr.com/radarr/installation/docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.10.2'
|
majorVersion: '5.11.0'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AddNewMovieModalContent.css';
|
import styles from './AddNewMovieModalContent.css';
|
||||||
@@ -115,13 +118,28 @@ class AddNewMovieModalContent extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...minimumAvailability}
|
{...minimumAvailability}
|
||||||
|
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ImportMovieHeader.css';
|
import styles from './ImportMovieHeader.css';
|
||||||
|
|
||||||
@@ -46,7 +50,19 @@ function ImportMovieHeader(props) {
|
|||||||
className={styles.minimumAvailability}
|
className={styles.minimumAvailability}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
>
|
>
|
||||||
{translate('MinAvailability')}
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.detailsIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
</VirtualTableHeaderCell>
|
</VirtualTableHeaderCell>
|
||||||
|
|
||||||
<VirtualTableHeaderCell
|
<VirtualTableHeaderCell
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function MovieMinimumAvailabilityPopoverContent() {
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Announced')}
|
||||||
|
data={translate('AnnouncedMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('InCinemas')}
|
||||||
|
data={translate('InCinemasMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Released')}
|
||||||
|
data={translate('ReleasedMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieMinimumAvailabilityPopoverContent;
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
|
import MovieCreditAppState from './MovieCreditAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
@@ -64,6 +65,7 @@ interface AppState {
|
|||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
movieCollections: MovieCollectionAppState;
|
movieCollections: MovieCollectionAppState;
|
||||||
|
movieCredits: MovieCreditAppState;
|
||||||
movieFiles: MovieFilesAppState;
|
movieFiles: MovieFilesAppState;
|
||||||
movieIndex: MovieIndexAppState;
|
movieIndex: MovieIndexAppState;
|
||||||
movies: MoviesAppState;
|
movies: MoviesAppState;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
|
||||||
|
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
|
||||||
|
|
||||||
|
export default MovieCreditAppState;
|
||||||
@@ -27,6 +27,7 @@ export interface MovieIndexAppState {
|
|||||||
showTmdbRating: boolean;
|
showTmdbRating: boolean;
|
||||||
showImdbRating: boolean;
|
showImdbRating: boolean;
|
||||||
showRottenTomatoesRating: boolean;
|
showRottenTomatoesRating: boolean;
|
||||||
|
showTraktRating: boolean;
|
||||||
showTags: boolean;
|
showTags: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ function getValue(input, selectedFilterBuilderProp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||||
return parseInt(input);
|
const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
|
||||||
|
|
||||||
|
return Number(input).toFixed(numberFractionDigits);
|
||||||
}
|
}
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
const availabilityOptions = [
|
|
||||||
{
|
|
||||||
key: 'announced',
|
|
||||||
get value() {
|
|
||||||
return translate('Announced');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'inCinemas',
|
|
||||||
get value() {
|
|
||||||
return translate('InCinemas');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'released',
|
|
||||||
get value() {
|
|
||||||
return translate('Released');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function AvailabilitySelectInput(props) {
|
|
||||||
const values = [...availabilityOptions];
|
|
||||||
|
|
||||||
const {
|
|
||||||
includeNoChange,
|
|
||||||
includeMixed
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
value: translate('NoChange'),
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMixed) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'mixed',
|
|
||||||
value: '(Mixed)',
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...props}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AvailabilitySelectInput.propTypes = {
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
includeMixed: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AvailabilitySelectInput.defaultProps = {
|
|
||||||
includeNoChange: false,
|
|
||||||
includeMixed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AvailabilitySelectInput;
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
interface AvailabilitySelectInputProps {
|
||||||
|
includeNoChange: boolean;
|
||||||
|
includeNoChangeDisabled?: boolean;
|
||||||
|
includeMixed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMovieAvailabilityOption {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
format?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movieAvailabilityOptions: IMovieAvailabilityOption[] = [
|
||||||
|
{
|
||||||
|
key: 'announced',
|
||||||
|
get value() {
|
||||||
|
return translate('Announced');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'inCinemas',
|
||||||
|
get value() {
|
||||||
|
return translate('InCinemas');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'released',
|
||||||
|
get value() {
|
||||||
|
return translate('Released');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function AvailabilitySelectInput(props: AvailabilitySelectInputProps) {
|
||||||
|
const values = [...movieAvailabilityOptions];
|
||||||
|
|
||||||
|
const {
|
||||||
|
includeNoChange = false,
|
||||||
|
includeNoChangeDisabled = true,
|
||||||
|
includeMixed = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (includeNoChange) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'noChange',
|
||||||
|
value: translate('NoChange'),
|
||||||
|
isDisabled: includeNoChangeDisabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeMixed) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'mixed',
|
||||||
|
value: `(${translate('Mixed')})`,
|
||||||
|
isDisabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EnhancedSelectInput {...props} values={values} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvailabilitySelectInput;
|
||||||
@@ -5,17 +5,20 @@ import translate from 'Utilities/String/translate';
|
|||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function MovieMonitoredSelectInput(props) {
|
function MovieMonitoredSelectInput(props) {
|
||||||
const values = [...monitorOptions];
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
includeNoChange,
|
includeNoChange,
|
||||||
includeMixed
|
includeMixed,
|
||||||
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const values = [...monitorOptions];
|
||||||
|
|
||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
isDisabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -23,14 +26,16 @@ function MovieMonitoredSelectInput(props) {
|
|||||||
if (includeMixed) {
|
if (includeMixed) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
get value() {
|
||||||
|
return `(${translate('Mixed')})`;
|
||||||
|
},
|
||||||
isDisabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInput
|
<EnhancedSelectInput
|
||||||
{...props}
|
{...otherProps}
|
||||||
values={values}
|
values={values}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'image': string;
|
'image': string;
|
||||||
|
'wrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function ImdbRating(props: ImdbRatingProps) {
|
|||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
anchor={
|
anchor={
|
||||||
<span>
|
<span className={styles.wrapper}>
|
||||||
{!hideIcon && (
|
{!hideIcon && (
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'image': string;
|
'image': string;
|
||||||
|
'wrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function RottenTomatoRating(props: RottenTomatoRatingProps) {
|
|||||||
const ratingImage = value > 50 ? rtFresh : rtRotten;
|
const ratingImage = value > 50 ? rtFresh : rtRotten;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span className={styles.wrapper}>
|
||||||
{!hideIcon && (
|
{!hideIcon && (
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'image': string;
|
'image': string;
|
||||||
|
'wrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function TmdbRating(props: TmdbRatingProps) {
|
|||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
anchor={
|
anchor={
|
||||||
<span>
|
<span className={styles.wrapper}>
|
||||||
{!hideIcon && (
|
{!hideIcon && (
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
align-content: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: -0.125em;
|
||||||
|
}
|
||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
// 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 {
|
||||||
'blankpad': string;
|
'image': string;
|
||||||
|
'wrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import { Ratings } from 'Movie/Movie';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './TraktRating.css';
|
||||||
|
|
||||||
|
interface TraktRatingProps {
|
||||||
|
ratings: Ratings;
|
||||||
|
iconSize?: number;
|
||||||
|
hideIcon?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TraktRating(props: TraktRatingProps) {
|
||||||
|
const { ratings, iconSize = 14, hideIcon = false } = props;
|
||||||
|
|
||||||
|
const traktImage =
|
||||||
|
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgICAgdmlld0JveD0iMCAwIDE0NC44IDE0NC44IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxNDQuOCAxNDQuOCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+ICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik0yOS41LDExMS44YzEwLjYsMTEuNiwyNS45LDE4LjgsNDIuOSwxOC44YzguNywwLDE2LjktMS45LDI0LjMtNS4zTDU2LjMsODVMMjkuNSwxMTEuOHoiLz4gICAgPHBhdGggZmlsbD0iI0VEMjIyNCIgZD0iTTU2LjEsNjAuNkwyNS41LDkxLjFMMjEuNCw4N2wzMi4yLTMyLjJoMGwzNy42LTM3LjZjLTUuOS0yLTEyLjItMy4xLTE4LjgtMy4xYy0zMi4yLDAtNTguMywyNi4xLTU4LjMsNTguMyAgICAgICBjMCwxMy4xLDQuMywyNS4yLDExLjcsMzVsMzAuNS0zMC41bDIuMSwybDQzLjcsNDMuN2MwLjktMC41LDEuNy0xLDIuNS0xLjZMNTYuMyw3Mi43TDI3LDEwMmwtNC4xLTQuMWwzMy40LTMzLjRsMi4xLDJsNTEsNTAuOSAgICAgICBjMC44LTAuNiwxLjUtMS4zLDIuMi0xLjlsLTU1LTU1TDU2LjEsNjAuNnoiLz4gICAgPHBhdGggZmlsbD0iI0VEMUMyNCIgZD0iTTExNS43LDExMS40YzkuMy0xMC4zLDE1LTI0LDE1LTM5YzAtMjMuNC0xMy44LTQzLjUtMzMuNi01Mi44TDYwLjQsNTYuMkwxMTUuNywxMTEuNHogTTc0LjUsNjYuOGwtNC4xLTQuMSAgICAgICBsMjguOS0yOC45bDQuMSw0LjFMNzQuNSw2Ni44eiBNMTAxLjksMjcuMUw2OC42LDYwLjRsLTQuMS00LjFMOTcuOCwyM0wxMDEuOSwyNy4xeiIvPiAgICA8Zz4gICAgICAgPGc+ICAgICAgICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik03Mi40LDE0NC44QzMyLjUsMTQ0LjgsMCwxMTIuMywwLDcyLjRDMCwzMi41LDMyLjUsMCw3Mi40LDBzNzIuNCwzMi41LDcyLjQsNzIuNCAgICAgICAgICAgICBDMTQ0LjgsMTEyLjMsMTEyLjMsMTQ0LjgsNzIuNCwxNDQuOHogTTcyLjQsNy4zQzM2LjUsNy4zLDcuMywzNi41LDcuMyw3Mi40czI5LjIsNjUuMSw2NS4xLDY1LjFzNjUuMS0yOS4yLDY1LjEtNjUuMSAgICAgICAgICAgICBTMTA4LjMsNy4zLDcyLjQsNy4zeiIvPiAgICAgICA8L2c+ICAgIDwvZz48L2c+PC9zdmc+';
|
||||||
|
|
||||||
|
const { value = 0, votes = 0 } = ratings.trakt;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
anchor={
|
||||||
|
<span className={styles.wrapper}>
|
||||||
|
{!hideIcon && (
|
||||||
|
<img
|
||||||
|
className={styles.image}
|
||||||
|
alt={translate('TraktRating')}
|
||||||
|
src={traktImage}
|
||||||
|
style={{
|
||||||
|
height: `${iconSize}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(value * 10).toFixed()}%
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
tooltip={translate('CountVotes', { votes })}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TraktRating;
|
||||||
@@ -110,6 +110,15 @@ function DiscoverMovieSortMenu(props) {
|
|||||||
{translate('RottenTomatoesRating')}
|
{translate('RottenTomatoesRating')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
|
<SortMenuItem
|
||||||
|
name="traktRating"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('TraktRating')}
|
||||||
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="certification"
|
name="certification"
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Link from 'Components/Link/Link';
|
|||||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@@ -92,6 +93,7 @@ class DiscoverMoviePoster extends Component {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
ratings,
|
ratings,
|
||||||
isExisting,
|
isExisting,
|
||||||
isExcluded,
|
isExcluded,
|
||||||
@@ -223,6 +225,12 @@ class DiscoverMoviePoster extends Component {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showTraktRating && !!ratings.trakt ? (
|
||||||
|
<div className={styles.title}>
|
||||||
|
<TraktRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<DiscoverMoviePosterInfo
|
<DiscoverMoviePosterInfo
|
||||||
showRelativeDates={showRelativeDates}
|
showRelativeDates={showRelativeDates}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
@@ -232,6 +240,7 @@ class DiscoverMoviePoster extends Component {
|
|||||||
showTmdbRating={showTmdbRating}
|
showTmdbRating={showTmdbRating}
|
||||||
showImdbRating={showImdbRating}
|
showImdbRating={showImdbRating}
|
||||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||||
|
showTraktRating={showTraktRating}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -274,6 +283,7 @@ DiscoverMoviePoster.propTypes = {
|
|||||||
showTmdbRating: PropTypes.bool.isRequired,
|
showTmdbRating: PropTypes.bool.isRequired,
|
||||||
showImdbRating: PropTypes.bool.isRequired,
|
showImdbRating: PropTypes.bool.isRequired,
|
||||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||||
|
showTraktRating: PropTypes.bool.isRequired,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Icon from 'Components/Icon';
|
|||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||||
@@ -28,7 +29,8 @@ function DiscoverMoviePosterInfo(props) {
|
|||||||
movieRuntimeFormat,
|
movieRuntimeFormat,
|
||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (sortKey === 'status' && status) {
|
if (sortKey === 'status' && status) {
|
||||||
@@ -141,6 +143,14 @@ function DiscoverMoviePosterInfo(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!showTraktRating && sortKey === 'traktRating' && !!ratings.trakt) {
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
<TraktRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +170,8 @@ DiscoverMoviePosterInfo.propTypes = {
|
|||||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||||
showTmdbRating: PropTypes.bool.isRequired,
|
showTmdbRating: PropTypes.bool.isRequired,
|
||||||
showImdbRating: PropTypes.bool.isRequired,
|
showImdbRating: PropTypes.bool.isRequired,
|
||||||
showRottenTomatoesRating: PropTypes.bool.isRequired
|
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||||
|
showTraktRating: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DiscoverMoviePosterInfo;
|
export default DiscoverMoviePosterInfo;
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
|||||||
showTitle,
|
showTitle,
|
||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
const heights = [
|
const heights = [
|
||||||
@@ -64,6 +65,10 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTraktRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
switch (sortKey) {
|
switch (sortKey) {
|
||||||
case 'studio':
|
case 'studio':
|
||||||
case 'inCinemas':
|
case 'inCinemas':
|
||||||
@@ -88,6 +93,11 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'traktRating':
|
||||||
|
if (!showTraktRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// No need to add a height of 0
|
// No need to add a height of 0
|
||||||
}
|
}
|
||||||
@@ -219,7 +229,8 @@ class DiscoverMoviePosters extends Component {
|
|||||||
showTitle,
|
showTitle,
|
||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
const movieIdx = rowIndex * columnCount + columnIndex;
|
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||||
@@ -248,6 +259,7 @@ class DiscoverMoviePosters extends Component {
|
|||||||
showTmdbRating={showTmdbRating}
|
showTmdbRating={showTmdbRating}
|
||||||
showImdbRating={showImdbRating}
|
showImdbRating={showImdbRating}
|
||||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||||
|
showTraktRating={showTraktRating}
|
||||||
showRelativeDates={showRelativeDates}
|
showRelativeDates={showRelativeDates}
|
||||||
shortDateFormat={shortDateFormat}
|
shortDateFormat={shortDateFormat}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
showTmdbRating: props.showTmdbRating,
|
showTmdbRating: props.showTmdbRating,
|
||||||
showImdbRating: props.showImdbRating,
|
showImdbRating: props.showImdbRating,
|
||||||
showRottenTomatoesRating: props.showRottenTomatoesRating,
|
showRottenTomatoesRating: props.showRottenTomatoesRating,
|
||||||
|
showTraktRating: props.showTraktRating,
|
||||||
includeRecommendations: props.includeRecommendations,
|
includeRecommendations: props.includeRecommendations,
|
||||||
includeTrending: props.includeTrending,
|
includeTrending: props.includeTrending,
|
||||||
includePopular: props.includePopular
|
includePopular: props.includePopular
|
||||||
@@ -61,6 +62,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
includeRecommendations,
|
includeRecommendations,
|
||||||
includeTrending,
|
includeTrending,
|
||||||
includePopular
|
includePopular
|
||||||
@@ -88,6 +90,10 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
state.showRottenTomatoesRating = showRottenTomatoesRating;
|
state.showRottenTomatoesRating = showRottenTomatoesRating;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTraktRating !== prevProps.showTraktRating) {
|
||||||
|
state.showTraktRating = showTraktRating;
|
||||||
|
}
|
||||||
|
|
||||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||||
state.includeRecommendations = includeRecommendations;
|
state.includeRecommendations = includeRecommendations;
|
||||||
}
|
}
|
||||||
@@ -140,6 +146,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
includeRecommendations,
|
includeRecommendations,
|
||||||
includeTrending,
|
includeTrending,
|
||||||
includePopular
|
includePopular
|
||||||
@@ -248,6 +255,18 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
|||||||
onChange={this.onChangePosterOption}
|
onChange={this.onChangePosterOption}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowTraktRating')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showTraktRating"
|
||||||
|
value={showTraktRating}
|
||||||
|
helpText={translate('ShowTraktRatingPosterHelpText')}
|
||||||
|
onChange={this.onChangePosterOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</Form>
|
</Form>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
@@ -269,6 +288,7 @@ DiscoverMoviePosterOptionsModalContent.propTypes = {
|
|||||||
showTmdbRating: PropTypes.bool.isRequired,
|
showTmdbRating: PropTypes.bool.isRequired,
|
||||||
showImdbRating: PropTypes.bool.isRequired,
|
showImdbRating: PropTypes.bool.isRequired,
|
||||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||||
|
showTraktRating: PropTypes.bool.isRequired,
|
||||||
includeRecommendations: PropTypes.bool.isRequired,
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
includeTrending: PropTypes.bool.isRequired,
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
includePopular: PropTypes.bool.isRequired,
|
includePopular: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
.tmdbRating,
|
.tmdbRating,
|
||||||
.imdbRating,
|
.imdbRating,
|
||||||
.rottenTomatoesRating,
|
.rottenTomatoesRating,
|
||||||
|
.traktRating,
|
||||||
.runtime {
|
.runtime {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface CssExports {
|
|||||||
'status': string;
|
'status': string;
|
||||||
'studio': string;
|
'studio': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
|
'traktRating': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
.tmdbRating,
|
.tmdbRating,
|
||||||
.imdbRating,
|
.imdbRating,
|
||||||
.rottenTomatoesRating,
|
.rottenTomatoesRating,
|
||||||
|
.traktRating,
|
||||||
.runtime {
|
.runtime {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface CssExports {
|
|||||||
'statusIcon': string;
|
'statusIcon': string;
|
||||||
'studio': string;
|
'studio': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
|
'traktRating': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
|||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@@ -291,6 +292,17 @@ class DiscoverMovieRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'traktRating') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
>
|
||||||
|
{ratings.trakt ? <TraktRating ratings={ratings} /> : null}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'popularity') {
|
if (name === 'popularity') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
|
||||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from '../MovieCreditPoster.css';
|
|
||||||
|
|
||||||
class MovieCastPoster extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hasPosterError: false,
|
|
||||||
isEditImportListModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditImportListPress = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddImportListPress = () => {
|
|
||||||
this.props.onImportListSelect();
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditImportListModalClose = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoad = () => {
|
|
||||||
if (this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoadError = () => {
|
|
||||||
if (!this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tmdbId,
|
|
||||||
personName,
|
|
||||||
character,
|
|
||||||
images,
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
importList
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
hasPosterError
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const elementStyle = {
|
|
||||||
width: `${posterWidth}px`,
|
|
||||||
height: `${posterHeight}px`,
|
|
||||||
borderRadius: '5px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentStyle = {
|
|
||||||
width: `${posterWidth}px`
|
|
||||||
};
|
|
||||||
|
|
||||||
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
|
|
||||||
const importListId = importList ? importList.id : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.content}
|
|
||||||
style={contentStyle}
|
|
||||||
>
|
|
||||||
<div className={styles.posterContainer}>
|
|
||||||
<div className={styles.toggleMonitoredContainer}>
|
|
||||||
<MonitorToggleButton
|
|
||||||
className={styles.monitorToggleButton}
|
|
||||||
monitored={monitored}
|
|
||||||
size={20}
|
|
||||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Label className={styles.controls}>
|
|
||||||
<span className={styles.externalLinks}>
|
|
||||||
<Popover
|
|
||||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
|
||||||
title={translate('Links')}
|
|
||||||
body={
|
|
||||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
|
||||||
<Label
|
|
||||||
className={styles.externalLinkLabel}
|
|
||||||
kind={kinds.INFO}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
{translate('TMDb')}
|
|
||||||
</Label>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={elementStyle}
|
|
||||||
>
|
|
||||||
<MovieHeadshot
|
|
||||||
className={styles.poster}
|
|
||||||
style={elementStyle}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
lazy={false}
|
|
||||||
overflow={true}
|
|
||||||
onError={this.onPosterLoadError}
|
|
||||||
onLoad={this.onPosterLoad}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
hasPosterError &&
|
|
||||||
<div className={styles.overlayTitle}>
|
|
||||||
{personName}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
|
||||||
{personName}
|
|
||||||
</div>
|
|
||||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
|
||||||
{character}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EditImportListModalConnector
|
|
||||||
id={importListId}
|
|
||||||
isOpen={this.state.isEditImportListModalOpen}
|
|
||||||
onModalClose={this.onEditImportListModalClose}
|
|
||||||
onDeleteImportListPress={this.onDeleteImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCastPoster.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
personName: PropTypes.string.isRequired,
|
|
||||||
character: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
posterWidth: PropTypes.number.isRequired,
|
|
||||||
posterHeight: PropTypes.number.isRequired,
|
|
||||||
importList: PropTypes.object,
|
|
||||||
onImportListSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieCastPoster;
|
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||||
|
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||||
|
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||||
|
import ImportList from 'typings/ImportList';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from '../MovieCreditPoster.css';
|
||||||
|
|
||||||
|
export interface MovieCastPosterProps
|
||||||
|
extends Pick<MovieCredit, 'personName' | 'images' | 'character'> {
|
||||||
|
tmdbId: number;
|
||||||
|
posterWidth: number;
|
||||||
|
posterHeight: number;
|
||||||
|
importList?: ImportList;
|
||||||
|
onImportListSelect(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCastPoster(props: MovieCastPosterProps) {
|
||||||
|
const {
|
||||||
|
tmdbId,
|
||||||
|
personName,
|
||||||
|
character,
|
||||||
|
images = [],
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
importList,
|
||||||
|
onImportListSelect,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const importListId = importList?.id ?? 0;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [hasPosterError, setHasPosterError] = useState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isEditImportListModalOpen,
|
||||||
|
setEditImportListModalOpen,
|
||||||
|
setEditImportListModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isDeleteImportListModalOpen,
|
||||||
|
setDeleteImportListModalOpen,
|
||||||
|
setDeleteImportListModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const handlePosterLoadError = useCallback(() => {
|
||||||
|
setHasPosterError(true);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const handlePosterLoad = useCallback(() => {
|
||||||
|
setHasPosterError(false);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const handleManageImportListPress = useCallback(() => {
|
||||||
|
if (importListId === 0) {
|
||||||
|
onImportListSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditImportListModalOpen();
|
||||||
|
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
|
||||||
|
|
||||||
|
const handleDeleteImportListConfirmed = useCallback(() => {
|
||||||
|
dispatch(deleteImportList({ id: importListId }));
|
||||||
|
|
||||||
|
setEditImportListModalClosed();
|
||||||
|
setDeleteImportListModalClosed();
|
||||||
|
}, [
|
||||||
|
importListId,
|
||||||
|
setEditImportListModalClosed,
|
||||||
|
setDeleteImportListModalClosed,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitored =
|
||||||
|
importList?.enabled === true && importList?.enableAuto === true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content} style={contentStyle}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
<div className={styles.toggleMonitoredContainer}>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={20}
|
||||||
|
onPress={handleManageImportListPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Label className={styles.controls}>
|
||||||
|
<span className={styles.externalLinks}>
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||||
|
title={translate('Links')}
|
||||||
|
body={
|
||||||
|
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||||
|
<Label
|
||||||
|
className={styles.externalLinkLabel}
|
||||||
|
kind={kinds.INFO}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{translate('TMDb')}
|
||||||
|
</Label>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div style={elementStyle}>
|
||||||
|
<MovieHeadshot
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
onError={handlePosterLoadError}
|
||||||
|
onLoad={handlePosterLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasPosterError && (
|
||||||
|
<div className={styles.overlayTitle}>{personName}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
|
{personName}
|
||||||
|
</div>
|
||||||
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
|
{character}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditImportListModalConnector
|
||||||
|
id={importListId}
|
||||||
|
isOpen={isEditImportListModalOpen}
|
||||||
|
onModalClose={setEditImportListModalClosed}
|
||||||
|
onDeleteImportListPress={setDeleteImportListModalOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteImportListModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteImportList')}
|
||||||
|
message={translate('DeleteImportListMessageText', {
|
||||||
|
name: importList?.name ?? personName,
|
||||||
|
})}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={handleDeleteImportListConfirmed}
|
||||||
|
onCancel={setDeleteImportListModalClosed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCastPoster;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
|
||||||
|
import MovieCreditPosters from '../MovieCreditPosters';
|
||||||
|
import MovieCastPoster from './MovieCastPoster';
|
||||||
|
|
||||||
|
interface MovieCastPostersProps {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCastPosters({ isSmallScreen }: MovieCastPostersProps) {
|
||||||
|
const { items: castCredits } = useSelector(
|
||||||
|
createMovieCreditsSelector('cast')
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MovieCreditPosters
|
||||||
|
items={castCredits}
|
||||||
|
itemComponent={MovieCastPoster}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCastPosters;
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import MovieCreditPosters from '../MovieCreditPosters';
|
|
||||||
import MovieCastPoster from './MovieCastPoster';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCredits.items,
|
|
||||||
(credits) => {
|
|
||||||
const cast = _.reduce(credits, (acc, credit) => {
|
|
||||||
if (credit.type === 'cast') {
|
|
||||||
acc.push(credit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: cast
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieCastPostersConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MovieCreditPosters
|
|
||||||
{...this.props}
|
|
||||||
itemComponent={MovieCastPoster}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieCastPostersConnector);
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
|
||||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from '../MovieCreditPoster.css';
|
|
||||||
|
|
||||||
class MovieCrewPoster extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hasPosterError: false,
|
|
||||||
isEditImportListModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditImportListPress = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddImportListPress = () => {
|
|
||||||
this.props.onImportListSelect();
|
|
||||||
this.setState({ isEditImportListModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditImportListModalClose = () => {
|
|
||||||
this.setState({ isEditImportListModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoad = () => {
|
|
||||||
if (this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPosterLoadError = () => {
|
|
||||||
if (!this.state.hasPosterError) {
|
|
||||||
this.setState({ hasPosterError: true });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tmdbId,
|
|
||||||
personName,
|
|
||||||
job,
|
|
||||||
images,
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
importList
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
hasPosterError
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const elementStyle = {
|
|
||||||
width: `${posterWidth}px`,
|
|
||||||
height: `${posterHeight}px`,
|
|
||||||
borderRadius: '5px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentStyle = {
|
|
||||||
width: `${posterWidth}px`
|
|
||||||
};
|
|
||||||
|
|
||||||
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
|
|
||||||
const importListId = importList ? importList.id : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.content}
|
|
||||||
style={contentStyle}
|
|
||||||
>
|
|
||||||
<div className={styles.posterContainer}>
|
|
||||||
<div className={styles.toggleMonitoredContainer}>
|
|
||||||
<MonitorToggleButton
|
|
||||||
className={styles.monitorToggleButton}
|
|
||||||
monitored={monitored}
|
|
||||||
size={20}
|
|
||||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Label className={styles.controls}>
|
|
||||||
<span className={styles.externalLinks}>
|
|
||||||
<Popover
|
|
||||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
|
||||||
title={translate('Links')}
|
|
||||||
body={
|
|
||||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
|
||||||
<Label
|
|
||||||
className={styles.externalLinkLabel}
|
|
||||||
kind={kinds.INFO}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
{translate('TMDb')}
|
|
||||||
</Label>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={elementStyle}
|
|
||||||
>
|
|
||||||
<MovieHeadshot
|
|
||||||
className={styles.poster}
|
|
||||||
style={elementStyle}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
lazy={false}
|
|
||||||
overflow={true}
|
|
||||||
onError={this.onPosterLoadError}
|
|
||||||
onLoad={this.onPosterLoad}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
hasPosterError &&
|
|
||||||
<div className={styles.overlayTitle}>
|
|
||||||
{personName}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
|
||||||
{personName}
|
|
||||||
</div>
|
|
||||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
|
||||||
{job}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EditImportListModalConnector
|
|
||||||
id={importListId}
|
|
||||||
isOpen={this.state.isEditImportListModalOpen}
|
|
||||||
onModalClose={this.onEditImportListModalClose}
|
|
||||||
onDeleteImportListPress={this.onDeleteImportListPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCrewPoster.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
personName: PropTypes.string.isRequired,
|
|
||||||
job: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
posterWidth: PropTypes.number.isRequired,
|
|
||||||
posterHeight: PropTypes.number.isRequired,
|
|
||||||
importList: PropTypes.object,
|
|
||||||
onImportListSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieCrewPoster;
|
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||||
|
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||||
|
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||||
|
import ImportList from 'typings/ImportList';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from '../MovieCreditPoster.css';
|
||||||
|
|
||||||
|
export interface MovieCrewPosterProps
|
||||||
|
extends Pick<MovieCredit, 'personName' | 'images' | 'job'> {
|
||||||
|
tmdbId: number;
|
||||||
|
posterWidth: number;
|
||||||
|
posterHeight: number;
|
||||||
|
importList?: ImportList;
|
||||||
|
onImportListSelect(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCrewPoster(props: MovieCrewPosterProps) {
|
||||||
|
const {
|
||||||
|
tmdbId,
|
||||||
|
personName,
|
||||||
|
job,
|
||||||
|
images = [],
|
||||||
|
posterWidth,
|
||||||
|
posterHeight,
|
||||||
|
importList,
|
||||||
|
onImportListSelect,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const importListId = importList?.id ?? 0;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [hasPosterError, setHasPosterError] = useState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isEditImportListModalOpen,
|
||||||
|
setEditImportListModalOpen,
|
||||||
|
setEditImportListModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isDeleteImportListModalOpen,
|
||||||
|
setDeleteImportListModalOpen,
|
||||||
|
setDeleteImportListModalClosed,
|
||||||
|
] = useModalOpenState(false);
|
||||||
|
|
||||||
|
const handlePosterLoadError = useCallback(() => {
|
||||||
|
setHasPosterError(true);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const handlePosterLoad = useCallback(() => {
|
||||||
|
setHasPosterError(false);
|
||||||
|
}, [setHasPosterError]);
|
||||||
|
|
||||||
|
const handleManageImportListPress = useCallback(() => {
|
||||||
|
if (importListId === 0) {
|
||||||
|
onImportListSelect();
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditImportListModalOpen();
|
||||||
|
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
|
||||||
|
|
||||||
|
const handleDeleteImportListConfirmed = useCallback(() => {
|
||||||
|
dispatch(deleteImportList({ id: importListId }));
|
||||||
|
|
||||||
|
setEditImportListModalClosed();
|
||||||
|
setDeleteImportListModalClosed();
|
||||||
|
}, [
|
||||||
|
importListId,
|
||||||
|
setEditImportListModalClosed,
|
||||||
|
setDeleteImportListModalClosed,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px',
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentStyle = {
|
||||||
|
width: `${posterWidth}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitored =
|
||||||
|
importList?.enabled === true && importList?.enableAuto === true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content} style={contentStyle}>
|
||||||
|
<div className={styles.posterContainer}>
|
||||||
|
<div className={styles.toggleMonitoredContainer}>
|
||||||
|
<MonitorToggleButton
|
||||||
|
className={styles.monitorToggleButton}
|
||||||
|
monitored={monitored}
|
||||||
|
size={20}
|
||||||
|
onPress={handleManageImportListPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Label className={styles.controls}>
|
||||||
|
<span className={styles.externalLinks}>
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||||
|
title={translate('Links')}
|
||||||
|
body={
|
||||||
|
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||||
|
<Label
|
||||||
|
className={styles.externalLinkLabel}
|
||||||
|
kind={kinds.INFO}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
{translate('TMDb')}
|
||||||
|
</Label>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div style={elementStyle}>
|
||||||
|
<MovieHeadshot
|
||||||
|
className={styles.poster}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
onError={handlePosterLoadError}
|
||||||
|
onLoad={handlePosterLoad}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{hasPosterError && (
|
||||||
|
<div className={styles.overlayTitle}>{personName}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||||
|
{personName}
|
||||||
|
</div>
|
||||||
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
|
||||||
|
|
||||||
|
<EditImportListModalConnector
|
||||||
|
id={importListId}
|
||||||
|
isOpen={isEditImportListModalOpen}
|
||||||
|
onModalClose={setEditImportListModalClosed}
|
||||||
|
onDeleteImportListPress={setDeleteImportListModalOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteImportListModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('DeleteImportList')}
|
||||||
|
message={translate('DeleteImportListMessageText', {
|
||||||
|
name: importList?.name ?? personName,
|
||||||
|
})}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
onConfirm={handleDeleteImportListConfirmed}
|
||||||
|
onCancel={setDeleteImportListModalClosed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCrewPoster;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
|
||||||
|
import MovieCreditPosters from '../MovieCreditPosters';
|
||||||
|
import MovieCrewPoster from './MovieCrewPoster';
|
||||||
|
|
||||||
|
interface MovieCrewPostersProps {
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCrewPosters({ isSmallScreen }: MovieCrewPostersProps) {
|
||||||
|
const { items: crewCredits } = useSelector(
|
||||||
|
createMovieCreditsSelector('crew')
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MovieCreditPosters
|
||||||
|
items={crewCredits}
|
||||||
|
itemComponent={MovieCrewPoster}
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCrewPosters;
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import MovieCreditPosters from '../MovieCreditPosters';
|
|
||||||
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() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCredits.items,
|
|
||||||
(credits) => {
|
|
||||||
const crew = _.reduce(credits, (acc, credit) => {
|
|
||||||
if (credit.type === 'crew') {
|
|
||||||
acc.push(credit);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sortedCrew = crew.sort(crewSort);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: _.uniqBy(sortedCrew, 'personName')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieCrewPostersConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MovieCreditPosters
|
|
||||||
{...this.props}
|
|
||||||
itemComponent={MovieCrewPoster}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieCrewPostersConnector);
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectImportListSchema,
|
||||||
|
setImportListFieldValue,
|
||||||
|
setImportListValue,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import createMovieCreditImportListSelector from 'Store/Selectors/createMovieCreditImportListSelector';
|
||||||
|
import { MovieCastPosterProps } from './Cast/MovieCastPoster';
|
||||||
|
import { MovieCrewPosterProps } from './Crew/MovieCrewPoster';
|
||||||
|
|
||||||
|
type MovieCreditPosterProps = {
|
||||||
|
component: React.ElementType;
|
||||||
|
} & (
|
||||||
|
| Omit<MovieCrewPosterProps, 'onImportListSelect'>
|
||||||
|
| Omit<MovieCastPosterProps, 'onImportListSelect'>
|
||||||
|
);
|
||||||
|
|
||||||
|
function MovieCreditPoster({
|
||||||
|
component: ItemComponent,
|
||||||
|
tmdbId,
|
||||||
|
personName,
|
||||||
|
...otherProps
|
||||||
|
}: MovieCreditPosterProps) {
|
||||||
|
const importList = useSelector(createMovieCreditImportListSelector(tmdbId));
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleImportListSelect = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
selectImportListSchema({
|
||||||
|
implementation: 'TMDbPersonImport',
|
||||||
|
implementationName: 'TMDb Person',
|
||||||
|
presetName: undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error 'setImportListFieldValue' isn't typed yet
|
||||||
|
setImportListFieldValue({ name: 'personId', value: tmdbId.toString() })
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
// @ts-expect-error 'setImportListValue' isn't typed yet
|
||||||
|
setImportListValue({ name: 'name', value: `${personName} - ${tmdbId}` })
|
||||||
|
);
|
||||||
|
}, [dispatch, tmdbId, personName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemComponent
|
||||||
|
{...otherProps}
|
||||||
|
tmdbId={tmdbId}
|
||||||
|
personName={personName}
|
||||||
|
importList={importList}
|
||||||
|
onImportListSelect={handleImportListSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCreditPoster;
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
|
|
||||||
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieCreditListSelector(),
|
|
||||||
(importList) => {
|
|
||||||
return {
|
|
||||||
importList
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
selectImportListSchema,
|
|
||||||
setImportListFieldValue,
|
|
||||||
setImportListValue
|
|
||||||
};
|
|
||||||
|
|
||||||
class MovieCreditPosterConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onImportListSelect = () => {
|
|
||||||
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', implementationName: 'TMDb Person', presetName: undefined });
|
|
||||||
this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
|
|
||||||
this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tmdbId,
|
|
||||||
component: ItemComponent,
|
|
||||||
personName
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ItemComponent
|
|
||||||
{...this.props}
|
|
||||||
tmdbId={tmdbId}
|
|
||||||
personName={personName}
|
|
||||||
onImportListSelect={this.onImportListSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCreditPosterConnector.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
personName: PropTypes.string.isRequired,
|
|
||||||
component: PropTypes.elementType.isRequired,
|
|
||||||
selectImportListSchema: PropTypes.func.isRequired,
|
|
||||||
setImportListFieldValue: PropTypes.func.isRequired,
|
|
||||||
setImportListValue: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCreditPosterConnector);
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Navigation } from 'swiper';
|
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
|
|
||||||
import styles from './MovieCreditPosters.css';
|
|
||||||
|
|
||||||
// Import Swiper styles
|
|
||||||
import 'swiper/css';
|
|
||||||
import 'swiper/css/navigation';
|
|
||||||
|
|
||||||
// Poster container dimensions
|
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
|
||||||
|
|
||||||
function calculateRowHeight(posterHeight, isSmallScreen) {
|
|
||||||
const titleHeight = 19;
|
|
||||||
const characterHeight = 19;
|
|
||||||
|
|
||||||
const heights = [
|
|
||||||
posterHeight,
|
|
||||||
titleHeight,
|
|
||||||
characterHeight,
|
|
||||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
|
||||||
];
|
|
||||||
|
|
||||||
return heights.reduce((acc, height) => acc + height, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieCreditPosters extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
width: 0,
|
|
||||||
columnWidth: 182,
|
|
||||||
columnCount: 1,
|
|
||||||
posterWidth: 162,
|
|
||||||
posterHeight: 238,
|
|
||||||
rowHeight: calculateRowHeight(238, props.isSmallScreen)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
itemComponent,
|
|
||||||
isSmallScreen
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
posterWidth,
|
|
||||||
posterHeight,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
|
|
||||||
<div className={styles.sliderContainer}>
|
|
||||||
<Swiper
|
|
||||||
slidesPerView='auto'
|
|
||||||
spaceBetween={10}
|
|
||||||
slidesPerGroup={isSmallScreen ? 1 : 3}
|
|
||||||
navigation={true}
|
|
||||||
loop={false}
|
|
||||||
loopFillGroupWithBlank={true}
|
|
||||||
className="mySwiper"
|
|
||||||
modules={[Navigation]}
|
|
||||||
onInit={(swiper) => {
|
|
||||||
swiper.navigation.init();
|
|
||||||
swiper.navigation.update();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map((credit) => (
|
|
||||||
<SwiperSlide key={credit.id} style={{ width: posterWidth, height: rowHeight }}>
|
|
||||||
<MovieCreditPosterConnector
|
|
||||||
key={credit.id}
|
|
||||||
component={itemComponent}
|
|
||||||
posterWidth={posterWidth}
|
|
||||||
posterHeight={posterHeight}
|
|
||||||
tmdbId={credit.personTmdbId}
|
|
||||||
personName={credit.personName}
|
|
||||||
job={credit.job}
|
|
||||||
character={credit.character}
|
|
||||||
images={credit.images}
|
|
||||||
/>
|
|
||||||
</SwiperSlide>
|
|
||||||
))}
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieCreditPosters.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
itemComponent: PropTypes.elementType.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieCreditPosters;
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Navigation } from 'swiper';
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import { Swiper as SwiperClass } from 'swiper/types';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
import MovieCreditPoster from './MovieCreditPoster';
|
||||||
|
import styles from './MovieCreditPosters.css';
|
||||||
|
|
||||||
|
// Import Swiper styles
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
|
||||||
|
// Poster container dimensions
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.movieIndexColumnPaddingSmallScreen
|
||||||
|
);
|
||||||
|
|
||||||
|
interface MovieCreditPostersProps {
|
||||||
|
items: MovieCredit[];
|
||||||
|
itemComponent: React.ElementType;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieCreditPosters(props: MovieCreditPostersProps) {
|
||||||
|
const { items, itemComponent, isSmallScreen } = props;
|
||||||
|
|
||||||
|
const posterWidth = 162;
|
||||||
|
const posterHeight = 238;
|
||||||
|
|
||||||
|
const rowHeight = useMemo(() => {
|
||||||
|
const titleHeight = 19;
|
||||||
|
const characterHeight = 19;
|
||||||
|
|
||||||
|
const heights = [
|
||||||
|
posterHeight,
|
||||||
|
titleHeight,
|
||||||
|
characterHeight,
|
||||||
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
|
||||||
|
];
|
||||||
|
|
||||||
|
return heights.reduce((acc, height) => acc + height, 0);
|
||||||
|
}, [posterHeight, isSmallScreen]);
|
||||||
|
|
||||||
|
const handleSwiperInit = useCallback((swiper: SwiperClass) => {
|
||||||
|
swiper.navigation.init();
|
||||||
|
swiper.navigation.update();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.sliderContainer}>
|
||||||
|
<Swiper
|
||||||
|
slidesPerView="auto"
|
||||||
|
spaceBetween={10}
|
||||||
|
slidesPerGroup={isSmallScreen ? 1 : 3}
|
||||||
|
navigation={true}
|
||||||
|
loop={false}
|
||||||
|
loopFillGroupWithBlank={true}
|
||||||
|
className="mySwiper"
|
||||||
|
modules={[Navigation]}
|
||||||
|
onInit={handleSwiperInit}
|
||||||
|
>
|
||||||
|
{items.map((credit) => (
|
||||||
|
<SwiperSlide
|
||||||
|
key={credit.id}
|
||||||
|
style={{ width: posterWidth, height: rowHeight }}
|
||||||
|
>
|
||||||
|
<MovieCreditPoster
|
||||||
|
key={credit.id}
|
||||||
|
component={itemComponent}
|
||||||
|
posterWidth={posterWidth}
|
||||||
|
posterHeight={posterHeight}
|
||||||
|
tmdbId={credit.personTmdbId}
|
||||||
|
personName={credit.personName}
|
||||||
|
images={credit.images}
|
||||||
|
job={credit.job}
|
||||||
|
character={credit.character}
|
||||||
|
/>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCreditPosters;
|
||||||
@@ -20,12 +20,14 @@ import RottenTomatoRating from 'Components/RottenTomatoRating';
|
|||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
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 DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||||
|
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
||||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||||
@@ -37,9 +39,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 MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
import MovieCastPosters from './Credits/Cast/MovieCastPosters';
|
||||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
import MovieCrewPosters from './Credits/Crew/MovieCrewPosters';
|
||||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
|
||||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||||
import MovieReleaseDates from './MovieReleaseDates';
|
import MovieReleaseDates from './MovieReleaseDates';
|
||||||
import MovieStatusLabel from './MovieStatusLabel';
|
import MovieStatusLabel from './MovieStatusLabel';
|
||||||
@@ -497,31 +498,44 @@ class MovieDetails extends Component {
|
|||||||
|
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
{
|
{
|
||||||
!!ratings.tmdb &&
|
ratings.tmdb ?
|
||||||
<span className={styles.rating}>
|
<span className={styles.rating}>
|
||||||
<TmdbRating
|
<TmdbRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!!ratings.imdb &&
|
ratings.imdb ?
|
||||||
<span className={styles.rating}>
|
<span className={styles.rating}>
|
||||||
<ImdbRating
|
<ImdbRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!!ratings.rottenTomatoes &&
|
ratings.rottenTomatoes ?
|
||||||
<span className={styles.rating}>
|
<span className={styles.rating}>
|
||||||
<RottenTomatoRating
|
<RottenTomatoRating
|
||||||
ratings={ratings}
|
ratings={ratings}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
{
|
||||||
|
ratings.trakt ?
|
||||||
|
<span className={styles.rating}>
|
||||||
|
<TraktRating
|
||||||
|
ratings={ratings}
|
||||||
|
iconSize={20}
|
||||||
|
/>
|
||||||
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -685,13 +699,13 @@ class MovieDetails extends Component {
|
|||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Cast')}>
|
<FieldSet legend={translate('Cast')}>
|
||||||
<MovieCastPostersConnector
|
<MovieCastPosters
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
/>
|
/>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Crew')}>
|
<FieldSet legend={translate('Crew')}>
|
||||||
<MovieCrewPostersConnector
|
<MovieCrewPosters
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
/>
|
/>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
|
|
||||||
class MovieTitlesRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
sourceType
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
|
|
||||||
<TableRowCell>
|
|
||||||
{title}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell>
|
|
||||||
{titleCase(sourceType)}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieTitlesRow.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
sourceType: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieTitlesRow;
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
|
||||||
|
interface MovieTitlesRowProps {
|
||||||
|
title: string;
|
||||||
|
sourceType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieTitlesRow({ title, sourceType }: MovieTitlesRowProps) {
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableRowCell>{title}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>{titleCase(sourceType)}</TableRowCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieTitlesRow;
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
.blankpad {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
border: 1px solid var(--borderColor);
|
border: 1px solid var(--borderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// 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 {
|
||||||
|
'blankpad': string;
|
||||||
'container': string;
|
'container': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
|
|
||||||
import styles from './MovieTitlesTable.css';
|
|
||||||
|
|
||||||
function MovieTitlesTable(props) {
|
|
||||||
const {
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<MovieTitlesTableContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieTitlesTable.propTypes = {
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieTitlesTable;
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import MovieTitlesRow from './MovieTitlesRow';
|
||||||
|
import styles from './MovieTitlesTable.css';
|
||||||
|
|
||||||
|
const columns: Column[] = [
|
||||||
|
{
|
||||||
|
name: 'alternativeTitle',
|
||||||
|
label: () => translate('AlternativeTitle'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceType',
|
||||||
|
label: () => translate('Type'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function movieAlternativeTitlesSelector(movieId: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.movies,
|
||||||
|
(movies) => {
|
||||||
|
const { isFetching, isPopulated, error, items } = movies;
|
||||||
|
|
||||||
|
const alternateTitles =
|
||||||
|
items.find((m) => m.id === movieId)?.alternateTitles ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items: alternateTitles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieTitlesProps {
|
||||||
|
movieId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieTitlesTable({ movieId }: MovieTitlesProps) {
|
||||||
|
const { isFetching, isPopulated, error, items } = useSelector(
|
||||||
|
movieAlternativeTitlesSelector(movieId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedItems = items.sort(sortByProp('title'));
|
||||||
|
|
||||||
|
if (!isFetching && !!error) {
|
||||||
|
return (
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('AlternativeTitlesLoadError')}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{isFetching && <LoadingIndicator />}
|
||||||
|
|
||||||
|
{isPopulated && !items.length && !error ? (
|
||||||
|
<div className={styles.blankpad}>
|
||||||
|
{translate('NoAlternativeTitles')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPopulated && !!items.length && !error ? (
|
||||||
|
<Table columns={columns}>
|
||||||
|
<TableBody>
|
||||||
|
{sortedItems.map((item) => (
|
||||||
|
<MovieTitlesRow
|
||||||
|
key={item.id}
|
||||||
|
title={item.title}
|
||||||
|
sourceType={item.sourceType}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieTitlesTable;
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
.blankpad {
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import MovieTitlesRow from './MovieTitlesRow';
|
|
||||||
import styles from './MovieTitlesTableContent.css';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'altTitle',
|
|
||||||
label: () => translate('AlternativeTitle'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sourceType',
|
|
||||||
label: () => translate('Type'),
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class MovieTitlesTableContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const hasItems = !!items.length;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<div className={styles.blankpad}>
|
|
||||||
{translate('UnableToLoadAltTitle')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !hasItems && !error &&
|
|
||||||
<div className={styles.blankpad}>
|
|
||||||
{translate('NoAltTitle')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && hasItems && !error &&
|
|
||||||
<Table columns={columns}>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.reverse().map((item) => {
|
|
||||||
return (
|
|
||||||
<MovieTitlesRow
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieTitlesTableContent.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieTitlesTableContent;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import MovieTitlesTableContent from './MovieTitlesTableContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { movieId }) => movieId,
|
|
||||||
(state) => state.movies,
|
|
||||||
(movieId, movies) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = movies;
|
|
||||||
|
|
||||||
const alternateTitles = items.find((m) => m.id === movieId)?.alternateTitles;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
alternateTitles
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MovieTitlesTableContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
alternateTitles,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MovieTitlesTableContent
|
|
||||||
{...otherProps}
|
|
||||||
items={alternateTitles}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieTitlesTableContentConnector.propTypes = {
|
|
||||||
movieId: PropTypes.number.isRequired,
|
|
||||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MovieTitlesTableContentConnector.defaultProps = {
|
|
||||||
alternateTitles: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieTitlesTableContentConnector);
|
|
||||||
@@ -4,8 +4,6 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tagInternalInput {
|
.labelIcon {
|
||||||
composes: internalInput from '~Components/Form/TagInput.css';
|
margin-left: 8px;
|
||||||
|
|
||||||
flex: 0 0 100%;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'deleteButton': string;
|
'deleteButton': string;
|
||||||
'tagInternalInput': string;
|
'labelIcon': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditMovieModalContent.css';
|
import styles from './EditMovieModalContent.css';
|
||||||
@@ -103,7 +106,21 @@ class EditMovieModalContent extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
|||||||
@@ -136,6 +136,15 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
|
|||||||
{translate('RottenTomatoesRating')}
|
{translate('RottenTomatoesRating')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
|
<SortMenuItem
|
||||||
|
name="traktRating"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('TraktRating')}
|
||||||
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="popularity"
|
name="popularity"
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import RottenTomatoRating from 'Components/RottenTomatoRating';
|
|||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
@@ -54,6 +55,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
showTags,
|
showTags,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
} = useSelector(selectPosterOptions);
|
} = useSelector(selectPosterOptions);
|
||||||
@@ -310,6 +312,12 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{showTraktRating && !!ratings.trakt ? (
|
||||||
|
<div className={styles.title}>
|
||||||
|
<TraktRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{showTags && tags.length ? (
|
{showTags && tags.length ? (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
<div className={styles.tagsList}>
|
<div className={styles.tagsList}>
|
||||||
@@ -347,6 +355,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
showTmdbRating={showTmdbRating}
|
showTmdbRating={showTmdbRating}
|
||||||
showImdbRating={showImdbRating}
|
showImdbRating={showImdbRating}
|
||||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||||
|
showTraktRating={showTraktRating}
|
||||||
showTags={showTags}
|
showTags={showTags}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ImdbRating from 'Components/ImdbRating';
|
|||||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import { Ratings } from 'Movie/Movie';
|
import { Ratings } from 'Movie/Movie';
|
||||||
@@ -43,6 +44,7 @@ interface MovieIndexPosterInfoProps {
|
|||||||
showTmdbRating: boolean;
|
showTmdbRating: boolean;
|
||||||
showImdbRating: boolean;
|
showImdbRating: boolean;
|
||||||
showRottenTomatoesRating: boolean;
|
showRottenTomatoesRating: boolean;
|
||||||
|
showTraktRating: boolean;
|
||||||
showTags: boolean;
|
showTags: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
showTags,
|
showTags,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -221,6 +224,14 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!showTraktRating && sortKey === 'traktRating' && !!ratings.trakt) {
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
<TraktRating ratings={ratings} iconSize={12} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!showTags && sortKey === 'tags' && tags.length) {
|
if (!showTags && sortKey === 'tags' && tags.length) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.tags}>
|
<div className={styles.tags}>
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
showTags,
|
showTags,
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
|
|
||||||
@@ -199,6 +200,10 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showTraktRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
if (showTags) {
|
if (showTags) {
|
||||||
heights.push(21);
|
heights.push(21);
|
||||||
}
|
}
|
||||||
@@ -253,6 +258,11 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
|||||||
heights.push(19);
|
heights.push(19);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'traktRating':
|
||||||
|
if (!showTraktRating) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'tags':
|
case 'tags':
|
||||||
if (!showTags) {
|
if (!showTags) {
|
||||||
heights.push(21);
|
heights.push(21);
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
showTmdbRating,
|
showTmdbRating,
|
||||||
showImdbRating,
|
showImdbRating,
|
||||||
showRottenTomatoesRating,
|
showRottenTomatoesRating,
|
||||||
|
showTraktRating,
|
||||||
showTags,
|
showTags,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
} = posterOptions;
|
} = posterOptions;
|
||||||
@@ -222,6 +223,18 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
|
<FormLabel>{translate('ShowTraktRating')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showTraktRating"
|
||||||
|
value={showTraktRating}
|
||||||
|
helpText={translate('ShowTraktRatingPosterHelpText')}
|
||||||
|
onChange={onPosterOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>{translate('ShowTags')}</FormLabel>
|
<FormLabel>{translate('ShowTags')}</FormLabel>
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,8 @@
|
|||||||
|
|
||||||
.imdbRating,
|
.imdbRating,
|
||||||
.tmdbRating,
|
.tmdbRating,
|
||||||
.rottenTomatoesRating {
|
.rottenTomatoesRating,
|
||||||
|
.traktRating {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 80px;
|
flex: 0 0 80px;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ interface CssExports {
|
|||||||
'studio': string;
|
'studio': string;
|
||||||
'tags': string;
|
'tags': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
|
'traktRating': string;
|
||||||
'year': string;
|
'year': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import Column from 'Components/Table/Column';
|
|||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import TraktRating from 'Components/TraktRating';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
@@ -387,6 +388,14 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'traktRating') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{ratings.trakt ? <TraktRating ratings={ratings} /> : null}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'popularity') {
|
if (name === 'popularity') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
|||||||
@@ -82,7 +82,8 @@
|
|||||||
|
|
||||||
.imdbRating,
|
.imdbRating,
|
||||||
.tmdbRating,
|
.tmdbRating,
|
||||||
.rottenTomatoesRating {
|
.rottenTomatoesRating,
|
||||||
|
.traktRating {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 80px;
|
flex: 0 0 80px;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ interface CssExports {
|
|||||||
'studio': string;
|
'studio': string;
|
||||||
'tags': string;
|
'tags': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
|
'traktRating': string;
|
||||||
'year': string;
|
'year': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type MovieStatus =
|
|||||||
| 'released'
|
| 'released'
|
||||||
| 'deleted';
|
| 'deleted';
|
||||||
|
|
||||||
export type CoverType = 'poster' | 'fanart';
|
export type CoverType = 'poster' | 'fanart' | 'headshot';
|
||||||
|
|
||||||
export interface Image {
|
export interface Image {
|
||||||
coverType: CoverType;
|
coverType: CoverType;
|
||||||
@@ -37,6 +37,12 @@ export interface Ratings {
|
|||||||
tmdb: RatingValues;
|
tmdb: RatingValues;
|
||||||
metacritic: RatingValues;
|
metacritic: RatingValues;
|
||||||
rottenTomatoes: RatingValues;
|
rottenTomatoes: RatingValues;
|
||||||
|
trakt: RatingValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlternativeTitle extends ModelBase {
|
||||||
|
sourceType: string;
|
||||||
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Movie extends ModelBase {
|
interface Movie extends ModelBase {
|
||||||
@@ -52,6 +58,7 @@ interface Movie extends ModelBase {
|
|||||||
originalTitle: string;
|
originalTitle: string;
|
||||||
originalLanguage: Language;
|
originalLanguage: Language;
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
|
alternateTitles: AlternativeTitle[];
|
||||||
studio: string;
|
studio: string;
|
||||||
qualityProfileId: number;
|
qualityProfileId: number;
|
||||||
added: string;
|
added: string;
|
||||||
@@ -72,6 +79,7 @@ interface Movie extends ModelBase {
|
|||||||
images: Image[];
|
images: Image[];
|
||||||
movieFile: MovieFile;
|
movieFile: MovieFile;
|
||||||
hasFile: boolean;
|
hasFile: boolean;
|
||||||
|
lastSearchTime?: string;
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import MovieImage from './MovieImage';
|
|
||||||
|
|
||||||
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
|
|
||||||
|
|
||||||
function MovieHeadshot(props) {
|
|
||||||
return (
|
|
||||||
<MovieImage
|
|
||||||
{...props}
|
|
||||||
coverType="headshot"
|
|
||||||
placeholder={posterPlaceholder}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieHeadshot.propTypes = {
|
|
||||||
size: PropTypes.number.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MovieHeadshot.defaultProps = {
|
|
||||||
size: 250
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieHeadshot;
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MovieImage, { MovieImageProps } from './MovieImage';
|
||||||
|
|
||||||
|
const posterPlaceholder =
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
|
||||||
|
|
||||||
|
interface MovieHeadshotProps
|
||||||
|
extends Omit<MovieImageProps, 'coverType' | 'placeholder'> {
|
||||||
|
size?: 250 | 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieHeadshot({ size = 250, ...otherProps }: MovieHeadshotProps) {
|
||||||
|
return (
|
||||||
|
<MovieImage
|
||||||
|
{...otherProps}
|
||||||
|
size={size}
|
||||||
|
coverType="headshot"
|
||||||
|
placeholder={posterPlaceholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieHeadshot;
|
||||||
@@ -43,7 +43,7 @@ function MovieImage({
|
|||||||
}: MovieImageProps) {
|
}: MovieImageProps) {
|
||||||
const [url, setUrl] = useState<string | null>(null);
|
const [url, setUrl] = useState<string | null>(null);
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
const [isLoaded, setIsLoaded] = useState(false);
|
const [isLoaded, setIsLoaded] = useState(true);
|
||||||
const image = useRef<Image | null>(null);
|
const image = useRef<Image | null>(null);
|
||||||
|
|
||||||
const handleLoad = useCallback(() => {
|
const handleLoad = useCallback(() => {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import styles from './ImportListExclusions.css';
|
|||||||
|
|
||||||
const COLUMNS: Column[] = [
|
const COLUMNS: Column[] = [
|
||||||
{
|
{
|
||||||
name: 'tmdbid',
|
name: 'tmdbId',
|
||||||
label: () => translate('TMDBId'),
|
label: () => translate('TMDBId'),
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.labelIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
composes: alert from '~Components/Alert.css';
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'deleteButton': string;
|
'deleteButton': string;
|
||||||
|
'labelIcon': string;
|
||||||
'message': string;
|
'message': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
@@ -13,7 +15,8 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@@ -160,12 +163,28 @@ function EditImportListModalContent(props) {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
{...minimumAvailability}
|
{...minimumAvailability}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -174,24 +174,21 @@ class MediaManagement extends Component {
|
|||||||
<FieldSet
|
<FieldSet
|
||||||
legend={translate('Importing')}
|
legend={translate('Importing')}
|
||||||
>
|
>
|
||||||
{
|
<FormGroup
|
||||||
!isWindows &&
|
advancedSettings={advancedSettings}
|
||||||
<FormGroup
|
isAdvanced={true}
|
||||||
advancedSettings={advancedSettings}
|
size={sizes.MEDIUM}
|
||||||
isAdvanced={true}
|
>
|
||||||
size={sizes.MEDIUM}
|
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||||
>
|
|
||||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="skipFreeSpaceCheckWhenImporting"
|
name="skipFreeSpaceCheckWhenImporting"
|
||||||
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
|
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ class EditQualityProfileModalContent extends Component {
|
|||||||
upgradeAllowed,
|
upgradeAllowed,
|
||||||
cutoff,
|
cutoff,
|
||||||
minFormatScore,
|
minFormatScore,
|
||||||
|
minUpgradeFormatScore,
|
||||||
cutoffFormatScore,
|
cutoffFormatScore,
|
||||||
language,
|
language,
|
||||||
items,
|
items,
|
||||||
@@ -249,6 +250,25 @@ class EditQualityProfileModalContent extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
upgradeAllowed.value && formatItems.value.length > 0 ?
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
{translate('MinimumCustomFormatScoreIncrement')}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="minUpgradeFormatScore"
|
||||||
|
min={1}
|
||||||
|
{...minUpgradeFormatScore}
|
||||||
|
helpText={translate('MinimumCustomFormatScoreIncrementHelpText')}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
<FormLabel size={sizes.SMALL}>
|
<FormLabel size={sizes.SMALL}>
|
||||||
{translate('Language')}
|
{translate('Language')}
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ export const defaultState = {
|
|||||||
showTitle: false,
|
showTitle: false,
|
||||||
showTmdbRating: false,
|
showTmdbRating: false,
|
||||||
showImdbRating: false,
|
showImdbRating: false,
|
||||||
showRottenTomatoesRating: false
|
showRottenTomatoesRating: false,
|
||||||
|
showTraktRating: false
|
||||||
},
|
},
|
||||||
|
|
||||||
overviewOptions: {
|
overviewOptions: {
|
||||||
@@ -180,6 +181,12 @@ export const defaultState = {
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'traktRating',
|
||||||
|
label: () => translate('TraktRating'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'popularity',
|
name: 'popularity',
|
||||||
label: () => translate('Popularity'),
|
label: () => translate('Popularity'),
|
||||||
@@ -293,6 +300,10 @@ export const defaultState = {
|
|||||||
|
|
||||||
rottenTomatoesRating: function({ ratings = {} }) {
|
rottenTomatoesRating: function({ ratings = {} }) {
|
||||||
return ratings.rottenTomatoes ? ratings.rottenTomatoes.value : -1;
|
return ratings.rottenTomatoes ? ratings.rottenTomatoes.value : -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
traktRating: function({ ratings = {} }) {
|
||||||
|
return ratings.trakt ? ratings.trakt.value : 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -482,6 +493,16 @@ export const defaultState = {
|
|||||||
label: () => translate('ImdbVotes'),
|
label: () => translate('ImdbVotes'),
|
||||||
type: filterBuilderTypes.NUMBER
|
type: filterBuilderTypes.NUMBER
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'traktRating',
|
||||||
|
label: () => translate('TraktRating'),
|
||||||
|
type: filterBuilderTypes.NUMBER
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'traktVotes',
|
||||||
|
label: () => translate('TraktVotes'),
|
||||||
|
type: filterBuilderTypes.NUMBER
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'popularity',
|
name: 'popularity',
|
||||||
label: () => translate('Popularity'),
|
label: () => translate('Popularity'),
|
||||||
|
|||||||
@@ -156,42 +156,58 @@ export const filterPredicates = {
|
|||||||
return dateFilterPredicate(item.digitalRelease, filterValue, type);
|
return dateFilterPredicate(item.digitalRelease, filterValue, type);
|
||||||
},
|
},
|
||||||
|
|
||||||
tmdbRating: function(item, filterValue, type) {
|
tmdbRating: function({ ratings = {} }, filterValue, type) {
|
||||||
const predicate = filterTypePredicates[type];
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
const rating = item.ratings.tmdb ? item.ratings.tmdb.value : 0;
|
const rating = ratings.tmdb ? ratings.tmdb.value : 0;
|
||||||
|
|
||||||
return predicate(rating * 10, filterValue);
|
return predicate(rating * 10, filterValue);
|
||||||
},
|
},
|
||||||
|
|
||||||
tmdbVotes: function(item, filterValue, type) {
|
tmdbVotes: function({ ratings = {} }, filterValue, type) {
|
||||||
const predicate = filterTypePredicates[type];
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
const rating = item.ratings.tmdb ? item.ratings.tmdb.votes : 0;
|
const rating = ratings.tmdb ? ratings.tmdb.votes : 0;
|
||||||
|
|
||||||
return predicate(rating, filterValue);
|
return predicate(rating, filterValue);
|
||||||
},
|
},
|
||||||
|
|
||||||
imdbRating: function(item, filterValue, type) {
|
imdbRating: function({ ratings = {} }, filterValue, type) {
|
||||||
const predicate = filterTypePredicates[type];
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
const rating = item.ratings.imdb ? item.ratings.imdb.value : 0;
|
const rating = ratings.imdb ? ratings.imdb.value : 0;
|
||||||
|
|
||||||
return predicate(rating, filterValue);
|
return predicate(rating, filterValue);
|
||||||
},
|
},
|
||||||
|
|
||||||
rottenTomatoesRating: function(item, filterValue, type) {
|
imdbVotes: function({ ratings = {} }, filterValue, type) {
|
||||||
const predicate = filterTypePredicates[type];
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
const rating = item.ratings.rottenTomatoes ? item.ratings.rottenTomatoes.value : 0;
|
const rating = ratings.imdb ? ratings.imdb.votes : 0;
|
||||||
|
|
||||||
return predicate(rating, filterValue);
|
return predicate(rating, filterValue);
|
||||||
},
|
},
|
||||||
|
|
||||||
imdbVotes: function(item, filterValue, type) {
|
rottenTomatoesRating: function({ ratings = {} }, filterValue, type) {
|
||||||
const predicate = filterTypePredicates[type];
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
const rating = item.ratings.imdb ? item.ratings.imdb.votes : 0;
|
const rating = ratings.rottenTomatoes ? ratings.rottenTomatoes.value : 0;
|
||||||
|
|
||||||
|
return predicate(rating, filterValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
traktRating: function({ ratings = {} }, filterValue, type) {
|
||||||
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
|
const rating = ratings.trakt ? ratings.trakt.value : 0;
|
||||||
|
|
||||||
|
return predicate(rating * 10, filterValue);
|
||||||
|
},
|
||||||
|
|
||||||
|
traktVotes: function({ ratings = {} }, filterValue, type) {
|
||||||
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
|
const rating = ratings.trakt ? ratings.trakt.votes : 0;
|
||||||
|
|
||||||
return predicate(rating, filterValue);
|
return predicate(rating, filterValue);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const defaultState = {
|
|||||||
showTmdbRating: false,
|
showTmdbRating: false,
|
||||||
showImdbRating: false,
|
showImdbRating: false,
|
||||||
showRottenTomatoesRating: false,
|
showRottenTomatoesRating: false,
|
||||||
|
showTraktRating: false,
|
||||||
showTags: false,
|
showTags: false,
|
||||||
showSearchAction: false
|
showSearchAction: false
|
||||||
},
|
},
|
||||||
@@ -158,7 +159,7 @@ export const defaultState = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'minimumAvailability',
|
name: 'minimumAvailability',
|
||||||
label: () => translate('MinAvailability'),
|
label: () => translate('MinimumAvailability'),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
@@ -204,6 +205,12 @@ export const defaultState = {
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'traktRating',
|
||||||
|
label: () => translate('TraktRating'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'popularity',
|
name: 'popularity',
|
||||||
label: () => translate('Popularity'),
|
label: () => translate('Popularity'),
|
||||||
@@ -278,6 +285,10 @@ export const defaultState = {
|
|||||||
|
|
||||||
rottenTomatoesRating: function({ ratings = {} }) {
|
rottenTomatoesRating: function({ ratings = {} }) {
|
||||||
return ratings.rottenTomatoes ? ratings.rottenTomatoes.value : -1;
|
return ratings.rottenTomatoes ? ratings.rottenTomatoes.value : -1;
|
||||||
|
},
|
||||||
|
|
||||||
|
traktRating: function({ ratings = {} }) {
|
||||||
|
return ratings.trakt ? ratings.trakt.value : 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -489,6 +500,12 @@ export const defaultState = {
|
|||||||
{
|
{
|
||||||
name: 'imdbRating',
|
name: 'imdbRating',
|
||||||
label: () => translate('ImdbRating'),
|
label: () => translate('ImdbRating'),
|
||||||
|
type: filterBuilderTypes.NUMBER,
|
||||||
|
numberFractionDigits: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'imdbVotes',
|
||||||
|
label: () => translate('ImdbVotes'),
|
||||||
type: filterBuilderTypes.NUMBER
|
type: filterBuilderTypes.NUMBER
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -497,8 +514,13 @@ export const defaultState = {
|
|||||||
type: filterBuilderTypes.NUMBER
|
type: filterBuilderTypes.NUMBER
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'imdbVotes',
|
name: 'traktRating',
|
||||||
label: () => translate('ImdbVotes'),
|
label: () => translate('TraktRating'),
|
||||||
|
type: filterBuilderTypes.NUMBER
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'traktVotes',
|
||||||
|
label: () => translate('TraktVotes'),
|
||||||
type: filterBuilderTypes.NUMBER
|
type: filterBuilderTypes.NUMBER
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ export const defaultState = {
|
|||||||
{
|
{
|
||||||
name: 'actions',
|
name: 'actions',
|
||||||
columnLabel: () => translate('Actions'),
|
columnLabel: () => translate('Actions'),
|
||||||
isSortable: true,
|
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isModifiable: false
|
isModifiable: false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ export const defaultState = {
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'movies.lastSearchTime',
|
||||||
|
label: () => translate('LastSearched'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
label: () => translate('Status'),
|
label: () => translate('Status'),
|
||||||
@@ -143,6 +149,12 @@ export const defaultState = {
|
|||||||
label: () => translate('Languages'),
|
label: () => translate('Languages'),
|
||||||
isVisible: false
|
isVisible: false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'movies.lastSearchTime',
|
||||||
|
label: () => translate('LastSearched'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'status',
|
name: 'status',
|
||||||
label: () => translate('Status'),
|
label: () => translate('Status'),
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import ImportList from 'typings/ImportList';
|
||||||
|
|
||||||
|
function createMovieCreditImportListSelector(tmdbId: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.importLists.items,
|
||||||
|
(importLists) => {
|
||||||
|
const importListIds = importLists.reduce(
|
||||||
|
(acc: ImportList[], importList) => {
|
||||||
|
if (importList.implementation === 'TMDbPersonImport') {
|
||||||
|
const personIdValue = importList.fields.find(
|
||||||
|
(field) => field.name === 'personId'
|
||||||
|
)?.value as string | null;
|
||||||
|
|
||||||
|
if (personIdValue && parseInt(personIdValue) === tmdbId) {
|
||||||
|
acc.push(importList);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (importListIds.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return importListIds[0];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createMovieCreditImportListSelector;
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
function createMovieCreditListSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { tmdbId }) => tmdbId,
|
|
||||||
(state) => state.settings.importLists.items,
|
|
||||||
(tmdbId, importLists) => {
|
|
||||||
const importListIds = _.reduce(importLists, (acc, list) => {
|
|
||||||
if (list.implementation === 'TMDbPersonImport') {
|
|
||||||
const personIdField = list.fields.find((field) => {
|
|
||||||
return field.name === 'personId';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (personIdField && parseInt(personIdField.value) === tmdbId) {
|
|
||||||
acc.push(list);
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (importListIds.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return importListIds[0];
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createMovieCreditListSelector;
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import { MovieCreditType } from 'typings/MovieCredit';
|
||||||
|
|
||||||
|
function createMovieCreditsSelector(movieCreditType: MovieCreditType) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.movieCredits.items,
|
||||||
|
(movieCredits) => {
|
||||||
|
const credits = movieCredits.filter(
|
||||||
|
({ type }) => type === movieCreditType
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedCredits = credits.sort((a, b) => a.order - b.order);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: _.uniqBy(sortedCredits, 'personName'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createMovieCreditsSelector;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import appState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
import { createMovieSelectorForHook } from './createMovieSelector';
|
import { createMovieSelectorForHook } from './createMovieSelector';
|
||||||
|
|
||||||
function createMovieQualityProfileSelector(movieId: number) {
|
function createMovieQualityProfileSelector(movieId: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: appState) => state.settings.qualityProfiles.items,
|
(state: AppState) => state.settings.qualityProfiles.items,
|
||||||
createMovieSelectorForHook(movieId),
|
createMovieSelectorForHook(movieId),
|
||||||
(qualityProfiles, movie = {} as Movie) => {
|
(qualityProfiles, movie = {} as Movie) => {
|
||||||
return qualityProfiles.find(
|
return qualityProfiles.find(
|
||||||
|
|||||||
@@ -126,14 +126,16 @@ class CutoffUnmetConnector extends Component {
|
|||||||
onSearchSelectedPress = (selected) => {
|
onSearchSelectedPress = (selected) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.MOVIE_SEARCH,
|
name: commandNames.MOVIE_SEARCH,
|
||||||
movieIds: selected
|
movieIds: selected,
|
||||||
|
commandFinished: this.repopulate
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchAllCutoffUnmetPress = (monitored) => {
|
onSearchAllCutoffUnmetPress = (monitored) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
|
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
|
||||||
monitored
|
monitored,
|
||||||
|
commandFinished: this.repopulate
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function CutoffUnmetRow(props) {
|
|||||||
inCinemas,
|
inCinemas,
|
||||||
digitalRelease,
|
digitalRelease,
|
||||||
physicalRelease,
|
physicalRelease,
|
||||||
|
lastSearchTime,
|
||||||
isSelected,
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
onSelectedChange
|
onSelectedChange
|
||||||
@@ -110,6 +111,16 @@ function CutoffUnmetRow(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'movies.lastSearchTime') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCell
|
||||||
|
key={name}
|
||||||
|
date={lastSearchTime}
|
||||||
|
includeSeconds={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'status') {
|
if (name === 'status') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
@@ -148,6 +159,7 @@ CutoffUnmetRow.propTypes = {
|
|||||||
movieFileId: PropTypes.number,
|
movieFileId: PropTypes.number,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
|
lastSearchTime: PropTypes.string,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
inCinemas: PropTypes.string,
|
inCinemas: PropTypes.string,
|
||||||
digitalRelease: PropTypes.string,
|
digitalRelease: PropTypes.string,
|
||||||
|
|||||||
@@ -117,14 +117,16 @@ class MissingConnector extends Component {
|
|||||||
onSearchSelectedPress = (selected) => {
|
onSearchSelectedPress = (selected) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.MOVIE_SEARCH,
|
name: commandNames.MOVIE_SEARCH,
|
||||||
movieIds: selected
|
movieIds: selected,
|
||||||
|
commandFinished: this.repopulate
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchAllMissingPress = (monitored) => {
|
onSearchAllMissingPress = (monitored) => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.MISSING_MOVIES_SEARCH,
|
name: commandNames.MISSING_MOVIES_SEARCH,
|
||||||
monitored
|
monitored,
|
||||||
|
commandFinished: this.repopulate
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ function MissingRow(props) {
|
|||||||
inCinemas,
|
inCinemas,
|
||||||
digitalRelease,
|
digitalRelease,
|
||||||
physicalRelease,
|
physicalRelease,
|
||||||
|
lastSearchTime,
|
||||||
isSelected,
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
onSelectedChange
|
onSelectedChange
|
||||||
@@ -100,6 +101,16 @@ function MissingRow(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'movies.lastSearchTime') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCell
|
||||||
|
key={name}
|
||||||
|
date={lastSearchTime}
|
||||||
|
includeSeconds={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'status') {
|
if (name === 'status') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
@@ -138,6 +149,7 @@ MissingRow.propTypes = {
|
|||||||
movieFileId: PropTypes.number,
|
movieFileId: PropTypes.number,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
|
lastSearchTime: PropTypes.string,
|
||||||
titleSlug: PropTypes.string.isRequired,
|
titleSlug: PropTypes.string.isRequired,
|
||||||
inCinemas: PropTypes.string,
|
inCinemas: PropTypes.string,
|
||||||
digitalRelease: PropTypes.string,
|
digitalRelease: PropTypes.string,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import { Image } from 'Movie/Movie';
|
||||||
|
|
||||||
|
export type MovieCreditType = 'cast' | 'crew';
|
||||||
|
|
||||||
|
interface MovieCredit extends ModelBase {
|
||||||
|
personTmdbId: number;
|
||||||
|
personName: string;
|
||||||
|
images: Image[];
|
||||||
|
type: MovieCreditType;
|
||||||
|
department: string;
|
||||||
|
job: string;
|
||||||
|
character: string;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieCredit;
|
||||||
@@ -16,6 +16,7 @@ interface QualityProfile {
|
|||||||
items: QualityProfileQualityItem[];
|
items: QualityProfileQualityItem[];
|
||||||
minFormatScore: number;
|
minFormatScore: number;
|
||||||
cutoffFormatScore: number;
|
cutoffFormatScore: number;
|
||||||
|
minUpgradeFormatScore: number;
|
||||||
formatItems: QualityProfileFormatItem[];
|
formatItems: QualityProfileFormatItem[];
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
|||||||
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
|
[TestCase(@"https://discord.com/api/webhooks/mySecret")]
|
||||||
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
|
[TestCase(@"https://discord.com/api/webhooks/mySecret/01233210")]
|
||||||
|
|
||||||
|
// Telegram
|
||||||
|
[TestCase(@"https://api.telegram.org/bot1234567890:mySecret/sendmessage: chat_id=123456&parse_mode=HTML&text=<text>")]
|
||||||
|
[TestCase(@"https://api.telegram.org/bot1234567890:mySecret/")]
|
||||||
|
|
||||||
public void should_clean_message(string message)
|
public void should_clean_message(string message)
|
||||||
{
|
{
|
||||||
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
var cleansedMessage = CleanseLogMessage.Cleanse(message);
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ namespace NzbDrone.Common.Instrumentation
|
|||||||
new (@"api/v[0-9]/notification/radarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
new (@"api/v[0-9]/notification/radarr/(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
// Discord
|
// Discord
|
||||||
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
new (@"discord.com/api/webhooks/((?<secret>[\w-]+)/)?(?<secret>[\w-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||||
|
|
||||||
|
// Telegram
|
||||||
|
new (@"api.telegram.org/bot(?<id>[\d]+):(?<secret>[\w-]+)/", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
private static readonly Regex CleanseRemoteIPRegex = new (@"(?:Auth-\w+(?<!Failure|Unauthorized) ip|from) (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", RegexOptions.Compiled);
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.Disk;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
{
|
||||||
|
public class FreeSpaceSpecificationFixture : CoreTest<FreeSpaceSpecification>
|
||||||
|
{
|
||||||
|
private RemoteMovie _remoteMovie;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_remoteMovie = new RemoteMovie() { Release = new ReleaseInfo(), Movie = new Movie { Path = @"C:\Test\Films\Movie".AsOsAgnostic() } };
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithMinimumFreeSpace(int size)
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>().SetupGet(c => c.MinimumFreeSpaceWhenImporting).Returns(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithAvailableSpace(int size)
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IDiskProvider>().Setup(s => s.GetAvailableSpace(It.IsAny<string>())).Returns(size.Megabytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithSize(int size)
|
||||||
|
{
|
||||||
|
_remoteMovie.Release.Size = size.Megabytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_when_available_space_is_more_than_size()
|
||||||
|
{
|
||||||
|
WithMinimumFreeSpace(0);
|
||||||
|
WithAvailableSpace(200);
|
||||||
|
WithSize(100);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_when_available_space_minus_size_is_more_than_minimum_free_space()
|
||||||
|
{
|
||||||
|
WithMinimumFreeSpace(50);
|
||||||
|
WithAvailableSpace(200);
|
||||||
|
WithSize(100);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_available_space_is_less_than_size()
|
||||||
|
{
|
||||||
|
WithMinimumFreeSpace(0);
|
||||||
|
WithAvailableSpace(200);
|
||||||
|
WithSize(1000);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_when_available_space_minus_size_is_less_than_minimum_free_space()
|
||||||
|
{
|
||||||
|
WithMinimumFreeSpace(150);
|
||||||
|
WithAvailableSpace(200);
|
||||||
|
WithSize(100);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_if_skip_free_space_check_is_true()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IConfigService>()
|
||||||
|
.Setup(s => s.SkipFreeSpaceCheckWhenImporting)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
WithMinimumFreeSpace(150);
|
||||||
|
WithAvailableSpace(200);
|
||||||
|
WithSize(100);
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ using NzbDrone.Core.CustomFormats;
|
|||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
|
using NzbDrone.Core.Profiles;
|
||||||
using NzbDrone.Core.Profiles.Qualities;
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Test.CustomFormats;
|
using NzbDrone.Core.Test.CustomFormats;
|
||||||
@@ -160,5 +161,95 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
new List<CustomFormat>())
|
new List<CustomFormat>())
|
||||||
.Should().Be(UpgradeableRejectReason.QualityCutoff);
|
.Should().Be(UpgradeableRejectReason.QualityCutoff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_false_if_minimum_custom_score_is_not_met()
|
||||||
|
{
|
||||||
|
var customFormatOne = new CustomFormat
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Name = "One"
|
||||||
|
};
|
||||||
|
|
||||||
|
var customFormatTwo = new CustomFormat
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
Name = "Two"
|
||||||
|
};
|
||||||
|
|
||||||
|
var profile = new QualityProfile
|
||||||
|
{
|
||||||
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
|
UpgradeAllowed = true,
|
||||||
|
MinUpgradeFormatScore = 11,
|
||||||
|
CutoffFormatScore = 100,
|
||||||
|
FormatItems = new List<ProfileFormatItem>
|
||||||
|
{
|
||||||
|
new ProfileFormatItem
|
||||||
|
{
|
||||||
|
Format = customFormatOne,
|
||||||
|
Score = 10
|
||||||
|
},
|
||||||
|
new ProfileFormatItem
|
||||||
|
{
|
||||||
|
Format = customFormatTwo,
|
||||||
|
Score = 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Subject.IsUpgradable(
|
||||||
|
profile,
|
||||||
|
new QualityModel(Quality.DVD),
|
||||||
|
new List<CustomFormat> { customFormatOne },
|
||||||
|
new QualityModel(Quality.DVD),
|
||||||
|
new List<CustomFormat> { customFormatTwo })
|
||||||
|
.Should().Be(UpgradeableRejectReason.MinCustomFormatScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_true_if_minimum_custom_score_is_met()
|
||||||
|
{
|
||||||
|
var customFormatOne = new CustomFormat
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Name = "One"
|
||||||
|
};
|
||||||
|
|
||||||
|
var customFormatTwo = new CustomFormat
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
Name = "Two"
|
||||||
|
};
|
||||||
|
|
||||||
|
var profile = new QualityProfile
|
||||||
|
{
|
||||||
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
|
UpgradeAllowed = true,
|
||||||
|
MinUpgradeFormatScore = 10,
|
||||||
|
CutoffFormatScore = 100,
|
||||||
|
FormatItems = new List<ProfileFormatItem>
|
||||||
|
{
|
||||||
|
new ProfileFormatItem
|
||||||
|
{
|
||||||
|
Format = customFormatOne,
|
||||||
|
Score = 10
|
||||||
|
},
|
||||||
|
new ProfileFormatItem
|
||||||
|
{
|
||||||
|
Format = customFormatTwo,
|
||||||
|
Score = 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Subject.IsUpgradable(
|
||||||
|
profile,
|
||||||
|
new QualityModel(Quality.DVD),
|
||||||
|
new List<CustomFormat> { customFormatOne },
|
||||||
|
new QualityModel(Quality.DVD),
|
||||||
|
new List<CustomFormat> { customFormatTwo })
|
||||||
|
.Should().Be(UpgradeableRejectReason.None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:newznab="http://www.newznab.com/DTD/2010/feeds/attributes/">
|
||||||
|
<channel>
|
||||||
|
<title>somenewznabindexer.com</title>
|
||||||
|
<description>somenewznabindexer.com Feed</description>
|
||||||
|
<link>https://somenewznabindexer.com/</link>
|
||||||
|
<language>en-gb</language>
|
||||||
|
<webMaster>contact@somenewznabindexer.com</webMaster>
|
||||||
|
<category/>
|
||||||
|
<newznab:response offset="0" total="100"/>
|
||||||
|
<item>
|
||||||
|
<title>title</title>
|
||||||
|
<guid isPermaLink="true">no custom attributes</guid>
|
||||||
|
<link>link</link>
|
||||||
|
<comments>comments</comments>
|
||||||
|
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||||
|
<category>category</category>
|
||||||
|
<description>description</description>
|
||||||
|
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>title</title>
|
||||||
|
<guid isPermaLink="true">prematch=1 attribute</guid>
|
||||||
|
<link>link</link>
|
||||||
|
<comments>comments</comments>
|
||||||
|
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||||
|
<category>category</category>
|
||||||
|
<description>description</description>
|
||||||
|
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||||
|
|
||||||
|
<newznab:attr name="prematch" value="1"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>title</title>
|
||||||
|
<guid isPermaLink="true">haspretime=1 attribute</guid>
|
||||||
|
<link>link</link>
|
||||||
|
<comments>comments</comments>
|
||||||
|
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||||
|
<category>category</category>
|
||||||
|
<description>description</description>
|
||||||
|
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||||
|
|
||||||
|
<newznab:attr name="haspretime" value="1"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>title</title>
|
||||||
|
<guid isPermaLink="true">prematch=0 attribute</guid>
|
||||||
|
<link>link</link>
|
||||||
|
<comments>comments</comments>
|
||||||
|
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||||
|
<category>category</category>
|
||||||
|
<description>description</description>
|
||||||
|
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||||
|
|
||||||
|
<newznab:attr name="prematch" value="0"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>title</title>
|
||||||
|
<guid isPermaLink="true">haspretime=0 attribute</guid>
|
||||||
|
<link>link</link>
|
||||||
|
<comments>comments</comments>
|
||||||
|
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||||
|
<category>category</category>
|
||||||
|
<description>description</description>
|
||||||
|
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||||
|
|
||||||
|
<newznab:attr name="haspretime" value="0"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>title</title>
|
||||||
|
<guid isPermaLink="true">nuked=1 attribute</guid>
|
||||||
|
<link>link</link>
|
||||||
|
<comments>comments</comments>
|
||||||
|
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||||
|
<category>category</category>
|
||||||
|
<description>description</description>
|
||||||
|
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||||
|
|
||||||
|
<newznab:attr name="nuked" value="1"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>title</title>
|
||||||
|
<guid isPermaLink="true">nuked=0 attribute</guid>
|
||||||
|
<link>link</link>
|
||||||
|
<comments>comments</comments>
|
||||||
|
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||||
|
<category>category</category>
|
||||||
|
<description>description</description>
|
||||||
|
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||||
|
|
||||||
|
<newznab:attr name="nuked" value="0"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>title</title>
|
||||||
|
<guid isPermaLink="true">prematch=1 and nuked=1 attributes</guid>
|
||||||
|
<link>link</link>
|
||||||
|
<comments>comments</comments>
|
||||||
|
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||||
|
<category>category</category>
|
||||||
|
<description>description</description>
|
||||||
|
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||||
|
|
||||||
|
<newznab:attr name="prematch" value="1"/>
|
||||||
|
<newznab:attr name="nuked" value="1"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<title>title</title>
|
||||||
|
<guid isPermaLink="true">haspretime=0 and nuked=0 attributes</guid>
|
||||||
|
<link>link</link>
|
||||||
|
<comments>comments</comments>
|
||||||
|
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||||
|
<category>category</category>
|
||||||
|
<description>description</description>
|
||||||
|
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||||
|
|
||||||
|
<newznab:attr name="haspretime" value="0"/>
|
||||||
|
<newznab:attr name="nuked" value="0"/>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
@@ -9,6 +9,7 @@ using NzbDrone.Common.Http;
|
|||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Indexers.Newznab;
|
using NzbDrone.Core.Indexers.Newznab;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
||||||
@@ -98,5 +99,29 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
|
|||||||
releases[1].Languages.Should().BeEquivalentTo(new[] { Language.English, Language.Spanish });
|
releases[1].Languages.Should().BeEquivalentTo(new[] { Language.English, Language.Spanish });
|
||||||
releases[2].Languages.Should().BeEquivalentTo(new[] { Language.French });
|
releases[2].Languages.Should().BeEquivalentTo(new[] { Language.French });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("no custom attributes")]
|
||||||
|
[TestCase("prematch=1 attribute", IndexerFlags.G_Scene)]
|
||||||
|
[TestCase("haspretime=1 attribute", IndexerFlags.G_Scene)]
|
||||||
|
[TestCase("prematch=0 attribute")]
|
||||||
|
[TestCase("haspretime=0 attribute")]
|
||||||
|
[TestCase("nuked=1 attribute", IndexerFlags.Nuked)]
|
||||||
|
[TestCase("nuked=0 attribute")]
|
||||||
|
[TestCase("prematch=1 and nuked=1 attributes", IndexerFlags.G_Scene, IndexerFlags.Nuked)]
|
||||||
|
[TestCase("haspretime=0 and nuked=0 attributes")]
|
||||||
|
public async Task should_parse_indexer_flags(string releaseGuid, params IndexerFlags[] indexerFlags)
|
||||||
|
{
|
||||||
|
var feed = ReadAllText(@"Files/Indexers/Newznab/newznab_indexerflags.xml");
|
||||||
|
|
||||||
|
Mocker.GetMock<IHttpClient>()
|
||||||
|
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||||
|
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), feed)));
|
||||||
|
|
||||||
|
var releases = await Subject.FetchRecent();
|
||||||
|
|
||||||
|
var release = releases.Should().ContainSingle(r => r.Guid == releaseGuid).Subject;
|
||||||
|
|
||||||
|
indexerFlags.ToList().ForEach(f => release.IndexerFlags.Should().HaveFlag(f));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user