1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-05 13:21:25 -05:00

Compare commits

...

44 Commits

Author SHA1 Message Date
Mark McDowall
7b43c2e345 Fixed: Loading movie images after placeholder in Safari
Closes #10474
2024-09-25 06:48:30 +03:00
Bogdan
dc599b6531 Sort allowed sorting keys 2024-09-25 06:47:37 +03:00
Weblate
1421179654 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: FloatStream <1213193613@qq.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: liuwqq <843384478@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-09-25 02:35:29 +03:00
Bogdan
41dcf32e24 Fix translations for MovieMonitoredSelectInput 2024-09-25 02:33:02 +03:00
Bogdan
7a813a44b6 Use UTC to calculate movie status 2024-09-25 02:33:02 +03:00
Bogdan
54a5059080 Convert AvailabilitySelectInput to TypeScript 2024-09-25 02:33:02 +03:00
Bogdan
adaf7444d3 Add movie availability descriptions 2024-09-25 02:33:02 +03:00
Robin Dadswell
49d11e59b3 Fixed: Telegram Log Message 2024-09-24 16:59:30 +01:00
Servarr
a7eb4a4a04 Automated API Docs update 2024-09-24 12:10:31 +03:00
Bogdan
66a6a663ba Prevent line wraps on mobile for ratings 2024-09-24 03:31:43 +03:00
Bogdan
f735e31835 New: Trakt ratings 2024-09-24 03:31:43 +03:00
Bogdan
b8f1286abb Fixed: Sorting queue by columns 2024-09-22 07:30:46 +03:00
Mark McDowall
9df45199d0 Reprocessing manual import items unable to detect sample
(cherry picked from commit 27da0413882dc87e1617a5d091ac5111589e61a6)

Closes #10463
2024-09-22 05:21:38 +03:00
Servarr
a692c35b03 Automated API Docs update 2024-09-21 23:42:37 +03:00
momo
ddcad270c3 Fix description for API key as query parameter
(cherry picked from commit 30c36fdc3baa686102ff124833c7963fc786f251)
2024-09-21 21:18:15 +03:00
Bogdan
b06f1d7c12 Bump version to 5.11.0 2024-09-21 03:57:59 +03:00
Mark McDowall
480bb50b85 Fixed: Rejections for Custom Format score increment 2024-09-21 02:17:18 +03:00
Bogdan
dbc94dbe4e Simplify fallback to default for allowed sort keys
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-09-21 01:35:47 +03:00
Mark McDowall
b89271fc01 Fixed: Unable to login when instance name contained brackets 2024-09-21 01:23:25 +03:00
Bogdan
66fcde7325 Include current quality in rejection message for not an upgrade 2024-09-21 00:41:26 +03:00
Bogdan
463741da1f New: Fetch up to 1000 movies from Plex Watchlist 2024-09-18 03:49:49 +03:00
Bogdan
3388fae1a5 Fix translation key for Skip Free Space Check help text 2024-09-17 17:46:33 +03:00
bakerboy448
72b2cfe8be Fixed: Parse TELESYNCH as TELESYNC (#10445)
Fixes #10414
2024-09-17 02:34:12 +03:00
Servarr
d5dd5e08ca Automated API Docs update 2024-09-17 01:22:25 +03:00
Bogdan
fabd40cbae New: Allowed sort keys for paginated resources 2024-09-16 20:27:34 +03:00
Servarr
3ca327f611 Multiple Translations updated by Weblate (#10418)
ignore-downstream






Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: genoher <genoher@gmail.com>
Co-authored-by: rookie7420 <yuanchong2001@qq.com>
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-09-16 18:34:20 +03:00
Bogdan
c804140896 Fix use of min score increment in rejection message 2024-09-16 18:19:02 +03:00
somniumV
bb43d0c796 New: Minimum Upgrade Score for Custom Formats
(cherry picked from commit 8b20a9449c1ae5ffd1e8d12f1ca771727b8c52a5)
2024-09-16 18:19:02 +03:00
Mark McDowall
5757fa797f New: Use instance name in forms authentication cookie name
Closes #10416
2024-09-16 01:29:10 +03:00
Bogdan
2fc32189d8 Convert Movie Titles to TypeScript 2024-09-16 01:18:23 +03:00
Bogdan
5975be3690 Fixed: Removing import lists for cast and crew from movie details
Convert movie credits to TypeScript

Switching to metadata based order for crew
2024-09-16 01:18:23 +03:00
amdavie
6095819005 New: Scene and Nuked IndexerFlags for Newznab indexers
(cherry picked from commit 278c7891a3add639b4ff5bc1f4f5e8912dabc897)
2024-09-15 23:08:20 +03:00
Bogdan
7528882adf Gotify notification updates
New: Option to include links for Gotify notifications
New: Include images and links for Android

(cherry picked from commit 3c857135c59029635b0972f959f9a8255bcff21f)

Closes #10433
Fixes #10410
2024-09-15 22:42:59 +03:00
Mark McDowall
c1f1307345 New: Add exception to SSL Certificate validation message
(cherry picked from commit d84c4500949a530fac92d73f7f2f8e8462b37244)

Closes #10437
2024-09-15 22:42:59 +03:00
Mark McDowall
348060351a New: Check for available space before grabbing
(cherry picked from commit 4b5ff3927d3c123f9e3a2bc74328323fab1b0745)

Closes #10429
2024-09-15 22:42:59 +03:00
Mark McDowall
ca31cdd33a New: Add additional archive exentions
(cherry picked from commit 750a9353f82da4e016bee25e0c625cd6d8613b57)
2024-09-15 20:41:48 +03:00
Bogdan
36e278aa82 Bump version to 5.10.4 2024-09-15 15:52:54 +03:00
Bogdan
927e84654f Fixed: Filtering by IMDb decimal ratings 2024-09-13 01:25:37 +03:00
Bogdan
96e60906c5 Fixed: Empty or private MDBList lists shown as valid on save 2024-09-12 19:36:04 +03:00
Bogdan
7a55b563c0 New: Importing sup files as subtitles
Towards #10412
2024-09-11 20:44:38 +03:00
Servarr
b4bbb71a9b Automated API Docs update 2024-09-09 20:53:17 +03:00
ManiMatter
0361299a73 New: Last Searched column on Wanted screens (#10392)
* Adding lastSearchTime to API and "Last Searched" to Frontend (cutoff unmet & missing)
Picking lastSearchTime from movie instead of movieMetaData

---------

Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-09-08 20:56:11 +03:00
Bogdan
e11339fb83 Fix weblate widget 2024-09-08 11:26:02 +03:00
Bogdan
fbdd3129f5 Bump version to 5.10.3 2024-09-08 11:12:25 +03:00
177 changed files with 2698 additions and 1484 deletions

View File

@@ -1,7 +1,7 @@
# Radarr
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Translation status](https://translate.servarr.com/widget/servarr/radarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation/docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)

View File

@@ -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)'

View File

@@ -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>

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import MovieCredit from 'typings/MovieCredit';
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
export default MovieCreditAppState;

View File

@@ -27,6 +27,7 @@ export interface MovieIndexAppState {
showTmdbRating: boolean;
showImdbRating: boolean;
showRottenTomatoesRating: boolean;
showTraktRating: boolean;
showTags: boolean;
showSearchAction: boolean;
};

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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}
/>
);

View File

@@ -1,3 +1,7 @@
.wrapper {
display: inline-block;
}
.image {
align-content: center;
margin-right: 5px;

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'image': string;
'wrapper': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -23,7 +23,7 @@ function ImdbRating(props: ImdbRatingProps) {
return (
<Tooltip
anchor={
<span>
<span className={styles.wrapper}>
{!hideIcon && (
<img
className={styles.image}

View File

@@ -1,3 +1,7 @@
.wrapper {
display: inline-block;
}
.image {
align-content: center;
margin-right: 5px;

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'image': string;
'wrapper': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -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}

View File

@@ -1,3 +1,7 @@
.wrapper {
display: inline-block;
}
.image {
align-content: center;
margin-right: 5px;

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'image': string;
'wrapper': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -22,7 +22,7 @@ function TmdbRating(props: TmdbRatingProps) {
return (
<Tooltip
anchor={
<span>
<span className={styles.wrapper}>
{!hideIcon && (
<img
className={styles.image}

View File

@@ -0,0 +1,9 @@
.wrapper {
display: inline-block;
}
.image {
align-content: center;
margin-right: 5px;
vertical-align: -0.125em;
}

View File

@@ -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;

View 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;

View File

@@ -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}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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}

View File

@@ -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,

View File

@@ -35,6 +35,7 @@
.tmdbRating,
.imdbRating,
.rottenTomatoesRating,
.traktRating,
.runtime {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';

View File

@@ -21,6 +21,7 @@ interface CssExports {
'status': string;
'studio': string;
'tmdbRating': string;
'traktRating': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -60,6 +60,7 @@
.tmdbRating,
.imdbRating,
.rottenTomatoesRating,
.traktRating,
.runtime {
composes: cell;

View File

@@ -27,6 +27,7 @@ interface CssExports {
'statusIcon': string;
'studio': string;
'tmdbRating': string;
'traktRating': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -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]}>

View File

@@ -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;

View 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;

View 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;

View File

@@ -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);

View File

@@ -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;

View 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;

View 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;

View File

@@ -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);

View 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;

View File

@@ -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);

View File

@@ -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;

View 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;

View File

@@ -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>

View File

@@ -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;

View 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;

View File

@@ -1,3 +1,9 @@
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -1,5 +0,0 @@
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -4,8 +4,6 @@
margin-right: auto;
}
.tagInternalInput {
composes: internalInput from '~Components/Form/TagInput.css';
flex: 0 0 100%;
.labelIcon {
margin-left: 8px;
}

View File

@@ -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;

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}
/>

View File

@@ -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}>

View File

@@ -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);

View File

@@ -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>

View File

@@ -92,7 +92,8 @@
.imdbRating,
.tmdbRating,
.rottenTomatoesRating {
.rottenTomatoesRating,
.traktRating {
composes: cell;
flex: 0 0 80px;

View File

@@ -30,6 +30,7 @@ interface CssExports {
'studio': string;
'tags': string;
'tmdbRating': string;
'traktRating': string;
'year': string;
}
export const cssExports: CssExports;

View File

@@ -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]}>

View File

@@ -82,7 +82,8 @@
.imdbRating,
.tmdbRating,
.rottenTomatoesRating {
.rottenTomatoesRating,
.traktRating {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 80px;

View File

@@ -27,6 +27,7 @@ interface CssExports {
'studio': string;
'tags': string;
'tmdbRating': string;
'traktRating': string;
'year': string;
}
export const cssExports: CssExports;

View File

@@ -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;
}

View File

@@ -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;

View 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;

View File

@@ -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(() => {

View File

@@ -42,7 +42,7 @@ import styles from './ImportListExclusions.css';
const COLUMNS: Column[] = [
{
name: 'tmdbid',
name: 'tmdbId',
label: () => translate('TMDBId'),
isVisible: true,
isSortable: true,

View File

@@ -4,6 +4,10 @@
margin-right: auto;
}
.labelIcon {
margin-left: 8px;
}
.message {
composes: alert from '~Components/Alert.css';

View File

@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'deleteButton': string;
'labelIcon': string;
'message': string;
}
export const cssExports: CssExports;

View File

@@ -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>

View File

@@ -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}

View File

@@ -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')}

View File

@@ -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'),

View File

@@ -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);
},

View File

@@ -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
},
{

View File

@@ -110,7 +110,6 @@ export const defaultState = {
{
name: 'actions',
columnLabel: () => translate('Actions'),
isSortable: true,
isVisible: true,
isModifiable: false
}

View File

@@ -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'),

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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(

View File

@@ -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
});
};

View File

@@ -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,

View File

@@ -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
});
};

View File

@@ -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,

View 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;

View File

@@ -16,6 +16,7 @@ interface QualityProfile {
items: QualityProfileQualityItem[];
minFormatScore: number;
cutoffFormatScore: number;
minUpgradeFormatScore: number;
formatItems: QualityProfileFormatItem[];
id: number;
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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