mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-05 13:21:25 -05:00
Compare commits
44 Commits
v5.10.2.91
...
v5.11.0.92
| 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
|
||||
|
||||
[](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)
|
||||

|
||||
[](#backers)
|
||||
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.10.2'
|
||||
majorVersion: '5.11.0'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AddNewMovieModalContent.css';
|
||||
@@ -115,13 +118,28 @@ class AddNewMovieModalContent extends Component {
|
||||
</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
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
onChange={onInputChange}
|
||||
{...minimumAvailability}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Icon from 'Components/Icon';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
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 styles from './ImportMovieHeader.css';
|
||||
|
||||
@@ -46,7 +50,19 @@ function ImportMovieHeader(props) {
|
||||
className={styles.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
|
||||
|
||||
@@ -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 CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||
import MovieCreditAppState from './MovieCreditAppState';
|
||||
import MovieFilesAppState from './MovieFilesAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
@@ -64,6 +65,7 @@ interface AppState {
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieCollections: MovieCollectionAppState;
|
||||
movieCredits: MovieCreditAppState;
|
||||
movieFiles: MovieFilesAppState;
|
||||
movieIndex: MovieIndexAppState;
|
||||
movies: MoviesAppState;
|
||||
|
||||
6
frontend/src/App/State/MovieCreditAppState.ts
Normal file
6
frontend/src/App/State/MovieCreditAppState.ts
Normal file
@@ -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;
|
||||
showImdbRating: boolean;
|
||||
showRottenTomatoesRating: boolean;
|
||||
showTraktRating: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
@@ -56,7 +56,9 @@ function getValue(input, selectedFilterBuilderProp) {
|
||||
}
|
||||
|
||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||
return parseInt(input);
|
||||
const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
|
||||
|
||||
return Number(input).toFixed(numberFractionDigits);
|
||||
}
|
||||
|
||||
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;
|
||||
67
frontend/src/Components/Form/AvailabilitySelectInput.tsx
Normal file
67
frontend/src/Components/Form/AvailabilitySelectInput.tsx
Normal file
@@ -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';
|
||||
|
||||
function MovieMonitoredSelectInput(props) {
|
||||
const values = [...monitorOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed
|
||||
includeMixed,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const values = [...monitorOptions];
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
@@ -23,14 +26,16 @@ function MovieMonitoredSelectInput(props) {
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
get value() {
|
||||
return `(${translate('Mixed')})`;
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
{...otherProps}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
|
||||
1
frontend/src/Components/ImdbRating.css.d.ts
vendored
1
frontend/src/Components/ImdbRating.css.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'image': string;
|
||||
'wrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -23,7 +23,7 @@ function ImdbRating(props: ImdbRatingProps) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={
|
||||
<span>
|
||||
<span className={styles.wrapper}>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'image': string;
|
||||
'wrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -24,7 +24,7 @@ function RottenTomatoRating(props: RottenTomatoRatingProps) {
|
||||
const ratingImage = value > 50 ? rtFresh : rtRotten;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.wrapper}>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
|
||||
1
frontend/src/Components/TmdbRating.css.d.ts
vendored
1
frontend/src/Components/TmdbRating.css.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'image': string;
|
||||
'wrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -22,7 +22,7 @@ function TmdbRating(props: TmdbRatingProps) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={
|
||||
<span>
|
||||
<span className={styles.wrapper}>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
|
||||
9
frontend/src/Components/TraktRating.css
Normal file
9
frontend/src/Components/TraktRating.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'blankpad': string;
|
||||
'image': string;
|
||||
'wrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
46
frontend/src/Components/TraktRating.tsx
Normal file
46
frontend/src/Components/TraktRating.tsx
Normal file
@@ -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')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="traktRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('TraktRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="certification"
|
||||
sortKey={sortKey}
|
||||
|
||||
@@ -9,6 +9,7 @@ import Link from 'Components/Link/Link';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -92,6 +93,7 @@ class DiscoverMoviePoster extends Component {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
ratings,
|
||||
isExisting,
|
||||
isExcluded,
|
||||
@@ -223,6 +225,12 @@ class DiscoverMoviePoster extends Component {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showTraktRating && !!ratings.trakt ? (
|
||||
<div className={styles.title}>
|
||||
<TraktRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<DiscoverMoviePosterInfo
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
@@ -232,6 +240,7 @@ class DiscoverMoviePoster extends Component {
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
showTraktRating={showTraktRating}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
@@ -274,6 +283,7 @@ DiscoverMoviePoster.propTypes = {
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
showTraktRating: PropTypes.bool.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -4,6 +4,7 @@ import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||
@@ -28,7 +29,8 @@ function DiscoverMoviePosterInfo(props) {
|
||||
movieRuntimeFormat,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating
|
||||
} = props;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -160,7 +170,8 @@ DiscoverMoviePosterInfo.propTypes = {
|
||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
showTraktRating: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default DiscoverMoviePosterInfo;
|
||||
|
||||
@@ -39,7 +39,8 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating
|
||||
} = posterOptions;
|
||||
|
||||
const heights = [
|
||||
@@ -64,6 +65,10 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showTraktRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
switch (sortKey) {
|
||||
case 'studio':
|
||||
case 'inCinemas':
|
||||
@@ -88,6 +93,11 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'traktRating':
|
||||
if (!showTraktRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// No need to add a height of 0
|
||||
}
|
||||
@@ -219,7 +229,8 @@ class DiscoverMoviePosters extends Component {
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating
|
||||
} = posterOptions;
|
||||
|
||||
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||
@@ -248,6 +259,7 @@ class DiscoverMoviePosters extends Component {
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
showTraktRating={showTraktRating}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
|
||||
@@ -48,6 +48,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
showTmdbRating: props.showTmdbRating,
|
||||
showImdbRating: props.showImdbRating,
|
||||
showRottenTomatoesRating: props.showRottenTomatoesRating,
|
||||
showTraktRating: props.showTraktRating,
|
||||
includeRecommendations: props.includeRecommendations,
|
||||
includeTrending: props.includeTrending,
|
||||
includePopular: props.includePopular
|
||||
@@ -61,6 +62,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -88,6 +90,10 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
state.showRottenTomatoesRating = showRottenTomatoesRating;
|
||||
}
|
||||
|
||||
if (showTraktRating !== prevProps.showTraktRating) {
|
||||
state.showTraktRating = showTraktRating;
|
||||
}
|
||||
|
||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||
state.includeRecommendations = includeRecommendations;
|
||||
}
|
||||
@@ -140,6 +146,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -248,6 +255,18 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowTraktRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showTraktRating"
|
||||
value={showTraktRating}
|
||||
helpText={translate('ShowTraktRatingPosterHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
@@ -269,6 +288,7 @@ DiscoverMoviePosterOptionsModalContent.propTypes = {
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
showTraktRating: PropTypes.bool.isRequired,
|
||||
includeRecommendations: PropTypes.bool.isRequired,
|
||||
includeTrending: PropTypes.bool.isRequired,
|
||||
includePopular: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
.tmdbRating,
|
||||
.imdbRating,
|
||||
.rottenTomatoesRating,
|
||||
.traktRating,
|
||||
.runtime {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ interface CssExports {
|
||||
'status': string;
|
||||
'studio': string;
|
||||
'tmdbRating': string;
|
||||
'traktRating': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
.tmdbRating,
|
||||
.imdbRating,
|
||||
.rottenTomatoesRating,
|
||||
.traktRating,
|
||||
.runtime {
|
||||
composes: cell;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ interface CssExports {
|
||||
'statusIcon': string;
|
||||
'studio': string;
|
||||
'tmdbRating': string;
|
||||
'traktRating': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -11,6 +11,7 @@ import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||
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') {
|
||||
return (
|
||||
<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;
|
||||
179
frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.tsx
Normal file
179
frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.tsx
Normal file
@@ -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;
|
||||
25
frontend/src/Movie/Details/Credits/Cast/MovieCastPosters.tsx
Normal file
25
frontend/src/Movie/Details/Credits/Cast/MovieCastPosters.tsx
Normal file
@@ -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;
|
||||
177
frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.tsx
Normal file
177
frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.tsx
Normal file
@@ -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;
|
||||
25
frontend/src/Movie/Details/Credits/Crew/MovieCrewPosters.tsx
Normal file
25
frontend/src/Movie/Details/Credits/Crew/MovieCrewPosters.tsx
Normal file
@@ -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);
|
||||
60
frontend/src/Movie/Details/Credits/MovieCreditPoster.tsx
Normal file
60
frontend/src/Movie/Details/Credits/MovieCreditPoster.tsx
Normal file
@@ -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;
|
||||
87
frontend/src/Movie/Details/Credits/MovieCreditPosters.tsx
Normal file
87
frontend/src/Movie/Details/Credits/MovieCreditPosters.tsx
Normal file
@@ -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 Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
@@ -37,9 +39,8 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
||||
import MovieCastPosters from './Credits/Cast/MovieCastPosters';
|
||||
import MovieCrewPosters from './Credits/Crew/MovieCrewPosters';
|
||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||
import MovieReleaseDates from './MovieReleaseDates';
|
||||
import MovieStatusLabel from './MovieStatusLabel';
|
||||
@@ -497,31 +498,44 @@ class MovieDetails extends Component {
|
||||
|
||||
<div className={styles.details}>
|
||||
{
|
||||
!!ratings.tmdb &&
|
||||
ratings.tmdb ?
|
||||
<span className={styles.rating}>
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
iconSize={20}
|
||||
/>
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
{
|
||||
!!ratings.imdb &&
|
||||
ratings.imdb ?
|
||||
<span className={styles.rating}>
|
||||
<ImdbRating
|
||||
ratings={ratings}
|
||||
iconSize={20}
|
||||
/>
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
{
|
||||
!!ratings.rottenTomatoes &&
|
||||
ratings.rottenTomatoes ?
|
||||
<span className={styles.rating}>
|
||||
<RottenTomatoRating
|
||||
ratings={ratings}
|
||||
iconSize={20}
|
||||
/>
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
{
|
||||
ratings.trakt ?
|
||||
<span className={styles.rating}>
|
||||
<TraktRating
|
||||
ratings={ratings}
|
||||
iconSize={20}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -685,13 +699,13 @@ class MovieDetails extends Component {
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Cast')}>
|
||||
<MovieCastPostersConnector
|
||||
<MovieCastPosters
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Crew')}>
|
||||
<MovieCrewPostersConnector
|
||||
<MovieCrewPosters
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</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;
|
||||
21
frontend/src/Movie/Details/Titles/MovieTitlesRow.tsx
Normal file
21
frontend/src/Movie/Details/Titles/MovieTitlesRow.tsx
Normal file
@@ -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 {
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'blankpad': string;
|
||||
'container': string;
|
||||
}
|
||||
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;
|
||||
94
frontend/src/Movie/Details/Titles/MovieTitlesTable.tsx
Normal file
94
frontend/src/Movie/Details/Titles/MovieTitlesTable.tsx
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
.tagInternalInput {
|
||||
composes: internalInput from '~Components/Form/TagInput.css';
|
||||
|
||||
flex: 0 0 100%;
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'deleteButton': string;
|
||||
'tagInternalInput': string;
|
||||
'labelIcon': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditMovieModalContent.css';
|
||||
@@ -103,7 +106,21 @@ class EditMovieModalContent extends Component {
|
||||
</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
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
|
||||
@@ -136,6 +136,15 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
|
||||
{translate('RottenTomatoesRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="traktRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('TraktRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="popularity"
|
||||
sortKey={sortKey}
|
||||
|
||||
@@ -11,6 +11,7 @@ import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
@@ -54,6 +55,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
showTags,
|
||||
showSearchAction,
|
||||
} = useSelector(selectPosterOptions);
|
||||
@@ -310,6 +312,12 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showTraktRating && !!ratings.trakt ? (
|
||||
<div className={styles.title}>
|
||||
<TraktRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showTags && tags.length ? (
|
||||
<div className={styles.tags}>
|
||||
<div className={styles.tagsList}>
|
||||
@@ -347,6 +355,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
showTraktRating={showTraktRating}
|
||||
showTags={showTags}
|
||||
/>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import ImdbRating from 'Components/ImdbRating';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { Ratings } from 'Movie/Movie';
|
||||
@@ -43,6 +44,7 @@ interface MovieIndexPosterInfoProps {
|
||||
showTmdbRating: boolean;
|
||||
showImdbRating: boolean;
|
||||
showRottenTomatoesRating: boolean;
|
||||
showTraktRating: boolean;
|
||||
showTags: boolean;
|
||||
}
|
||||
|
||||
@@ -76,6 +78,7 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
showTags,
|
||||
} = 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) {
|
||||
return (
|
||||
<div className={styles.tags}>
|
||||
|
||||
@@ -150,6 +150,7 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
showTags,
|
||||
} = posterOptions;
|
||||
|
||||
@@ -199,6 +200,10 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showTraktRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showTags) {
|
||||
heights.push(21);
|
||||
}
|
||||
@@ -253,6 +258,11 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'traktRating':
|
||||
if (!showTraktRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'tags':
|
||||
if (!showTags) {
|
||||
heights.push(21);
|
||||
|
||||
@@ -59,6 +59,7 @@ function MovieIndexPosterOptionsModalContent(
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
showTags,
|
||||
showSearchAction,
|
||||
} = posterOptions;
|
||||
@@ -222,6 +223,18 @@ function MovieIndexPosterOptionsModalContent(
|
||||
/>
|
||||
</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}>
|
||||
<FormLabel>{translate('ShowTags')}</FormLabel>
|
||||
|
||||
|
||||
@@ -92,7 +92,8 @@
|
||||
|
||||
.imdbRating,
|
||||
.tmdbRating,
|
||||
.rottenTomatoesRating {
|
||||
.rottenTomatoesRating,
|
||||
.traktRating {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 80px;
|
||||
|
||||
@@ -30,6 +30,7 @@ interface CssExports {
|
||||
'studio': string;
|
||||
'tags': string;
|
||||
'tmdbRating': string;
|
||||
'traktRating': string;
|
||||
'year': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -14,6 +14,7 @@ import Column from 'Components/Table/Column';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
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') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
|
||||
.imdbRating,
|
||||
.tmdbRating,
|
||||
.rottenTomatoesRating {
|
||||
.rottenTomatoesRating,
|
||||
.traktRating {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 80px;
|
||||
|
||||
@@ -27,6 +27,7 @@ interface CssExports {
|
||||
'studio': string;
|
||||
'tags': string;
|
||||
'tmdbRating': string;
|
||||
'traktRating': string;
|
||||
'year': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -9,7 +9,7 @@ export type MovieStatus =
|
||||
| 'released'
|
||||
| 'deleted';
|
||||
|
||||
export type CoverType = 'poster' | 'fanart';
|
||||
export type CoverType = 'poster' | 'fanart' | 'headshot';
|
||||
|
||||
export interface Image {
|
||||
coverType: CoverType;
|
||||
@@ -37,6 +37,12 @@ export interface Ratings {
|
||||
tmdb: RatingValues;
|
||||
metacritic: RatingValues;
|
||||
rottenTomatoes: RatingValues;
|
||||
trakt: RatingValues;
|
||||
}
|
||||
|
||||
export interface AlternativeTitle extends ModelBase {
|
||||
sourceType: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Movie extends ModelBase {
|
||||
@@ -52,6 +58,7 @@ interface Movie extends ModelBase {
|
||||
originalTitle: string;
|
||||
originalLanguage: Language;
|
||||
collection: Collection;
|
||||
alternateTitles: AlternativeTitle[];
|
||||
studio: string;
|
||||
qualityProfileId: number;
|
||||
added: string;
|
||||
@@ -72,6 +79,7 @@ interface Movie extends ModelBase {
|
||||
images: Image[];
|
||||
movieFile: MovieFile;
|
||||
hasFile: boolean;
|
||||
lastSearchTime?: string;
|
||||
isAvailable: 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;
|
||||
23
frontend/src/Movie/MovieHeadshot.tsx
Normal file
23
frontend/src/Movie/MovieHeadshot.tsx
Normal file
@@ -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) {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [isLoaded, setIsLoaded] = useState(true);
|
||||
const image = useRef<Image | null>(null);
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
|
||||
@@ -42,7 +42,7 @@ import styles from './ImportListExclusions.css';
|
||||
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'tmdbid',
|
||||
name: 'tmdbId',
|
||||
label: () => translate('TMDBId'),
|
||||
isVisible: true,
|
||||
isSortable: true,
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'deleteButton': string;
|
||||
'labelIcon': string;
|
||||
'message': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -13,7 +15,8 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -160,12 +163,28 @@ function EditImportListModalContent(props) {
|
||||
</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
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
{...minimumAvailability}
|
||||
onChange={onInputChange}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
@@ -174,24 +174,21 @@ class MediaManagement extends Component {
|
||||
<FieldSet
|
||||
legend={translate('Importing')}
|
||||
>
|
||||
{
|
||||
!isWindows &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipFreeSpaceCheckWhenImporting"
|
||||
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipFreeSpaceCheckWhenImporting"
|
||||
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
|
||||
@@ -125,6 +125,7 @@ class EditQualityProfileModalContent extends Component {
|
||||
upgradeAllowed,
|
||||
cutoff,
|
||||
minFormatScore,
|
||||
minUpgradeFormatScore,
|
||||
cutoffFormatScore,
|
||||
language,
|
||||
items,
|
||||
@@ -249,6 +250,25 @@ class EditQualityProfileModalContent extends Component {
|
||||
</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}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
{translate('Language')}
|
||||
|
||||
@@ -61,7 +61,8 @@ export const defaultState = {
|
||||
showTitle: false,
|
||||
showTmdbRating: false,
|
||||
showImdbRating: false,
|
||||
showRottenTomatoesRating: false
|
||||
showRottenTomatoesRating: false,
|
||||
showTraktRating: false
|
||||
},
|
||||
|
||||
overviewOptions: {
|
||||
@@ -180,6 +181,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'traktRating',
|
||||
label: () => translate('TraktRating'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'popularity',
|
||||
label: () => translate('Popularity'),
|
||||
@@ -293,6 +300,10 @@ export const defaultState = {
|
||||
|
||||
rottenTomatoesRating: function({ ratings = {} }) {
|
||||
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'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'traktRating',
|
||||
label: () => translate('TraktRating'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'traktVotes',
|
||||
label: () => translate('TraktVotes'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'popularity',
|
||||
label: () => translate('Popularity'),
|
||||
|
||||
@@ -156,42 +156,58 @@ export const filterPredicates = {
|
||||
return dateFilterPredicate(item.digitalRelease, filterValue, type);
|
||||
},
|
||||
|
||||
tmdbRating: function(item, filterValue, type) {
|
||||
tmdbRating: function({ ratings = {} }, filterValue, 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);
|
||||
},
|
||||
|
||||
tmdbVotes: function(item, filterValue, type) {
|
||||
tmdbVotes: function({ ratings = {} }, filterValue, 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);
|
||||
},
|
||||
|
||||
imdbRating: function(item, filterValue, type) {
|
||||
imdbRating: function({ ratings = {} }, filterValue, 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);
|
||||
},
|
||||
|
||||
rottenTomatoesRating: function(item, filterValue, type) {
|
||||
imdbVotes: function({ ratings = {} }, filterValue, 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);
|
||||
},
|
||||
|
||||
imdbVotes: function(item, filterValue, type) {
|
||||
rottenTomatoesRating: function({ ratings = {} }, filterValue, 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);
|
||||
},
|
||||
|
||||
@@ -40,6 +40,7 @@ export const defaultState = {
|
||||
showTmdbRating: false,
|
||||
showImdbRating: false,
|
||||
showRottenTomatoesRating: false,
|
||||
showTraktRating: false,
|
||||
showTags: false,
|
||||
showSearchAction: false
|
||||
},
|
||||
@@ -158,7 +159,7 @@ export const defaultState = {
|
||||
},
|
||||
{
|
||||
name: 'minimumAvailability',
|
||||
label: () => translate('MinAvailability'),
|
||||
label: () => translate('MinimumAvailability'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
@@ -204,6 +205,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'traktRating',
|
||||
label: () => translate('TraktRating'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'popularity',
|
||||
label: () => translate('Popularity'),
|
||||
@@ -278,6 +285,10 @@ export const defaultState = {
|
||||
|
||||
rottenTomatoesRating: function({ ratings = {} }) {
|
||||
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',
|
||||
label: () => translate('ImdbRating'),
|
||||
type: filterBuilderTypes.NUMBER,
|
||||
numberFractionDigits: 1
|
||||
},
|
||||
{
|
||||
name: 'imdbVotes',
|
||||
label: () => translate('ImdbVotes'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
@@ -497,8 +514,13 @@ export const defaultState = {
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'imdbVotes',
|
||||
label: () => translate('ImdbVotes'),
|
||||
name: 'traktRating',
|
||||
label: () => translate('TraktRating'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'traktVotes',
|
||||
label: () => translate('TraktVotes'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
|
||||
@@ -110,7 +110,6 @@ export const defaultState = {
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: () => translate('Actions'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
isModifiable: false
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'movies.lastSearchTime',
|
||||
label: () => translate('LastSearched'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: () => translate('Status'),
|
||||
@@ -143,6 +149,12 @@ export const defaultState = {
|
||||
label: () => translate('Languages'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'movies.lastSearchTime',
|
||||
label: () => translate('LastSearched'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: '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;
|
||||
23
frontend/src/Store/Selectors/createMovieCreditsSelector.ts
Normal file
23
frontend/src/Store/Selectors/createMovieCreditsSelector.ts
Normal file
@@ -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 appState from 'App/State/AppState';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Movie from 'Movie/Movie';
|
||||
import { createMovieSelectorForHook } from './createMovieSelector';
|
||||
|
||||
function createMovieQualityProfileSelector(movieId: number) {
|
||||
return createSelector(
|
||||
(state: appState) => state.settings.qualityProfiles.items,
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
createMovieSelectorForHook(movieId),
|
||||
(qualityProfiles, movie = {} as Movie) => {
|
||||
return qualityProfiles.find(
|
||||
|
||||
@@ -126,14 +126,16 @@ class CutoffUnmetConnector extends Component {
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: selected
|
||||
movieIds: selected,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllCutoffUnmetPress = (monitored) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
|
||||
monitored
|
||||
monitored,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ function CutoffUnmetRow(props) {
|
||||
inCinemas,
|
||||
digitalRelease,
|
||||
physicalRelease,
|
||||
lastSearchTime,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange
|
||||
@@ -110,6 +111,16 @@ function CutoffUnmetRow(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
includeSeconds={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -148,6 +159,7 @@ CutoffUnmetRow.propTypes = {
|
||||
movieFileId: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
lastSearchTime: PropTypes.string,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
inCinemas: PropTypes.string,
|
||||
digitalRelease: PropTypes.string,
|
||||
|
||||
@@ -117,14 +117,16 @@ class MissingConnector extends Component {
|
||||
onSearchSelectedPress = (selected) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MOVIE_SEARCH,
|
||||
movieIds: selected
|
||||
movieIds: selected,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
onSearchAllMissingPress = (monitored) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.MISSING_MOVIES_SEARCH,
|
||||
monitored
|
||||
monitored,
|
||||
commandFinished: this.repopulate
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ function MissingRow(props) {
|
||||
inCinemas,
|
||||
digitalRelease,
|
||||
physicalRelease,
|
||||
lastSearchTime,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange
|
||||
@@ -100,6 +101,16 @@ function MissingRow(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.lastSearchTime') {
|
||||
return (
|
||||
<RelativeDateCell
|
||||
key={name}
|
||||
date={lastSearchTime}
|
||||
includeSeconds={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -138,6 +149,7 @@ MissingRow.propTypes = {
|
||||
movieFileId: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
lastSearchTime: PropTypes.string,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
inCinemas: PropTypes.string,
|
||||
digitalRelease: PropTypes.string,
|
||||
|
||||
17
frontend/src/typings/MovieCredit.ts
Normal file
17
frontend/src/typings/MovieCredit.ts
Normal file
@@ -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[];
|
||||
minFormatScore: number;
|
||||
cutoffFormatScore: number;
|
||||
minUpgradeFormatScore: number;
|
||||
formatItems: QualityProfileFormatItem[];
|
||||
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/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)
|
||||
{
|
||||
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),
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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.Specifications;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Test.CustomFormats;
|
||||
@@ -160,5 +161,95 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
new List<CustomFormat>())
|
||||
.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.Newznab;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
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[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