mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-16 21:15:33 -04:00
Compare commits
69 Commits
v5.10.2.91
...
v5.12.1.92
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75c7a3cfc6 | ||
|
|
cfdb7a15de | ||
|
|
63a7d33e7e | ||
|
|
c9836f997c | ||
|
|
d37e71415f | ||
|
|
9a5f4bef63 | ||
|
|
40551ba5a3 | ||
|
|
6e04dc894b | ||
|
|
ac767ed386 | ||
|
|
42fbb79017 | ||
|
|
c43bd77dae | ||
|
|
68dfa55b35 | ||
|
|
fa190c85a3 | ||
|
|
172dcf6f8d | ||
|
|
0736fc955f | ||
|
|
9d0b8d974d | ||
|
|
9a3e89f283 | ||
|
|
e33e45ec73 | ||
|
|
5893d88058 | ||
|
|
a81d27acda | ||
|
|
d2b279a6be | ||
|
|
6686fa0600 | ||
|
|
1d286df85d | ||
|
|
be2e1e4fdb | ||
|
|
08868e5d01 | ||
|
|
7b43c2e345 | ||
|
|
dc599b6531 | ||
|
|
1421179654 | ||
|
|
41dcf32e24 | ||
|
|
7a813a44b6 | ||
|
|
54a5059080 | ||
|
|
adaf7444d3 | ||
|
|
49d11e59b3 | ||
|
|
a7eb4a4a04 | ||
|
|
66a6a663ba | ||
|
|
f735e31835 | ||
|
|
b8f1286abb | ||
|
|
9df45199d0 | ||
|
|
a692c35b03 | ||
|
|
ddcad270c3 | ||
|
|
b06f1d7c12 | ||
|
|
480bb50b85 | ||
|
|
dbc94dbe4e | ||
|
|
b89271fc01 | ||
|
|
66fcde7325 | ||
|
|
463741da1f | ||
|
|
3388fae1a5 | ||
|
|
72b2cfe8be | ||
|
|
d5dd5e08ca | ||
|
|
fabd40cbae | ||
|
|
3ca327f611 | ||
|
|
c804140896 | ||
|
|
bb43d0c796 | ||
|
|
5757fa797f | ||
|
|
2fc32189d8 | ||
|
|
5975be3690 | ||
|
|
6095819005 | ||
|
|
7528882adf | ||
|
|
c1f1307345 | ||
|
|
348060351a | ||
|
|
ca31cdd33a | ||
|
|
36e278aa82 | ||
|
|
927e84654f | ||
|
|
96e60906c5 | ||
|
|
7a55b563c0 | ||
|
|
b4bbb71a9b | ||
|
|
0361299a73 | ||
|
|
e11339fb83 | ||
|
|
fbdd3129f5 |
@@ -1,7 +1,7 @@
|
||||
# Radarr
|
||||
|
||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||
[](https://translate.servarr.com/engage/servarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/radarr/installation/docker)
|
||||

|
||||
[](#backers)
|
||||
@@ -9,7 +9,7 @@
|
||||
[](#mega-sponsors)
|
||||
|
||||
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
|
||||
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
|
||||
Note that only one type of a given movie is supported. If you want both a 4k version and 1080p version of a given movie you will need multiple instances.
|
||||
|
||||
## Major Features Include
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.10.2'
|
||||
majorVersion: '5.12.1'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AddNewMovieModalContent.css';
|
||||
@@ -115,13 +118,28 @@ class AddNewMovieModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
onChange={onInputChange}
|
||||
{...minimumAvailability}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Icon from 'Components/Icon';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportMovieHeader.css';
|
||||
|
||||
@@ -46,7 +50,19 @@ function ImportMovieHeader(props) {
|
||||
className={styles.minimumAvailability}
|
||||
name="minimumAvailability"
|
||||
>
|
||||
{translate('MinAvailability')}
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.detailsIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function MovieMinimumAvailabilityPopoverContent() {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Announced')}
|
||||
data={translate('AnnouncedMovieAvailabilityDescription')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('InCinemas')}
|
||||
data={translate('InCinemasMovieAvailabilityDescription')}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Released')}
|
||||
data={translate('ReleasedMovieAvailabilityDescription')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieMinimumAvailabilityPopoverContent;
|
||||
@@ -1,9 +1,10 @@
|
||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||
import MovieCreditAppState from './MovieCreditAppState';
|
||||
import MovieFilesAppState from './MovieFilesAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
@@ -64,6 +65,7 @@ interface AppState {
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieCollections: MovieCollectionAppState;
|
||||
movieCredits: MovieCreditAppState;
|
||||
movieFiles: MovieFilesAppState;
|
||||
movieIndex: MovieIndexAppState;
|
||||
movies: MoviesAppState;
|
||||
|
||||
6
frontend/src/App/State/MovieCreditAppState.ts
Normal file
6
frontend/src/App/State/MovieCreditAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import MovieCredit from 'typings/MovieCredit';
|
||||
|
||||
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
|
||||
|
||||
export default MovieCreditAppState;
|
||||
@@ -27,6 +27,7 @@ export interface MovieIndexAppState {
|
||||
showTmdbRating: boolean;
|
||||
showImdbRating: boolean;
|
||||
showRottenTomatoesRating: boolean;
|
||||
showTraktRating: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import General from 'typings/Settings/General';
|
||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
@@ -49,6 +50,12 @@ export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export interface ReleaseProfilesAppState
|
||||
extends AppSectionState<ReleaseProfile>,
|
||||
AppSectionSaveState {
|
||||
pendingChanges: Partial<ReleaseProfile>;
|
||||
}
|
||||
|
||||
export interface CustomFormatAppState
|
||||
extends AppSectionState<CustomFormat>,
|
||||
AppSectionDeleteState,
|
||||
@@ -83,6 +90,7 @@ interface SettingsAppState {
|
||||
languages: LanguageSettingsAppState;
|
||||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
releaseProfiles: ReleaseProfilesAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ function getValue(input, selectedFilterBuilderProp) {
|
||||
}
|
||||
|
||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||
return parseInt(input);
|
||||
const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
|
||||
|
||||
return Number(input).toFixed(numberFractionDigits);
|
||||
}
|
||||
|
||||
return input;
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const availabilityOptions = [
|
||||
{
|
||||
key: 'announced',
|
||||
get value() {
|
||||
return translate('Announced');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'inCinemas',
|
||||
get value() {
|
||||
return translate('InCinemas');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'released',
|
||||
get value() {
|
||||
return translate('Released');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
function AvailabilitySelectInput(props) {
|
||||
const values = [...availabilityOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed
|
||||
} = props;
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
AvailabilitySelectInput.propTypes = {
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
includeMixed: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AvailabilitySelectInput.defaultProps = {
|
||||
includeNoChange: false,
|
||||
includeMixed: false
|
||||
};
|
||||
|
||||
export default AvailabilitySelectInput;
|
||||
67
frontend/src/Components/Form/AvailabilitySelectInput.tsx
Normal file
67
frontend/src/Components/Form/AvailabilitySelectInput.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
interface AvailabilitySelectInputProps {
|
||||
includeNoChange: boolean;
|
||||
includeNoChangeDisabled?: boolean;
|
||||
includeMixed?: boolean;
|
||||
}
|
||||
|
||||
interface IMovieAvailabilityOption {
|
||||
key: string;
|
||||
value: string;
|
||||
format?: string;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const movieAvailabilityOptions: IMovieAvailabilityOption[] = [
|
||||
{
|
||||
key: 'announced',
|
||||
get value() {
|
||||
return translate('Announced');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'inCinemas',
|
||||
get value() {
|
||||
return translate('InCinemas');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'released',
|
||||
get value() {
|
||||
return translate('Released');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function AvailabilitySelectInput(props: AvailabilitySelectInputProps) {
|
||||
const values = [...movieAvailabilityOptions];
|
||||
|
||||
const {
|
||||
includeNoChange = false,
|
||||
includeNoChangeDisabled = true,
|
||||
includeMixed = false,
|
||||
} = props;
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
isDisabled: includeNoChangeDisabled,
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: `(${translate('Mixed')})`,
|
||||
isDisabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
return <EnhancedSelectInput {...props} values={values} />;
|
||||
}
|
||||
|
||||
export default AvailabilitySelectInput;
|
||||
@@ -272,6 +272,8 @@ FormInputGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
values: PropTypes.arrayOf(PropTypes.any),
|
||||
placeholder: PropTypes.string,
|
||||
delimiters: PropTypes.arrayOf(PropTypes.string),
|
||||
isDisabled: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
@@ -284,8 +286,10 @@ FormInputGroup.propTypes = {
|
||||
helpTextWarning: PropTypes.string,
|
||||
helpLink: PropTypes.string,
|
||||
autoFocus: PropTypes.bool,
|
||||
canEdit: PropTypes.bool,
|
||||
includeNoChange: PropTypes.bool,
|
||||
includeNoChangeDisabled: PropTypes.bool,
|
||||
includeAny: PropTypes.bool,
|
||||
selectedValueOptions: PropTypes.object,
|
||||
indexerFlags: PropTypes.number,
|
||||
pending: PropTypes.bool,
|
||||
|
||||
@@ -5,17 +5,20 @@ import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function MovieMonitoredSelectInput(props) {
|
||||
const values = [...monitorOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed
|
||||
includeMixed,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const values = [...monitorOptions];
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
@@ -23,14 +26,16 @@ function MovieMonitoredSelectInput(props) {
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
get value() {
|
||||
return `(${translate('Mixed')})`;
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
{...otherProps}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
|
||||
1
frontend/src/Components/ImdbRating.css.d.ts
vendored
1
frontend/src/Components/ImdbRating.css.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'image': string;
|
||||
'wrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -23,7 +23,7 @@ function ImdbRating(props: ImdbRatingProps) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={
|
||||
<span>
|
||||
<span className={styles.wrapper}>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'image': string;
|
||||
'wrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -24,7 +24,7 @@ function RottenTomatoRating(props: RottenTomatoRatingProps) {
|
||||
const ratingImage = value > 50 ? rtFresh : rtRotten;
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.wrapper}>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
|
||||
1
frontend/src/Components/TmdbRating.css.d.ts
vendored
1
frontend/src/Components/TmdbRating.css.d.ts
vendored
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'image': string;
|
||||
'wrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -22,7 +22,7 @@ function TmdbRating(props: TmdbRatingProps) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={
|
||||
<span>
|
||||
<span className={styles.wrapper}>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
|
||||
9
frontend/src/Components/TraktRating.css
Normal file
9
frontend/src/Components/TraktRating.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.image {
|
||||
align-content: center;
|
||||
margin-right: 5px;
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'blankpad': string;
|
||||
'image': string;
|
||||
'wrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
46
frontend/src/Components/TraktRating.tsx
Normal file
46
frontend/src/Components/TraktRating.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { Ratings } from 'Movie/Movie';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TraktRating.css';
|
||||
|
||||
interface TraktRatingProps {
|
||||
ratings: Ratings;
|
||||
iconSize?: number;
|
||||
hideIcon?: boolean;
|
||||
}
|
||||
|
||||
function TraktRating(props: TraktRatingProps) {
|
||||
const { ratings, iconSize = 14, hideIcon = false } = props;
|
||||
|
||||
const traktImage =
|
||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgICAgdmlld0JveD0iMCAwIDE0NC44IDE0NC44IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxNDQuOCAxNDQuOCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+ICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik0yOS41LDExMS44YzEwLjYsMTEuNiwyNS45LDE4LjgsNDIuOSwxOC44YzguNywwLDE2LjktMS45LDI0LjMtNS4zTDU2LjMsODVMMjkuNSwxMTEuOHoiLz4gICAgPHBhdGggZmlsbD0iI0VEMjIyNCIgZD0iTTU2LjEsNjAuNkwyNS41LDkxLjFMMjEuNCw4N2wzMi4yLTMyLjJoMGwzNy42LTM3LjZjLTUuOS0yLTEyLjItMy4xLTE4LjgtMy4xYy0zMi4yLDAtNTguMywyNi4xLTU4LjMsNTguMyAgICAgICBjMCwxMy4xLDQuMywyNS4yLDExLjcsMzVsMzAuNS0zMC41bDIuMSwybDQzLjcsNDMuN2MwLjktMC41LDEuNy0xLDIuNS0xLjZMNTYuMyw3Mi43TDI3LDEwMmwtNC4xLTQuMWwzMy40LTMzLjRsMi4xLDJsNTEsNTAuOSAgICAgICBjMC44LTAuNiwxLjUtMS4zLDIuMi0xLjlsLTU1LTU1TDU2LjEsNjAuNnoiLz4gICAgPHBhdGggZmlsbD0iI0VEMUMyNCIgZD0iTTExNS43LDExMS40YzkuMy0xMC4zLDE1LTI0LDE1LTM5YzAtMjMuNC0xMy44LTQzLjUtMzMuNi01Mi44TDYwLjQsNTYuMkwxMTUuNywxMTEuNHogTTc0LjUsNjYuOGwtNC4xLTQuMSAgICAgICBsMjguOS0yOC45bDQuMSw0LjFMNzQuNSw2Ni44eiBNMTAxLjksMjcuMUw2OC42LDYwLjRsLTQuMS00LjFMOTcuOCwyM0wxMDEuOSwyNy4xeiIvPiAgICA8Zz4gICAgICAgPGc+ICAgICAgICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik03Mi40LDE0NC44QzMyLjUsMTQ0LjgsMCwxMTIuMywwLDcyLjRDMCwzMi41LDMyLjUsMCw3Mi40LDBzNzIuNCwzMi41LDcyLjQsNzIuNCAgICAgICAgICAgICBDMTQ0LjgsMTEyLjMsMTEyLjMsMTQ0LjgsNzIuNCwxNDQuOHogTTcyLjQsNy4zQzM2LjUsNy4zLDcuMywzNi41LDcuMyw3Mi40czI5LjIsNjUuMSw2NS4xLDY1LjFzNjUuMS0yOS4yLDY1LjEtNjUuMSAgICAgICAgICAgICBTMTA4LjMsNy4zLDcyLjQsNy4zeiIvPiAgICAgICA8L2c+ICAgIDwvZz48L2c+PC9zdmc+';
|
||||
|
||||
const { value = 0, votes = 0 } = ratings.trakt;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={
|
||||
<span className={styles.wrapper}>
|
||||
{!hideIcon && (
|
||||
<img
|
||||
className={styles.image}
|
||||
alt={translate('TraktRating')}
|
||||
src={traktImage}
|
||||
style={{
|
||||
height: `${iconSize}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(value * 10).toFixed()}%
|
||||
</span>
|
||||
}
|
||||
tooltip={translate('CountVotes', { votes })}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default TraktRating;
|
||||
@@ -110,6 +110,15 @@ function DiscoverMovieSortMenu(props) {
|
||||
{translate('RottenTomatoesRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="traktRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('TraktRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="certification"
|
||||
sortKey={sortKey}
|
||||
|
||||
@@ -9,6 +9,7 @@ import Link from 'Components/Link/Link';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -92,6 +93,7 @@ class DiscoverMoviePoster extends Component {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
ratings,
|
||||
isExisting,
|
||||
isExcluded,
|
||||
@@ -223,6 +225,12 @@ class DiscoverMoviePoster extends Component {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showTraktRating && !!ratings.trakt ? (
|
||||
<div className={styles.title}>
|
||||
<TraktRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<DiscoverMoviePosterInfo
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
@@ -232,6 +240,7 @@ class DiscoverMoviePoster extends Component {
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
showTraktRating={showTraktRating}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
@@ -274,6 +283,7 @@ DiscoverMoviePoster.propTypes = {
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
showTraktRating: PropTypes.bool.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -4,6 +4,7 @@ import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||
@@ -28,7 +29,8 @@ function DiscoverMoviePosterInfo(props) {
|
||||
movieRuntimeFormat,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating
|
||||
} = props;
|
||||
|
||||
if (sortKey === 'status' && status) {
|
||||
@@ -141,6 +143,14 @@ function DiscoverMoviePosterInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!showTraktRating && sortKey === 'traktRating' && !!ratings.trakt) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<TraktRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -160,7 +170,8 @@ DiscoverMoviePosterInfo.propTypes = {
|
||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
showTraktRating: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default DiscoverMoviePosterInfo;
|
||||
|
||||
@@ -39,7 +39,8 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating
|
||||
} = posterOptions;
|
||||
|
||||
const heights = [
|
||||
@@ -64,6 +65,10 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showTraktRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
switch (sortKey) {
|
||||
case 'studio':
|
||||
case 'inCinemas':
|
||||
@@ -88,6 +93,11 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'traktRating':
|
||||
if (!showTraktRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// No need to add a height of 0
|
||||
}
|
||||
@@ -219,7 +229,8 @@ class DiscoverMoviePosters extends Component {
|
||||
showTitle,
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating
|
||||
} = posterOptions;
|
||||
|
||||
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||
@@ -248,6 +259,7 @@ class DiscoverMoviePosters extends Component {
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
showTraktRating={showTraktRating}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
|
||||
@@ -48,6 +48,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
showTmdbRating: props.showTmdbRating,
|
||||
showImdbRating: props.showImdbRating,
|
||||
showRottenTomatoesRating: props.showRottenTomatoesRating,
|
||||
showTraktRating: props.showTraktRating,
|
||||
includeRecommendations: props.includeRecommendations,
|
||||
includeTrending: props.includeTrending,
|
||||
includePopular: props.includePopular
|
||||
@@ -61,6 +62,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -88,6 +90,10 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
state.showRottenTomatoesRating = showRottenTomatoesRating;
|
||||
}
|
||||
|
||||
if (showTraktRating !== prevProps.showTraktRating) {
|
||||
state.showTraktRating = showTraktRating;
|
||||
}
|
||||
|
||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||
state.includeRecommendations = includeRecommendations;
|
||||
}
|
||||
@@ -140,6 +146,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
includeRecommendations,
|
||||
includeTrending,
|
||||
includePopular
|
||||
@@ -248,6 +255,18 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowTraktRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showTraktRating"
|
||||
value={showTraktRating}
|
||||
helpText={translate('ShowTraktRatingPosterHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
@@ -269,6 +288,7 @@ DiscoverMoviePosterOptionsModalContent.propTypes = {
|
||||
showTmdbRating: PropTypes.bool.isRequired,
|
||||
showImdbRating: PropTypes.bool.isRequired,
|
||||
showRottenTomatoesRating: PropTypes.bool.isRequired,
|
||||
showTraktRating: PropTypes.bool.isRequired,
|
||||
includeRecommendations: PropTypes.bool.isRequired,
|
||||
includeTrending: PropTypes.bool.isRequired,
|
||||
includePopular: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
.tmdbRating,
|
||||
.imdbRating,
|
||||
.rottenTomatoesRating,
|
||||
.traktRating,
|
||||
.runtime {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ interface CssExports {
|
||||
'status': string;
|
||||
'studio': string;
|
||||
'tmdbRating': string;
|
||||
'traktRating': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
.tmdbRating,
|
||||
.imdbRating,
|
||||
.rottenTomatoesRating,
|
||||
.traktRating,
|
||||
.runtime {
|
||||
composes: cell;
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ interface CssExports {
|
||||
'statusIcon': string;
|
||||
'studio': string;
|
||||
'tmdbRating': string;
|
||||
'traktRating': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -11,6 +11,7 @@ import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
|
||||
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
|
||||
import { icons } from 'Helpers/Props';
|
||||
@@ -291,6 +292,17 @@ class DiscoverMovieRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'traktRating') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{ratings.trakt ? <TraktRating ratings={ratings} /> : null}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'popularity') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from '../MovieCreditPoster.css';
|
||||
|
||||
class MovieCastPoster extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
hasPosterError: false,
|
||||
isEditImportListModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditImportListPress = () => {
|
||||
this.setState({ isEditImportListModalOpen: true });
|
||||
};
|
||||
|
||||
onAddImportListPress = () => {
|
||||
this.props.onImportListSelect();
|
||||
this.setState({ isEditImportListModalOpen: true });
|
||||
};
|
||||
|
||||
onEditImportListModalClose = () => {
|
||||
this.setState({ isEditImportListModalOpen: false });
|
||||
};
|
||||
|
||||
onPosterLoad = () => {
|
||||
if (this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: false });
|
||||
}
|
||||
};
|
||||
|
||||
onPosterLoadError = () => {
|
||||
if (!this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: true });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
tmdbId,
|
||||
personName,
|
||||
character,
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
importList
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
hasPosterError
|
||||
} = this.state;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
borderRadius: '5px'
|
||||
};
|
||||
|
||||
const contentStyle = {
|
||||
width: `${posterWidth}px`
|
||||
};
|
||||
|
||||
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
|
||||
const importListId = importList ? importList.id : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.content}
|
||||
style={contentStyle}
|
||||
>
|
||||
<div className={styles.posterContainer}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
size={20}
|
||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Label className={styles.controls}>
|
||||
<span className={styles.externalLinks}>
|
||||
<Popover
|
||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||
<Label
|
||||
className={styles.externalLinkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{translate('TMDb')}
|
||||
</Label>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<div
|
||||
style={elementStyle}
|
||||
>
|
||||
<MovieHeadshot
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={this.onPosterLoadError}
|
||||
onLoad={this.onPosterLoad}
|
||||
/>
|
||||
|
||||
{
|
||||
hasPosterError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{personName}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||
{personName}
|
||||
</div>
|
||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||
{character}
|
||||
</div>
|
||||
|
||||
<EditImportListModalConnector
|
||||
id={importListId}
|
||||
isOpen={this.state.isEditImportListModalOpen}
|
||||
onModalClose={this.onEditImportListModalClose}
|
||||
onDeleteImportListPress={this.onDeleteImportListPress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieCastPoster.propTypes = {
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
personName: PropTypes.string.isRequired,
|
||||
character: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
importList: PropTypes.object,
|
||||
onImportListSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieCastPoster;
|
||||
179
frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.tsx
Normal file
179
frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import MovieCredit from 'typings/MovieCredit';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from '../MovieCreditPoster.css';
|
||||
|
||||
export interface MovieCastPosterProps
|
||||
extends Pick<MovieCredit, 'personName' | 'images' | 'character'> {
|
||||
tmdbId: number;
|
||||
posterWidth: number;
|
||||
posterHeight: number;
|
||||
importList?: ImportList;
|
||||
onImportListSelect(): void;
|
||||
}
|
||||
|
||||
function MovieCastPoster(props: MovieCastPosterProps) {
|
||||
const {
|
||||
tmdbId,
|
||||
personName,
|
||||
character,
|
||||
images = [],
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
importList,
|
||||
onImportListSelect,
|
||||
} = props;
|
||||
|
||||
const importListId = importList?.id ?? 0;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [hasPosterError, setHasPosterError] = useState(false);
|
||||
|
||||
const [
|
||||
isEditImportListModalOpen,
|
||||
setEditImportListModalOpen,
|
||||
setEditImportListModalClosed,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const [
|
||||
isDeleteImportListModalOpen,
|
||||
setDeleteImportListModalOpen,
|
||||
setDeleteImportListModalClosed,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const handlePosterLoadError = useCallback(() => {
|
||||
setHasPosterError(true);
|
||||
}, [setHasPosterError]);
|
||||
|
||||
const handlePosterLoad = useCallback(() => {
|
||||
setHasPosterError(false);
|
||||
}, [setHasPosterError]);
|
||||
|
||||
const handleManageImportListPress = useCallback(() => {
|
||||
if (importListId === 0) {
|
||||
onImportListSelect();
|
||||
}
|
||||
|
||||
setEditImportListModalOpen();
|
||||
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
|
||||
|
||||
const handleDeleteImportListConfirmed = useCallback(() => {
|
||||
dispatch(deleteImportList({ id: importListId }));
|
||||
|
||||
setEditImportListModalClosed();
|
||||
setDeleteImportListModalClosed();
|
||||
}, [
|
||||
importListId,
|
||||
setEditImportListModalClosed,
|
||||
setDeleteImportListModalClosed,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
borderRadius: '5px',
|
||||
};
|
||||
|
||||
const contentStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
};
|
||||
|
||||
const monitored =
|
||||
importList?.enabled === true && importList?.enableAuto === true;
|
||||
|
||||
return (
|
||||
<div className={styles.content} style={contentStyle}>
|
||||
<div className={styles.posterContainer}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
size={20}
|
||||
onPress={handleManageImportListPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Label className={styles.controls}>
|
||||
<span className={styles.externalLinks}>
|
||||
<Popover
|
||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||
<Label
|
||||
className={styles.externalLinkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{translate('TMDb')}
|
||||
</Label>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<div style={elementStyle}>
|
||||
<MovieHeadshot
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={handlePosterLoadError}
|
||||
onLoad={handlePosterLoad}
|
||||
/>
|
||||
|
||||
{hasPosterError && (
|
||||
<div className={styles.overlayTitle}>{personName}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||
{personName}
|
||||
</div>
|
||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||
{character}
|
||||
</div>
|
||||
|
||||
<EditImportListModalConnector
|
||||
id={importListId}
|
||||
isOpen={isEditImportListModalOpen}
|
||||
onModalClose={setEditImportListModalClosed}
|
||||
onDeleteImportListPress={setDeleteImportListModalOpen}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteImportListModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteImportList')}
|
||||
message={translate('DeleteImportListMessageText', {
|
||||
name: importList?.name ?? personName,
|
||||
})}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={handleDeleteImportListConfirmed}
|
||||
onCancel={setDeleteImportListModalClosed}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieCastPoster;
|
||||
25
frontend/src/Movie/Details/Credits/Cast/MovieCastPosters.tsx
Normal file
25
frontend/src/Movie/Details/Credits/Cast/MovieCastPosters.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
|
||||
import MovieCreditPosters from '../MovieCreditPosters';
|
||||
import MovieCastPoster from './MovieCastPoster';
|
||||
|
||||
interface MovieCastPostersProps {
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
function MovieCastPosters({ isSmallScreen }: MovieCastPostersProps) {
|
||||
const { items: castCredits } = useSelector(
|
||||
createMovieCreditsSelector('cast')
|
||||
);
|
||||
|
||||
return (
|
||||
<MovieCreditPosters
|
||||
items={castCredits}
|
||||
itemComponent={MovieCastPoster}
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieCastPosters;
|
||||
@@ -1,43 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import MovieCreditPosters from '../MovieCreditPosters';
|
||||
import MovieCastPoster from './MovieCastPoster';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieCredits.items,
|
||||
(credits) => {
|
||||
const cast = _.reduce(credits, (acc, credit) => {
|
||||
if (credit.type === 'cast') {
|
||||
acc.push(credit);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
items: cast
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class MovieCastPostersConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<MovieCreditPosters
|
||||
{...this.props}
|
||||
itemComponent={MovieCastPoster}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(MovieCastPostersConnector);
|
||||
@@ -1,175 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from '../MovieCreditPoster.css';
|
||||
|
||||
class MovieCrewPoster extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
hasPosterError: false,
|
||||
isEditImportListModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditImportListPress = () => {
|
||||
this.setState({ isEditImportListModalOpen: true });
|
||||
};
|
||||
|
||||
onAddImportListPress = () => {
|
||||
this.props.onImportListSelect();
|
||||
this.setState({ isEditImportListModalOpen: true });
|
||||
};
|
||||
|
||||
onEditImportListModalClose = () => {
|
||||
this.setState({ isEditImportListModalOpen: false });
|
||||
};
|
||||
|
||||
onPosterLoad = () => {
|
||||
if (this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: false });
|
||||
}
|
||||
};
|
||||
|
||||
onPosterLoadError = () => {
|
||||
if (!this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: true });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
tmdbId,
|
||||
personName,
|
||||
job,
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
importList
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
hasPosterError
|
||||
} = this.state;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
borderRadius: '5px'
|
||||
};
|
||||
|
||||
const contentStyle = {
|
||||
width: `${posterWidth}px`
|
||||
};
|
||||
|
||||
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
|
||||
const importListId = importList ? importList.id : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.content}
|
||||
style={contentStyle}
|
||||
>
|
||||
<div className={styles.posterContainer}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
size={20}
|
||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Label className={styles.controls}>
|
||||
<span className={styles.externalLinks}>
|
||||
<Popover
|
||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||
<Label
|
||||
className={styles.externalLinkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{translate('TMDb')}
|
||||
</Label>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<div
|
||||
style={elementStyle}
|
||||
>
|
||||
<MovieHeadshot
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={this.onPosterLoadError}
|
||||
onLoad={this.onPosterLoad}
|
||||
/>
|
||||
|
||||
{
|
||||
hasPosterError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{personName}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||
{personName}
|
||||
</div>
|
||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||
{job}
|
||||
</div>
|
||||
|
||||
<EditImportListModalConnector
|
||||
id={importListId}
|
||||
isOpen={this.state.isEditImportListModalOpen}
|
||||
onModalClose={this.onEditImportListModalClose}
|
||||
onDeleteImportListPress={this.onDeleteImportListPress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieCrewPoster.propTypes = {
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
personName: PropTypes.string.isRequired,
|
||||
job: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
importList: PropTypes.object,
|
||||
onImportListSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieCrewPoster;
|
||||
177
frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.tsx
Normal file
177
frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import MovieCredit from 'typings/MovieCredit';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from '../MovieCreditPoster.css';
|
||||
|
||||
export interface MovieCrewPosterProps
|
||||
extends Pick<MovieCredit, 'personName' | 'images' | 'job'> {
|
||||
tmdbId: number;
|
||||
posterWidth: number;
|
||||
posterHeight: number;
|
||||
importList?: ImportList;
|
||||
onImportListSelect(): void;
|
||||
}
|
||||
|
||||
function MovieCrewPoster(props: MovieCrewPosterProps) {
|
||||
const {
|
||||
tmdbId,
|
||||
personName,
|
||||
job,
|
||||
images = [],
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
importList,
|
||||
onImportListSelect,
|
||||
} = props;
|
||||
|
||||
const importListId = importList?.id ?? 0;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [hasPosterError, setHasPosterError] = useState(false);
|
||||
|
||||
const [
|
||||
isEditImportListModalOpen,
|
||||
setEditImportListModalOpen,
|
||||
setEditImportListModalClosed,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const [
|
||||
isDeleteImportListModalOpen,
|
||||
setDeleteImportListModalOpen,
|
||||
setDeleteImportListModalClosed,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const handlePosterLoadError = useCallback(() => {
|
||||
setHasPosterError(true);
|
||||
}, [setHasPosterError]);
|
||||
|
||||
const handlePosterLoad = useCallback(() => {
|
||||
setHasPosterError(false);
|
||||
}, [setHasPosterError]);
|
||||
|
||||
const handleManageImportListPress = useCallback(() => {
|
||||
if (importListId === 0) {
|
||||
onImportListSelect();
|
||||
}
|
||||
|
||||
setEditImportListModalOpen();
|
||||
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
|
||||
|
||||
const handleDeleteImportListConfirmed = useCallback(() => {
|
||||
dispatch(deleteImportList({ id: importListId }));
|
||||
|
||||
setEditImportListModalClosed();
|
||||
setDeleteImportListModalClosed();
|
||||
}, [
|
||||
importListId,
|
||||
setEditImportListModalClosed,
|
||||
setDeleteImportListModalClosed,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
borderRadius: '5px',
|
||||
};
|
||||
|
||||
const contentStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
};
|
||||
|
||||
const monitored =
|
||||
importList?.enabled === true && importList?.enableAuto === true;
|
||||
|
||||
return (
|
||||
<div className={styles.content} style={contentStyle}>
|
||||
<div className={styles.posterContainer}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
size={20}
|
||||
onPress={handleManageImportListPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Label className={styles.controls}>
|
||||
<span className={styles.externalLinks}>
|
||||
<Popover
|
||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||
<Label
|
||||
className={styles.externalLinkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{translate('TMDb')}
|
||||
</Label>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<div style={elementStyle}>
|
||||
<MovieHeadshot
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={handlePosterLoadError}
|
||||
onLoad={handlePosterLoad}
|
||||
/>
|
||||
|
||||
{hasPosterError && (
|
||||
<div className={styles.overlayTitle}>{personName}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>
|
||||
{personName}
|
||||
</div>
|
||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
|
||||
|
||||
<EditImportListModalConnector
|
||||
id={importListId}
|
||||
isOpen={isEditImportListModalOpen}
|
||||
onModalClose={setEditImportListModalClosed}
|
||||
onDeleteImportListPress={setDeleteImportListModalOpen}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteImportListModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteImportList')}
|
||||
message={translate('DeleteImportListMessageText', {
|
||||
name: importList?.name ?? personName,
|
||||
})}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={handleDeleteImportListConfirmed}
|
||||
onCancel={setDeleteImportListModalClosed}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieCrewPoster;
|
||||
25
frontend/src/Movie/Details/Credits/Crew/MovieCrewPosters.tsx
Normal file
25
frontend/src/Movie/Details/Credits/Crew/MovieCrewPosters.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
|
||||
import MovieCreditPosters from '../MovieCreditPosters';
|
||||
import MovieCrewPoster from './MovieCrewPoster';
|
||||
|
||||
interface MovieCrewPostersProps {
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
function MovieCrewPosters({ isSmallScreen }: MovieCrewPostersProps) {
|
||||
const { items: crewCredits } = useSelector(
|
||||
createMovieCreditsSelector('crew')
|
||||
);
|
||||
|
||||
return (
|
||||
<MovieCreditPosters
|
||||
items={crewCredits}
|
||||
itemComponent={MovieCrewPoster}
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieCrewPosters;
|
||||
@@ -1,68 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import MovieCreditPosters from '../MovieCreditPosters';
|
||||
import MovieCrewPoster from './MovieCrewPoster';
|
||||
|
||||
function crewSort(a, b) {
|
||||
const jobOrder = ['Director', 'Writer', 'Producer', 'Executive Producer', 'Director of Photography'];
|
||||
|
||||
const indexA = jobOrder.indexOf(a.job);
|
||||
const indexB = jobOrder.indexOf(b.job);
|
||||
|
||||
if (indexA === -1 && indexB === -1) {
|
||||
return 0;
|
||||
} else if (indexA === -1) {
|
||||
return 1;
|
||||
} else if (indexB === -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (indexA < indexB) {
|
||||
return -1;
|
||||
} else if (indexA > indexB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieCredits.items,
|
||||
(credits) => {
|
||||
const crew = _.reduce(credits, (acc, credit) => {
|
||||
if (credit.type === 'crew') {
|
||||
acc.push(credit);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const sortedCrew = crew.sort(crewSort);
|
||||
|
||||
return {
|
||||
items: _.uniqBy(sortedCrew, 'personName')
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class MovieCrewPostersConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<MovieCreditPosters
|
||||
{...this.props}
|
||||
itemComponent={MovieCrewPoster}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(MovieCrewPostersConnector);
|
||||
60
frontend/src/Movie/Details/Credits/MovieCreditPoster.tsx
Normal file
60
frontend/src/Movie/Details/Credits/MovieCreditPoster.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
selectImportListSchema,
|
||||
setImportListFieldValue,
|
||||
setImportListValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createMovieCreditImportListSelector from 'Store/Selectors/createMovieCreditImportListSelector';
|
||||
import { MovieCastPosterProps } from './Cast/MovieCastPoster';
|
||||
import { MovieCrewPosterProps } from './Crew/MovieCrewPoster';
|
||||
|
||||
type MovieCreditPosterProps = {
|
||||
component: React.ElementType;
|
||||
} & (
|
||||
| Omit<MovieCrewPosterProps, 'onImportListSelect'>
|
||||
| Omit<MovieCastPosterProps, 'onImportListSelect'>
|
||||
);
|
||||
|
||||
function MovieCreditPoster({
|
||||
component: ItemComponent,
|
||||
tmdbId,
|
||||
personName,
|
||||
...otherProps
|
||||
}: MovieCreditPosterProps) {
|
||||
const importList = useSelector(createMovieCreditImportListSelector(tmdbId));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleImportListSelect = useCallback(() => {
|
||||
dispatch(
|
||||
selectImportListSchema({
|
||||
implementation: 'TMDbPersonImport',
|
||||
implementationName: 'TMDb Person',
|
||||
presetName: undefined,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
// @ts-expect-error 'setImportListFieldValue' isn't typed yet
|
||||
setImportListFieldValue({ name: 'personId', value: tmdbId.toString() })
|
||||
);
|
||||
|
||||
dispatch(
|
||||
// @ts-expect-error 'setImportListValue' isn't typed yet
|
||||
setImportListValue({ name: 'name', value: `${personName} - ${tmdbId}` })
|
||||
);
|
||||
}, [dispatch, tmdbId, personName]);
|
||||
|
||||
return (
|
||||
<ItemComponent
|
||||
{...otherProps}
|
||||
tmdbId={tmdbId}
|
||||
personName={personName}
|
||||
importList={importList}
|
||||
onImportListSelect={handleImportListSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieCreditPoster;
|
||||
@@ -1,66 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
|
||||
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieCreditListSelector(),
|
||||
(importList) => {
|
||||
return {
|
||||
importList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
selectImportListSchema,
|
||||
setImportListFieldValue,
|
||||
setImportListValue
|
||||
};
|
||||
|
||||
class MovieCreditPosterConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onImportListSelect = () => {
|
||||
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', implementationName: 'TMDb Person', presetName: undefined });
|
||||
this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
|
||||
this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
tmdbId,
|
||||
component: ItemComponent,
|
||||
personName
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ItemComponent
|
||||
{...this.props}
|
||||
tmdbId={tmdbId}
|
||||
personName={personName}
|
||||
onImportListSelect={this.onImportListSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieCreditPosterConnector.propTypes = {
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
personName: PropTypes.string.isRequired,
|
||||
component: PropTypes.elementType.isRequired,
|
||||
selectImportListSchema: PropTypes.func.isRequired,
|
||||
setImportListFieldValue: PropTypes.func.isRequired,
|
||||
setImportListValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCreditPosterConnector);
|
||||
@@ -1,109 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Navigation } from 'swiper';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
|
||||
import styles from './MovieCreditPosters.css';
|
||||
|
||||
// Import Swiper styles
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
|
||||
// Poster container dimensions
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||
|
||||
function calculateRowHeight(posterHeight, isSmallScreen) {
|
||||
const titleHeight = 19;
|
||||
const characterHeight = 19;
|
||||
|
||||
const heights = [
|
||||
posterHeight,
|
||||
titleHeight,
|
||||
characterHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
];
|
||||
|
||||
return heights.reduce((acc, height) => acc + height, 0);
|
||||
}
|
||||
|
||||
class MovieCreditPosters extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0,
|
||||
columnWidth: 182,
|
||||
columnCount: 1,
|
||||
posterWidth: 162,
|
||||
posterHeight: 238,
|
||||
rowHeight: calculateRowHeight(238, props.isSmallScreen)
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
itemComponent,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
|
||||
<div className={styles.sliderContainer}>
|
||||
<Swiper
|
||||
slidesPerView='auto'
|
||||
spaceBetween={10}
|
||||
slidesPerGroup={isSmallScreen ? 1 : 3}
|
||||
navigation={true}
|
||||
loop={false}
|
||||
loopFillGroupWithBlank={true}
|
||||
className="mySwiper"
|
||||
modules={[Navigation]}
|
||||
onInit={(swiper) => {
|
||||
swiper.navigation.init();
|
||||
swiper.navigation.update();
|
||||
}}
|
||||
>
|
||||
{items.map((credit) => (
|
||||
<SwiperSlide key={credit.id} style={{ width: posterWidth, height: rowHeight }}>
|
||||
<MovieCreditPosterConnector
|
||||
key={credit.id}
|
||||
component={itemComponent}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
tmdbId={credit.personTmdbId}
|
||||
personName={credit.personName}
|
||||
job={credit.job}
|
||||
character={credit.character}
|
||||
images={credit.images}
|
||||
/>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieCreditPosters.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
itemComponent: PropTypes.elementType.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default MovieCreditPosters;
|
||||
87
frontend/src/Movie/Details/Credits/MovieCreditPosters.tsx
Normal file
87
frontend/src/Movie/Details/Credits/MovieCreditPosters.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Navigation } from 'swiper';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Swiper as SwiperClass } from 'swiper/types';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import MovieCredit from 'typings/MovieCredit';
|
||||
import MovieCreditPoster from './MovieCreditPoster';
|
||||
import styles from './MovieCreditPosters.css';
|
||||
|
||||
// Import Swiper styles
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/navigation';
|
||||
|
||||
// Poster container dimensions
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(
|
||||
dimensions.movieIndexColumnPaddingSmallScreen
|
||||
);
|
||||
|
||||
interface MovieCreditPostersProps {
|
||||
items: MovieCredit[];
|
||||
itemComponent: React.ElementType;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
function MovieCreditPosters(props: MovieCreditPostersProps) {
|
||||
const { items, itemComponent, isSmallScreen } = props;
|
||||
|
||||
const posterWidth = 162;
|
||||
const posterHeight = 238;
|
||||
|
||||
const rowHeight = useMemo(() => {
|
||||
const titleHeight = 19;
|
||||
const characterHeight = 19;
|
||||
|
||||
const heights = [
|
||||
posterHeight,
|
||||
titleHeight,
|
||||
characterHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
|
||||
];
|
||||
|
||||
return heights.reduce((acc, height) => acc + height, 0);
|
||||
}, [posterHeight, isSmallScreen]);
|
||||
|
||||
const handleSwiperInit = useCallback((swiper: SwiperClass) => {
|
||||
swiper.navigation.init();
|
||||
swiper.navigation.update();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.sliderContainer}>
|
||||
<Swiper
|
||||
slidesPerView="auto"
|
||||
spaceBetween={10}
|
||||
slidesPerGroup={isSmallScreen ? 1 : 3}
|
||||
navigation={true}
|
||||
loop={false}
|
||||
loopFillGroupWithBlank={true}
|
||||
className="mySwiper"
|
||||
modules={[Navigation]}
|
||||
onInit={handleSwiperInit}
|
||||
>
|
||||
{items.map((credit) => (
|
||||
<SwiperSlide
|
||||
key={credit.id}
|
||||
style={{ width: posterWidth, height: rowHeight }}
|
||||
>
|
||||
<MovieCreditPoster
|
||||
key={credit.id}
|
||||
component={itemComponent}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
tmdbId={credit.personTmdbId}
|
||||
personName={credit.personName}
|
||||
images={credit.images}
|
||||
job={credit.job}
|
||||
character={credit.character}
|
||||
/>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieCreditPosters;
|
||||
@@ -20,12 +20,14 @@ import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
@@ -37,9 +39,8 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
||||
import MovieCastPosters from './Credits/Cast/MovieCastPosters';
|
||||
import MovieCrewPosters from './Credits/Crew/MovieCrewPosters';
|
||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||
import MovieReleaseDates from './MovieReleaseDates';
|
||||
import MovieStatusLabel from './MovieStatusLabel';
|
||||
@@ -421,14 +422,15 @@ class MovieDetails extends Component {
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
{
|
||||
!!certification &&
|
||||
certification ?
|
||||
<span className={styles.certification}>
|
||||
{certification}
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
year > 0 &&
|
||||
year > 0 ?
|
||||
<span className={styles.year}>
|
||||
<Popover
|
||||
anchor={
|
||||
@@ -444,14 +446,16 @@ class MovieDetails extends Component {
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!runtime &&
|
||||
runtime ?
|
||||
<span className={styles.runtime}>
|
||||
{formatRuntime(runtime, movieRuntimeFormat)}
|
||||
</span>
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
@@ -497,31 +501,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 +702,13 @@ class MovieDetails extends Component {
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Cast')}>
|
||||
<MovieCastPostersConnector
|
||||
<MovieCastPosters
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Crew')}>
|
||||
<MovieCrewPostersConnector
|
||||
<MovieCrewPosters
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
@@ -2,23 +2,25 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Movie from 'Movie/Movie';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieReleaseDates.css';
|
||||
|
||||
interface MovieReleaseDatesProps {
|
||||
inCinemas?: string;
|
||||
digitalRelease?: string;
|
||||
physicalRelease?: string;
|
||||
}
|
||||
type MovieReleaseDatesProps = Pick<
|
||||
Movie,
|
||||
'inCinemas' | 'digitalRelease' | 'physicalRelease'
|
||||
>;
|
||||
|
||||
function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
||||
const { inCinemas, digitalRelease, physicalRelease } = props;
|
||||
|
||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
function MovieReleaseDates({
|
||||
inCinemas,
|
||||
digitalRelease,
|
||||
physicalRelease,
|
||||
}: MovieReleaseDatesProps) {
|
||||
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
if (!inCinemas && !physicalRelease && !digitalRelease) {
|
||||
return (
|
||||
@@ -34,10 +36,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
||||
return (
|
||||
<>
|
||||
{inCinemas ? (
|
||||
<div title={translate('InCinemas')}>
|
||||
<div
|
||||
title={`${translate('InCinemas')}: ${formatDate(
|
||||
inCinemas,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon name={icons.IN_CINEMAS} />
|
||||
</div>
|
||||
|
||||
{getRelativeDate({
|
||||
date: inCinemas,
|
||||
shortDateFormat,
|
||||
@@ -49,10 +57,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
||||
) : null}
|
||||
|
||||
{digitalRelease ? (
|
||||
<div title={translate('DigitalRelease')}>
|
||||
<div
|
||||
title={`${translate('DigitalRelease')}: ${formatDate(
|
||||
digitalRelease,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon name={icons.MOVIE_FILE} />
|
||||
</div>
|
||||
|
||||
{getRelativeDate({
|
||||
date: digitalRelease,
|
||||
shortDateFormat,
|
||||
@@ -64,10 +78,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
||||
) : null}
|
||||
|
||||
{physicalRelease ? (
|
||||
<div title={translate('PhysicalRelease')}>
|
||||
<div
|
||||
title={`${translate('PhysicalRelease')}: ${formatDate(
|
||||
physicalRelease,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon name={icons.DISC} />
|
||||
</div>
|
||||
|
||||
{getRelativeDate({
|
||||
date: physicalRelease,
|
||||
shortDateFormat,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
|
||||
class MovieTitlesRow extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
sourceType
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
|
||||
<TableRowCell>
|
||||
{title}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{titleCase(sourceType)}
|
||||
</TableRowCell>
|
||||
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieTitlesRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
sourceType: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default MovieTitlesRow;
|
||||
21
frontend/src/Movie/Details/Titles/MovieTitlesRow.tsx
Normal file
21
frontend/src/Movie/Details/Titles/MovieTitlesRow.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
|
||||
interface MovieTitlesRowProps {
|
||||
title: string;
|
||||
sourceType: string;
|
||||
}
|
||||
|
||||
function MovieTitlesRow({ title, sourceType }: MovieTitlesRowProps) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>{title}</TableRowCell>
|
||||
|
||||
<TableRowCell>{titleCase(sourceType)}</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieTitlesRow;
|
||||
@@ -1,3 +1,9 @@
|
||||
.blankpad {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.container {
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'blankpad': string;
|
||||
'container': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
|
||||
import styles from './MovieTitlesTable.css';
|
||||
|
||||
function MovieTitlesTable(props) {
|
||||
const {
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<MovieTitlesTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MovieTitlesTable.propTypes = {
|
||||
};
|
||||
|
||||
export default MovieTitlesTable;
|
||||
94
frontend/src/Movie/Details/Titles/MovieTitlesTable.tsx
Normal file
94
frontend/src/Movie/Details/Titles/MovieTitlesTable.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieTitlesRow from './MovieTitlesRow';
|
||||
import styles from './MovieTitlesTable.css';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'alternativeTitle',
|
||||
label: () => translate('AlternativeTitle'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'sourceType',
|
||||
label: () => translate('Type'),
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
function movieAlternativeTitlesSelector(movieId: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.movies,
|
||||
(movies) => {
|
||||
const { isFetching, isPopulated, error, items } = movies;
|
||||
|
||||
const alternateTitles =
|
||||
items.find((m) => m.id === movieId)?.alternateTitles ?? [];
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items: alternateTitles,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface MovieTitlesProps {
|
||||
movieId: number;
|
||||
}
|
||||
|
||||
function MovieTitlesTable({ movieId }: MovieTitlesProps) {
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
movieAlternativeTitlesSelector(movieId)
|
||||
);
|
||||
|
||||
const sortedItems = items.sort(sortByProp('title'));
|
||||
|
||||
if (!isFetching && !!error) {
|
||||
return (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AlternativeTitlesLoadError')}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{isFetching && <LoadingIndicator />}
|
||||
|
||||
{isPopulated && !items.length && !error ? (
|
||||
<div className={styles.blankpad}>
|
||||
{translate('NoAlternativeTitles')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length && !error ? (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{sortedItems.map((item) => (
|
||||
<MovieTitlesRow
|
||||
key={item.id}
|
||||
title={item.title}
|
||||
sourceType={item.sourceType}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieTitlesTable;
|
||||
@@ -1,5 +0,0 @@
|
||||
.blankpad {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieTitlesRow from './MovieTitlesRow';
|
||||
import styles from './MovieTitlesTableContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'altTitle',
|
||||
label: () => translate('AlternativeTitle'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sourceType',
|
||||
label: () => translate('Type'),
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class MovieTitlesTableContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const hasItems = !!items.length;
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div className={styles.blankpad}>
|
||||
{translate('UnableToLoadAltTitle')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !hasItems && !error &&
|
||||
<div className={styles.blankpad}>
|
||||
{translate('NoAltTitle')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && hasItems && !error &&
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{
|
||||
items.reverse().map((item) => {
|
||||
return (
|
||||
<MovieTitlesRow
|
||||
key={item.id}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieTitlesTableContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default MovieTitlesTableContent;
|
||||
@@ -1,60 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import MovieTitlesTableContent from './MovieTitlesTableContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { movieId }) => movieId,
|
||||
(state) => state.movies,
|
||||
(movieId, movies) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = movies;
|
||||
|
||||
const alternateTitles = items.find((m) => m.id === movieId)?.alternateTitles;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
alternateTitles
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class MovieTitlesTableContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
alternateTitles,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<MovieTitlesTableContent
|
||||
{...otherProps}
|
||||
items={alternateTitles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieTitlesTableContentConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
MovieTitlesTableContentConnector.defaultProps = {
|
||||
alternateTitles: []
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(MovieTitlesTableContentConnector);
|
||||
@@ -4,8 +4,6 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.tagInternalInput {
|
||||
composes: internalInput from '~Components/Form/TagInput.css';
|
||||
|
||||
flex: 0 0 100%;
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'deleteButton': string;
|
||||
'tagInternalInput': string;
|
||||
'labelIcon': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditMovieModalContent.css';
|
||||
@@ -103,7 +106,21 @@ class EditMovieModalContent extends Component {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
|
||||
@@ -136,6 +136,15 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
|
||||
{translate('RottenTomatoesRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="traktRating"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('TraktRating')}
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="popularity"
|
||||
sortKey={sortKey}
|
||||
|
||||
@@ -11,6 +11,7 @@ import RottenTomatoRating from 'Components/RottenTomatoRating';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
@@ -21,6 +22,7 @@ import { Statistics } from 'Movie/Movie';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import createMovieIndexItemSelector from '../createMovieIndexItemSelector';
|
||||
@@ -54,6 +56,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
showTags,
|
||||
showSearchAction,
|
||||
} = useSelector(selectPosterOptions);
|
||||
@@ -241,7 +244,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
) : null}
|
||||
|
||||
{showCinemaRelease && inCinemas ? (
|
||||
<div className={styles.title} title={translate('InCinemas')}>
|
||||
<div
|
||||
className={styles.title}
|
||||
title={`${translate('InCinemas')}: ${formatDate(
|
||||
inCinemas,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<Icon name={icons.IN_CINEMAS} />{' '}
|
||||
{getRelativeDate({
|
||||
date: inCinemas,
|
||||
@@ -254,7 +263,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
) : null}
|
||||
|
||||
{showDigitalRelease && digitalRelease ? (
|
||||
<div className={styles.title} title={translate('DigitalRelease')}>
|
||||
<div
|
||||
className={styles.title}
|
||||
title={`${translate('DigitalRelease')}: ${formatDate(
|
||||
digitalRelease,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<Icon name={icons.MOVIE_FILE} />{' '}
|
||||
{getRelativeDate({
|
||||
date: digitalRelease,
|
||||
@@ -267,7 +282,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
) : null}
|
||||
|
||||
{showPhysicalRelease && physicalRelease ? (
|
||||
<div className={styles.title} title={translate('PhysicalRelease')}>
|
||||
<div
|
||||
className={styles.title}
|
||||
title={`${translate('PhysicalRelease')}: ${formatDate(
|
||||
physicalRelease,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<Icon name={icons.DISC} />{' '}
|
||||
{getRelativeDate({
|
||||
date: physicalRelease,
|
||||
@@ -280,7 +301,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
) : null}
|
||||
|
||||
{showReleaseDate && releaseDate ? (
|
||||
<div className={styles.title} title={translate('ReleaseDate')}>
|
||||
<div
|
||||
className={styles.title}
|
||||
title={`${translate('ReleaseDate')}: ${formatDate(
|
||||
releaseDate,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<Icon name={icons.CALENDAR} />{' '}
|
||||
{getRelativeDate({
|
||||
date: releaseDate,
|
||||
@@ -310,6 +337,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 +380,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
showTmdbRating={showTmdbRating}
|
||||
showImdbRating={showImdbRating}
|
||||
showRottenTomatoesRating={showRottenTomatoesRating}
|
||||
showTraktRating={showTraktRating}
|
||||
showTags={showTags}
|
||||
/>
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ 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';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import formatDate from 'Utilities/Date/formatDate';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
@@ -43,6 +45,7 @@ interface MovieIndexPosterInfoProps {
|
||||
showTmdbRating: boolean;
|
||||
showImdbRating: boolean;
|
||||
showRottenTomatoesRating: boolean;
|
||||
showTraktRating: boolean;
|
||||
showTags: boolean;
|
||||
}
|
||||
|
||||
@@ -76,6 +79,7 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
showTags,
|
||||
} = props;
|
||||
|
||||
@@ -136,7 +140,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.info} title={translate('InCinemas')}>
|
||||
<div
|
||||
className={styles.info}
|
||||
title={`${translate('InCinemas')}: ${formatDate(
|
||||
inCinemas,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<Icon name={icons.IN_CINEMAS} /> {inCinemasDate}
|
||||
</div>
|
||||
);
|
||||
@@ -152,7 +162,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.info} title={translate('DigitalRelease')}>
|
||||
<div
|
||||
className={styles.info}
|
||||
title={`${translate('DigitalRelease')}: ${formatDate(
|
||||
digitalRelease,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<Icon name={icons.MOVIE_FILE} /> {digitalReleaseDate}
|
||||
</div>
|
||||
);
|
||||
@@ -172,7 +188,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.info} title={translate('PhysicalRelease')}>
|
||||
<div
|
||||
className={styles.info}
|
||||
title={`${translate('PhysicalRelease')}: ${formatDate(
|
||||
physicalRelease,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<Icon name={icons.DISC} /> {physicalReleaseDate}
|
||||
</div>
|
||||
);
|
||||
@@ -180,7 +202,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
||||
|
||||
if (sortKey === 'releaseDate' && releaseDate && !showReleaseDate) {
|
||||
return (
|
||||
<div className={styles.info} title={translate('ReleaseDate')}>
|
||||
<div
|
||||
className={styles.info}
|
||||
title={`${translate('ReleaseDate')}: ${formatDate(
|
||||
releaseDate,
|
||||
longDateFormat
|
||||
)}`}
|
||||
>
|
||||
<Icon name={icons.CALENDAR} />{' '}
|
||||
{getRelativeDate({
|
||||
date: releaseDate,
|
||||
@@ -221,6 +249,14 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!showTraktRating && sortKey === 'traktRating' && !!ratings.trakt) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
<TraktRating ratings={ratings} iconSize={12} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!showTags && sortKey === 'tags' && tags.length) {
|
||||
return (
|
||||
<div className={styles.tags}>
|
||||
|
||||
@@ -150,6 +150,7 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
showTags,
|
||||
} = posterOptions;
|
||||
|
||||
@@ -199,6 +200,10 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showTraktRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showTags) {
|
||||
heights.push(21);
|
||||
}
|
||||
@@ -253,6 +258,11 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'traktRating':
|
||||
if (!showTraktRating) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'tags':
|
||||
if (!showTags) {
|
||||
heights.push(21);
|
||||
|
||||
@@ -59,6 +59,7 @@ function MovieIndexPosterOptionsModalContent(
|
||||
showTmdbRating,
|
||||
showImdbRating,
|
||||
showRottenTomatoesRating,
|
||||
showTraktRating,
|
||||
showTags,
|
||||
showSearchAction,
|
||||
} = posterOptions;
|
||||
@@ -222,6 +223,18 @@ function MovieIndexPosterOptionsModalContent(
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('ShowTraktRating')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showTraktRating"
|
||||
value={showTraktRating}
|
||||
helpText={translate('ShowTraktRatingPosterHelpText')}
|
||||
onChange={onPosterOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('ShowTags')}</FormLabel>
|
||||
|
||||
|
||||
@@ -92,7 +92,8 @@
|
||||
|
||||
.imdbRating,
|
||||
.tmdbRating,
|
||||
.rottenTomatoesRating {
|
||||
.rottenTomatoesRating,
|
||||
.traktRating {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 80px;
|
||||
|
||||
@@ -30,6 +30,7 @@ interface CssExports {
|
||||
'studio': string;
|
||||
'tags': string;
|
||||
'tmdbRating': string;
|
||||
'traktRating': string;
|
||||
'year': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -14,6 +14,7 @@ import Column from 'Components/Table/Column';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
@@ -387,6 +388,14 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'traktRating') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{ratings.trakt ? <TraktRating ratings={ratings} /> : null}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'popularity') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
|
||||
.imdbRating,
|
||||
.tmdbRating,
|
||||
.rottenTomatoesRating {
|
||||
.rottenTomatoesRating,
|
||||
.traktRating {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 80px;
|
||||
|
||||
@@ -27,6 +27,7 @@ interface CssExports {
|
||||
'studio': string;
|
||||
'tags': string;
|
||||
'tmdbRating': string;
|
||||
'traktRating': string;
|
||||
'year': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -9,7 +9,7 @@ export type MovieStatus =
|
||||
| 'released'
|
||||
| 'deleted';
|
||||
|
||||
export type CoverType = 'poster' | 'fanart';
|
||||
export type CoverType = 'poster' | 'fanart' | 'headshot';
|
||||
|
||||
export interface Image {
|
||||
coverType: CoverType;
|
||||
@@ -37,6 +37,12 @@ export interface Ratings {
|
||||
tmdb: RatingValues;
|
||||
metacritic: RatingValues;
|
||||
rottenTomatoes: RatingValues;
|
||||
trakt: RatingValues;
|
||||
}
|
||||
|
||||
export interface AlternativeTitle extends ModelBase {
|
||||
sourceType: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Movie extends ModelBase {
|
||||
@@ -52,6 +58,7 @@ interface Movie extends ModelBase {
|
||||
originalTitle: string;
|
||||
originalLanguage: Language;
|
||||
collection: Collection;
|
||||
alternateTitles: AlternativeTitle[];
|
||||
studio: string;
|
||||
qualityProfileId: number;
|
||||
added: string;
|
||||
@@ -72,6 +79,7 @@ interface Movie extends ModelBase {
|
||||
images: Image[];
|
||||
movieFile: MovieFile;
|
||||
hasFile: boolean;
|
||||
lastSearchTime?: string;
|
||||
isAvailable: boolean;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MovieImage from './MovieImage';
|
||||
|
||||
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
|
||||
|
||||
function MovieHeadshot(props) {
|
||||
return (
|
||||
<MovieImage
|
||||
{...props}
|
||||
coverType="headshot"
|
||||
placeholder={posterPlaceholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
MovieHeadshot.propTypes = {
|
||||
size: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
MovieHeadshot.defaultProps = {
|
||||
size: 250
|
||||
};
|
||||
|
||||
export default MovieHeadshot;
|
||||
23
frontend/src/Movie/MovieHeadshot.tsx
Normal file
23
frontend/src/Movie/MovieHeadshot.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import MovieImage, { MovieImageProps } from './MovieImage';
|
||||
|
||||
const posterPlaceholder =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
|
||||
|
||||
interface MovieHeadshotProps
|
||||
extends Omit<MovieImageProps, 'coverType' | 'placeholder'> {
|
||||
size?: 250 | 500;
|
||||
}
|
||||
|
||||
function MovieHeadshot({ size = 250, ...otherProps }: MovieHeadshotProps) {
|
||||
return (
|
||||
<MovieImage
|
||||
{...otherProps}
|
||||
size={size}
|
||||
coverType="headshot"
|
||||
placeholder={posterPlaceholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieHeadshot;
|
||||
@@ -43,7 +43,7 @@ function MovieImage({
|
||||
}: MovieImageProps) {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const [isLoaded, setIsLoaded] = useState(true);
|
||||
const image = useRef<Image | null>(null);
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
|
||||
@@ -19,7 +19,7 @@ function EditImportListExclusionModal(
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onModalClosePress = useCallback(() => {
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(
|
||||
clearPendingChanges({
|
||||
section: 'settings.importListExclusions',
|
||||
@@ -29,10 +29,10 @@ function EditImportListExclusionModal(
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}>
|
||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditImportListExclusionModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onModalClosePress}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -32,12 +32,6 @@ const newImportListExclusion = {
|
||||
tmdbId: 0,
|
||||
};
|
||||
|
||||
interface EditImportListExclusionModalContentProps {
|
||||
id?: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteImportListExclusionPress?: () => void;
|
||||
}
|
||||
|
||||
function createImportListExclusionSelector(id?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.importListExclusions,
|
||||
@@ -63,12 +57,24 @@ function createImportListExclusionSelector(id?: number) {
|
||||
);
|
||||
}
|
||||
|
||||
function EditImportListExclusionModalContent(
|
||||
props: EditImportListExclusionModalContentProps
|
||||
) {
|
||||
const { id, onModalClose, onDeleteImportListExclusionPress } = props;
|
||||
interface EditImportListExclusionModalContentProps {
|
||||
id?: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteImportListExclusionPress?: () => void;
|
||||
}
|
||||
|
||||
function EditImportListExclusionModalContent({
|
||||
id,
|
||||
onModalClose,
|
||||
onDeleteImportListExclusionPress,
|
||||
}: EditImportListExclusionModalContentProps) {
|
||||
const { isFetching, isSaving, item, error, saveError, ...otherProps } =
|
||||
useSelector(createImportListExclusionSelector(id));
|
||||
|
||||
const { movieTitle, movieYear, tmdbId } = item;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const previousIsSaving = usePrevious(isSaving);
|
||||
|
||||
const dispatchSetImportListExclusionValue = (payload: {
|
||||
name: string;
|
||||
@@ -78,20 +84,10 @@ function EditImportListExclusionModalContent(
|
||||
dispatch(setImportListExclusionValue(payload));
|
||||
};
|
||||
|
||||
const { isFetching, isSaving, item, error, saveError, ...otherProps } =
|
||||
useSelector(createImportListExclusionSelector(props.id));
|
||||
const previousIsSaving = usePrevious(isSaving);
|
||||
|
||||
const { movieTitle, movieYear, tmdbId } = item;
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
Object.keys(newImportListExclusion).forEach((name) => {
|
||||
dispatchSetImportListExclusionValue({
|
||||
name,
|
||||
value:
|
||||
newImportListExclusion[name as keyof typeof newImportListExclusion],
|
||||
});
|
||||
Object.entries(newImportListExclusion).forEach(([name, value]) => {
|
||||
dispatchSetImportListExclusionValue({ name, value });
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -101,7 +97,7 @@ function EditImportListExclusionModalContent(
|
||||
if (previousIsSaving && !isSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
});
|
||||
}, [previousIsSaving, isSaving, saveError, onModalClose]);
|
||||
|
||||
const onSavePress = useCallback(() => {
|
||||
dispatch(saveImportListExclusion({ id }));
|
||||
|
||||
@@ -42,7 +42,7 @@ import styles from './ImportListExclusions.css';
|
||||
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'tmdbid',
|
||||
name: 'tmdbId',
|
||||
label: () => translate('TMDBId'),
|
||||
isVisible: true,
|
||||
isSortable: true,
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'deleteButton': string;
|
||||
'labelIcon': string;
|
||||
'message': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -13,7 +15,8 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -160,12 +163,28 @@ function EditImportListModalContent(props) {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
{...minimumAvailability}
|
||||
onChange={onInputChange}
|
||||
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
@@ -174,24 +174,21 @@ class MediaManagement extends Component {
|
||||
<FieldSet
|
||||
legend={translate('Importing')}
|
||||
>
|
||||
{
|
||||
!isWindows &&
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipFreeSpaceCheckWhenImporting"
|
||||
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipFreeSpaceCheckWhenImporting"
|
||||
helpText={translate('SkipFreeSpaceCheckHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.skipFreeSpaceCheckWhenImporting}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
|
||||
@@ -13,33 +13,6 @@ import translate from 'Utilities/String/translate';
|
||||
import NamingModal from './NamingModal';
|
||||
import styles from './Naming.css';
|
||||
|
||||
const colonReplacementOptions = [
|
||||
{
|
||||
key: 'delete',
|
||||
get value() {
|
||||
return translate('Delete');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'dash',
|
||||
get value() {
|
||||
return translate('ReplaceWithDash');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'spaceDash',
|
||||
get value() {
|
||||
return translate('ReplaceWithSpaceDash');
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'spaceDashSpace',
|
||||
get value() {
|
||||
return translate('ReplaceWithSpaceDashSpace');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
class Naming extends Component {
|
||||
|
||||
//
|
||||
@@ -103,6 +76,14 @@ class Naming extends Component {
|
||||
const renameMovies = hasSettings && settings.renameMovies.value;
|
||||
const replaceIllegalCharacters = hasSettings && settings.replaceIllegalCharacters.value;
|
||||
|
||||
const colonReplacementOptions = [
|
||||
{ key: 'delete', value: translate('Delete') },
|
||||
{ key: 'dash', value: translate('ReplaceWithDash') },
|
||||
{ key: 'spaceDash', value: translate('ReplaceWithSpaceDash') },
|
||||
{ key: 'spaceDashSpace', value: translate('ReplaceWithSpaceDashSpace') },
|
||||
{ key: 'smart', value: translate('SmartReplace'), hint: translate('SmartReplaceHint') }
|
||||
];
|
||||
|
||||
const standardMovieFormatHelpTexts = [];
|
||||
const standardMovieFormatErrors = [];
|
||||
const movieFolderFormatHelpTexts = [];
|
||||
@@ -112,17 +93,13 @@ class Naming extends Component {
|
||||
if (examples.movieExample) {
|
||||
standardMovieFormatHelpTexts.push(`${translate('Movie')}: ${examples.movieExample}`);
|
||||
} else {
|
||||
standardMovieFormatErrors.push({ get message() {
|
||||
return translate('MovieInvalidFormat');
|
||||
} });
|
||||
standardMovieFormatErrors.push({ message: translate('MovieInvalidFormat') });
|
||||
}
|
||||
|
||||
if (examples.movieFolderExample) {
|
||||
movieFolderFormatHelpTexts.push(`${translate('Example')}: ${examples.movieFolderExample}`);
|
||||
} else {
|
||||
movieFolderFormatErrors.push({ get message() {
|
||||
return translate('InvalidFormat');
|
||||
} });
|
||||
movieFolderFormatErrors.push({ message: translate('InvalidFormat') });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -15,11 +14,11 @@ function createMapStateToProps() {
|
||||
(state) => state.settings.advancedSettings,
|
||||
(state) => state.settings.namingExamples,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, examples, sectionSettings) => {
|
||||
(advancedSettings, namingExamples, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
examples: examples.item,
|
||||
examplesPopulated: !_.isEmpty(examples.item),
|
||||
examples: namingExamples.item,
|
||||
examplesPopulated: namingExamples.isPopulated,
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@ const movieTokens = [
|
||||
{ token: '{Movie Title}', example: 'Movie\'s Title', footNote: 1 },
|
||||
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: 1 },
|
||||
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: 1 },
|
||||
{ token: '{Movie CleanTitle:DE}', example: 'Titel des Films', footNote: 1 },
|
||||
{ token: '{Movie TitleThe}', example: 'Movie\'s Title, The', footNote: 1 },
|
||||
{ token: '{Movie CleanTitleThe}', example: 'Movies Title, The', footNote: 1 },
|
||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
|
||||
{ token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
|
||||
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
||||
|
||||
@@ -7,7 +7,7 @@ import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
|
||||
import QualityProfilesConnector from './Quality/QualityProfilesConnector';
|
||||
import ReleaseProfilesConnector from './Release/ReleaseProfilesConnector';
|
||||
import ReleaseProfiles from './Release/ReleaseProfiles';
|
||||
|
||||
// Only a single DragDrop Context can exist so it's done here to allow editing
|
||||
// quality profiles and reordering delay profiles to work.
|
||||
@@ -26,7 +26,7 @@ class Profiles extends Component {
|
||||
<DndProvider options={HTML5toTouch}>
|
||||
<QualityProfilesConnector />
|
||||
<DelayProfilesConnector />
|
||||
<ReleaseProfilesConnector />
|
||||
<ReleaseProfiles />
|
||||
</DndProvider>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EditReleaseProfileModalContentConnector from './EditReleaseProfileModalContentConnector';
|
||||
|
||||
function EditReleaseProfileModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditReleaseProfileModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditReleaseProfileModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditReleaseProfileModal;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditReleaseProfileModalContent from './EditReleaseProfileModalContent';
|
||||
|
||||
interface EditReleaseProfileModalProps {
|
||||
id?: number;
|
||||
isOpen: boolean;
|
||||
onModalClose: () => void;
|
||||
onDeleteReleaseProfilePress?: () => void;
|
||||
}
|
||||
|
||||
function EditReleaseProfileModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditReleaseProfileModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(
|
||||
clearPendingChanges({
|
||||
section: 'settings.releaseProfiles',
|
||||
})
|
||||
);
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditReleaseProfileModalContent
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditReleaseProfileModal;
|
||||
@@ -1,39 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditReleaseProfileModal from './EditReleaseProfileModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditReleaseProfileModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'settings.releaseProfiles' });
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditReleaseProfileModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditReleaseProfileModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(EditReleaseProfileModalConnector);
|
||||
@@ -1,5 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -10,33 +12,97 @@ 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 usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
saveReleaseProfile,
|
||||
setReleaseProfileValue,
|
||||
} from 'Store/Actions/Settings/releaseProfiles';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { PendingSection } from 'typings/pending';
|
||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditReleaseProfileModalContent.css';
|
||||
|
||||
const tagInputDelimiters = ['Tab', 'Enter'];
|
||||
|
||||
function EditReleaseProfileModalContent(props) {
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onDeleteReleaseProfilePress,
|
||||
...otherProps
|
||||
} = props;
|
||||
const newReleaseProfile = {
|
||||
enabled: true,
|
||||
required: [],
|
||||
ignored: [],
|
||||
tags: [],
|
||||
indexerId: 0,
|
||||
};
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enabled,
|
||||
required,
|
||||
ignored,
|
||||
tags,
|
||||
indexerId
|
||||
} = item;
|
||||
function createReleaseProfileSelector(id?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.releaseProfiles,
|
||||
(releaseProfiles) => {
|
||||
const { items, isFetching, error, isSaving, saveError, pendingChanges } =
|
||||
releaseProfiles;
|
||||
|
||||
const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile;
|
||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings as PendingSection<ReleaseProfile>,
|
||||
...settings,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface EditReleaseProfileModalContentProps {
|
||||
id?: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteReleaseProfilePress?: () => void;
|
||||
}
|
||||
|
||||
function EditReleaseProfileModalContent({
|
||||
id,
|
||||
onModalClose,
|
||||
onDeleteReleaseProfilePress,
|
||||
}: EditReleaseProfileModalContentProps) {
|
||||
const { item, isFetching, isSaving, error, saveError, ...otherProps } =
|
||||
useSelector(createReleaseProfileSelector(id));
|
||||
|
||||
const { name, enabled, required, ignored, tags, indexerId } = item;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const previousIsSaving = usePrevious(isSaving);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
Object.entries(newReleaseProfile).forEach(([name, value]) => {
|
||||
// @ts-expect-error 'setReleaseProfileValue' isn't typed yet
|
||||
dispatch(setReleaseProfileValue({ name, value }));
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (previousIsSaving && !isSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [previousIsSaving, isSaving, saveError, onModalClose]);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
dispatch(saveReleaseProfile({ id }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(payload: { name: string; value: string | number }) => {
|
||||
// @ts-expect-error 'setReleaseProfileValue' isn't typed yet
|
||||
dispatch(setReleaseProfileValue(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -46,7 +112,6 @@ function EditReleaseProfileModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
<Form {...otherProps}>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
@@ -56,7 +121,7 @@ function EditReleaseProfileModalContent(props) {
|
||||
{...name}
|
||||
placeholder={translate('OptionalName')}
|
||||
canEdit={true}
|
||||
onChange={onInputChange}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -68,7 +133,7 @@ function EditReleaseProfileModalContent(props) {
|
||||
name="enabled"
|
||||
helpText={translate('EnableProfileHelpText')}
|
||||
{...enabled}
|
||||
onChange={onInputChange}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -85,7 +150,7 @@ function EditReleaseProfileModalContent(props) {
|
||||
placeholder={translate('AddNewRestriction')}
|
||||
delimiters={tagInputDelimiters}
|
||||
canEdit={true}
|
||||
onChange={onInputChange}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -102,7 +167,7 @@ function EditReleaseProfileModalContent(props) {
|
||||
placeholder={translate('AddNewRestriction')}
|
||||
delimiters={tagInputDelimiters}
|
||||
canEdit={true}
|
||||
onChange={onInputChange}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -113,10 +178,12 @@ function EditReleaseProfileModalContent(props) {
|
||||
type={inputTypes.INDEXER_SELECT}
|
||||
name="indexerId"
|
||||
helpText={translate('ReleaseProfileIndexerHelpText')}
|
||||
helpTextWarning={translate('ReleaseProfileIndexerHelpTextWarning')}
|
||||
helpTextWarning={translate(
|
||||
'ReleaseProfileIndexerHelpTextWarning'
|
||||
)}
|
||||
{...indexerId}
|
||||
includeAny={true}
|
||||
onChange={onInputChange}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -128,33 +195,28 @@ function EditReleaseProfileModalContent(props) {
|
||||
name="tags"
|
||||
helpText={translate('ReleaseProfileTagMovieHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteReleaseProfilePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
{id ? (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteReleaseProfilePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
onPress={handleSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
@@ -163,14 +225,4 @@ function EditReleaseProfileModalContent(props) {
|
||||
);
|
||||
}
|
||||
|
||||
EditReleaseProfileModalContent.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onDeleteReleaseProfilePress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditReleaseProfileModalContent;
|
||||
@@ -1,112 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveReleaseProfile, setReleaseProfileValue } from 'Store/Actions/settingsActions';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import EditReleaseProfileModalContent from './EditReleaseProfileModalContent';
|
||||
|
||||
const newReleaseProfile = {
|
||||
enabled: true,
|
||||
required: [],
|
||||
ignored: [],
|
||||
tags: [],
|
||||
indexerId: 0
|
||||
};
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.settings.releaseProfiles,
|
||||
(id, releaseProfiles) => {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges,
|
||||
items
|
||||
} = releaseProfiles;
|
||||
|
||||
const profile = id ? items.find((i) => i.id === id) : newReleaseProfile;
|
||||
const settings = selectSettings(profile, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setReleaseProfileValue,
|
||||
saveReleaseProfile
|
||||
};
|
||||
|
||||
class EditReleaseProfileModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id) {
|
||||
Object.keys(newReleaseProfile).forEach((name) => {
|
||||
this.props.setReleaseProfileValue({
|
||||
name,
|
||||
value: newReleaseProfile[name]
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setReleaseProfileValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveReleaseProfile({ id: this.props.id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditReleaseProfileModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditReleaseProfileModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setReleaseProfileValue: PropTypes.func.isRequired,
|
||||
saveReleaseProfile: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditReleaseProfileModalContentConnector);
|
||||
@@ -1,197 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector';
|
||||
import styles from './ReleaseProfile.css';
|
||||
|
||||
class ReleaseProfile extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditReleaseProfileModalOpen: false,
|
||||
isDeleteReleaseProfileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditReleaseProfilePress = () => {
|
||||
this.setState({ isEditReleaseProfileModalOpen: true });
|
||||
};
|
||||
|
||||
onEditReleaseProfileModalClose = () => {
|
||||
this.setState({ isEditReleaseProfileModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteReleaseProfilePress = () => {
|
||||
this.setState({
|
||||
isEditReleaseProfileModalOpen: false,
|
||||
isDeleteReleaseProfileModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteReleaseProfileModalClose = () => {
|
||||
this.setState({ isDeleteReleaseProfileModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmDeleteReleaseProfile = () => {
|
||||
this.props.onConfirmDeleteReleaseProfile(this.props.id);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enabled,
|
||||
required,
|
||||
ignored,
|
||||
tags,
|
||||
indexerId,
|
||||
tagList,
|
||||
indexerList
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isEditReleaseProfileModalOpen,
|
||||
isDeleteReleaseProfileModalOpen
|
||||
} = this.state;
|
||||
|
||||
const indexer = indexerId !== 0 && indexerList.find((i) => i.id === indexerId);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.releaseProfile}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditReleaseProfilePress}
|
||||
>
|
||||
{
|
||||
name ?
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div>
|
||||
{
|
||||
required.map((item) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
className={styles.label}
|
||||
key={item}
|
||||
kind={kinds.SUCCESS}
|
||||
>
|
||||
{item}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
ignored.map((item) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
className={styles.label}
|
||||
key={item}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{item}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{
|
||||
!enabled &&
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
{translate('Disabled')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
indexer &&
|
||||
<Label
|
||||
kind={kinds.INFO}
|
||||
outline={true}
|
||||
>
|
||||
{indexer.name}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditReleaseProfileModalConnector
|
||||
id={id}
|
||||
isOpen={isEditReleaseProfileModalOpen}
|
||||
onModalClose={this.onEditReleaseProfileModalClose}
|
||||
onDeleteReleaseProfilePress={this.onDeleteReleaseProfilePress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteReleaseProfileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteReleaseProfile')}
|
||||
message={translate('DeleteReleaseProfileMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteReleaseProfile}
|
||||
onCancel={this.onDeleteReleaseProfileModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReleaseProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
required: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
ignored: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ReleaseProfile.defaultProps = {
|
||||
enabled: true,
|
||||
required: [],
|
||||
ignored: [],
|
||||
indexerId: 0
|
||||
};
|
||||
|
||||
export default ReleaseProfile;
|
||||
130
frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx
Normal file
130
frontend/src/Settings/Profiles/Release/ReleaseProfileRow.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Tag } from 'App/State/TagsAppState';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { deleteReleaseProfile } from 'Store/Actions/Settings/releaseProfiles';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditReleaseProfileModal from './EditReleaseProfileModal';
|
||||
import styles from './ReleaseProfileRow.css';
|
||||
|
||||
interface ReleaseProfileProps extends ReleaseProfile {
|
||||
tagList: Tag[];
|
||||
indexerList: Indexer[];
|
||||
}
|
||||
|
||||
function ReleaseProfileRow(props: ReleaseProfileProps) {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enabled = true,
|
||||
required = [],
|
||||
ignored = [],
|
||||
tags,
|
||||
indexerId = 0,
|
||||
tagList,
|
||||
indexerList,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [
|
||||
isEditReleaseProfileModalOpen,
|
||||
setEditReleaseProfileModalOpen,
|
||||
setEditReleaseProfileModalClosed,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const [
|
||||
isDeleteReleaseProfileModalOpen,
|
||||
setDeleteReleaseProfileModalOpen,
|
||||
setDeleteReleaseProfileModalClosed,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const handleDeletePress = useCallback(() => {
|
||||
dispatch(deleteReleaseProfile({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const indexer =
|
||||
indexerId !== 0 && indexerList.find((i) => i.id === indexerId);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.releaseProfile}
|
||||
overlayContent={true}
|
||||
onPress={setEditReleaseProfileModalOpen}
|
||||
>
|
||||
{name ? <div className={styles.name}>{name}</div> : null}
|
||||
|
||||
<div>
|
||||
{required.map((item) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label key={item} className={styles.label} kind={kinds.SUCCESS}>
|
||||
{item}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{ignored.map((item) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label key={item} className={styles.label} kind={kinds.DANGER}>
|
||||
{item}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<TagList tags={tags} tagList={tagList} />
|
||||
|
||||
<div>
|
||||
{enabled ? null : (
|
||||
<Label kind={kinds.DISABLED} outline={true}>
|
||||
{translate('Disabled')}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
{indexer ? (
|
||||
<Label kind={kinds.INFO} outline={true}>
|
||||
{indexer.name}
|
||||
</Label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<EditReleaseProfileModal
|
||||
id={id}
|
||||
isOpen={isEditReleaseProfileModalOpen}
|
||||
onModalClose={setEditReleaseProfileModalClosed}
|
||||
onDeleteReleaseProfilePress={setDeleteReleaseProfileModalOpen}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteReleaseProfileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteReleaseProfile')}
|
||||
message={translate('DeleteReleaseProfileMessageText', {
|
||||
name: name ?? id,
|
||||
})}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={handleDeletePress}
|
||||
onCancel={setDeleteReleaseProfileModalClosed}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReleaseProfileRow;
|
||||
@@ -4,7 +4,7 @@
|
||||
}
|
||||
|
||||
.addReleaseProfile {
|
||||
composes: releaseProfile from '~./ReleaseProfile.css';
|
||||
composes: releaseProfile from '~./ReleaseProfileRow.css';
|
||||
|
||||
background-color: var(--cardAlternateBackgroundColor);
|
||||
color: var(--gray);
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector';
|
||||
import ReleaseProfile from './ReleaseProfile';
|
||||
import styles from './ReleaseProfiles.css';
|
||||
|
||||
class ReleaseProfiles extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddReleaseProfileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddReleaseProfilePress = () => {
|
||||
this.setState({ isAddReleaseProfileModalOpen: true });
|
||||
};
|
||||
|
||||
onAddReleaseProfileModalClose = () => {
|
||||
this.setState({ isAddReleaseProfileModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
tagList,
|
||||
indexerList,
|
||||
onConfirmDeleteReleaseProfile,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('ReleaseProfiles')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('ReleaseProfilesLoadError')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.releaseProfiles}>
|
||||
<Card
|
||||
className={styles.addReleaseProfile}
|
||||
onPress={this.onAddReleaseProfilePress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ReleaseProfile
|
||||
key={item.id}
|
||||
tagList={tagList}
|
||||
indexerList={indexerList}
|
||||
{...item}
|
||||
onConfirmDeleteReleaseProfile={onConfirmDeleteReleaseProfile}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditReleaseProfileModalConnector
|
||||
isOpen={this.state.isAddReleaseProfileModalOpen}
|
||||
onModalClose={this.onAddReleaseProfileModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReleaseProfiles.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ReleaseProfiles;
|
||||
81
frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx
Normal file
81
frontend/src/Settings/Profiles/Release/ReleaseProfiles.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { ReleaseProfilesAppState } from 'App/State/SettingsAppState';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ReleaseProfileRow from 'Settings/Profiles/Release/ReleaseProfileRow';
|
||||
import { fetchIndexers } from 'Store/Actions/Settings/indexers';
|
||||
import { fetchReleaseProfiles } from 'Store/Actions/Settings/releaseProfiles';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditReleaseProfileModal from './EditReleaseProfileModal';
|
||||
import styles from './ReleaseProfiles.css';
|
||||
|
||||
function ReleaseProfiles() {
|
||||
const { items, isFetching, isPopulated, error }: ReleaseProfilesAppState =
|
||||
useSelector(createClientSideCollectionSelector('settings.releaseProfiles'));
|
||||
|
||||
const tagList = useSelector(createTagsSelector());
|
||||
const indexerList = useSelector(
|
||||
(state: AppState) => state.settings.indexers.items
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [
|
||||
isAddReleaseProfileModalOpen,
|
||||
setAddReleaseProfileModalOpen,
|
||||
setAddReleaseProfileModalClosed,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchReleaseProfiles());
|
||||
dispatch(fetchIndexers());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('ReleaseProfiles')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('ReleaseProfilesLoadError')}
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
error={error}
|
||||
>
|
||||
<div className={styles.releaseProfiles}>
|
||||
<Card
|
||||
className={styles.addReleaseProfile}
|
||||
onPress={setAddReleaseProfileModalOpen}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon name={icons.ADD} size={45} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<ReleaseProfileRow
|
||||
key={item.id}
|
||||
tagList={tagList}
|
||||
indexerList={indexerList}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<EditReleaseProfileModal
|
||||
isOpen={isAddReleaseProfileModalOpen}
|
||||
onModalClose={setAddReleaseProfileModalClosed}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReleaseProfiles;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user